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