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 +4 -4
- data/README.md +18 -18
- data/lib/pursuit/constants.rb +1 -1
- data/lib/pursuit/predicate_parser.rb +4 -4
- data/lib/pursuit/predicate_search.rb +26 -23
- data/lib/pursuit/simple_search.rb +15 -13
- data/lib/pursuit/term_search.rb +18 -13
- data/spec/internal/app/models/product.rb +2 -29
- data/spec/internal/app/models/product_category.rb +2 -22
- data/spec/internal/app/models/product_variation.rb +2 -25
- data/spec/internal/app/searches/product_category_search.rb +50 -0
- data/spec/internal/app/searches/product_search.rb +53 -0
- data/spec/internal/app/searches/product_variation_search.rb +53 -0
- data/spec/lib/pursuit/predicate_search_spec.rb +65 -29
- data/spec/lib/pursuit/simple_search_spec.rb +33 -19
- data/spec/lib/pursuit/term_search_spec.rb +33 -19
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 70e763ac9fa96f98268f46e1824852e5908874fd7b667a8e5e8df5049ad14cd1
|
4
|
+
data.tar.gz: a375cf25c07d547f4bf8321796de8537881a05c740a198269ab6b4dac6c90ad9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
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.
|
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(
|
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(
|
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:
|
data/lib/pursuit/constants.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Pursuit
|
4
|
-
# Parser for predicate
|
4
|
+
# Parser for predicate queries.
|
5
5
|
#
|
6
|
-
# Predicate
|
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
|
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
|
-
#
|
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 [
|
11
|
+
# @return [Arel::Table] The default table to retrieve attributes from.
|
8
12
|
#
|
9
|
-
attr_accessor :
|
13
|
+
attr_accessor :default_table
|
10
14
|
|
11
|
-
# @return [
|
15
|
+
# @return [Boolean] `true` when aggregate modifiers can be used, `false` otherwise.
|
12
16
|
#
|
13
|
-
|
17
|
+
attr_accessor :permit_aggregate_modifiers
|
14
18
|
|
15
|
-
# @return [
|
19
|
+
# @return [Hash<Symbol, Arel::Attributes::Attribute>] The attributes permitted for use in queries.
|
16
20
|
#
|
17
|
-
|
21
|
+
attr_accessor :permitted_attributes
|
18
22
|
|
19
23
|
# Creates a new predicate search instance.
|
20
24
|
#
|
21
|
-
# @param
|
22
|
-
# @param permit_aggregate_modifiers [Boolean]
|
23
|
-
# @param block [Proc]
|
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(
|
26
|
-
@
|
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 =
|
53
|
-
permitted_attributes[name] = attribute ||
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
+
parser.parse(query),
|
68
|
+
permit_aggregate_modifiers: permit_aggregate_modifiers,
|
69
|
+
permitted_attributes: permitted_attributes
|
67
70
|
)
|
68
71
|
end
|
69
72
|
|
70
|
-
#
|
73
|
+
# Applies the predicate clauses derived from `query` to `relation`.
|
71
74
|
#
|
72
|
-
# @param query
|
73
|
-
# @
|
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
|
-
#
|
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
|
-
|
10
|
+
attr_accessor :attributes
|
10
11
|
|
11
|
-
# @return [
|
12
|
+
# @return [Arel::Table] The default table to retrieve attributes from.
|
12
13
|
#
|
13
|
-
|
14
|
+
attr_accessor :default_table
|
14
15
|
|
15
16
|
# Creates a new simple search instance.
|
16
17
|
#
|
17
|
-
# @param
|
18
|
-
# @param block
|
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(
|
21
|
+
def initialize(default_table: nil, &block)
|
21
22
|
@attributes = Set.new
|
22
|
-
@
|
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 =
|
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
|
-
#
|
55
|
+
# Applies the simple clauses derived from `query` to `relation`.
|
55
56
|
#
|
56
|
-
# @param query
|
57
|
-
# @
|
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
|
data/lib/pursuit/term_search.rb
CHANGED
@@ -1,25 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Pursuit
|
4
|
-
#
|
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
|
-
|
13
|
+
attr_accessor :attributes
|
10
14
|
|
11
|
-
# @return [
|
15
|
+
# @return [Arel::Table] The default table to retrieve attributes from.
|
12
16
|
#
|
13
|
-
|
17
|
+
attr_accessor :default_table
|
14
18
|
|
15
19
|
# Creates a new term search instance.
|
16
20
|
#
|
17
|
-
# @param
|
18
|
-
# @param block
|
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(
|
24
|
+
def initialize(default_table: nil, &block)
|
21
25
|
@attributes = Set.new
|
22
|
-
@
|
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 =
|
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
|
-
#
|
63
|
+
# Applies the term clauses derived from `query` to `relation`.
|
60
64
|
#
|
61
|
-
# @param query
|
62
|
-
# @
|
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 #
|
17
|
-
expect(predicate_search).to have_attributes(
|
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
|
25
|
-
expect(
|
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
|
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)
|
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 #
|
15
|
-
expect(simple_search).to have_attributes(
|
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
|
19
|
-
expect(
|
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)
|
25
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
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
|
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)
|
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 #
|
15
|
-
expect(term_search).to have_attributes(
|
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
|
19
|
-
expect(
|
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)
|
37
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
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
|
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
|
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-
|
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
|