pursuit 0.4.3 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rubygem.yaml +46 -0
  3. data/.ruby-version +1 -1
  4. data/Gemfile +15 -0
  5. data/Gemfile.lock +127 -86
  6. data/LICENSE +174 -21
  7. data/README.md +210 -27
  8. data/bin/console +10 -0
  9. data/config.ru +2 -3
  10. data/lib/pursuit/aggregate_modifier_not_found.rb +20 -0
  11. data/lib/pursuit/aggregate_modifier_required.rb +20 -0
  12. data/lib/pursuit/aggregate_modifiers_not_available.rb +13 -0
  13. data/lib/pursuit/attribute_not_found.rb +20 -0
  14. data/lib/pursuit/constants.rb +1 -1
  15. data/lib/pursuit/error.rb +7 -0
  16. data/lib/pursuit/predicate_parser.rb +181 -0
  17. data/lib/pursuit/predicate_search.rb +83 -0
  18. data/lib/pursuit/predicate_transform.rb +231 -0
  19. data/lib/pursuit/query_error.rb +7 -0
  20. data/lib/pursuit/simple_search.rb +64 -0
  21. data/lib/pursuit/term_parser.rb +44 -0
  22. data/lib/pursuit/term_search.rb +69 -0
  23. data/lib/pursuit/term_transform.rb +35 -0
  24. data/lib/pursuit.rb +19 -5
  25. data/pursuit.gemspec +5 -18
  26. data/spec/internal/app/models/application_record.rb +5 -0
  27. data/spec/internal/app/models/product.rb +25 -9
  28. data/spec/internal/app/models/product_category.rb +23 -1
  29. data/spec/internal/app/models/product_variation.rb +26 -1
  30. data/spec/lib/pursuit/predicate_parser_spec.rb +1604 -0
  31. data/spec/lib/pursuit/predicate_search_spec.rb +80 -0
  32. data/spec/lib/pursuit/predicate_transform_spec.rb +624 -0
  33. data/spec/lib/pursuit/simple_search_spec.rb +59 -0
  34. data/spec/lib/pursuit/term_parser_spec.rb +271 -0
  35. data/spec/lib/pursuit/term_search_spec.rb +71 -0
  36. data/spec/lib/pursuit/term_transform_spec.rb +105 -0
  37. data/spec/spec_helper.rb +2 -3
  38. data/travis/gemfiles/{5.2.gemfile → 7.1.gemfile} +2 -2
  39. metadata +38 -197
  40. data/.travis.yml +0 -25
  41. data/lib/pursuit/dsl.rb +0 -28
  42. data/lib/pursuit/railtie.rb +0 -13
  43. data/lib/pursuit/search.rb +0 -172
  44. data/lib/pursuit/search_options.rb +0 -86
  45. data/lib/pursuit/search_term_parser.rb +0 -46
  46. data/spec/lib/pursuit/dsl_spec.rb +0 -22
  47. data/spec/lib/pursuit/search_options_spec.rb +0 -146
  48. data/spec/lib/pursuit/search_spec.rb +0 -516
  49. data/spec/lib/pursuit/search_term_parser_spec.rb +0 -34
  50. data/travis/gemfiles/6.0.gemfile +0 -8
  51. data/travis/gemfiles/6.1.gemfile +0 -8
  52. data/travis/gemfiles/7.0.gemfile +0 -8
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
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
12
+
13
+ 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))
16
+ end
17
+
18
+ it 'is expected to evaluate the passed block' do
19
+ expect(simple_search.attributes).to be_present
20
+ end
21
+ end
22
+
23
+ describe '#search_attribute' do
24
+ subject(:search_attribute) do
25
+ simple_search.search_attribute(ProductVariation.arel_table[:currency])
26
+ end
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])
31
+ end
32
+ end
33
+
34
+ describe '#parse' do
35
+ subject(:parse) { simple_search.parse('Shirt') }
36
+
37
+ it 'is expected to equal the ARel node' do
38
+ expect(parse).to eq(
39
+ Product.arel_table[:title].matches('%Shirt%').or(
40
+ ProductVariation.arel_table[:title].matches('%Shirt%')
41
+ )
42
+ )
43
+ end
44
+ end
45
+
46
+ describe '#apply' do
47
+ subject(:apply) { simple_search.apply('Shirt') }
48
+
49
+ it 'is expected to equal #relation with clauses applied' do
50
+ expect(apply).to eq(
51
+ Product.left_outer_joins(:variations).group(:id).where(
52
+ Product.arel_table[:title].matches('%Shirt%').or(
53
+ ProductVariation.arel_table[:title].matches('%Shirt%')
54
+ )
55
+ )
56
+ )
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,271 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Pursuit::TermParser do
4
+ subject(:parser) { described_class.new }
5
+
6
+ describe '#space' do
7
+ subject(:space) { parser.space }
8
+
9
+ context 'when parsing an empty value' do
10
+ subject(:parse) { space.parse('') }
11
+
12
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
13
+ end
14
+
15
+ context 'when parsing one or more spaces' do
16
+ subject(:parse) { space.parse(value) }
17
+
18
+ let(:value) { ' ' * rand(1..10) }
19
+
20
+ it 'is expected to eq the parsed spaces' do
21
+ expect(parse).to eq(value)
22
+ end
23
+ end
24
+ end
25
+
26
+ describe '#space?' do
27
+ subject(:space?) { parser.space? }
28
+
29
+ context 'when parsing an empty value' do
30
+ subject(:parse) { space?.parse('') }
31
+
32
+ it 'is expected to eq the empty value' do
33
+ expect(parse).to eq('')
34
+ end
35
+ end
36
+
37
+ context 'when parsing one or more spaces' do
38
+ subject(:parse) { space?.parse(value) }
39
+
40
+ let(:value) { ' ' * rand(1..10) }
41
+
42
+ it 'is expected to eq the parsed spaces' do
43
+ expect(parse).to eq(value)
44
+ end
45
+ end
46
+ end
47
+
48
+ describe '#escaped_character' do
49
+ subject(:escaped_character) { parser.escaped_character }
50
+
51
+ context 'when parsing an escaped character' do
52
+ subject(:parse) { escaped_character.parse('\\"') }
53
+
54
+ it 'is expected to eq the escaped character' do
55
+ expect(parse).to eq('\\"')
56
+ end
57
+ end
58
+
59
+ context 'when parsing an unescaped character' do
60
+ subject(:parse) { escaped_character.parse('"') }
61
+
62
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
63
+ end
64
+ end
65
+
66
+ describe '#string_double_quotes' do
67
+ subject(:string_double_quotes) { parser.string_double_quotes }
68
+
69
+ context 'when parsing a double quoted string' do
70
+ subject(:parse) { string_double_quotes.parse('"Double \"Quoted\""') }
71
+
72
+ it 'is expected to capture the contents of the double quotes as :string_double_quotes' do
73
+ expect(parse).to eq(string_double_quotes: 'Double \"Quoted\"')
74
+ end
75
+ end
76
+
77
+ context 'when parsing a single quoted string' do
78
+ subject(:parse) { string_double_quotes.parse("'Single Quoted'") }
79
+
80
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
81
+ end
82
+
83
+ context 'when parsing an unquoted string' do
84
+ subject(:parse) { string_double_quotes.parse('Unquoted') }
85
+
86
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
87
+ end
88
+ end
89
+
90
+ describe '#string_single_quotes' do
91
+ subject(:string_single_quotes) { parser.string_single_quotes }
92
+
93
+ context 'when parsing a single quoted string' do
94
+ subject(:parse) { string_single_quotes.parse("'Single \\'Quoted\\''") }
95
+
96
+ it 'is expected to capture the contents of the single quotes as :string_single_quotes' do
97
+ expect(parse).to eq(string_single_quotes: "Single \\'Quoted\\'")
98
+ end
99
+ end
100
+
101
+ context 'when parsing a double quoted string' do
102
+ subject(:parse) { string_single_quotes.parse('"Double Quoted"') }
103
+
104
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
105
+ end
106
+
107
+ context 'when parsing an unquoted string' do
108
+ subject(:parse) { string_single_quotes.parse('Unquoted') }
109
+
110
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
111
+ end
112
+ end
113
+
114
+ describe '#string_no_quotes' do
115
+ subject(:string_no_quotes) { parser.string_no_quotes }
116
+
117
+ context 'when parsing an unquoted string with no spaces or symbols' do
118
+ subject(:parse) { string_no_quotes.parse('Unquoted') }
119
+
120
+ it 'is expected to eq the parsed value' do
121
+ expect(parse).to eq(string_no_quotes: 'Unquoted')
122
+ end
123
+ end
124
+
125
+ context 'when parsing an unquoted string with spaces' do
126
+ subject(:parse) { string_no_quotes.parse('No Quotes') }
127
+
128
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
129
+ end
130
+
131
+ context 'when parsing an unquoted string with symbols' do
132
+ subject(:parse) { string_no_quotes.parse('No:Quotes') }
133
+
134
+ it 'is expected to eq the parsed value' do
135
+ expect(parse).to eq(string_no_quotes: 'No:Quotes')
136
+ end
137
+ end
138
+ end
139
+
140
+ describe '#string' do
141
+ subject(:string) { parser.string }
142
+
143
+ context 'when parsing a double quoted string' do
144
+ subject(:parse) { string.parse('"Double \"Quoted\""') }
145
+
146
+ it 'is expected to capture the contents of the double quotes as :string_double_quotes' do
147
+ expect(parse).to eq(string_double_quotes: 'Double \"Quoted\"')
148
+ end
149
+ end
150
+
151
+ context 'when parsing a single quoted string' do
152
+ subject(:parse) { string.parse("'Single \\'Quoted\\''") }
153
+
154
+ it 'is expected to capture the contents of the single quotes as :string_single_quotes' do
155
+ expect(parse).to eq(string_single_quotes: "Single \\'Quoted\\'")
156
+ end
157
+ end
158
+
159
+ context 'when parsing an unquoted string' do
160
+ subject(:parse) { string.parse('Unquoted') }
161
+
162
+ it 'is expected to eq the parsed value' do
163
+ expect(parse).to eq(string_no_quotes: 'Unquoted')
164
+ end
165
+ end
166
+
167
+ context 'when parsing an unquoted string with spaces' do
168
+ subject(:parse) { string.parse('No Quotes') }
169
+
170
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
171
+ end
172
+
173
+ context 'when parsing an unquoted string with symbols' do
174
+ subject(:parse) { string.parse('a@b.c') }
175
+
176
+ it 'is expected to eq the parsed value' do
177
+ expect(parse).to eq(string_no_quotes: 'a@b.c')
178
+ end
179
+ end
180
+ end
181
+
182
+ describe '#term' do
183
+ subject(:term) { parser.term }
184
+
185
+ context 'when parsing a string' do
186
+ subject(:parse) { term.parse('Hello') }
187
+
188
+ it 'is expected to capture the value as :term' do
189
+ expect(parse).to match(hash_including(term: { string_no_quotes: 'Hello' }))
190
+ end
191
+ end
192
+ end
193
+
194
+ describe '#term_pair' do
195
+ subject(:term_pair) { parser.term_pair }
196
+
197
+ context 'when parsing a term' do
198
+ subject(:parse) { term_pair.parse('Hello') }
199
+
200
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
201
+ end
202
+
203
+ context 'when parsing two terms separated by a space' do
204
+ subject(:parse) { term_pair.parse('Hello World') }
205
+
206
+ it 'is expected to capture the first term as :left' do
207
+ expect(parse).to match(hash_including(left: { term: { string_no_quotes: 'Hello' } }))
208
+ end
209
+
210
+ it 'is expected to capture the second term as :right' do
211
+ expect(parse).to match(hash_including(right: { term: { string_no_quotes: 'World' } }))
212
+ end
213
+ end
214
+ end
215
+
216
+ describe '#term_node' do
217
+ subject(:term_node) { parser.term_node }
218
+
219
+ context 'when parsing a term' do
220
+ subject(:parse) { term_node.parse('Hello') }
221
+
222
+ it 'is expected to eq the term result' do
223
+ expect(parse).to match(hash_including(term: { string_no_quotes: 'Hello' }))
224
+ end
225
+ end
226
+
227
+ context 'when parsing multiple terms' do
228
+ subject(:parse) { term_node.parse('Hello World') }
229
+
230
+ it 'is expected to eq the term list result' do
231
+ expect(parse).to match(
232
+ hash_including(
233
+ left: { term: { string_no_quotes: 'Hello' } },
234
+ right: { term: { string_no_quotes: 'World' } }
235
+ )
236
+ )
237
+ end
238
+ end
239
+ end
240
+
241
+ describe '#terms' do
242
+ subject(:terms) { parser.terms }
243
+
244
+ context 'when parsing a complex list of terms' do
245
+ subject(:parse) { terms.parse(" FirstTerm \"Second Term\" Third<>Term 'Fourth Term' ") }
246
+
247
+ it 'is expected to eq the correct term tree' do
248
+ expect(parse).to match(
249
+ hash_including(
250
+ left: { term: { string_no_quotes: 'FirstTerm' } },
251
+ right: {
252
+ left: { term: { string_double_quotes: 'Second Term' } },
253
+ right: {
254
+ left: { term: { string_no_quotes: 'Third<>Term' } },
255
+ right: { term: { string_single_quotes: 'Fourth Term' } }
256
+ }
257
+ }
258
+ )
259
+ )
260
+ end
261
+ end
262
+ end
263
+
264
+ describe '#root' do
265
+ subject(:root) { parser.root }
266
+
267
+ it 'is expected to eq #terms' do
268
+ expect(root).to eq(parser.terms)
269
+ end
270
+ end
271
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
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
12
+
13
+ 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))
16
+ end
17
+
18
+ it 'is expected to evaluate the passed block' do
19
+ expect(term_search.attributes).to be_present
20
+ end
21
+ end
22
+
23
+ describe '#parser' do
24
+ subject(:parser) { term_search.parser }
25
+
26
+ it { is_expected.to be_a(Pursuit::TermParser) }
27
+ end
28
+
29
+ describe '#transform' do
30
+ subject(:transform) { term_search.transform }
31
+
32
+ it { is_expected.to be_a(Pursuit::TermTransform) }
33
+ end
34
+
35
+ describe '#search_attribute' do
36
+ subject(:search_attribute) do
37
+ term_search.search_attribute(ProductVariation.arel_table[:currency])
38
+ end
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])
43
+ end
44
+ end
45
+
46
+ describe '#parse' do
47
+ subject(:parse) { term_search.parse('Shirt') }
48
+
49
+ it 'is expected to equal the ARel node' do
50
+ expect(parse).to eq(
51
+ Product.arel_table[:title].matches('%Shirt%').or(
52
+ ProductVariation.arel_table[:title].matches('%Shirt%')
53
+ )
54
+ )
55
+ end
56
+ end
57
+
58
+ describe '#apply' do
59
+ subject(:apply) { term_search.apply('Shirt') }
60
+
61
+ it 'is expected to equal #relation with term clauses applied' do
62
+ expect(apply).to eq(
63
+ Product.left_outer_joins(:variations).group(:id).where(
64
+ Product.arel_table[:title].matches('%Shirt%').or(
65
+ ProductVariation.arel_table[:title].matches('%Shirt%')
66
+ )
67
+ )
68
+ )
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Pursuit::TermTransform do
4
+ subject(:transform) { described_class.new }
5
+
6
+ describe '#apply' do
7
+ subject(:apply) { transform.apply(tree, attributes: attributes) }
8
+
9
+ let(:attributes) do
10
+ [
11
+ Product.arel_table[:title],
12
+ ProductVariation.arel_table[:title]
13
+ ].to_set
14
+ end
15
+
16
+ context 'when passed an empty double quoted string' do
17
+ let(:tree) do
18
+ { string_double_quotes: [] }
19
+ end
20
+
21
+ it 'is expected to equal an empty string' do
22
+ expect(apply).to eq('')
23
+ end
24
+ end
25
+
26
+ context 'when passed a double quoted string' do
27
+ let(:tree) do
28
+ { string_double_quotes: 'Hello \\"World\\"' }
29
+ end
30
+
31
+ it 'is expected to equal the correct string' do
32
+ expect(apply).to eq('Hello "World"')
33
+ end
34
+ end
35
+
36
+ context 'when passed an empty single quoted string' do
37
+ let(:tree) do
38
+ { string_single_quotes: [] }
39
+ end
40
+
41
+ it 'is expected to equal an empty string' do
42
+ expect(apply).to eq('')
43
+ end
44
+ end
45
+
46
+ context 'when passed a single quoted string' do
47
+ let(:tree) do
48
+ { string_single_quotes: "Hello \\'World\\'" }
49
+ end
50
+
51
+ it 'is expected to equal the correct string' do
52
+ expect(apply).to eq("Hello 'World'")
53
+ end
54
+ end
55
+
56
+ context 'when passed an unquoted string' do
57
+ let(:tree) do
58
+ { string_no_quotes: 'hello_world' }
59
+ end
60
+
61
+ it 'is expected to equal the correct string' do
62
+ expect(apply).to eq('hello_world')
63
+ end
64
+ end
65
+
66
+ context 'when passed a term' do
67
+ let(:tree) do
68
+ { term: { string_no_quotes: 'Shirt' } }
69
+ end
70
+
71
+ it 'is expected to equal the correct ARel node' do
72
+ expect(apply).to eq(
73
+ Product.arel_table[:title].matches('%Shirt%').or(
74
+ ProductVariation.arel_table[:title].matches('%Shirt%')
75
+ )
76
+ )
77
+ end
78
+ end
79
+
80
+ context 'when passed multiple terms' do
81
+ let(:tree) do
82
+ {
83
+ left: {
84
+ term: { string_no_quotes: 'Shirt' }
85
+ },
86
+ right: {
87
+ term: { string_no_quotes: 'Green' }
88
+ }
89
+ }
90
+ end
91
+
92
+ it 'is expected to equal the correct ARel node' do
93
+ expect(apply).to eq(
94
+ Product.arel_table[:title].matches('%Shirt%').or(
95
+ ProductVariation.arel_table[:title].matches('%Shirt%')
96
+ ).and(
97
+ Product.arel_table[:title].matches('%Green%').or(
98
+ ProductVariation.arel_table[:title].matches('%Green%')
99
+ )
100
+ )
101
+ )
102
+ end
103
+ end
104
+ end
105
+ end
data/spec/spec_helper.rb CHANGED
@@ -13,13 +13,12 @@
13
13
  # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
14
14
 
15
15
  require 'combustion'
16
- Combustion.initialize! :active_record
16
+ Combustion.initialize!(:active_record)
17
17
 
18
18
  require 'bundler'
19
- Bundler.require :default, :development
19
+ Bundler.require(:default, :development)
20
20
 
21
21
  require 'rspec/rails'
22
-
23
22
  RSpec.configure do |config|
24
23
  # rspec-expectations config goes here. You can use an alternate assertion/expectation library such as wrong or the
25
24
  # stdlib/minitest assertions if you prefer.
@@ -4,5 +4,5 @@ source 'https://rubygems.org'
4
4
 
5
5
  gemspec path: '../../'
6
6
 
7
- gem 'activerecord', '~> 5.2.0'
8
- gem 'activesupport', '~> 5.2.0'
7
+ gem 'activerecord', '~> 7.1.0'
8
+ gem 'activesupport', '~> 7.1.0'