pursuit 0.4.3 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rubygem.yaml +46 -0
  3. data/.ruby-version +1 -1
  4. data/Gemfile +15 -0
  5. data/Gemfile.lock +127 -86
  6. data/LICENSE +174 -21
  7. data/README.md +210 -27
  8. data/bin/console +10 -0
  9. data/config.ru +2 -3
  10. data/lib/pursuit/aggregate_modifier_not_found.rb +20 -0
  11. data/lib/pursuit/aggregate_modifier_required.rb +20 -0
  12. data/lib/pursuit/aggregate_modifiers_not_available.rb +13 -0
  13. data/lib/pursuit/attribute_not_found.rb +20 -0
  14. data/lib/pursuit/constants.rb +1 -1
  15. data/lib/pursuit/error.rb +7 -0
  16. data/lib/pursuit/predicate_parser.rb +181 -0
  17. data/lib/pursuit/predicate_search.rb +83 -0
  18. data/lib/pursuit/predicate_transform.rb +231 -0
  19. data/lib/pursuit/query_error.rb +7 -0
  20. data/lib/pursuit/simple_search.rb +64 -0
  21. data/lib/pursuit/term_parser.rb +44 -0
  22. data/lib/pursuit/term_search.rb +69 -0
  23. data/lib/pursuit/term_transform.rb +35 -0
  24. data/lib/pursuit.rb +19 -5
  25. data/pursuit.gemspec +5 -18
  26. data/spec/internal/app/models/application_record.rb +5 -0
  27. data/spec/internal/app/models/product.rb +25 -9
  28. data/spec/internal/app/models/product_category.rb +23 -1
  29. data/spec/internal/app/models/product_variation.rb +26 -1
  30. data/spec/lib/pursuit/predicate_parser_spec.rb +1604 -0
  31. data/spec/lib/pursuit/predicate_search_spec.rb +80 -0
  32. data/spec/lib/pursuit/predicate_transform_spec.rb +624 -0
  33. data/spec/lib/pursuit/simple_search_spec.rb +59 -0
  34. data/spec/lib/pursuit/term_parser_spec.rb +271 -0
  35. data/spec/lib/pursuit/term_search_spec.rb +71 -0
  36. data/spec/lib/pursuit/term_transform_spec.rb +105 -0
  37. data/spec/spec_helper.rb +2 -3
  38. data/travis/gemfiles/{5.2.gemfile → 7.1.gemfile} +2 -2
  39. metadata +38 -197
  40. data/.travis.yml +0 -25
  41. data/lib/pursuit/dsl.rb +0 -28
  42. data/lib/pursuit/railtie.rb +0 -13
  43. data/lib/pursuit/search.rb +0 -172
  44. data/lib/pursuit/search_options.rb +0 -86
  45. data/lib/pursuit/search_term_parser.rb +0 -46
  46. data/spec/lib/pursuit/dsl_spec.rb +0 -22
  47. data/spec/lib/pursuit/search_options_spec.rb +0 -146
  48. data/spec/lib/pursuit/search_spec.rb +0 -516
  49. data/spec/lib/pursuit/search_term_parser_spec.rb +0 -34
  50. data/travis/gemfiles/6.0.gemfile +0 -8
  51. data/travis/gemfiles/6.1.gemfile +0 -8
  52. data/travis/gemfiles/7.0.gemfile +0 -8
@@ -0,0 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pursuit
4
+ # Transform for the tree produced by `Pursuit::PredicateParser`.
5
+ #
6
+ # @see Pursuit::PredicateParser
7
+ #
8
+ class PredicateTransform < Parslet::Transform
9
+ # @return [Hash<String, Symbol>] The list of supported aggregate modifiers, and the method name to invoke on the
10
+ # attribute node in order to obtain the function node.
11
+ #
12
+ AGGREGATE_MODIFIERS = {
13
+ '#' => :count,
14
+ '*' => :sum,
15
+ '+' => :maximum,
16
+ '-' => :minimum,
17
+ '~' => :average
18
+ }.freeze
19
+
20
+ # Boolean Types
21
+
22
+ rule(truthy: simple(:_)) { true }
23
+ rule(falsey: simple(:_)) { false }
24
+
25
+ # Numeric Types
26
+
27
+ rule(integer: simple(:value)) { Integer(value) }
28
+ rule(decimal: simple(:value)) { BigDecimal(value) }
29
+
30
+ # String Types
31
+
32
+ rule(string_double_quotes: []) { '' }
33
+ rule(string_double_quotes: simple(:value)) { value.to_s.gsub(/\\(.)/, '\1') }
34
+
35
+ rule(string_single_quotes: []) { '' }
36
+ rule(string_single_quotes: simple(:value)) { value.to_s.gsub(/\\(.)/, '\1') }
37
+
38
+ rule(string_no_quotes: simple(:value)) { value.to_s }
39
+
40
+ # Comparisons
41
+
42
+ rule(attribute: simple(:attribute), comparator: '=', value: simple(:value)) do |context|
43
+ eq_node(attribute(context), context[:value])
44
+ end
45
+
46
+ rule(attribute: simple(:attribute), comparator: '!=', value: simple(:value)) do |context|
47
+ not_eq_node(attribute(context), context[:value])
48
+ end
49
+
50
+ rule(attribute: simple(:attribute), comparator: '<', value: simple(:value)) do |context|
51
+ lt_node(attribute(context), context[:value])
52
+ end
53
+
54
+ rule(attribute: simple(:attribute), comparator: '>', value: simple(:value)) do |context|
55
+ gt_node(attribute(context), context[:value])
56
+ end
57
+
58
+ rule(attribute: simple(:attribute), comparator: '<=', value: simple(:value)) do |context|
59
+ lteq_node(attribute(context), context[:value])
60
+ end
61
+
62
+ rule(attribute: simple(:attribute), comparator: '>=', value: simple(:value)) do |context|
63
+ gteq_node(attribute(context), context[:value])
64
+ end
65
+
66
+ rule(attribute: simple(:attribute), comparator: '~', value: simple(:value)) do |context|
67
+ match_node(attribute(context), context[:value])
68
+ end
69
+
70
+ rule(attribute: simple(:attribute), comparator: '!~', value: simple(:value)) do |context|
71
+ does_not_match_node(attribute(context), context[:value])
72
+ end
73
+
74
+ # Aggregate Comparisons
75
+
76
+ rule(
77
+ aggregate_modifier: simple(:aggregate_modifier),
78
+ attribute: simple(:attribute),
79
+ comparator: '=',
80
+ value: simple(:value)
81
+ ) do |context|
82
+ eq_node(aggregate_attribute(context), context[:value])
83
+ end
84
+
85
+ rule(
86
+ aggregate_modifier: simple(:aggregate_modifier),
87
+ attribute: simple(:attribute),
88
+ comparator: '!=',
89
+ value: simple(:value)
90
+ ) do |context|
91
+ not_eq_node(aggregate_attribute(context), context[:value])
92
+ end
93
+
94
+ rule(
95
+ aggregate_modifier: simple(:aggregate_modifier),
96
+ attribute: simple(:attribute),
97
+ comparator: '<',
98
+ value: simple(:value)
99
+ ) do |context|
100
+ lt_node(aggregate_attribute(context), context[:value])
101
+ end
102
+
103
+ rule(
104
+ aggregate_modifier: simple(:aggregate_modifier),
105
+ attribute: simple(:attribute),
106
+ comparator: '>',
107
+ value: simple(:value)
108
+ ) do |context|
109
+ gt_node(aggregate_attribute(context), context[:value])
110
+ end
111
+
112
+ rule(
113
+ aggregate_modifier: simple(:aggregate_modifier),
114
+ attribute: simple(:attribute),
115
+ comparator: '<=',
116
+ value: simple(:value)
117
+ ) do |context|
118
+ lteq_node(aggregate_attribute(context), context[:value])
119
+ end
120
+
121
+ rule(
122
+ aggregate_modifier: simple(:aggregate_modifier),
123
+ attribute: simple(:attribute),
124
+ comparator: '>=',
125
+ value: simple(:value)
126
+ ) do |context|
127
+ gteq_node(aggregate_attribute(context), context[:value])
128
+ end
129
+
130
+ rule(
131
+ aggregate_modifier: simple(:aggregate_modifier),
132
+ attribute: simple(:attribute),
133
+ comparator: '~',
134
+ value: simple(:value)
135
+ ) do |context|
136
+ match_node(aggregate_attribute(context), context[:value])
137
+ end
138
+
139
+ rule(
140
+ aggregate_modifier: simple(:aggregate_modifier),
141
+ attribute: simple(:attribute),
142
+ comparator: '!~',
143
+ value: simple(:value)
144
+ ) do |context|
145
+ does_not_match_node(aggregate_attribute(context), context[:value])
146
+ end
147
+
148
+ # Joins
149
+
150
+ rule(left: simple(:left), joiner: '&', right: simple(:right)) do
151
+ left.and(right)
152
+ end
153
+
154
+ rule(left: simple(:left), joiner: '|', right: simple(:right)) do
155
+ left.or(right)
156
+ end
157
+
158
+ # Helpers
159
+
160
+ class << self
161
+ def attribute(context)
162
+ attribute_name = context[:attribute].to_sym
163
+ attribute = context.dig(:permitted_attributes, attribute_name)
164
+ raise AttributeNotFound, attribute_name if attribute.blank?
165
+ raise AggregateModifierRequired, attribute_name if attribute.name == Arel.star
166
+
167
+ attribute
168
+ end
169
+
170
+ def aggregate_attribute(context)
171
+ raise AggregateModifiersNotAvailable unless context[:permit_aggregate_modifiers]
172
+
173
+ attribute_name = context[:attribute].to_sym
174
+ attribute = context.dig(:permitted_attributes, attribute_name)
175
+ raise AttributeNotFound, attribute_name if attribute.blank?
176
+
177
+ aggregate_modifier_name = context[:aggregate_modifier].to_s
178
+ aggregate_modifier = AGGREGATE_MODIFIERS[aggregate_modifier_name]
179
+ raise AggregateModifierNotFound, aggregate_modifier_name unless aggregate_modifier
180
+
181
+ attribute.public_send(aggregate_modifier)
182
+ end
183
+
184
+ def eq_node(attribute, value)
185
+ value = ActiveRecord::Base.sanitize_sql(value) if value.is_a?(String)
186
+ return attribute.eq(value) if value.present?
187
+
188
+ attribute.eq(nil).or(attribute.matches_regexp('^\s*$'))
189
+ end
190
+
191
+ def not_eq_node(attribute, value)
192
+ value = ActiveRecord::Base.sanitize_sql(value) if value.is_a?(String)
193
+ return attribute.not_eq(value) if value.present?
194
+
195
+ attribute.not_eq(nil).and(attribute.does_not_match_regexp('^\s*$'))
196
+ end
197
+
198
+ def gt_node(attribute, value)
199
+ value = ActiveRecord::Base.sanitize_sql(value) if value.is_a?(String)
200
+ attribute.gt(value)
201
+ end
202
+
203
+ def gteq_node(attribute, value)
204
+ value = ActiveRecord::Base.sanitize_sql(value) if value.is_a?(String)
205
+ attribute.gteq(value)
206
+ end
207
+
208
+ def lt_node(attribute, value)
209
+ value = ActiveRecord::Base.sanitize_sql(value) if value.is_a?(String)
210
+ attribute.lt(value)
211
+ end
212
+
213
+ def lteq_node(attribute, value)
214
+ value = ActiveRecord::Base.sanitize_sql(value) if value.is_a?(String)
215
+ attribute.lteq(value)
216
+ end
217
+
218
+ def match_node(attribute, value)
219
+ value = ActiveRecord::Base.sanitize_sql_like(value) if value.is_a?(String)
220
+ value = value.blank? ? '%' : "%#{value}%"
221
+ attribute.matches(value)
222
+ end
223
+
224
+ def does_not_match_node(attribute, value)
225
+ value = ActiveRecord::Base.sanitize_sql_like(value) if value.is_a?(String)
226
+ value = value.blank? ? '%' : "%#{value}%"
227
+ attribute.does_not_match(value)
228
+ end
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pursuit
4
+ # Base error class for all query related errors.
5
+ #
6
+ class QueryError < Error; end
7
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pursuit
4
+ # :nodoc:
5
+ #
6
+ class SimpleSearch
7
+ # @return [Set<Arel::Attributes::Attribute>] The attributes to match against.
8
+ #
9
+ attr_reader :attributes
10
+
11
+ # @return [ActiveRecord::Relation] The relation to which the clauses are added.
12
+ #
13
+ attr_reader :relation
14
+
15
+ # Creates a new simple search instance.
16
+ #
17
+ # @param relation [ActiveRecord::Relation] The relation to which the clauses are added.
18
+ # @param block [Proc] The proc to invoke in the search instance (optional).
19
+ #
20
+ def initialize(relation, &block)
21
+ @attributes = Set.new
22
+ @relation = relation
23
+
24
+ instance_eval(&block) if block
25
+ end
26
+
27
+ # Adds an attribute to match against in queries.
28
+ #
29
+ # @param attribute [Arel::Attributes::Attribute, Symbol] The underlying attribute to query.
30
+ # @return [Arel::Attributes::Attribute] The underlying attribute to query.
31
+ #
32
+ def search_attribute(attribute)
33
+ attribute = relation.klass.arel_table[attribute] if attribute.is_a?(Symbol)
34
+ attributes.add(attribute)
35
+ end
36
+
37
+ # Parse a simple query into an ARel node.
38
+ #
39
+ # @param query [String] The simple query.
40
+ # @return [Arel::Nodes::Node] The ARel node representing the simple query.
41
+ #
42
+ def parse(query)
43
+ value = ActiveRecord::Base.sanitize_sql_like(query)
44
+ value = "%#{value}%"
45
+
46
+ attributes.inject(nil) do |previous_node, attribute|
47
+ node = attribute.matches(value)
48
+ next node unless previous_node
49
+
50
+ previous_node.or(node)
51
+ end
52
+ end
53
+
54
+ # Returns #relation filtered by the query.
55
+ #
56
+ # @param query [String] The simple query.
57
+ # @return [ActiveRecord::Relation] The updated relation with the clauses added.
58
+ #
59
+ def apply(query)
60
+ node = parse(query)
61
+ node ? relation.where(node) : relation.none
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pursuit
4
+ # Parser for a list of terms.
5
+ #
6
+ class TermParser < Parslet::Parser
7
+ # Whitespace
8
+
9
+ rule(:space) { match('\s').repeat(1) }
10
+ rule(:space?) { match('\s').repeat(0) }
11
+
12
+ # Character Types
13
+
14
+ rule(:escaped_character) do
15
+ str('\\') >> match('.')
16
+ end
17
+
18
+ # String Types
19
+
20
+ rule(:string_double_quotes) do
21
+ str('"') >> (escaped_character | match('[^"]')).repeat(0).as(:string_double_quotes) >> str('"')
22
+ end
23
+
24
+ rule(:string_single_quotes) do
25
+ str("'") >> (escaped_character | match("[^']")).repeat(0).as(:string_single_quotes) >> str("'")
26
+ end
27
+
28
+ rule(:string_no_quotes) do
29
+ match('[^\s]').repeat(1).as(:string_no_quotes)
30
+ end
31
+
32
+ rule(:string) do
33
+ string_double_quotes | string_single_quotes | string_no_quotes
34
+ end
35
+
36
+ # Terms
37
+
38
+ rule(:term) { string.as(:term) }
39
+ rule(:term_pair) { term.as(:left) >> space >> term_node.as(:right) }
40
+ rule(:term_node) { term_pair | term }
41
+ rule(:terms) { space? >> term_node >> space? }
42
+ root(:terms)
43
+ end
44
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pursuit
4
+ # :nodoc:
5
+ #
6
+ class TermSearch
7
+ # @return [Set<Arel::Attributes::Attribute>] The attributes to match against.
8
+ #
9
+ attr_reader :attributes
10
+
11
+ # @return [ActiveRecord::Relation] The relation to which the term clauses are added.
12
+ #
13
+ attr_reader :relation
14
+
15
+ # Creates a new term search instance.
16
+ #
17
+ # @param relation [ActiveRecord::Relation] The relation to which the term clauses are added.
18
+ # @param block [Proc] The proc to invoke in the search instance (optional).
19
+ #
20
+ def initialize(relation, &block)
21
+ @attributes = Set.new
22
+ @relation = relation
23
+
24
+ instance_eval(&block) if block
25
+ end
26
+
27
+ # @return [Pursuit::TermParser] The parser which converts queries into trees.
28
+ #
29
+ def parser
30
+ @parser ||= TermParser.new
31
+ end
32
+
33
+ # @return [Pursuit::TermTransform] The transform which converts trees into ARel nodes.
34
+ #
35
+ def transform
36
+ @transform ||= TermTransform.new
37
+ end
38
+
39
+ # Adds an attribute to match against in term queries.
40
+ #
41
+ # @param attribute [Arel::Attributes::Attribute, Symbol] The underlying attribute to query.
42
+ # @return [Arel::Attributes::Attribute] The underlying attribute to query.
43
+ #
44
+ def search_attribute(attribute)
45
+ attribute = relation.klass.arel_table[attribute] if attribute.is_a?(Symbol)
46
+ attributes.add(attribute)
47
+ end
48
+
49
+ # Parse a term query into an ARel node.
50
+ #
51
+ # @param query [String] The term query.
52
+ # @return [Arel::Nodes::Node] The ARel node representing the term query.
53
+ #
54
+ def parse(query)
55
+ tree = parser.parse(query)
56
+ transform.apply(tree, attributes: attributes)
57
+ end
58
+
59
+ # Returns #relation filtered by the term query.
60
+ #
61
+ # @param query [String] The term query.
62
+ # @return [ActiveRecord::Relation] The updated relation with the term clauses added.
63
+ #
64
+ def apply(query)
65
+ node = parse(query)
66
+ node ? relation.where(node) : relation.none
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pursuit
4
+ # Transform for a list of terms.
5
+ #
6
+ class TermTransform < Parslet::Transform
7
+ # String Types
8
+
9
+ rule(string_double_quotes: []) { '' }
10
+ rule(string_double_quotes: simple(:value)) { value.to_s.gsub(/\\(.)/, '\1') }
11
+
12
+ rule(string_single_quotes: []) { '' }
13
+ rule(string_single_quotes: simple(:value)) { value.to_s.gsub(/\\(.)/, '\1') }
14
+
15
+ rule(string_no_quotes: simple(:value)) { value.to_s }
16
+
17
+ # Terms
18
+
19
+ rule(term: simple(:term)) do |context|
20
+ value = ActiveRecord::Base.sanitize_sql_like(context[:term])
21
+ value = "%#{value}%"
22
+
23
+ context[:attributes].inject(nil) do |previous_node, attribute|
24
+ node = attribute.matches(value)
25
+ next node unless previous_node
26
+
27
+ previous_node.or(node)
28
+ end
29
+ end
30
+
31
+ # Joins
32
+
33
+ rule(left: simple(:left), right: simple(:right)) { left.and(right) }
34
+ end
35
+ end
data/lib/pursuit.rb CHANGED
@@ -1,7 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'pursuit/constants'
4
- require 'pursuit/search_options'
5
- require 'pursuit/search_term_parser'
6
- require 'pursuit/search'
7
- require 'pursuit/railtie' if defined?(Rails::Railtie)
3
+ require 'active_record'
4
+ require 'active_support'
5
+ require 'bigdecimal'
6
+ require 'parslet'
7
+
8
+ require_relative 'pursuit/constants'
9
+ require_relative 'pursuit/error'
10
+ require_relative 'pursuit/query_error'
11
+ require_relative 'pursuit/aggregate_modifier_not_found'
12
+ require_relative 'pursuit/aggregate_modifier_required'
13
+ require_relative 'pursuit/aggregate_modifiers_not_available'
14
+ require_relative 'pursuit/attribute_not_found'
15
+ require_relative 'pursuit/predicate_parser'
16
+ require_relative 'pursuit/predicate_transform'
17
+ require_relative 'pursuit/predicate_search'
18
+ require_relative 'pursuit/term_parser'
19
+ require_relative 'pursuit/term_transform'
20
+ require_relative 'pursuit/term_search'
21
+ require_relative 'pursuit/simple_search'
data/pursuit.gemspec CHANGED
@@ -12,8 +12,8 @@ Gem::Specification.new do |spec|
12
12
  spec.email = ['support@nialtoservices.co.uk']
13
13
 
14
14
  spec.summary = 'Advanced key-based searching for ActiveRecord objects.'
15
- spec.homepage = 'https://github.com/nialtoservices/pursuit'
16
- spec.license = 'MIT'
15
+ spec.homepage = 'https://github.com/NialtoServices/pursuit'
16
+ spec.license = 'Apache-2.0'
17
17
 
18
18
  spec.files = `git ls-files -z`.split("\x0")
19
19
  spec.require_paths = ['lib']
@@ -23,20 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.metadata['rubygems_mfa_required'] = 'true'
24
24
  spec.metadata['yard.run'] = 'yri'
25
25
 
26
- spec.add_runtime_dependency 'activerecord', '>= 5.2.0', '< 7.1.0'
27
- spec.add_runtime_dependency 'activesupport', '>= 5.2.0', '< 7.1.0'
28
-
29
- spec.add_development_dependency 'bundler', '~> 2.0'
30
- spec.add_development_dependency 'combustion', '~> 1.3'
31
- spec.add_development_dependency 'guard', '~> 2.18'
32
- spec.add_development_dependency 'guard-rspec', '~> 4.7'
33
- spec.add_development_dependency 'pry', '~> 0.14'
34
- spec.add_development_dependency 'rake', '~> 13.0'
35
- spec.add_development_dependency 'rspec', '~> 3.12'
36
- spec.add_development_dependency 'rspec-rails', '~> 6.0'
37
- spec.add_development_dependency 'rubocop', '~> 1.44'
38
- spec.add_development_dependency 'rubocop-rake', '~> 0.6'
39
- spec.add_development_dependency 'rubocop-rspec', '~> 2.18'
40
- spec.add_development_dependency 'sqlite3', '~> 1.6'
41
- spec.add_development_dependency 'yard', '~> 0.9'
26
+ spec.add_runtime_dependency 'activerecord', '>= 5.2.0', '<= 8.0.0'
27
+ spec.add_runtime_dependency 'activesupport', '>= 5.2.0', '<= 8.0.0'
28
+ spec.add_runtime_dependency 'parslet', '~> 2.0'
42
29
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end
@@ -1,22 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Product < ActiveRecord::Base
3
+ class Product < ApplicationRecord
4
4
  belongs_to :category, class_name: 'ProductCategory', inverse_of: :products, optional: true
5
5
 
6
6
  has_many :variations, class_name: 'ProductVariation', inverse_of: :product
7
7
 
8
8
  validates :title, presence: true
9
9
 
10
- searchable do |o|
11
- o.relation :variations, :title, :stock_status
10
+ def self.predicate_search
11
+ @predicate_search ||= Pursuit::PredicateSearch.new(
12
+ left_outer_joins(:category, :variations).group(:id).order(:title)
13
+ ) do
14
+ permit_attribute :title
15
+ permit_attribute :category, ProductCategory.arel_table[:id]
16
+ permit_attribute :category_name, ProductCategory.arel_table[:name]
17
+ permit_attribute :variation, ProductVariation.arel_table[:id]
18
+ permit_attribute :variation_title, ProductVariation.arel_table[:title]
19
+ permit_attribute :variation_currency, ProductVariation.arel_table[:currency]
20
+ permit_attribute :variation_amount, ProductVariation.arel_table[:amount]
21
+ end
22
+ end
12
23
 
13
- o.attribute :title
14
- o.attribute :description
15
- o.attribute :rating, unkeyed: false
16
- o.attribute :title_length, unkeyed: false do
17
- Arel::Nodes::NamedFunction.new('LENGTH', [arel_table[:title]])
24
+ def self.term_search
25
+ @term_search ||= Pursuit::TermSearch.new(
26
+ left_outer_joins(:category).group(:id).order(:title)
27
+ ) do
28
+ search_attribute :title
29
+ search_attribute ProductCategory.arel_table[:name]
18
30
  end
31
+ end
19
32
 
20
- o.attribute :category, :category_id
33
+ def self.search(query)
34
+ predicate_search.apply(query)
35
+ rescue Parslet::ParseFailed
36
+ term_search.apply(query)
21
37
  end
22
38
  end
@@ -1,7 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ProductCategory < ActiveRecord::Base
3
+ class ProductCategory < ApplicationRecord
4
4
  has_many :products, class_name: 'Product', foreign_key: :category_id, inverse_of: :category, dependent: :nullify
5
5
 
6
6
  validates :name, presence: true
7
+
8
+ def self.predicate_search
9
+ @predicate_search ||= Pursuit::PredicateSearch.new(
10
+ left_outer_joins(:products).group(:id)
11
+ ) do
12
+ permit_attribute :name
13
+ permit_attribute :product, Product.arel_table[:id]
14
+ permit_attribute :product_title, Product.arel_table[:title]
15
+ end
16
+ end
17
+
18
+ def self.term_search
19
+ @term_search ||= Pursuit::TermSearch.new(all) do
20
+ search_attribute :name
21
+ end
22
+ end
23
+
24
+ def self.search(query)
25
+ predicate_search.apply(query)
26
+ rescue Parslet::ParseFailed
27
+ term_search.apply(query)
28
+ end
7
29
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ProductVariation < ActiveRecord::Base
3
+ class ProductVariation < ApplicationRecord
4
4
  belongs_to :product
5
5
 
6
6
  enum stock_status: { in_stock: 1, low_stock: 2, out_of_stock: 3 }
@@ -9,4 +9,29 @@ class ProductVariation < ActiveRecord::Base
9
9
 
10
10
  validates :currency, presence: true
11
11
  validates :amount, presence: true, numericality: true
12
+
13
+ def self.predicate_search
14
+ @predicate_search ||= Pursuit::PredicateSearch.new(
15
+ left_outer_joins(:product).group(:id)
16
+ ) do
17
+ permit_attribute :title
18
+ permit_attribute :stock_status
19
+ permit_attribute :currency
20
+ permit_attribute :amount
21
+ permit_attribute :product, Product.arel_table[:id]
22
+ permit_attribute :product_title, Product.arel_table[:title]
23
+ end
24
+ end
25
+
26
+ def self.term_search
27
+ @term_search ||= Pursuit::TermSearch.new(all) do
28
+ search_attribute :title
29
+ end
30
+ end
31
+
32
+ def self.search(query)
33
+ predicate_search.apply(query)
34
+ rescue Parslet::ParseFailed
35
+ term_search.apply(query)
36
+ end
12
37
  end