pursuit 0.4.5 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rubygem.yaml +46 -0
  3. data/Gemfile +14 -13
  4. data/Gemfile.lock +14 -13
  5. data/README.md +210 -27
  6. data/bin/console +10 -0
  7. data/lib/pursuit/aggregate_modifier_not_found.rb +20 -0
  8. data/lib/pursuit/aggregate_modifier_required.rb +20 -0
  9. data/lib/pursuit/aggregate_modifiers_not_available.rb +13 -0
  10. data/lib/pursuit/attribute_not_found.rb +20 -0
  11. data/lib/pursuit/constants.rb +1 -1
  12. data/lib/pursuit/error.rb +7 -0
  13. data/lib/pursuit/predicate_parser.rb +181 -0
  14. data/lib/pursuit/predicate_search.rb +86 -0
  15. data/lib/pursuit/predicate_transform.rb +231 -0
  16. data/lib/pursuit/query_error.rb +7 -0
  17. data/lib/pursuit/simple_search.rb +66 -0
  18. data/lib/pursuit/term_parser.rb +44 -0
  19. data/lib/pursuit/term_search.rb +74 -0
  20. data/lib/pursuit/term_transform.rb +35 -0
  21. data/lib/pursuit.rb +18 -4
  22. data/pursuit.gemspec +4 -3
  23. data/spec/internal/app/models/application_record.rb +5 -0
  24. data/spec/internal/app/models/product.rb +3 -14
  25. data/spec/internal/app/models/product_category.rb +3 -1
  26. data/spec/internal/app/models/product_variation.rb +3 -1
  27. data/spec/internal/app/searches/product_category_search.rb +50 -0
  28. data/spec/internal/app/searches/product_search.rb +53 -0
  29. data/spec/internal/app/searches/product_variation_search.rb +53 -0
  30. data/spec/lib/pursuit/predicate_parser_spec.rb +1604 -0
  31. data/spec/lib/pursuit/predicate_search_spec.rb +116 -0
  32. data/spec/lib/pursuit/predicate_transform_spec.rb +624 -0
  33. data/spec/lib/pursuit/simple_search_spec.rb +73 -0
  34. data/spec/lib/pursuit/term_parser_spec.rb +271 -0
  35. data/spec/lib/pursuit/term_search_spec.rb +85 -0
  36. data/spec/lib/pursuit/term_transform_spec.rb +105 -0
  37. metadata +50 -25
  38. data/.travis.yml +0 -26
  39. data/lib/pursuit/dsl.rb +0 -28
  40. data/lib/pursuit/railtie.rb +0 -13
  41. data/lib/pursuit/search.rb +0 -172
  42. data/lib/pursuit/search_options.rb +0 -86
  43. data/lib/pursuit/search_term_parser.rb +0 -46
  44. data/spec/lib/pursuit/dsl_spec.rb +0 -22
  45. data/spec/lib/pursuit/search_options_spec.rb +0 -146
  46. data/spec/lib/pursuit/search_spec.rb +0 -516
  47. data/spec/lib/pursuit/search_term_parser_spec.rb +0 -34
  48. data/travis/gemfiles/5.2.gemfile +0 -8
  49. data/travis/gemfiles/6.0.gemfile +0 -8
  50. data/travis/gemfiles/6.1.gemfile +0 -8
  51. data/travis/gemfiles/7.0.gemfile +0 -8
@@ -0,0 +1,1604 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Pursuit::PredicateParser 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 '#boolean_true' do
49
+ subject(:boolean_true) { parser.boolean_true }
50
+
51
+ context 'when parsing "true"' do
52
+ subject(:parse) { boolean_true.parse('true') }
53
+
54
+ it 'is expected to capture the value as :truthy' do
55
+ expect(parse).to eq(truthy: 'true')
56
+ end
57
+ end
58
+
59
+ context 'when parsing "True"' do
60
+ subject(:parse) { boolean_true.parse('True') }
61
+
62
+ it 'is expected to capture the value as :truthy' do
63
+ expect(parse).to eq(truthy: 'True')
64
+ end
65
+ end
66
+
67
+ context 'when parsing "TRUE"' do
68
+ subject(:parse) { boolean_true.parse('TRUE') }
69
+
70
+ it 'is expected to capture the value as :truthy' do
71
+ expect(parse).to eq(truthy: 'TRUE')
72
+ end
73
+ end
74
+
75
+ context 'when parsing "t"' do
76
+ subject(:parse) { boolean_true.parse('t') }
77
+
78
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
79
+ end
80
+
81
+ context 'when parsing "yes"' do
82
+ subject(:parse) { boolean_true.parse('yes') }
83
+
84
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
85
+ end
86
+
87
+ context 'when parsing "1"' do
88
+ subject(:parse) { boolean_true.parse('1') }
89
+
90
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
91
+ end
92
+ end
93
+
94
+ describe '#boolean_false' do
95
+ subject(:boolean_false) { parser.boolean_false }
96
+
97
+ context 'when parsing "false"' do
98
+ subject(:parse) { boolean_false.parse('false') }
99
+
100
+ it 'is expected to capture the value as :falsey' do
101
+ expect(parse).to eq(falsey: 'false')
102
+ end
103
+ end
104
+
105
+ context 'when parsing "False"' do
106
+ subject(:parse) { boolean_false.parse('False') }
107
+
108
+ it 'is expected to capture the value as :falsey' do
109
+ expect(parse).to eq(falsey: 'False')
110
+ end
111
+ end
112
+
113
+ context 'when parsing "FALSE"' do
114
+ subject(:parse) { boolean_false.parse('FALSE') }
115
+
116
+ it 'is expected to capture the value as :falsey' do
117
+ expect(parse).to eq(falsey: 'FALSE')
118
+ end
119
+ end
120
+
121
+ context 'when parsing "f"' do
122
+ subject(:parse) { boolean_false.parse('f') }
123
+
124
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
125
+ end
126
+
127
+ context 'when parsing "no"' do
128
+ subject(:parse) { boolean_false.parse('no') }
129
+
130
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
131
+ end
132
+
133
+ context 'when parsing "0"' do
134
+ subject(:parse) { boolean_false.parse('0') }
135
+
136
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
137
+ end
138
+ end
139
+
140
+ describe '#boolean' do
141
+ subject(:boolean) { parser.boolean }
142
+
143
+ context 'when parsing "true"' do
144
+ subject(:parse) { boolean.parse('true') }
145
+
146
+ it 'is expected to capture the value as :truthy' do
147
+ expect(parse).to eq(truthy: 'true')
148
+ end
149
+ end
150
+
151
+ context 'when parsing "false"' do
152
+ subject(:parse) { boolean.parse('false') }
153
+
154
+ it 'is expected to capture the value as :falsey' do
155
+ expect(parse).to eq(falsey: 'false')
156
+ end
157
+ end
158
+
159
+ context 'when parsing a non-boolean value' do
160
+ subject(:parse) { boolean.parse('not-a-boolean') }
161
+
162
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
163
+ end
164
+ end
165
+
166
+ describe '#integer' do
167
+ subject(:integer) { parser.integer }
168
+
169
+ context 'when parsing one or more digits' do
170
+ subject(:parse) { integer.parse(value) }
171
+
172
+ let(:value) { rand(0..999_999).to_s }
173
+
174
+ it 'is expected to capture the value as :integer' do
175
+ expect(parse).to eq(integer: value)
176
+ end
177
+ end
178
+
179
+ context 'when parsing one or more digits prefixed with "+"' do
180
+ subject(:parse) { integer.parse(value) }
181
+
182
+ let(:value) { format('+%<value>d', value: rand(0..999_999)) }
183
+
184
+ it 'is expected to capture the value as :integer' do
185
+ expect(parse).to eq(integer: value)
186
+ end
187
+ end
188
+
189
+ context 'when parsing one or more digits prefixed with "-"' do
190
+ subject(:parse) { integer.parse(value) }
191
+
192
+ let(:value) { format('-%<value>d', value: rand(0..999_999)) }
193
+
194
+ it 'is expected to capture the value as :integer' do
195
+ expect(parse).to eq(integer: value)
196
+ end
197
+ end
198
+
199
+ context 'when parsing digits separated by "."' do
200
+ subject(:parse) { integer.parse(value) }
201
+
202
+ let(:value) { rand(10.0...11.0).to_s }
203
+
204
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
205
+ end
206
+
207
+ context 'when parsing a non-numeric value' do
208
+ subject(:parse) { integer.parse('one') }
209
+
210
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
211
+ end
212
+ end
213
+
214
+ describe '#decimal' do
215
+ subject(:decimal) { parser.decimal }
216
+
217
+ context 'when parsing digits separated by "."' do
218
+ subject(:parse) { decimal.parse(value) }
219
+
220
+ let(:value) { rand(0.0...999_999.0).to_s }
221
+
222
+ it 'is expected to capture the value as :decimal' do
223
+ expect(parse).to eq(decimal: value)
224
+ end
225
+ end
226
+
227
+ context 'when parsing digits separated by "." and prefixed with "+"' do
228
+ subject(:parse) { decimal.parse(value) }
229
+
230
+ let(:value) { format('+%<value>f', value: rand(0.0...999_999.0)) }
231
+
232
+ it 'is expected to capture the value as :decimal' do
233
+ expect(parse).to eq(decimal: value)
234
+ end
235
+ end
236
+
237
+ context 'when parsing digits separated by "." and prefixed with "-"' do
238
+ subject(:parse) { decimal.parse(value) }
239
+
240
+ let(:value) { format('-%<value>f', value: rand(0.0...999_999.0)) }
241
+
242
+ it 'is expected to capture the value as :decimal' do
243
+ expect(parse).to eq(decimal: value)
244
+ end
245
+ end
246
+
247
+ context 'when parsing digits prefixed with "."' do
248
+ subject(:parse) { decimal.parse(value) }
249
+
250
+ let(:value) { format('.%<value>d', value: rand(0...10)) }
251
+
252
+ it 'is expected to capture the value as :decimal' do
253
+ expect(parse).to eq(decimal: value)
254
+ end
255
+ end
256
+
257
+ context 'when parsing digits prefixed with "+."' do
258
+ subject(:parse) { decimal.parse(value) }
259
+
260
+ let(:value) { format('+.%<value>d', value: rand(0...10)) }
261
+
262
+ it 'is expected to capture the value as :decimal' do
263
+ expect(parse).to eq(decimal: value)
264
+ end
265
+ end
266
+
267
+ context 'when parsing digits prefixed with "-."' do
268
+ subject(:parse) { decimal.parse(value) }
269
+
270
+ let(:value) { format('-.%<value>d', value: rand(0...10)) }
271
+
272
+ it 'is expected to capture the value as :decimal' do
273
+ expect(parse).to eq(decimal: value)
274
+ end
275
+ end
276
+
277
+ context 'when parsing digits suffixed with "."' do
278
+ subject(:parse) { decimal.parse(value) }
279
+
280
+ let(:value) { format('%<value>d.', value: rand(0...10)) }
281
+
282
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
283
+ end
284
+
285
+ context 'when parsing digits separated by "." more than once' do
286
+ subject(:parse) { decimal.parse(value) }
287
+
288
+ let(:value) { format('%<a>d.%<b>d.%<c>d', a: rand(0...10), b: rand(0...10), c: rand(0...10)) }
289
+
290
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
291
+ end
292
+
293
+ context 'when parsing digits not separated by "."' do
294
+ subject(:parse) { decimal.parse(value) }
295
+
296
+ let(:value) { rand(0...10).to_s }
297
+
298
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
299
+ end
300
+
301
+ context 'when parsing a non-numeric value' do
302
+ subject(:parse) { decimal.parse('one') }
303
+
304
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
305
+ end
306
+ end
307
+
308
+ describe '#number' do
309
+ subject(:number) { parser.number }
310
+
311
+ context 'when parsing digits separated by "."' do
312
+ subject(:parse) { number.parse(value) }
313
+
314
+ let(:value) { rand(0.0...999_999.0).to_s }
315
+
316
+ it 'is expected to capture the value as :decimal' do
317
+ expect(parse).to eq(decimal: value)
318
+ end
319
+ end
320
+
321
+ context 'when parsing digits not separated by "."' do
322
+ subject(:parse) { number.parse(value) }
323
+
324
+ let(:value) { rand(0..999_999).to_s }
325
+
326
+ it 'is expected to capture the value as :integer' do
327
+ expect(parse).to eq(integer: value)
328
+ end
329
+ end
330
+
331
+ context 'when parsing a non-numeric value' do
332
+ subject(:parse) { number.parse('one') }
333
+
334
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
335
+ end
336
+ end
337
+
338
+ describe '#escaped_character' do
339
+ subject(:escaped_character) { parser.escaped_character }
340
+
341
+ context 'when parsing an escaped character' do
342
+ subject(:parse) { escaped_character.parse('\\"') }
343
+
344
+ it 'is expected to eq the escaped character' do
345
+ expect(parse).to eq('\\"')
346
+ end
347
+ end
348
+
349
+ context 'when parsing an unescaped character' do
350
+ subject(:parse) { escaped_character.parse('"') }
351
+
352
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
353
+ end
354
+ end
355
+
356
+ describe '#string_double_quotes' do
357
+ subject(:string_double_quotes) { parser.string_double_quotes }
358
+
359
+ context 'when parsing a double quoted string' do
360
+ subject(:parse) { string_double_quotes.parse('"Double \"Quoted\""') }
361
+
362
+ it 'is expected to capture the contents of the double quotes as :string_double_quotes' do
363
+ expect(parse).to eq(string_double_quotes: 'Double \"Quoted\"')
364
+ end
365
+ end
366
+
367
+ context 'when parsing a single quoted string' do
368
+ subject(:parse) { string_double_quotes.parse("'Single Quoted'") }
369
+
370
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
371
+ end
372
+
373
+ context 'when parsing an unquoted string' do
374
+ subject(:parse) { string_double_quotes.parse('Unquoted') }
375
+
376
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
377
+ end
378
+ end
379
+
380
+ describe '#string_single_quotes' do
381
+ subject(:string_single_quotes) { parser.string_single_quotes }
382
+
383
+ context 'when parsing a single quoted string' do
384
+ subject(:parse) { string_single_quotes.parse("'Single \\'Quoted\\''") }
385
+
386
+ it 'is expected to capture the contents of the single quotes as :string_single_quotes' do
387
+ expect(parse).to eq(string_single_quotes: "Single \\'Quoted\\'")
388
+ end
389
+ end
390
+
391
+ context 'when parsing a double quoted string' do
392
+ subject(:parse) { string_single_quotes.parse('"Double Quoted"') }
393
+
394
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
395
+ end
396
+
397
+ context 'when parsing an unquoted string' do
398
+ subject(:parse) { string_single_quotes.parse('Unquoted') }
399
+
400
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
401
+ end
402
+ end
403
+
404
+ describe '#string_no_quotes' do
405
+ subject(:string_no_quotes) { parser.string_no_quotes }
406
+
407
+ context 'when parsing an unquoted string with no spaces or symbols' do
408
+ subject(:parse) { string_no_quotes.parse('Unquoted') }
409
+
410
+ it 'is expected to eq the parsed value' do
411
+ expect(parse).to eq(string_no_quotes: 'Unquoted')
412
+ end
413
+ end
414
+
415
+ context 'when parsing an unquoted string with spaces' do
416
+ subject(:parse) { string_no_quotes.parse('No Quotes') }
417
+
418
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
419
+ end
420
+
421
+ context 'when parsing an unquoted string with unsupported symbols' do
422
+ subject(:parse) { string_no_quotes.parse('No&Quotes') }
423
+
424
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
425
+ end
426
+
427
+ context 'when parsing an unquoted string with the "_" symbol' do
428
+ subject(:parse) { string_no_quotes.parse('no_quotes') }
429
+
430
+ it 'is expected to eq the parsed value' do
431
+ expect(parse).to eq(string_no_quotes: 'no_quotes')
432
+ end
433
+ end
434
+
435
+ context 'when parsing an unquoted string with the "!" symbol' do
436
+ subject(:parse) { string_no_quotes.parse('no!quotes') }
437
+
438
+ it 'is expected to eq the parsed value' do
439
+ expect(parse).to eq(string_no_quotes: 'no!quotes')
440
+ end
441
+ end
442
+
443
+ context 'when parsing an unquoted string with the "\'" character' do
444
+ subject(:parse) { string_no_quotes.parse("no'quotes") }
445
+
446
+ it 'is expected to eq the parsed value' do
447
+ expect(parse).to eq(string_no_quotes: "no'quotes")
448
+ end
449
+ end
450
+
451
+ context 'when parsing an unquoted string with the "+" character' do
452
+ subject(:parse) { string_no_quotes.parse('no+quotes') }
453
+
454
+ it 'is expected to eq the parsed value' do
455
+ expect(parse).to eq(string_no_quotes: 'no+quotes')
456
+ end
457
+ end
458
+
459
+ context 'when parsing an unquoted string with the "," character' do
460
+ subject(:parse) { string_no_quotes.parse('no,quotes') }
461
+
462
+ it 'is expected to eq the parsed value' do
463
+ expect(parse).to eq(string_no_quotes: 'no,quotes')
464
+ end
465
+ end
466
+
467
+ context 'when parsing an unquoted string with the "-" character' do
468
+ subject(:parse) { string_no_quotes.parse('no-quotes') }
469
+
470
+ it 'is expected to eq the parsed value' do
471
+ expect(parse).to eq(string_no_quotes: 'no-quotes')
472
+ end
473
+ end
474
+
475
+ context 'when parsing an unquoted string with the "." character' do
476
+ subject(:parse) { string_no_quotes.parse('no.quotes') }
477
+
478
+ it 'is expected to eq the parsed value' do
479
+ expect(parse).to eq(string_no_quotes: 'no.quotes')
480
+ end
481
+ end
482
+
483
+ context 'when parsing an unquoted string with the "/" character' do
484
+ subject(:parse) { string_no_quotes.parse('no/quotes') }
485
+
486
+ it 'is expected to eq the parsed value' do
487
+ expect(parse).to eq(string_no_quotes: 'no/quotes')
488
+ end
489
+ end
490
+
491
+ context 'when parsing an unquoted string with the ":" character' do
492
+ subject(:parse) { string_no_quotes.parse('no:quotes') }
493
+
494
+ it 'is expected to eq the parsed value' do
495
+ expect(parse).to eq(string_no_quotes: 'no:quotes')
496
+ end
497
+ end
498
+
499
+ context 'when parsing an unquoted string with the "?" character' do
500
+ subject(:parse) { string_no_quotes.parse('no?quotes') }
501
+
502
+ it 'is expected to eq the parsed value' do
503
+ expect(parse).to eq(string_no_quotes: 'no?quotes')
504
+ end
505
+ end
506
+
507
+ context 'when parsing an unquoted string with the "@" character' do
508
+ subject(:parse) { string_no_quotes.parse('no@quotes') }
509
+
510
+ it 'is expected to eq the parsed value' do
511
+ expect(parse).to eq(string_no_quotes: 'no@quotes')
512
+ end
513
+ end
514
+ end
515
+
516
+ describe '#string' do
517
+ subject(:string) { parser.string }
518
+
519
+ context 'when parsing a double quoted string' do
520
+ subject(:parse) { string.parse('"Double \"Quoted\""') }
521
+
522
+ it 'is expected to capture the contents of the double quotes as :string_double_quotes' do
523
+ expect(parse).to eq(string_double_quotes: 'Double \"Quoted\"')
524
+ end
525
+ end
526
+
527
+ context 'when parsing a single quoted string' do
528
+ subject(:parse) { string.parse("'Single \\'Quoted\\''") }
529
+
530
+ it 'is expected to capture the contents of the single quotes as :string_single_quotes' do
531
+ expect(parse).to eq(string_single_quotes: "Single \\'Quoted\\'")
532
+ end
533
+ end
534
+
535
+ context 'when parsing an unquoted string' do
536
+ subject(:parse) { string.parse('Unquoted') }
537
+
538
+ it 'is expected to eq the parsed value' do
539
+ expect(parse).to eq(string_no_quotes: 'Unquoted')
540
+ end
541
+ end
542
+
543
+ context 'when parsing an unquoted string with spaces' do
544
+ subject(:parse) { string.parse('No Quotes') }
545
+
546
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
547
+ end
548
+
549
+ context 'when parsing an unquoted string with symbols' do
550
+ subject(:parse) { string.parse('a@b.c') }
551
+
552
+ it 'is expected to eq the parsed value' do
553
+ expect(parse).to eq(string_no_quotes: 'a@b.c')
554
+ end
555
+ end
556
+ end
557
+
558
+ describe '#operator_equal' do
559
+ subject(:operator_equal) { parser.operator_equal }
560
+
561
+ context 'when parsing "="' do
562
+ subject(:parse) { operator_equal.parse('=') }
563
+
564
+ it 'is expected to eq the parsed value' do
565
+ expect(parse).to eq('=')
566
+ end
567
+ end
568
+ end
569
+
570
+ describe '#operator_not_equal' do
571
+ subject(:operator_not_equal) { parser.operator_not_equal }
572
+
573
+ context 'when parsing "!="' do
574
+ subject(:parse) { operator_not_equal.parse('!=') }
575
+
576
+ it 'is expected to eq the parsed value' do
577
+ expect(parse).to eq('!=')
578
+ end
579
+ end
580
+ end
581
+
582
+ describe '#operator_contains' do
583
+ subject(:operator_contains) { parser.operator_contains }
584
+
585
+ context 'when parsing "~"' do
586
+ subject(:parse) { operator_contains.parse('~') }
587
+
588
+ it 'is expected to eq the parsed value' do
589
+ expect(parse).to eq('~')
590
+ end
591
+ end
592
+ end
593
+
594
+ describe '#operator_not_contains' do
595
+ subject(:operator_not_contains) { parser.operator_not_contains }
596
+
597
+ context 'when parsing "!~"' do
598
+ subject(:parse) { operator_not_contains.parse('!~') }
599
+
600
+ it 'is expected to eq the parsed value' do
601
+ expect(parse).to eq('!~')
602
+ end
603
+ end
604
+ end
605
+
606
+ describe '#operator_less_than' do
607
+ subject(:operator_less_than) { parser.operator_less_than }
608
+
609
+ context 'when parsing "<"' do
610
+ subject(:parse) { operator_less_than.parse('<') }
611
+
612
+ it 'is expected to eq the parsed value' do
613
+ expect(parse).to eq('<')
614
+ end
615
+ end
616
+ end
617
+
618
+ describe '#operator_greater_than' do
619
+ subject(:operator_greater_than) { parser.operator_greater_than }
620
+
621
+ context 'when parsing ">"' do
622
+ subject(:parse) { operator_greater_than.parse('>') }
623
+
624
+ it 'is expected to eq the parsed value' do
625
+ expect(parse).to eq('>')
626
+ end
627
+ end
628
+ end
629
+
630
+ describe '#operator_less_than_or_equal_to' do
631
+ subject(:operator_less_than_or_equal_to) { parser.operator_less_than_or_equal_to }
632
+
633
+ context 'when parsing "<="' do
634
+ subject(:parse) { operator_less_than_or_equal_to.parse('<=') }
635
+
636
+ it 'is expected to eq the parsed value' do
637
+ expect(parse).to eq('<=')
638
+ end
639
+ end
640
+ end
641
+
642
+ describe '#operator_greater_than_or_equal_to' do
643
+ subject(:operator_greater_than_or_equal_to) { parser.operator_greater_than_or_equal_to }
644
+
645
+ context 'when parsing ">="' do
646
+ subject(:parse) { operator_greater_than_or_equal_to.parse('>=') }
647
+
648
+ it 'is expected to eq the parsed value' do
649
+ expect(parse).to eq('>=')
650
+ end
651
+ end
652
+ end
653
+
654
+ describe '#operator_and' do
655
+ subject(:operator_and) { parser.operator_and }
656
+
657
+ context 'when parsing "&"' do
658
+ subject(:parse) { operator_and.parse('&') }
659
+
660
+ it 'is expected to eq the parsed value' do
661
+ expect(parse).to eq('&')
662
+ end
663
+ end
664
+ end
665
+
666
+ describe '#operator_or' do
667
+ subject(:operator_or) { parser.operator_or }
668
+
669
+ context 'when parsing "|"' do
670
+ subject(:parse) { operator_or.parse('|') }
671
+
672
+ it 'is expected to eq the parsed value' do
673
+ expect(parse).to eq('|')
674
+ end
675
+ end
676
+ end
677
+
678
+ describe '#comparator' do
679
+ subject(:comparator) { parser.comparator }
680
+
681
+ context 'when parsing "="' do
682
+ subject(:parse) { comparator.parse('=') }
683
+
684
+ it 'is expected to capture the value as :operator' do
685
+ expect(parse).to eq(comparator: '=')
686
+ end
687
+ end
688
+
689
+ context 'when parsing "!="' do
690
+ subject(:parse) { comparator.parse('!=') }
691
+
692
+ it 'is expected to capture the value as :operator' do
693
+ expect(parse).to eq(comparator: '!=')
694
+ end
695
+ end
696
+
697
+ context 'when parsing "~"' do
698
+ subject(:parse) { comparator.parse('~') }
699
+
700
+ it 'is expected to capture the value as :operator' do
701
+ expect(parse).to eq(comparator: '~')
702
+ end
703
+ end
704
+
705
+ context 'when parsing "!~"' do
706
+ subject(:parse) { comparator.parse('!~') }
707
+
708
+ it 'is expected to capture the value as :operator' do
709
+ expect(parse).to eq(comparator: '!~')
710
+ end
711
+ end
712
+
713
+ context 'when parsing "<"' do
714
+ subject(:parse) { comparator.parse('<') }
715
+
716
+ it 'is expected to capture the value as :operator' do
717
+ expect(parse).to eq(comparator: '<')
718
+ end
719
+ end
720
+
721
+ context 'when parsing ">"' do
722
+ subject(:parse) { comparator.parse('>') }
723
+
724
+ it 'is expected to capture the value as :operator' do
725
+ expect(parse).to eq(comparator: '>')
726
+ end
727
+ end
728
+
729
+ context 'when parsing "<="' do
730
+ subject(:parse) { comparator.parse('<=') }
731
+
732
+ it 'is expected to capture the value as :operator' do
733
+ expect(parse).to eq(comparator: '<=')
734
+ end
735
+ end
736
+
737
+ context 'when parsing ">="' do
738
+ subject(:parse) { comparator.parse('>=') }
739
+
740
+ it 'is expected to capture the value as :operator' do
741
+ expect(parse).to eq(comparator: '>=')
742
+ end
743
+ end
744
+
745
+ context 'when parsing "&"' do
746
+ subject(:parse) { comparator.parse('&') }
747
+
748
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
749
+ end
750
+
751
+ context 'when parsing "|"' do
752
+ subject(:parse) { comparator.parse('|') }
753
+
754
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
755
+ end
756
+ end
757
+
758
+ describe '#joiner' do
759
+ subject(:joiner) { parser.joiner }
760
+
761
+ context 'when parsing "&"' do
762
+ subject(:parse) { joiner.parse('&') }
763
+
764
+ it 'is expected to capture the value as :join' do
765
+ expect(parse).to eq(joiner: '&')
766
+ end
767
+ end
768
+
769
+ context 'when parsing "|"' do
770
+ subject(:parse) { joiner.parse('|') }
771
+
772
+ it 'is expected to capture the value as :join' do
773
+ expect(parse).to eq(joiner: '|')
774
+ end
775
+ end
776
+
777
+ context 'when parsing "="' do
778
+ subject(:parse) { joiner.parse('=') }
779
+
780
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
781
+ end
782
+
783
+ context 'when parsing "!="' do
784
+ subject(:parse) { joiner.parse('!=') }
785
+
786
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
787
+ end
788
+
789
+ context 'when parsing "~"' do
790
+ subject(:parse) { joiner.parse('~') }
791
+
792
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
793
+ end
794
+
795
+ context 'when parsing "!~"' do
796
+ subject(:parse) { joiner.parse('!~') }
797
+
798
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
799
+ end
800
+
801
+ context 'when parsing "<"' do
802
+ subject(:parse) { joiner.parse('<') }
803
+
804
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
805
+ end
806
+
807
+ context 'when parsing ">"' do
808
+ subject(:parse) { joiner.parse('>') }
809
+
810
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
811
+ end
812
+
813
+ context 'when parsing "<="' do
814
+ subject(:parse) { joiner.parse('<=') }
815
+
816
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
817
+ end
818
+
819
+ context 'when parsing ">="' do
820
+ subject(:parse) { joiner.parse('>=') }
821
+
822
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
823
+ end
824
+ end
825
+
826
+ describe '#aggregate_modifier' do
827
+ subject(:aggregate_modifier) { parser.aggregate_modifier }
828
+
829
+ context 'when parsing "#"' do
830
+ subject(:parse) { aggregate_modifier.parse('#') }
831
+
832
+ it 'is expected to capture the value as :aggregate_modifier' do
833
+ expect(parse).to eq(aggregate_modifier: '#')
834
+ end
835
+ end
836
+
837
+ context 'when parsing "*"' do
838
+ subject(:parse) { aggregate_modifier.parse('*') }
839
+
840
+ it 'is expected to capture the value as :aggregate_modifier' do
841
+ expect(parse).to eq(aggregate_modifier: '*')
842
+ end
843
+ end
844
+
845
+ context 'when parsing "+"' do
846
+ subject(:parse) { aggregate_modifier.parse('+') }
847
+
848
+ it 'is expected to capture the value as :aggregate_modifier' do
849
+ expect(parse).to eq(aggregate_modifier: '+')
850
+ end
851
+ end
852
+
853
+ context 'when parsing "-"' do
854
+ subject(:parse) { aggregate_modifier.parse('-') }
855
+
856
+ it 'is expected to capture the value as :aggregate_modifier' do
857
+ expect(parse).to eq(aggregate_modifier: '-')
858
+ end
859
+ end
860
+
861
+ context 'when parsing "~"' do
862
+ subject(:parse) { aggregate_modifier.parse('~') }
863
+
864
+ it 'is expected to capture the value as :aggregate_modifier' do
865
+ expect(parse).to eq(aggregate_modifier: '~')
866
+ end
867
+ end
868
+ end
869
+
870
+ describe '#attribute' do
871
+ subject(:attribute) { parser.attribute }
872
+
873
+ context 'when parsing a double quoted string' do
874
+ subject(:parse) { attribute.parse('"First Name"') }
875
+
876
+ it 'is expected to capture the value as :attribute' do
877
+ expect(parse).to match(hash_including(attribute: { string_double_quotes: 'First Name' }))
878
+ end
879
+ end
880
+
881
+ context 'when parsing a single quoted string' do
882
+ subject(:parse) { attribute.parse("'First Name'") }
883
+
884
+ it 'is expected to capture the value as :attribute' do
885
+ expect(parse).to match(hash_including(attribute: { string_single_quotes: 'First Name' }))
886
+ end
887
+ end
888
+
889
+ context 'when parsing an unquoted string' do
890
+ subject(:parse) { attribute.parse('first_name') }
891
+
892
+ it 'is expected to capture the value as :attribute' do
893
+ expect(parse).to match(hash_including(attribute: { string_no_quotes: 'first_name' }))
894
+ end
895
+ end
896
+
897
+ context 'when parsing an unquoted string with spaces' do
898
+ subject(:parse) { attribute.parse('first name') }
899
+
900
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
901
+ end
902
+ end
903
+
904
+ describe '#value' do
905
+ subject(:value) { parser.value }
906
+
907
+ context 'when parsing a boolean' do
908
+ subject(:parse) { value.parse('true') }
909
+
910
+ it 'is expected to capture the value as :value' do
911
+ expect(parse).to eq(value: { truthy: 'true' })
912
+ end
913
+ end
914
+
915
+ context 'when parsing a whole number' do
916
+ subject(:parse) { value.parse('123') }
917
+
918
+ it 'is expected to capture the value as :value' do
919
+ expect(parse).to eq(value: { integer: '123' })
920
+ end
921
+ end
922
+
923
+ context 'when parsing a decimal number' do
924
+ subject(:parse) { value.parse('123.456') }
925
+
926
+ it 'is expected to capture the value as :value' do
927
+ expect(parse).to eq(value: { decimal: '123.456' })
928
+ end
929
+ end
930
+
931
+ context 'when parsing a double quoted string' do
932
+ subject(:parse) { value.parse('"Double Quoted"') }
933
+
934
+ it 'is expected to capture the value as :value' do
935
+ expect(parse).to eq(value: { string_double_quotes: 'Double Quoted' })
936
+ end
937
+ end
938
+
939
+ context 'when parsing a single quoted string' do
940
+ subject(:parse) { value.parse("'Single Quoted'") }
941
+
942
+ it 'is expected to capture the value as :value' do
943
+ expect(parse).to eq(value: { string_single_quotes: 'Single Quoted' })
944
+ end
945
+ end
946
+
947
+ context 'when parsing an unquoted string' do
948
+ subject(:parse) { value.parse('Unquoted') }
949
+
950
+ it 'is expected to capture the value as :value' do
951
+ expect(parse).to eq(value: { string_no_quotes: 'Unquoted' })
952
+ end
953
+ end
954
+
955
+ context 'when parsing an unquoted string that starts with a number' do
956
+ subject(:parse) { value.parse('1a') }
957
+
958
+ it 'is expected to capture the value as :value' do
959
+ expect(parse).to eq(value: { string_no_quotes: '1a' })
960
+ end
961
+ end
962
+
963
+ context 'when parsing an unquoted string that starts with "true"' do
964
+ subject(:parse) { value.parse('trueness') }
965
+
966
+ it 'is expected to capture the value as :value' do
967
+ expect(parse).to eq(value: { string_no_quotes: 'trueness' })
968
+ end
969
+ end
970
+
971
+ context 'when parsing an unquoted string with spaces' do
972
+ subject(:parse) { value.parse('No Quotes') }
973
+
974
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
975
+ end
976
+
977
+ context 'when parsing an unquoted string with symbols' do
978
+ subject(:parse) { value.parse('a@b.c') }
979
+
980
+ it 'is expected to capture the value as :value' do
981
+ expect(parse).to eq(value: { string_no_quotes: 'a@b.c' })
982
+ end
983
+ end
984
+ end
985
+
986
+ describe '#comparison' do
987
+ subject(:comparison) { parser.comparison }
988
+
989
+ context 'when parsing a comparison without an aggregate modifier' do
990
+ subject(:parse) { comparison.parse('rating > 3') }
991
+
992
+ it 'is expected to capture the attribute as :attribute' do
993
+ expect(parse).to match(hash_including(attribute: { string_no_quotes: 'rating' }))
994
+ end
995
+
996
+ it 'is expected to capture the comparator as :comparator' do
997
+ expect(parse).to match(hash_including(comparator: '>'))
998
+ end
999
+
1000
+ it 'is expected to capture the value as :value' do
1001
+ expect(parse).to match(hash_including(value: { integer: '3' }))
1002
+ end
1003
+ end
1004
+
1005
+ context 'when parsing a comparison with an aggregate modifier' do
1006
+ subject(:parse) { comparison.parse('#variants >= 5') }
1007
+
1008
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
1009
+ end
1010
+ end
1011
+
1012
+ describe '#comparison_group' do
1013
+ subject(:comparison_group) { parser.comparison_group }
1014
+
1015
+ context 'when parsing a comparison with brackets' do
1016
+ subject(:parse) { comparison_group.parse('(title = Shirt)') }
1017
+
1018
+ it 'is expected to eq the comparison result' do
1019
+ expect(parse).to eq(
1020
+ attribute: { string_no_quotes: 'title' },
1021
+ comparator: '=',
1022
+ value: { string_no_quotes: 'Shirt' }
1023
+ )
1024
+ end
1025
+ end
1026
+
1027
+ context 'when parsing a nested group' do
1028
+ subject(:parse) { comparison_group.parse('((title = Shirt))') }
1029
+
1030
+ it 'is expected to eq the nested group result' do
1031
+ expect(parse).to eq(
1032
+ attribute: { string_no_quotes: 'title' },
1033
+ comparator: '=',
1034
+ value: { string_no_quotes: 'Shirt' }
1035
+ )
1036
+ end
1037
+ end
1038
+
1039
+ context 'when parsing a join' do
1040
+ subject(:parse) { comparison_group.parse('(title = Shirt | title = T-Shirt)') }
1041
+
1042
+ it 'is expected to eq the join result' do
1043
+ expect(parse).to eq(
1044
+ left: {
1045
+ attribute: { string_no_quotes: 'title' },
1046
+ comparator: '=',
1047
+ value: { string_no_quotes: 'Shirt' }
1048
+ },
1049
+ joiner: '|',
1050
+ right: {
1051
+ attribute: { string_no_quotes: 'title' },
1052
+ comparator: '=',
1053
+ value: { string_no_quotes: 'T-Shirt' }
1054
+ }
1055
+ )
1056
+ end
1057
+ end
1058
+
1059
+ context 'when parsing a comparison without brackets' do
1060
+ subject(:parse) { comparison_group.parse('title = Shirt') }
1061
+
1062
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
1063
+ end
1064
+ end
1065
+
1066
+ describe '#comparison_join' do
1067
+ subject(:comparison_join) { parser.comparison_join }
1068
+
1069
+ context 'when parsing a join of two comparisons' do
1070
+ subject(:parse) { comparison_join.parse('title = Shirt | title = T-Shirt') }
1071
+
1072
+ it 'is expected to capture the left comparison as :left' do
1073
+ expect(parse).to match(
1074
+ hash_including(
1075
+ left: {
1076
+ attribute: { string_no_quotes: 'title' },
1077
+ comparator: '=',
1078
+ value: { string_no_quotes: 'Shirt' }
1079
+ }
1080
+ )
1081
+ )
1082
+ end
1083
+
1084
+ it 'is expected to capture the joiner as :joiner' do
1085
+ expect(parse).to match(hash_including(joiner: '|'))
1086
+ end
1087
+
1088
+ it 'is expected to capture the right comparison as :right' do
1089
+ expect(parse).to match(
1090
+ hash_including(
1091
+ right: {
1092
+ attribute: { string_no_quotes: 'title' },
1093
+ comparator: '=',
1094
+ value: { string_no_quotes: 'T-Shirt' }
1095
+ }
1096
+ )
1097
+ )
1098
+ end
1099
+ end
1100
+
1101
+ context 'when parsing a join of a comparison to another join' do
1102
+ subject(:parse) { comparison_join.parse('rating >= 3 & title = Shirt | title = T-Shirt') }
1103
+
1104
+ it 'is expected to capture the left comparison as :left' do
1105
+ expect(parse).to match(
1106
+ hash_including(
1107
+ left: {
1108
+ attribute: { string_no_quotes: 'rating' },
1109
+ comparator: '>=',
1110
+ value: { integer: '3' }
1111
+ }
1112
+ )
1113
+ )
1114
+ end
1115
+
1116
+ it 'is expected to capture the joiner as :joiner' do
1117
+ expect(parse).to match(hash_including(joiner: '&'))
1118
+ end
1119
+
1120
+ it 'is expected to capture the right join as :right' do
1121
+ expect(parse).to match(
1122
+ hash_including(
1123
+ right: {
1124
+ left: {
1125
+ attribute: { string_no_quotes: 'title' },
1126
+ comparator: '=',
1127
+ value: { string_no_quotes: 'Shirt' }
1128
+ },
1129
+ joiner: '|',
1130
+ right: {
1131
+ attribute: { string_no_quotes: 'title' },
1132
+ comparator: '=',
1133
+ value: { string_no_quotes: 'T-Shirt' }
1134
+ }
1135
+ }
1136
+ )
1137
+ )
1138
+ end
1139
+ end
1140
+ end
1141
+
1142
+ describe '#comparison_node' do
1143
+ subject(:comparison_node) { parser.comparison_node }
1144
+
1145
+ context 'when parsing a comparison' do
1146
+ subject(:parse) { comparison_node.parse('rating >= 3') }
1147
+
1148
+ it 'is expected to eq the comparison result' do
1149
+ expect(parse).to match(
1150
+ hash_including(
1151
+ attribute: { string_no_quotes: 'rating' },
1152
+ comparator: '>=',
1153
+ value: { integer: '3' }
1154
+ )
1155
+ )
1156
+ end
1157
+ end
1158
+
1159
+ context 'when parsing a join' do
1160
+ subject(:parse) { comparison_node.parse('title = Shirt | title = T-Shirt') }
1161
+
1162
+ it 'is expected to eq the join result' do
1163
+ expect(parse).to match(
1164
+ hash_including(
1165
+ left: {
1166
+ attribute: { string_no_quotes: 'title' },
1167
+ comparator: '=',
1168
+ value: { string_no_quotes: 'Shirt' }
1169
+ },
1170
+ joiner: '|',
1171
+ right: {
1172
+ attribute: { string_no_quotes: 'title' },
1173
+ comparator: '=',
1174
+ value: { string_no_quotes: 'T-Shirt' }
1175
+ }
1176
+ )
1177
+ )
1178
+ end
1179
+ end
1180
+
1181
+ context 'when parsing a group' do
1182
+ subject(:parse) { comparison_node.parse('(title = Shirt)') }
1183
+
1184
+ it 'is expected to eq the group result' do
1185
+ expect(parse).to match(
1186
+ hash_including(
1187
+ attribute: { string_no_quotes: 'title' },
1188
+ comparator: '=',
1189
+ value: { string_no_quotes: 'Shirt' }
1190
+ )
1191
+ )
1192
+ end
1193
+ end
1194
+ end
1195
+
1196
+ describe '#aggregate_comparison' do
1197
+ subject(:aggregate_comparison) { parser.aggregate_comparison }
1198
+
1199
+ context 'when parsing a comparison with an aggregate modifier' do
1200
+ subject(:parse) { aggregate_comparison.parse('#variations >= 5') }
1201
+
1202
+ it 'is expected to capture the aggregate modifier as :aggregate_modifier' do
1203
+ expect(parse).to match(hash_including(aggregate_modifier: '#'))
1204
+ end
1205
+
1206
+ it 'is expected to capture the attribute as :attribute' do
1207
+ expect(parse).to match(hash_including(attribute: { string_no_quotes: 'variations' }))
1208
+ end
1209
+
1210
+ it 'is expected to capture the comparator as :comparator' do
1211
+ expect(parse).to match(hash_including(comparator: '>='))
1212
+ end
1213
+
1214
+ it 'is expected to capture the value as :value' do
1215
+ expect(parse).to match(hash_including(value: { integer: '5' }))
1216
+ end
1217
+ end
1218
+
1219
+ context 'when parsing a comparison without an aggregate modifier' do
1220
+ subject(:parse) { aggregate_comparison.parse('rating > 3') }
1221
+
1222
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
1223
+ end
1224
+ end
1225
+
1226
+ describe '#aggregate_comparison_group' do
1227
+ subject(:aggregate_comparison_group) { parser.aggregate_comparison_group }
1228
+
1229
+ context 'when parsing an aggregate comparison with brackets' do
1230
+ subject(:parse) { aggregate_comparison_group.parse('(#variations >= 5)') }
1231
+
1232
+ it 'is expected to eq the comparison result' do
1233
+ expect(parse).to eq(
1234
+ aggregate_modifier: '#',
1235
+ attribute: { string_no_quotes: 'variations' },
1236
+ comparator: '>=',
1237
+ value: { integer: '5' }
1238
+ )
1239
+ end
1240
+ end
1241
+
1242
+ context 'when parsing a nested group' do
1243
+ subject(:parse) { aggregate_comparison_group.parse('((#variations >= 5))') }
1244
+
1245
+ it 'is expected to eq the nested comparison group result' do
1246
+ expect(parse).to eq(
1247
+ aggregate_modifier: '#',
1248
+ attribute: { string_no_quotes: 'variations' },
1249
+ comparator: '>=',
1250
+ value: { integer: '5' }
1251
+ )
1252
+ end
1253
+ end
1254
+
1255
+ context 'when parsing a join' do
1256
+ subject(:parse) { aggregate_comparison_group.parse('(#variations >= 5 | #variations = 1)') }
1257
+
1258
+ it 'is expected to eq the aggregate comparison join result' do
1259
+ expect(parse).to eq(
1260
+ left: {
1261
+ aggregate_modifier: '#',
1262
+ attribute: { string_no_quotes: 'variations' },
1263
+ comparator: '>=',
1264
+ value: { integer: '5' }
1265
+ },
1266
+ joiner: '|',
1267
+ right: {
1268
+ aggregate_modifier: '#',
1269
+ attribute: { string_no_quotes: 'variations' },
1270
+ comparator: '=',
1271
+ value: { integer: '1' }
1272
+ }
1273
+ )
1274
+ end
1275
+ end
1276
+
1277
+ context 'when parsing an aggregate comparison without brackets' do
1278
+ subject(:parse) { aggregate_comparison_group.parse('#variations >= 5') }
1279
+
1280
+ it { expect { parse }.to raise_exception(Parslet::ParseFailed) }
1281
+ end
1282
+ end
1283
+
1284
+ describe '#aggregate_comparison_join' do
1285
+ subject(:aggregate_comparison_join) { parser.aggregate_comparison_join }
1286
+
1287
+ context 'when parsing a join of two aggregate comparisons' do
1288
+ subject(:parse) { aggregate_comparison_join.parse('#variations >= 5 | #variations = 1') }
1289
+
1290
+ it 'is expected to capture the left comparison as :left' do
1291
+ expect(parse).to match(
1292
+ hash_including(
1293
+ left: {
1294
+ aggregate_modifier: '#',
1295
+ attribute: { string_no_quotes: 'variations' },
1296
+ comparator: '>=',
1297
+ value: { integer: '5' }
1298
+ }
1299
+ )
1300
+ )
1301
+ end
1302
+
1303
+ it 'is expected to capture the joiner as :joiner' do
1304
+ expect(parse).to match(hash_including(joiner: '|'))
1305
+ end
1306
+
1307
+ it 'is expected to capture the right comparison as :right' do
1308
+ expect(parse).to match(
1309
+ hash_including(
1310
+ right: {
1311
+ aggregate_modifier: '#',
1312
+ attribute: { string_no_quotes: 'variations' },
1313
+ comparator: '=',
1314
+ value: { integer: '1' }
1315
+ }
1316
+ )
1317
+ )
1318
+ end
1319
+ end
1320
+
1321
+ context 'when parsing a join of an aggregate comparison to another join' do
1322
+ subject(:parse) { aggregate_comparison_join.parse('*views > 100 & #variations >= 5 | #variations = 1') }
1323
+
1324
+ it 'is expected to capture the left aggregate comparison as :left' do
1325
+ expect(parse).to match(
1326
+ hash_including(
1327
+ left: {
1328
+ aggregate_modifier: '*',
1329
+ attribute: { string_no_quotes: 'views' },
1330
+ comparator: '>',
1331
+ value: { integer: '100' }
1332
+ }
1333
+ )
1334
+ )
1335
+ end
1336
+
1337
+ it 'is expected to capture the joiner as :joiner' do
1338
+ expect(parse).to match(hash_including(joiner: '&'))
1339
+ end
1340
+
1341
+ it 'is expected to capture the right join as :right' do
1342
+ expect(parse).to match(
1343
+ hash_including(
1344
+ right: {
1345
+ left: {
1346
+ aggregate_modifier: '#',
1347
+ attribute: { string_no_quotes: 'variations' },
1348
+ comparator: '>=',
1349
+ value: { integer: '5' }
1350
+ },
1351
+ joiner: '|',
1352
+ right: {
1353
+ aggregate_modifier: '#',
1354
+ attribute: { string_no_quotes: 'variations' },
1355
+ comparator: '=',
1356
+ value: { integer: '1' }
1357
+ }
1358
+ }
1359
+ )
1360
+ )
1361
+ end
1362
+ end
1363
+ end
1364
+
1365
+ describe '#aggregate_comparison_node' do
1366
+ subject(:aggregate_comparison_node) { parser.aggregate_comparison_node }
1367
+
1368
+ context 'when parsing an aggregate comparison' do
1369
+ subject(:parse) { aggregate_comparison_node.parse('#variations >= 5') }
1370
+
1371
+ it 'is expected to eq the aggregate comparison result' do
1372
+ expect(parse).to match(
1373
+ hash_including(
1374
+ aggregate_modifier: '#',
1375
+ attribute: { string_no_quotes: 'variations' },
1376
+ comparator: '>=',
1377
+ value: { integer: '5' }
1378
+ )
1379
+ )
1380
+ end
1381
+ end
1382
+
1383
+ context 'when parsing a join' do
1384
+ subject(:parse) { aggregate_comparison_node.parse('#variations >= 5 | #variations = 1') }
1385
+
1386
+ it 'is expected to eq the join result' do
1387
+ expect(parse).to match(
1388
+ hash_including(
1389
+ left: {
1390
+ aggregate_modifier: '#',
1391
+ attribute: { string_no_quotes: 'variations' },
1392
+ comparator: '>=',
1393
+ value: { integer: '5' }
1394
+ },
1395
+ joiner: '|',
1396
+ right: {
1397
+ aggregate_modifier: '#',
1398
+ attribute: { string_no_quotes: 'variations' },
1399
+ comparator: '=',
1400
+ value: { integer: '1' }
1401
+ }
1402
+ )
1403
+ )
1404
+ end
1405
+ end
1406
+
1407
+ context 'when parsing a group' do
1408
+ subject(:parse) { aggregate_comparison_node.parse('(#variations >= 5)') }
1409
+
1410
+ it 'is expected to eq the group result' do
1411
+ expect(parse).to match(
1412
+ hash_including(
1413
+ aggregate_modifier: '#',
1414
+ attribute: { string_no_quotes: 'variations' },
1415
+ comparator: '>=',
1416
+ value: { integer: '5' }
1417
+ )
1418
+ )
1419
+ end
1420
+ end
1421
+ end
1422
+
1423
+ describe '#predicate_where' do
1424
+ subject(:predicate_where) { parser.predicate_where }
1425
+
1426
+ context 'when parsing a comparison' do
1427
+ subject(:parse) { predicate_where.parse('title = Shirt') }
1428
+
1429
+ it 'is expected to capture the comparison as :where' do
1430
+ expect(parse).to match(
1431
+ hash_including(
1432
+ where: {
1433
+ attribute: { string_no_quotes: 'title' },
1434
+ comparator: '=',
1435
+ value: { string_no_quotes: 'Shirt' }
1436
+ }
1437
+ )
1438
+ )
1439
+ end
1440
+ end
1441
+ end
1442
+
1443
+ describe '#predicate_having' do
1444
+ subject(:predicate_having) { parser.predicate_having }
1445
+
1446
+ context 'when parsing an aggregate comparison' do
1447
+ subject(:parse) { predicate_having.parse('#variations >= 5') }
1448
+
1449
+ it 'is expected to capture the aggregate comparison as :having' do
1450
+ expect(parse).to match(
1451
+ hash_including(
1452
+ having: {
1453
+ aggregate_modifier: '#',
1454
+ attribute: { string_no_quotes: 'variations' },
1455
+ comparator: '>=',
1456
+ value: { integer: '5' }
1457
+ }
1458
+ )
1459
+ )
1460
+ end
1461
+ end
1462
+ end
1463
+
1464
+ describe '#predicate' do
1465
+ subject(:predicate) { parser.predicate }
1466
+
1467
+ context 'when parsing a comparison' do
1468
+ subject(:parse) { predicate.parse('title = Shirt') }
1469
+
1470
+ it 'is expected to capture the comparison as :where' do
1471
+ expect(parse).to match(
1472
+ hash_including(
1473
+ where: {
1474
+ attribute: { string_no_quotes: 'title' },
1475
+ comparator: '=',
1476
+ value: { string_no_quotes: 'Shirt' }
1477
+ }
1478
+ )
1479
+ )
1480
+ end
1481
+ end
1482
+
1483
+ context 'when parsing an aggregate comparison' do
1484
+ subject(:parse) { predicate.parse('#variations >= 5') }
1485
+
1486
+ it 'is expected to capture the aggregate comparison as :having' do
1487
+ expect(parse).to match(
1488
+ hash_including(
1489
+ having: {
1490
+ aggregate_modifier: '#',
1491
+ attribute: { string_no_quotes: 'variations' },
1492
+ comparator: '>=',
1493
+ value: { integer: '5' }
1494
+ }
1495
+ )
1496
+ )
1497
+ end
1498
+ end
1499
+
1500
+ context 'when parsing a comparison followed by an aggregate comparison' do
1501
+ subject(:parse) { predicate.parse('title = Shirt & #variations >= 5') }
1502
+
1503
+ it 'is expected to capture the comparison as :where' do
1504
+ expect(parse).to match(
1505
+ hash_including(
1506
+ where: {
1507
+ attribute: { string_no_quotes: 'title' },
1508
+ comparator: '=',
1509
+ value: { string_no_quotes: 'Shirt' }
1510
+ }
1511
+ )
1512
+ )
1513
+ end
1514
+
1515
+ it 'is expected to capture the aggregate comparison as :having' do
1516
+ expect(parse).to match(
1517
+ hash_including(
1518
+ having: {
1519
+ aggregate_modifier: '#',
1520
+ attribute: { string_no_quotes: 'variations' },
1521
+ comparator: '>=',
1522
+ value: { integer: '5' }
1523
+ }
1524
+ )
1525
+ )
1526
+ end
1527
+ end
1528
+
1529
+ context 'when parsing an aggregate comparison followed by a comparison' do
1530
+ subject(:parse) { predicate.parse('#variations >= 5 & title = Shirt') }
1531
+
1532
+ it 'is expected to capture the comparison as :where' do
1533
+ expect(parse).to match(
1534
+ hash_including(
1535
+ where: {
1536
+ attribute: { string_no_quotes: 'title' },
1537
+ comparator: '=',
1538
+ value: { string_no_quotes: 'Shirt' }
1539
+ }
1540
+ )
1541
+ )
1542
+ end
1543
+
1544
+ it 'is expected to capture the aggregate comparison as :having' do
1545
+ expect(parse).to match(
1546
+ hash_including(
1547
+ having: {
1548
+ aggregate_modifier: '#',
1549
+ attribute: { string_no_quotes: 'variations' },
1550
+ comparator: '>=',
1551
+ value: { integer: '5' }
1552
+ }
1553
+ )
1554
+ )
1555
+ end
1556
+ end
1557
+
1558
+ context 'when parsing a complex query' do
1559
+ subject(:parse) do
1560
+ predicate.parse(' title ~ "Polo Shirts" & (rating > 2.5 | featured = true) & #variations >= 5 ')
1561
+ end
1562
+
1563
+ it 'is expected to eq the correct tree' do # rubocop:disable RSpec/ExampleLength
1564
+ expect(parse).to eq(
1565
+ where: {
1566
+ left: {
1567
+ attribute: { string_no_quotes: 'title' },
1568
+ comparator: '~',
1569
+ value: { string_double_quotes: 'Polo Shirts' }
1570
+ },
1571
+ joiner: '&',
1572
+ right: {
1573
+ left: {
1574
+ attribute: { string_no_quotes: 'rating' },
1575
+ comparator: '>',
1576
+ value: { decimal: '2.5' }
1577
+ },
1578
+ joiner: '|',
1579
+ right: {
1580
+ attribute: { string_no_quotes: 'featured' },
1581
+ comparator: '=',
1582
+ value: { truthy: 'true' }
1583
+ }
1584
+ }
1585
+ },
1586
+ having: {
1587
+ aggregate_modifier: '#',
1588
+ attribute: { string_no_quotes: 'variations' },
1589
+ comparator: '>=',
1590
+ value: { integer: '5' }
1591
+ }
1592
+ )
1593
+ end
1594
+ end
1595
+ end
1596
+
1597
+ describe '#root' do
1598
+ subject(:root) { parser.root }
1599
+
1600
+ it 'is expected to eq #predicate' do
1601
+ expect(root).to eq(parser.predicate)
1602
+ end
1603
+ end
1604
+ end