pursuit 1.0.1 → 1.1.0

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.
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