pursuit 0.4.3 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,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