Module: CrudComponents::LikeSpec
- Defined in:
- lib/crud_components/like_spec.rb
Overview
The declarative mini-language shared by ‘filter like:` and `search_in`: case-insensitive contains across columns, joining associations as needed.
:title own column
%i[title subtitle] several own columns, OR-combined
{ authors: %i[name email] } join, explicit columns
:publisher join, delegate to Publisher's search_in
{ user: :address } nested join, delegate to Address
Specs never contain SQL strings; conditions are built through Arel with LIKE wildcards escaped, so they are parameterized end to end.
Defined Under Namespace
Classes: Entry
Constant Summary collapse
- MAX_DELEGATIONS =
Only delegation hops (an association name resolved through the target’s search_in) can form a cycle; explicit nesting is bounded by the literal spec. So the guard counts delegations only.
5
Class Method Summary collapse
- .apply(scope, spec, value) ⇒ Object
- .deep_merge(left, right) ⇒ Object
-
.delegate(model, reflection, path, delegations) ⇒ Object
Association name without columns: use the target model’s search_in spec.
-
.expand(model, spec, path = [], delegations = 0) ⇒ Object
Resolves a spec into flat [path, klass, column] entries, expanding association names without columns through the target’s search_in spec.
-
.expand_assoc(model, assoc, sub, path, delegations) ⇒ Object
Explicit nesting ({ assoc => columns }) — bounded by the spec, not a cycle risk, so it does not count against the delegation limit.
- .expand_name(model, name, path, delegations) ⇒ Object
Class Method Details
.apply(scope, spec, value) ⇒ Object
21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/crud_components/like_spec.rb', line 21 def apply(scope, spec, value) model = scope.respond_to?(:model) ? scope.model : scope entries = (model, spec) return scope if entries.empty? pattern = "%#{ActiveRecord::Base.sanitize_sql_like(value)}%" condition = entries.map { |entry| entry.arel_condition(pattern) }.reduce(:or) joins = entries.filter_map(&:join_fragment).reduce({}) { |acc, j| deep_merge(acc, j) } return scope.where(condition) if joins.empty? # distinct only matters once a join can multiply rows scope.left_joins(joins).where(condition).distinct end |
.deep_merge(left, right) ⇒ Object
104 105 106 107 108 109 110 111 |
# File 'lib/crud_components/like_spec.rb', line 104 def deep_merge(left, right) normalize = ->(j) { j.is_a?(Hash) ? j : { j => {} } } l = normalize.call(left) normalize.call(right).each do |key, value| l[key] = l.key?(key) ? deep_merge(l[key], value) : value end l end |
.delegate(model, reflection, path, delegations) ⇒ Object
Association name without columns: use the target model’s search_in spec.
92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/crud_components/like_spec.rb', line 92 def delegate(model, reflection, path, delegations) target = reflection.klass target_spec = Structure.for(target).search_in_spec if target_spec.nil? raise DefinitionError, "cannot delegate like-spec to #{model}##{reflection.name}: " \ "#{target}'s search_in is a custom block — spell the columns out, " \ "e.g. { #{reflection.name}: %i[...] }" end (target, target_spec, path + [reflection.name], delegations + 1) end |
.expand(model, spec, path = [], delegations = 0) ⇒ Object
Resolves a spec into flat [path, klass, column] entries, expanding association names without columns through the target’s search_in spec. ‘delegations` counts only delegation hops (the cycle risk).
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/crud_components/like_spec.rb', line 53 def (model, spec, path = [], delegations = 0) if delegations > MAX_DELEGATIONS raise DefinitionError, "search_in/like delegation more than #{MAX_DELEGATIONS} levels deep " \ "starting at #{model} — most likely a delegation cycle" end Array.wrap(spec).flat_map do |item| case item when Symbol, String then (model, item.to_sym, path, delegations) when Hash then item.flat_map { |assoc, sub| (model, assoc.to_sym, sub, path, delegations) } else raise DefinitionError, "invalid like-spec element #{item.inspect} for #{model} — " \ 'use column symbols, association symbols, or { assoc => columns } hashes' end end end |
.expand_assoc(model, assoc, sub, path, delegations) ⇒ Object
Explicit nesting ({ assoc => columns }) — bounded by the spec, not a cycle risk, so it does not count against the delegation limit.
83 84 85 86 87 88 89 |
# File 'lib/crud_components/like_spec.rb', line 83 def (model, assoc, sub, path, delegations) reflection = model.reflect_on_association(assoc) raise DefinitionError, "like-spec references association '#{assoc}', " \ "which #{model} does not have" unless reflection (reflection.klass, sub, path + [assoc], delegations) end |
.expand_name(model, name, path, delegations) ⇒ Object
70 71 72 73 74 75 76 77 78 79 |
# File 'lib/crud_components/like_spec.rb', line 70 def (model, name, path, delegations) if model.columns_hash.key?(name.to_s) [Entry.new(path, model, name)] elsif (reflection = model.reflect_on_association(name)) delegate(model, reflection, path, delegations) else raise DefinitionError, "like-spec references '#{name}', which is neither a column nor " \ "an association of #{model}" end end |