pursuit 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 48b4becb7e8e0e0d39335cd5172fd4870989a9e26033a882e082480431330285
4
- data.tar.gz: 33265eafe36113d7e4f263a6f9a2dc9fd7d12cfda95073c29ea0866f627e4566
3
+ metadata.gz: 70e763ac9fa96f98268f46e1824852e5908874fd7b667a8e5e8df5049ad14cd1
4
+ data.tar.gz: a375cf25c07d547f4bf8321796de8537881a05c740a198269ab6b4dac6c90ad9
5
5
  SHA512:
6
- metadata.gz: 01fba1c0d143f914d3a040620f618538f8e77396e7e9fa17f503a06ef6b419c2b9eb0a55950e60b16608d944535eb9a3bf70888615709d08f525938d25850bf6
7
- data.tar.gz: b2d0106bc8ceea194d87f62dd8831c3cb4477c831c951f638038d6d94384ee91c64039891203783ee50a3bedf76672245641ba5206191b5350f72d46e1e462f3
6
+ metadata.gz: 317ec03777606abbc9c800b728a99305855ae7e4702a64eba53c68ba402e7645de7c21d91ac2d41c8cfd953c3f9a9f1ff9878776c058ab6f39a303d216858c4c
7
+ data.tar.gz: ca1240b3907b70541a9a26fd54a9c97088deef1d227eb26694085ed99f8c84782cb65b7a22ee9c97d2854b9329ceb30bf6a98ab7900d077fb99047961d0b72b9
data/README.md CHANGED
@@ -28,10 +28,10 @@ Simple takes the entire query and generates a SQL `LIKE` (or `ILIKE` for *Postgr
28
28
  added to the search instance. Here's an example of how you might use simple to search a hypothetical `Product` record:
29
29
 
30
30
  ```ruby
31
- search = Pursuit::SimpleSearch.new(Product.all)
31
+ search = Pursuit::SimpleSearch.new(default_table: Product.arel_table)
32
32
  search.search_attribute(:title)
33
33
  search.search_attribute(:subtitle)
34
- search.apply('Green Shirt')
34
+ search.apply('Green Shirt', Product.all)
35
35
  ```
36
36
 
37
37
  Which results in the following SQL query:
@@ -50,25 +50,23 @@ The initializer method also accepts a block, which is evaluated within the insta
50
50
  when declaring the searchable attributes:
51
51
 
52
52
  ```ruby
53
- search = Pursuit::SimpleSearch.new(Product.all) do
53
+ search = Pursuit::SimpleSearch.new(default_table: Product.arel_table) do
54
54
  search_attribute :title
55
55
  search_attribute :subtitle
56
56
  end
57
57
 
58
- search.apply('Green Shirt')
58
+ search.apply('Green Shirt', Product.all)
59
59
  ```
60
60
 
61
61
  You can also pass custom `Arel::Attribute::Attribute` objects, which are especially useful when using joins:
62
62
 
63
63
  ```ruby
64
- search = Pursuit::SimpleSearch.new(
65
- Product.left_outer_joins(:variations).group(:id)
66
- ) do
64
+ search = Pursuit::SimpleSearch.new(default_table: Product.arel_table) do
67
65
  search_attribute :title
68
66
  search_attribute ProductVariation.arel_table[:title]
69
67
  end
70
68
 
71
- search.apply('Green Shirt')
69
+ search.apply('Green Shirt', Product.left_outer_joins(:variations).group(:id))
72
70
  ```
73
71
 
74
72
  Which results in the following SQL query:
@@ -92,12 +90,12 @@ Term searches break a query into individual terms on spaces, while providing dou
92
90
  means to include spaces. Here's an example of using term searches on the same `Product` record from earlier:
93
91
 
94
92
  ```ruby
95
- search = Pursuit::TermSearch.new(Product.all) do
93
+ search = Pursuit::TermSearch.new(default_table: Product.arel_table) do
96
94
  search_attribute :title
97
95
  search_attribute :subtitle
98
96
  end
99
97
 
100
- search.apply('Green "Luxury Shirt"')
98
+ search.apply('Green "Luxury Shirt"', Product.all)
101
99
  ```
102
100
 
103
101
  Which results in a SQL query similar to the following:
@@ -128,9 +126,7 @@ You can also rename attributes, and add attributes for joined records.
128
126
  Here's a more complex example of using predicate-based searches with joins on the `Product` record from earlier:
129
127
 
130
128
  ```ruby
131
- search = Pursuit::PredicateSearch.new(
132
- Product.left_outer_join(:category, :variations).group(:id)
133
- ) do
129
+ search = Pursuit::PredicateSearch.new(default_table: Product.arel_table) do
134
130
  # Product Attributes
135
131
  permit_attribute :title
136
132
 
@@ -143,7 +139,10 @@ search = Pursuit::PredicateSearch.new(
143
139
  permit_attribute :variation_amount, ProductVariation.arel_table[:amount]
144
140
  end
145
141
 
146
- search.apply('title = "Luxury Shirt" & (variation_amount = 0 | variation_amount > 1000)')
142
+ search.apply(
143
+ 'title = "Luxury Shirt" & (variation_amount = 0 | variation_amount > 1000)',
144
+ Product.left_outer_join(:category, :variations).group(:id)
145
+ )
147
146
  ```
148
147
 
149
148
  This translates to "a product whose title is 'Luxury Shirt' and has at least one variation with either an amount of 0,
@@ -181,9 +180,7 @@ Predicate searches also support "aggregate modifiers" which enable the use of ag
181
180
  must be explicitly enabled and requires you to use a `GROUP BY` clause:
182
181
 
183
182
  ```ruby
184
- search = Pursuit::PredicateSearch.new(
185
- Product.left_outer_join(:category, :variations).group(:id)
186
- ) do
183
+ search = Pursuit::PredicateSearch.new(default_table: Product.arel_table, permit_aggregate_modifiers: true) do
187
184
  # Product Attributes
188
185
  permit_attribute :title
189
186
 
@@ -198,7 +195,10 @@ search = Pursuit::PredicateSearch.new(
198
195
  permit_attribute :variation_amount, ProductVariation.arel_table[:amount]
199
196
  end
200
197
 
201
- search.apply('title = "Luxury Shirt" & #variation > 5')
198
+ search.apply(
199
+ 'title = "Luxury Shirt" & #variation > 5',
200
+ Product.left_outer_join(:category, :variations).group(:id)
201
+ )
202
202
  ```
203
203
 
204
204
  And the resulting SQL from this query:
@@ -3,5 +3,5 @@
3
3
  module Pursuit
4
4
  # @return [String] The gem's semantic version number.
5
5
  #
6
- VERSION = '1.0.1'
6
+ VERSION = '1.1.0'
7
7
  end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pursuit
4
- # Parser for predicate-based queries.
4
+ # Parser for predicate queries.
5
5
  #
6
- # Predicate-based queries take an attribute to compare the value of, an operator (such as the equal sign), and the
7
- # value to compare with.
6
+ # Predicate queries take an attribute, an operator (such as the equal sign), and a value to compare with.
8
7
  #
9
8
  # For example, to search for records where the `first_name` attribute is equal to "John" and the `last_name`
10
- # attribute contains either "Doe" or "Smith", you would enter:
9
+ # attribute contains either "Doe" or "Smith", you might use:
10
+ #
11
11
  # => "first_name = John & (last_name ~ Doe | last_name ~ Smith)"
12
12
  #
13
13
  class PredicateParser < Parslet::Parser
@@ -1,29 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pursuit
4
- # :nodoc:
4
+ # Provides an interface for declaring which attributes can be used in a predicate query, and a method for applying
5
+ # a predicate query to an `ActiveRecord::Relation` instance.
6
+ #
7
+ # @see Pursuit::PredicateParser
8
+ # @see Pursuit::PredicateTransform
5
9
  #
6
10
  class PredicateSearch
7
- # @return [Boolean] `true` when aggregate modifiers can be used in queries, `false` otherwise.
11
+ # @return [Arel::Table] The default table to retrieve attributes from.
8
12
  #
9
- attr_accessor :permit_aggregate_modifiers
13
+ attr_accessor :default_table
10
14
 
11
- # @return [Hash<Symbol, Arel::Attributes::Attribute>] The attributes permitted for use in queries.
15
+ # @return [Boolean] `true` when aggregate modifiers can be used, `false` otherwise.
12
16
  #
13
- attr_reader :permitted_attributes
17
+ attr_accessor :permit_aggregate_modifiers
14
18
 
15
- # @return [ActiveRecord::Relation] The relation to which the predicate clauses are added.
19
+ # @return [Hash<Symbol, Arel::Attributes::Attribute>] The attributes permitted for use in queries.
16
20
  #
17
- attr_reader :relation
21
+ attr_accessor :permitted_attributes
18
22
 
19
23
  # Creates a new predicate search instance.
20
24
  #
21
- # @param relation [ActiveRecord::Relation] The relation to which the predicate clauses are added.
22
- # @param permit_aggregate_modifiers [Boolean] Whether aggregate modifiers can be used or not.
23
- # @param block [Proc] The proc to invoke in the search instance (optional).
25
+ # @param default_table [Arel::Table] The default table to retrieve attributes from.
26
+ # @param permit_aggregate_modifiers [Boolean] `true` when aggregate modifiers can be used, `false` otherwise.
27
+ # @param block [Proc] The proc to invoke in the search instance (optional).
24
28
  #
25
- def initialize(relation, permit_aggregate_modifiers: false, &block)
26
- @relation = relation
29
+ def initialize(default_table: nil, permit_aggregate_modifiers: false, &block)
30
+ @default_table = default_table
27
31
  @permit_aggregate_modifiers = permit_aggregate_modifiers
28
32
  @permitted_attributes = HashWithIndifferentAccess.new
29
33
 
@@ -49,8 +53,8 @@ module Pursuit
49
53
  # @return [Arel::Attributes::Attribute] The underlying attribute to query.
50
54
  #
51
55
  def permit_attribute(name, attribute = nil)
52
- attribute = relation.klass.arel_table[attribute] if attribute.is_a?(Symbol)
53
- permitted_attributes[name] = attribute || relation.klass.arel_table[name]
56
+ attribute = default_table[attribute] if attribute.is_a?(Symbol)
57
+ permitted_attributes[name] = attribute || default_table[name]
54
58
  end
55
59
 
56
60
  # Parse a predicate query into ARel nodes.
@@ -59,22 +63,21 @@ module Pursuit
59
63
  # @return [Hash<Symbol, Arel::Nodes::Node>] The ARel nodes representing the predicate query.
60
64
  #
61
65
  def parse(query)
62
- tree = parser.parse(query)
63
66
  transform.apply(
64
- tree,
65
- permitted_attributes: permitted_attributes,
66
- permit_aggregate_modifiers: permit_aggregate_modifiers
67
+ parser.parse(query),
68
+ permit_aggregate_modifiers: permit_aggregate_modifiers,
69
+ permitted_attributes: permitted_attributes
67
70
  )
68
71
  end
69
72
 
70
- # Returns #relation filtered by the predicate query.
73
+ # Applies the predicate clauses derived from `query` to `relation`.
71
74
  #
72
- # @param query [String] The predicate query.
73
- # @return [ActiveRecord::Relation] The updated relation with the predicate clauses added.
75
+ # @param query [String] The predicate query.
76
+ # @param relation [ActiveRecord::Relation] The base relation to apply the predicate clauses to.
77
+ # @return [ActiveRecord::Relation] The base relation with the predicate clauses applied.
74
78
  #
75
- def apply(query)
79
+ def apply(query, relation)
76
80
  nodes = parse(query)
77
- relation = self.relation
78
81
  relation = relation.where(nodes[:where]) if nodes[:where]
79
82
  relation = relation.having(nodes[:having]) if nodes[:having]
80
83
  relation
@@ -1,25 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pursuit
4
- # :nodoc:
4
+ # Provides an interface for declaring which attributes should be searched in a simple query, and a method for applying
5
+ # a simple query to an `ActiveRecord::Relation` instance.
5
6
  #
6
7
  class SimpleSearch
7
8
  # @return [Set<Arel::Attributes::Attribute>] The attributes to match against.
8
9
  #
9
- attr_reader :attributes
10
+ attr_accessor :attributes
10
11
 
11
- # @return [ActiveRecord::Relation] The relation to which the clauses are added.
12
+ # @return [Arel::Table] The default table to retrieve attributes from.
12
13
  #
13
- attr_reader :relation
14
+ attr_accessor :default_table
14
15
 
15
16
  # Creates a new simple search instance.
16
17
  #
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).
18
+ # @param default_table [Arel::Table] The default table to retrieve attributes from.
19
+ # @param block [Proc] The proc to invoke in the search instance (optional).
19
20
  #
20
- def initialize(relation, &block)
21
+ def initialize(default_table: nil, &block)
21
22
  @attributes = Set.new
22
- @relation = relation
23
+ @default_table = default_table
23
24
 
24
25
  instance_eval(&block) if block
25
26
  end
@@ -30,7 +31,7 @@ module Pursuit
30
31
  # @return [Arel::Attributes::Attribute] The underlying attribute to query.
31
32
  #
32
33
  def search_attribute(attribute)
33
- attribute = relation.klass.arel_table[attribute] if attribute.is_a?(Symbol)
34
+ attribute = default_table[attribute] if attribute.is_a?(Symbol)
34
35
  attributes.add(attribute)
35
36
  end
36
37
 
@@ -51,12 +52,13 @@ module Pursuit
51
52
  end
52
53
  end
53
54
 
54
- # Returns #relation filtered by the query.
55
+ # Applies the simple clauses derived from `query` to `relation`.
55
56
  #
56
- # @param query [String] The simple query.
57
- # @return [ActiveRecord::Relation] The updated relation with the clauses added.
57
+ # @param query [String] The simple query.
58
+ # @param relation [ActiveRecord::Relation] The base relation to apply the simple clauses to.
59
+ # @return [ActiveRecord::Relation] The base relation with the simple clauses applied.
58
60
  #
59
- def apply(query)
61
+ def apply(query, relation)
60
62
  node = parse(query)
61
63
  node ? relation.where(node) : relation.none
62
64
  end
@@ -1,25 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pursuit
4
- # :nodoc:
4
+ # Provides an interface for declaring which attributes should be searched in a term query, and a method for applying
5
+ # a term query to an `ActiveRecord::Relation` instance.
6
+ #
7
+ # @see Pursuit::TermParser
8
+ # @see Pursuit::TermTransform
5
9
  #
6
10
  class TermSearch
7
11
  # @return [Set<Arel::Attributes::Attribute>] The attributes to match against.
8
12
  #
9
- attr_reader :attributes
13
+ attr_accessor :attributes
10
14
 
11
- # @return [ActiveRecord::Relation] The relation to which the term clauses are added.
15
+ # @return [Arel::Table] The default table to retrieve attributes from.
12
16
  #
13
- attr_reader :relation
17
+ attr_accessor :default_table
14
18
 
15
19
  # Creates a new term search instance.
16
20
  #
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).
21
+ # @param default_table [Arel::Table] The default table to retrieve attributes from.
22
+ # @param block [Proc] The proc to invoke in the search instance (optional).
19
23
  #
20
- def initialize(relation, &block)
24
+ def initialize(default_table: nil, &block)
21
25
  @attributes = Set.new
22
- @relation = relation
26
+ @default_table = default_table
23
27
 
24
28
  instance_eval(&block) if block
25
29
  end
@@ -42,7 +46,7 @@ module Pursuit
42
46
  # @return [Arel::Attributes::Attribute] The underlying attribute to query.
43
47
  #
44
48
  def search_attribute(attribute)
45
- attribute = relation.klass.arel_table[attribute] if attribute.is_a?(Symbol)
49
+ attribute = default_table[attribute] if attribute.is_a?(Symbol)
46
50
  attributes.add(attribute)
47
51
  end
48
52
 
@@ -56,12 +60,13 @@ module Pursuit
56
60
  transform.apply(tree, attributes: attributes)
57
61
  end
58
62
 
59
- # Returns #relation filtered by the term query.
63
+ # Applies the term clauses derived from `query` to `relation`.
60
64
  #
61
- # @param query [String] The term query.
62
- # @return [ActiveRecord::Relation] The updated relation with the term clauses added.
65
+ # @param query [String] The term query.
66
+ # @param relation [ActiveRecord::Relation] The base relation to apply the term clauses to.
67
+ # @return [ActiveRecord::Relation] The base relation with the term clauses applied.
63
68
  #
64
- def apply(query)
69
+ def apply(query, relation)
65
70
  node = parse(query)
66
71
  node ? relation.where(node) : relation.none
67
72
  end
@@ -1,38 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Product < ApplicationRecord
4
+ include ProductSearch
5
+
4
6
  belongs_to :category, class_name: 'ProductCategory', inverse_of: :products, optional: true
5
7
 
6
8
  has_many :variations, class_name: 'ProductVariation', inverse_of: :product
7
9
 
8
10
  validates :title, presence: true
9
-
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
23
-
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]
30
- end
31
- end
32
-
33
- def self.search(query)
34
- predicate_search.apply(query)
35
- rescue Parslet::ParseFailed
36
- term_search.apply(query)
37
- end
38
11
  end
@@ -1,29 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ProductCategory < ApplicationRecord
4
+ include ProductCategorySearch
5
+
4
6
  has_many :products, class_name: 'Product', foreign_key: :category_id, inverse_of: :category, dependent: :nullify
5
7
 
6
8
  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
29
9
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ProductVariation < ApplicationRecord
4
+ include ProductVariationSearch
5
+
4
6
  belongs_to :product
5
7
 
6
8
  enum stock_status: { in_stock: 1, low_stock: 2, out_of_stock: 3 }
@@ -9,29 +11,4 @@ class ProductVariation < ApplicationRecord
9
11
 
10
12
  validates :currency, presence: true
11
13
  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
37
14
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductCategorySearch
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ # Search for records matching the specified predicate.
8
+ #
9
+ # @param query [String] The query with a predicate.
10
+ # @return [ActiveRecord::Relation] The current relation filtered by the predicate.
11
+ #
12
+ def predicate_search(query)
13
+ @predicate_search ||= Pursuit::PredicateSearch.new(default_table: arel_table) do
14
+ permit_attribute :name
15
+ permit_attribute :product, Product.arel_table[:id]
16
+ permit_attribute :product_title, Product.arel_table[:title]
17
+ end
18
+
19
+ @predicate_search.apply(query, left_outer_joins(:products).group(:id))
20
+ end
21
+
22
+ # Search for records matching the specified terms.
23
+ #
24
+ # @param query [String] The query with one or more terms.
25
+ # @return [ActiveRecord::Relation] The current relation filtered by the terms.
26
+ #
27
+ def term_search(query)
28
+ @term_search ||= Pursuit::TermSearch.new(default_table: arel_table) do
29
+ search_attribute :name
30
+ end
31
+
32
+ # Note that we're using `all` here, but this still works when used in a chain:
33
+ # => ProductVariation.where(stock_status: :in_stock).search('Green')
34
+ @term_search.apply(query, all)
35
+ end
36
+
37
+ # Search for records matching the specified query.
38
+ #
39
+ # @param query [String] The query.
40
+ # @return [ActiveRecord::Relation] The current relation filtered by the query.
41
+ #
42
+ def search(query)
43
+ return none if query.blank?
44
+
45
+ predicate_search(query)
46
+ rescue Parslet::ParseFailed
47
+ term_search(query)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductSearch
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ # Search for records matching the specified predicate.
8
+ #
9
+ # @param query [String] The query with a predicate.
10
+ # @return [ActiveRecord::Relation] The current relation filtered by the predicate.
11
+ #
12
+ def predicate_search(query)
13
+ @predicate_search ||= Pursuit::PredicateSearch.new(default_table: arel_table) 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
+
23
+ @predicate_search.apply(query, left_outer_joins(:category, :variations).group(:id).order(:title))
24
+ end
25
+
26
+ # Search for records matching the specified terms.
27
+ #
28
+ # @param query [String] The query with one or more terms.
29
+ # @return [ActiveRecord::Relation] The current relation filtered by the terms.
30
+ #
31
+ def term_search(query)
32
+ @term_search ||= Pursuit::TermSearch.new(default_table: arel_table) do
33
+ search_attribute :title
34
+ search_attribute ProductCategory.arel_table[:name]
35
+ end
36
+
37
+ @term_search.apply(query, left_outer_joins(:category).group(:id).order(:title))
38
+ end
39
+
40
+ # Search for records matching the specified query.
41
+ #
42
+ # @param query [String] The query.
43
+ # @return [ActiveRecord::Relation] The current relation filtered by the query.
44
+ #
45
+ def search(query)
46
+ return none if query.blank?
47
+
48
+ predicate_search(query)
49
+ rescue Parslet::ParseFailed
50
+ term_search(query)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductVariationSearch
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ # Search for records matching the specified predicate.
8
+ #
9
+ # @param query [String] The query with a predicate.
10
+ # @return [ActiveRecord::Relation] The current relation filtered by the predicate.
11
+ #
12
+ def predicate_search(query)
13
+ @predicate_search ||= Pursuit::PredicateSearch.new(default_table: arel_table) do
14
+ permit_attribute :title
15
+ permit_attribute :stock_status
16
+ permit_attribute :currency
17
+ permit_attribute :amount
18
+ permit_attribute :product, Product.arel_table[:id]
19
+ permit_attribute :product_title, Product.arel_table[:title]
20
+ end
21
+
22
+ @predicate_search.apply(query, left_outer_joins(:product).group(:id))
23
+ end
24
+
25
+ # Search for records matching the specified terms.
26
+ #
27
+ # @param query [String] The query with one or more terms.
28
+ # @return [ActiveRecord::Relation] The current relation filtered by the terms.
29
+ #
30
+ def term_search(query)
31
+ @term_search ||= Pursuit::TermSearch.new(default_table: arel_table) do
32
+ search_attribute :title
33
+ end
34
+
35
+ # Note that we're using `all` here, but this still works when used in a chain:
36
+ # => ProductVariation.where(stock_status: :in_stock).search('Green')
37
+ @term_search.apply(query, all)
38
+ end
39
+
40
+ # Search for records matching the specified query.
41
+ #
42
+ # @param query [String] The query.
43
+ # @return [ActiveRecord::Relation] The current relation filtered by the query.
44
+ #
45
+ def search(query)
46
+ return none if query.blank?
47
+
48
+ predicate_search(query)
49
+ rescue Parslet::ParseFailed
50
+ term_search(query)
51
+ end
52
+ end
53
+ end
@@ -2,27 +2,66 @@
2
2
 
3
3
  RSpec.describe Pursuit::PredicateSearch do
4
4
  subject(:predicate_search) do
5
- described_class.new(
6
- Product.left_outer_joins(:variations).group(:id),
7
- permit_aggregate_modifiers: true
8
- ) do
9
- permit_attribute :title
10
- permit_attribute :variation, ProductVariation.arel_table[:id]
11
- permit_attribute :variation_title, ProductVariation.arel_table[:title]
12
- end
5
+ described_class.new(default_table: Product.arel_table, permit_aggregate_modifiers: true)
13
6
  end
14
7
 
15
8
  describe '#initialize' do
16
- it 'is expected to set #relation eq `relation`' do
17
- expect(predicate_search).to have_attributes(relation: Product.left_outer_joins(:variations).group(:id))
9
+ it 'is expected to set #default_table eq `default_table`' do
10
+ expect(predicate_search).to have_attributes(default_table: Product.arel_table)
18
11
  end
19
12
 
20
13
  it 'is expected to set #permit_aggregate_modifiers eq `permit_aggregate_modifiers`' do
21
14
  expect(predicate_search).to have_attributes(permit_aggregate_modifiers: true)
22
15
  end
23
16
 
24
- it 'is expected to evaluate the passed block' do
25
- expect(predicate_search.permitted_attributes).to be_present
17
+ it 'is expected to invoke the passed block' do
18
+ expect { |block| described_class.new(&block) }.to yield_control
19
+ end
20
+ end
21
+
22
+ describe '#permit_attribute' do
23
+ subject(:permit_attribute) { predicate_search.permit_attribute(name, attribute) }
24
+
25
+ context 'when `attribute` is nil' do
26
+ let(:name) { :title }
27
+ let(:attribute) { nil }
28
+
29
+ it 'is expected to add the attribute from #default_table to #permitted_attributes' do
30
+ permit_attribute
31
+ expect(predicate_search.permitted_attributes).to match(
32
+ hash_including(
33
+ title: Product.arel_table[:title]
34
+ )
35
+ )
36
+ end
37
+ end
38
+
39
+ context 'when `attribute` is a Symbol' do
40
+ let(:name) { :name }
41
+ let(:attribute) { :title }
42
+
43
+ it 'is expected to add the attribute from #default_table to #permitted_attributes' do
44
+ permit_attribute
45
+ expect(predicate_search.permitted_attributes).to match(
46
+ hash_including(
47
+ name: Product.arel_table[:title]
48
+ )
49
+ )
50
+ end
51
+ end
52
+
53
+ context 'when `attribute` is an Arel::Attributes::Attribute' do
54
+ let(:name) { :variation_currency }
55
+ let(:attribute) { ProductVariation.arel_table[:currency] }
56
+
57
+ it 'is expected to add the attribute to #permitted_attributes' do
58
+ permit_attribute
59
+ expect(predicate_search.permitted_attributes).to match(
60
+ hash_including(
61
+ variation_currency: ProductVariation.arel_table[:currency]
62
+ )
63
+ )
64
+ end
26
65
  end
27
66
  end
28
67
 
@@ -38,24 +77,15 @@ RSpec.describe Pursuit::PredicateSearch do
38
77
  it { is_expected.to be_a(Pursuit::PredicateTransform) }
39
78
  end
40
79
 
41
- describe '#permit_attribute' do
42
- subject(:permit_attribute) do
43
- predicate_search.permit_attribute(:variation_currency, ProductVariation.arel_table[:currency])
44
- end
45
-
46
- it 'is expected to add the attribute to #permitted_attributes' do
47
- permit_attribute
48
- expect(predicate_search.permitted_attributes).to match(
49
- hash_including(
50
- variation_currency: ProductVariation.arel_table[:currency]
51
- )
52
- )
53
- end
54
- end
55
-
56
80
  describe '#parse' do
57
81
  subject(:parse) { predicate_search.parse('title ~ Shirt & #variation > 0') }
58
82
 
83
+ before do
84
+ predicate_search.permitted_attributes[:title] = Product.arel_table[:title]
85
+ predicate_search.permitted_attributes[:variation] = ProductVariation.arel_table[:id]
86
+ predicate_search.permitted_attributes[:variation_title] = ProductVariation.arel_table[:title]
87
+ end
88
+
59
89
  it 'is expected to equal a Hash containing the ARel nodes' do
60
90
  expect(parse).to eq(
61
91
  {
@@ -67,9 +97,15 @@ RSpec.describe Pursuit::PredicateSearch do
67
97
  end
68
98
 
69
99
  describe '#apply' do
70
- subject(:apply) { predicate_search.apply('title ~ Shirt') }
100
+ subject(:apply) { predicate_search.apply('title ~ Shirt', Product.left_outer_joins(:variations).group(:id)) }
101
+
102
+ before do
103
+ predicate_search.permitted_attributes[:title] = Product.arel_table[:title]
104
+ predicate_search.permitted_attributes[:variation] = ProductVariation.arel_table[:id]
105
+ predicate_search.permitted_attributes[:variation_title] = ProductVariation.arel_table[:title]
106
+ end
71
107
 
72
- it 'is expected to equal #relation with predicate clauses applied' do
108
+ it 'is expected to equal `relation` with predicate clauses applied' do
73
109
  expect(apply).to eq(
74
110
  Product.left_outer_joins(:variations).group(:id).where(
75
111
  Product.arel_table[:title].matches('%Shirt%')
@@ -1,39 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RSpec.describe Pursuit::SimpleSearch do
4
- subject(:simple_search) do
5
- described_class.new(
6
- Product.left_outer_joins(:variations).group(:id)
7
- ) do
8
- search_attribute :title
9
- search_attribute ProductVariation.arel_table[:title]
10
- end
11
- end
4
+ subject(:simple_search) { described_class.new(default_table: Product.arel_table) }
12
5
 
13
6
  describe '#initialize' do
14
- it 'is expected to set #relation eq `relation`' do
15
- expect(simple_search).to have_attributes(relation: Product.left_outer_joins(:variations).group(:id))
7
+ it 'is expected to set #default_table eq `default_table`' do
8
+ expect(simple_search).to have_attributes(default_table: Product.arel_table)
16
9
  end
17
10
 
18
- it 'is expected to evaluate the passed block' do
19
- expect(simple_search.attributes).to be_present
11
+ it 'is expected to invoke the passed block' do
12
+ expect { |block| described_class.new(&block) }.to yield_control
20
13
  end
21
14
  end
22
15
 
23
16
  describe '#search_attribute' do
24
- subject(:search_attribute) do
25
- simple_search.search_attribute(ProductVariation.arel_table[:currency])
17
+ subject(:search_attribute) { simple_search.search_attribute(attribute) }
18
+
19
+ context 'when `attribute` is a Symbol' do
20
+ let(:attribute) { :title }
21
+
22
+ it 'is expected to add the attribute from #default_table to #attributes' do
23
+ search_attribute
24
+ expect(simple_search.attributes).to include(Product.arel_table[:title])
25
+ end
26
26
  end
27
27
 
28
- it 'is expected to add the attribute to #attributes' do
29
- search_attribute
30
- expect(simple_search.attributes).to include(ProductVariation.arel_table[:currency])
28
+ context 'when `attribute` is an Arel::Attributes::Attribute' do
29
+ let(:attribute) { ProductVariation.arel_table[:currency] }
30
+
31
+ it 'is expected to add the attribute to #attributes' do
32
+ search_attribute
33
+ expect(simple_search.attributes).to include(ProductVariation.arel_table[:currency])
34
+ end
31
35
  end
32
36
  end
33
37
 
34
38
  describe '#parse' do
35
39
  subject(:parse) { simple_search.parse('Shirt') }
36
40
 
41
+ before do
42
+ simple_search.attributes << Product.arel_table[:title]
43
+ simple_search.attributes << ProductVariation.arel_table[:title]
44
+ end
45
+
37
46
  it 'is expected to equal the ARel node' do
38
47
  expect(parse).to eq(
39
48
  Product.arel_table[:title].matches('%Shirt%').or(
@@ -44,9 +53,14 @@ RSpec.describe Pursuit::SimpleSearch do
44
53
  end
45
54
 
46
55
  describe '#apply' do
47
- subject(:apply) { simple_search.apply('Shirt') }
56
+ subject(:apply) { simple_search.apply('Shirt', Product.left_outer_joins(:variations).group(:id)) }
57
+
58
+ before do
59
+ simple_search.attributes << Product.arel_table[:title]
60
+ simple_search.attributes << ProductVariation.arel_table[:title]
61
+ end
48
62
 
49
- it 'is expected to equal #relation with clauses applied' do
63
+ it 'is expected to equal `relation` with simple clauses applied' do
50
64
  expect(apply).to eq(
51
65
  Product.left_outer_joins(:variations).group(:id).where(
52
66
  Product.arel_table[:title].matches('%Shirt%').or(
@@ -1,22 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RSpec.describe Pursuit::TermSearch do
4
- subject(:term_search) do
5
- described_class.new(
6
- Product.left_outer_joins(:variations).group(:id)
7
- ) do
8
- search_attribute :title
9
- search_attribute ProductVariation.arel_table[:title]
10
- end
11
- end
4
+ subject(:term_search) { described_class.new(default_table: Product.arel_table) }
12
5
 
13
6
  describe '#initialize' do
14
- it 'is expected to set #relation eq `relation`' do
15
- expect(term_search).to have_attributes(relation: Product.left_outer_joins(:variations).group(:id))
7
+ it 'is expected to set #default_table eq `default_table`' do
8
+ expect(term_search).to have_attributes(default_table: Product.arel_table)
16
9
  end
17
10
 
18
- it 'is expected to evaluate the passed block' do
19
- expect(term_search.attributes).to be_present
11
+ it 'is expected to invoke the passed block' do
12
+ expect { |block| described_class.new(&block) }.to yield_control
20
13
  end
21
14
  end
22
15
 
@@ -33,19 +26,35 @@ RSpec.describe Pursuit::TermSearch do
33
26
  end
34
27
 
35
28
  describe '#search_attribute' do
36
- subject(:search_attribute) do
37
- term_search.search_attribute(ProductVariation.arel_table[:currency])
29
+ subject(:search_attribute) { term_search.search_attribute(attribute) }
30
+
31
+ context 'when `attribute` is a Symbol' do
32
+ let(:attribute) { :title }
33
+
34
+ it 'is expected to add the attribute from #default_table to #attributes' do
35
+ search_attribute
36
+ expect(term_search.attributes).to include(Product.arel_table[:title])
37
+ end
38
38
  end
39
39
 
40
- it 'is expected to add the attribute to #attributes' do
41
- search_attribute
42
- expect(term_search.attributes).to include(ProductVariation.arel_table[:currency])
40
+ context 'when `attribute` is an Arel::Attributes::Attribute' do
41
+ let(:attribute) { ProductVariation.arel_table[:currency] }
42
+
43
+ it 'is expected to add the attribute to #attributes' do
44
+ search_attribute
45
+ expect(term_search.attributes).to include(ProductVariation.arel_table[:currency])
46
+ end
43
47
  end
44
48
  end
45
49
 
46
50
  describe '#parse' do
47
51
  subject(:parse) { term_search.parse('Shirt') }
48
52
 
53
+ before do
54
+ term_search.attributes << Product.arel_table[:title]
55
+ term_search.attributes << ProductVariation.arel_table[:title]
56
+ end
57
+
49
58
  it 'is expected to equal the ARel node' do
50
59
  expect(parse).to eq(
51
60
  Product.arel_table[:title].matches('%Shirt%').or(
@@ -56,9 +65,14 @@ RSpec.describe Pursuit::TermSearch do
56
65
  end
57
66
 
58
67
  describe '#apply' do
59
- subject(:apply) { term_search.apply('Shirt') }
68
+ subject(:apply) { term_search.apply('Shirt', Product.left_outer_joins(:variations).group(:id)) }
69
+
70
+ before do
71
+ term_search.attributes << Product.arel_table[:title]
72
+ term_search.attributes << ProductVariation.arel_table[:title]
73
+ end
60
74
 
61
- it 'is expected to equal #relation with term clauses applied' do
75
+ it 'is expected to equal `relation` with term clauses applied' do
62
76
  expect(apply).to eq(
63
77
  Product.left_outer_joins(:variations).group(:id).where(
64
78
  Product.arel_table[:title].matches('%Shirt%').or(
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pursuit
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nialto Services
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-02 00:00:00.000000000 Z
11
+ date: 2023-11-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -106,6 +106,9 @@ files:
106
106
  - spec/internal/app/models/product.rb
107
107
  - spec/internal/app/models/product_category.rb
108
108
  - spec/internal/app/models/product_variation.rb
109
+ - spec/internal/app/searches/product_category_search.rb
110
+ - spec/internal/app/searches/product_search.rb
111
+ - spec/internal/app/searches/product_variation_search.rb
109
112
  - spec/internal/config/database.yml
110
113
  - spec/internal/db/schema.rb
111
114
  - spec/internal/log/.keep