Class: CafeCar::QueryBuilder
- Inherits:
-
Object
- Object
- CafeCar::QueryBuilder
- Defined in:
- lib/cafe_car/query_builder.rb
Defined Under Namespace
Classes: Op
Constant Summary collapse
- OPS =
Word-form comparison operators for URL filters —
price.min=10parses to{ "price" => { "min" => "10" } }, and each word maps to an Arel predicate.min/maxread friendlier thangte/lteand are the documented spelling. { "eq" => "==", "gt" => ">", "lt" => "<", "gte" => ">=", "lte" => "<=", "min" => ">=", "max" => "<=" }.freeze
Instance Attribute Summary collapse
-
#scope ⇒ Object
readonly
Returns the value of attribute scope.
Instance Method Summary collapse
- #arel(key) ⇒ Object
- #arel!(node) ⇒ Object
- #association!(name, value) ⇒ Object
- #association?(name) ⇒ Boolean
- #attribute!(key, value) ⇒ Object
- #attribute?(name) ⇒ Boolean
- #chomp(key) ⇒ Object
- #column(name) ⇒ Object
- #enum?(name) ⇒ Boolean
- #info(name) ⇒ Object
-
#initialize(scope) ⇒ QueryBuilder
constructor
A new instance of QueryBuilder.
- #not! ⇒ Object
-
#operators?(value) ⇒ Boolean
A filter value like
{ "min" => 10, "max" => 50 }— every key a known comparison operator — desugars to>= 10 AND <= 50rather than an equality against the literal hash. - #param!(key, value) ⇒ Object
- #parse(key, value) ⇒ Object
- #parse_range(key, value) ⇒ Object
- #parse_time(value) ⇒ Object
- #parse_value(key, value) ⇒ Object
- #query ⇒ Object
- #query!(params = nil) ⇒ Object
- #reflection(name) ⇒ Object
- #scope!(name, value) ⇒ Object
- #scope?(name) ⇒ Boolean
- #search!(term) ⇒ Object
- #unscoped ⇒ Object
- #update! ⇒ Object
Constructor Details
#initialize(scope) ⇒ QueryBuilder
Returns a new instance of QueryBuilder.
37 38 39 |
# File 'lib/cafe_car/query_builder.rb', line 37 def initialize(scope) @scope = scope end |
Instance Attribute Details
#scope ⇒ Object (readonly)
Returns the value of attribute scope.
35 36 37 |
# File 'lib/cafe_car/query_builder.rb', line 35 def scope @scope end |
Instance Method Details
#arel(key) ⇒ Object
42 |
# File 'lib/cafe_car/query_builder.rb', line 42 def arel(key) = @scope.arel_table[chomp(key)] |
#arel!(node) ⇒ Object
117 |
# File 'lib/cafe_car/query_builder.rb', line 117 def arel!(node) = @scope.where!(node) |
#association!(name, value) ⇒ Object
155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/cafe_car/query_builder.rb', line 155 def association!(name, value, ...) update! do case value when true then @scope.where_assoc_exists(name) when false then @scope.where_assoc_not_exists(name) when Integer, Range, /^\d+$/ @scope.where_assoc_count(parse(name, value), :==, name) when Op value = parse(name, value) @scope.where_assoc_count(value.rhs, value.flop, name) else @scope.where_assoc_exists(name) { all.query!(value, ...) } end end end |
#association?(name) ⇒ Boolean
113 |
# File 'lib/cafe_car/query_builder.rb', line 113 def association?(name) = reflection(name).present? |
#attribute!(key, value) ⇒ Object
138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/cafe_car/query_builder.rb', line 138 def attribute!(key, value) case [ key, value ] in _, Regexp @scope.where!(arel(key).matches_regexp(value.source, !value.casefold?)) in _, Op @scope.where!(parse(key, value).arel(arel(key))) in _, Hash if operators?(value) value.each { |word, rhs| attribute!(key, Op.new(OPS[word.to_s], rhs)) } else @scope.where!(key => parse(key, value)) end end |
#attribute?(name) ⇒ Boolean
114 |
# File 'lib/cafe_car/query_builder.rb', line 114 def attribute?(name) = column(name).present? |
#chomp(key) ⇒ Object
43 |
# File 'lib/cafe_car/query_builder.rb', line 43 def chomp(key) = key.to_s.sub(/\W+$/, "") |
#column(name) ⇒ Object
109 |
# File 'lib/cafe_car/query_builder.rb', line 109 def column(name) = @scope.columns_hash[name.to_s] |
#enum?(name) ⇒ Boolean
112 |
# File 'lib/cafe_car/query_builder.rb', line 112 def enum?(name) = info(name).enum? |
#info(name) ⇒ Object
111 |
# File 'lib/cafe_car/query_builder.rb', line 111 def info(name) = CafeCar[:ModelInfo].find(@scope.model).field(chomp(name)) |
#not! ⇒ Object
104 105 106 107 |
# File 'lib/cafe_car/query_builder.rb', line 104 def not!(&) inverted = unscoped.tap { _1.instance_exec(&) }.scope.invert_where update! { _1.and(inverted) } end |
#operators?(value) ⇒ Boolean
A filter value like { "min" => 10, "max" => 50 } — every key a known
comparison operator — desugars to >= 10 AND <= 50 rather than an
equality against the literal hash.
153 |
# File 'lib/cafe_car/query_builder.rb', line 153 def operators?(value) = value.any? && value.keys.all? { OPS.key?(_1.to_s) } |
#param!(key, value) ⇒ Object
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/cafe_car/query_builder.rb', line 119 def param!(key, value) case key when /^(.*?)\s*!$/ not! { param!($1, value) } when /^(.*?)\s*~$/ param!($1, Regexp.new(value, Regexp::IGNORECASE)) when /^(.*?)\s*([<>]=?)$/ param!($1, Op.new($2, value)) when method(:association?) association!(key, value) when method(:attribute?) attribute!(key, value) when method(:scope?) scope!(key, value) else raise MissingAttributeError, "can't find #{key.inspect} on #{@scope.model_name}" end end |
#parse(key, value) ⇒ Object
57 58 59 60 61 62 63 64 |
# File 'lib/cafe_car/query_builder.rb', line 57 def parse(key, value) new_value = parse_value(key, value) if new_value != value parse(key, new_value) else new_value end end |
#parse_range(key, value) ⇒ Object
51 52 53 54 55 |
# File 'lib/cafe_car/query_builder.rb', line 51 def parse_range(key, value) Range.new(parse_value(key, value.begin).then { _1.try(:begin) or _1 }, parse_value(key, value.end).then { value.exclude_end? ? _1.try(:begin) : _1.try(:end) or _1 }, value.exclude_end?) end |
#parse_time(value) ⇒ Object
45 46 47 48 49 |
# File 'lib/cafe_car/query_builder.rb', line 45 def parse_time(value) Chronic.parse(value, guess: false, context: :past) rescue NoMethodError nil end |
#parse_value(key, value) ⇒ Object
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/cafe_car/query_builder.rb', line 66 def parse_value(key, value) case value in Op(rhs: /^=(.*)$/) Op.new("#{value.op}=", $1) in Op(op: (:< | :>=), rhs: Range) value.map(&:begin) in Op(op: (:> | :<=), rhs: Range) value.map(&:end) in Range parse_range(key, value) in Array | Op value.map { parse_value(key, _1) } # An enum key ("archived") passes through untouched — ActiveRecord casts it # to the stored value itself; coercing by column type would `to_i` an # integer-backed enum's key to 0, the wrong bucket. in String if enum?(key) value in "true" then true in "false" then false in String case column(key)&.type || reflection(key)&.macro when :datetime then parse_time(value) || value when :integer then value.to_i when :float then value.to_f when :belongs_to, :has_many, :has_one value.to_i else value end else value end end |
#query ⇒ Object
195 |
# File 'lib/cafe_car/query_builder.rb', line 195 def query(...) = clone.update!(&:all).query!(...) |
#query!(params = nil) ⇒ Object
183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/cafe_car/query_builder.rb', line 183 def query!(params = nil) case params when Hash then params.each { param!(_1, _2) } when Array then params.each { query! _1 } when String then search!(params) # when Arel::Nodes::Node then arel!(params) when nil else raise ArgumentError, "cannot query on #{params}" end self end |
#reflection(name) ⇒ Object
110 |
# File 'lib/cafe_car/query_builder.rb', line 110 def reflection(name) = @scope.reflect_on_association(name) |
#scope!(name, value) ⇒ Object
170 171 172 173 174 175 176 |
# File 'lib/cafe_car/query_builder.rb', line 170 def scope!(name, value) value = parse_value(name, value) arity = (@scope.scopes[name] || @scope.method(name)).arity value = nil if arity == 0 and value == true update! { _1.public_send(name, *value) } end |
#scope?(name) ⇒ Boolean
115 |
# File 'lib/cafe_car/query_builder.rb', line 115 def scope?(name) = name.intern.in? @scope.local_methods |
#search!(term) ⇒ Object
178 179 180 181 |
# File 'lib/cafe_car/query_builder.rb', line 178 def search!(term) @scope.query!("body~": term) if @scope < ::ActionText::RichText update! { _1.respond_to?(:search) ? _1.search(term) : _1.default_search(term) } end |
#unscoped ⇒ Object
41 |
# File 'lib/cafe_car/query_builder.rb', line 41 def unscoped = QueryBuilder.new(@scope.unscope(:where)) |
#update! ⇒ Object
98 99 100 101 102 |
# File 'lib/cafe_car/query_builder.rb', line 98 def update!(&) scope = yield @scope @scope = scope if scope self end |