cuprum-collections 0.5.0 → 0.6.0.rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -0
  3. data/README.md +1 -1
  4. data/config/locales/en.rb +18 -0
  5. data/lib/cuprum/collections/adaptable/collection.rb +18 -0
  6. data/lib/cuprum/collections/adaptable/command.rb +22 -0
  7. data/lib/cuprum/collections/adaptable/commands/abstract_assign_one.rb +27 -0
  8. data/lib/cuprum/collections/adaptable/commands/abstract_build_one.rb +25 -0
  9. data/lib/cuprum/collections/adaptable/commands/abstract_validate_one.rb +35 -0
  10. data/lib/cuprum/collections/adaptable/commands.rb +15 -0
  11. data/lib/cuprum/collections/adaptable/query.rb +64 -0
  12. data/lib/cuprum/collections/adaptable.rb +13 -0
  13. data/lib/cuprum/collections/adapter.rb +300 -0
  14. data/lib/cuprum/collections/adapters/data_adapter.rb +82 -0
  15. data/lib/cuprum/collections/adapters/entity_adapter.rb +76 -0
  16. data/lib/cuprum/collections/adapters/hash_adapter.rb +48 -0
  17. data/lib/cuprum/collections/adapters.rb +14 -0
  18. data/lib/cuprum/collections/basic/collection.rb +2 -20
  19. data/lib/cuprum/collections/basic/commands/destroy_one.rb +1 -1
  20. data/lib/cuprum/collections/basic/commands/find_many.rb +0 -31
  21. data/lib/cuprum/collections/basic/commands/find_matching.rb +0 -94
  22. data/lib/cuprum/collections/basic/commands/find_one.rb +0 -18
  23. data/lib/cuprum/collections/basic/commands/insert_one.rb +1 -1
  24. data/lib/cuprum/collections/basic/commands/update_one.rb +1 -1
  25. data/lib/cuprum/collections/basic/scopes/criteria_scope.rb +36 -21
  26. data/lib/cuprum/collections/basic.rb +6 -5
  27. data/lib/cuprum/collections/collection.rb +6 -0
  28. data/lib/cuprum/collections/collection_command.rb +1 -1
  29. data/lib/cuprum/collections/commands/abstract_find_many.rb +40 -3
  30. data/lib/cuprum/collections/commands/abstract_find_matching.rb +102 -0
  31. data/lib/cuprum/collections/commands/abstract_find_one.rb +23 -1
  32. data/lib/cuprum/collections/commands/associations/find_many.rb +1 -3
  33. data/lib/cuprum/collections/commands/associations/require_many.rb +1 -1
  34. data/lib/cuprum/collections/commands/find_one_matching.rb +10 -10
  35. data/lib/cuprum/collections/commands/query_command.rb +6 -4
  36. data/lib/cuprum/collections/commands/upsert.rb +0 -2
  37. data/lib/cuprum/collections/constraints/order/attributes_array.rb +5 -4
  38. data/lib/cuprum/collections/constraints/order/attributes_hash.rb +5 -4
  39. data/lib/cuprum/collections/constraints/order/sort_direction.rb +2 -2
  40. data/lib/cuprum/collections/constraints/ordering.rb +11 -9
  41. data/lib/cuprum/collections/constraints/query_hash.rb +2 -2
  42. data/lib/cuprum/collections/errors/abstract_find_error.rb +101 -23
  43. data/lib/cuprum/collections/errors/extra_attributes.rb +3 -3
  44. data/lib/cuprum/collections/errors/failed_validation.rb +3 -3
  45. data/lib/cuprum/collections/errors/missing_default_contract.rb +12 -4
  46. data/lib/cuprum/collections/queries.rb +4 -0
  47. data/lib/cuprum/collections/relation.rb +0 -2
  48. data/lib/cuprum/collections/relations/parameters.rb +120 -68
  49. data/lib/cuprum/collections/repository.rb +71 -6
  50. data/lib/cuprum/collections/rspec/contracts/query_contracts.rb +23 -4
  51. data/lib/cuprum/collections/rspec/contracts/repository_contracts.rb +18 -0
  52. data/lib/cuprum/collections/rspec/contracts/scope_contracts.rb +51 -0
  53. data/lib/cuprum/collections/rspec/contracts/scopes/builder_contracts.rb +10 -0
  54. data/lib/cuprum/collections/rspec/contracts/scopes/composition_contracts.rb +8 -0
  55. data/lib/cuprum/collections/rspec/contracts/scopes/criteria_contracts.rb +18 -366
  56. data/lib/cuprum/collections/rspec/contracts/scopes/logical_contracts.rb +30 -0
  57. data/lib/cuprum/collections/rspec/contracts/scopes.rb +2 -0
  58. data/lib/cuprum/collections/rspec/contracts.rb +2 -10
  59. data/lib/cuprum/collections/rspec/deferred/adapter_examples.rb +1077 -0
  60. data/lib/cuprum/collections/rspec/deferred/collection_examples.rb +27 -7
  61. data/lib/cuprum/collections/rspec/deferred/commands/assign_one_examples.rb +4 -4
  62. data/lib/cuprum/collections/rspec/deferred/commands/build_one_examples.rb +2 -2
  63. data/lib/cuprum/collections/rspec/deferred/commands/destroy_one_examples.rb +2 -2
  64. data/lib/cuprum/collections/rspec/deferred/commands/find_many_examples.rb +5 -5
  65. data/lib/cuprum/collections/rspec/deferred/commands/find_matching_examples.rb +45 -12
  66. data/lib/cuprum/collections/rspec/deferred/commands/find_one_examples.rb +2 -2
  67. data/lib/cuprum/collections/rspec/deferred/commands/insert_one_examples.rb +1 -1
  68. data/lib/cuprum/collections/rspec/deferred/commands/update_one_examples.rb +1 -1
  69. data/lib/cuprum/collections/rspec/deferred/query_examples.rb +930 -0
  70. data/lib/cuprum/collections/rspec/deferred/relation_examples.rb +48 -17
  71. data/lib/cuprum/collections/rspec/deferred/repository_examples.rb +961 -0
  72. data/lib/cuprum/collections/rspec/deferred/scope_examples.rb +598 -0
  73. data/lib/cuprum/collections/rspec/deferred/scopes/all_examples.rb +391 -0
  74. data/lib/cuprum/collections/rspec/deferred/scopes/builder_examples.rb +857 -0
  75. data/lib/cuprum/collections/rspec/deferred/scopes/composition_examples.rb +93 -0
  76. data/lib/cuprum/collections/rspec/deferred/scopes/conjunction_examples.rb +438 -0
  77. data/lib/cuprum/collections/rspec/deferred/scopes/criteria_examples.rb +1941 -0
  78. data/lib/cuprum/collections/rspec/deferred/scopes/disjunction_examples.rb +415 -0
  79. data/lib/cuprum/collections/rspec/deferred/scopes/none_examples.rb +385 -0
  80. data/lib/cuprum/collections/rspec/deferred/scopes/parser_examples.rb +740 -0
  81. data/lib/cuprum/collections/rspec/deferred/scopes.rb +8 -0
  82. data/lib/cuprum/collections/scope.rb +2 -2
  83. data/lib/cuprum/collections/scopes/container.rb +5 -4
  84. data/lib/cuprum/collections/scopes/criteria/parser.rb +24 -48
  85. data/lib/cuprum/collections/scopes/criteria.rb +7 -6
  86. data/lib/cuprum/collections/version.rb +3 -3
  87. data/lib/cuprum/collections.rb +5 -1
  88. metadata +48 -11
  89. data/lib/cuprum/collections/rspec/contracts/association_contracts.rb +0 -2127
  90. data/lib/cuprum/collections/rspec/contracts/basic.rb +0 -11
  91. data/lib/cuprum/collections/rspec/contracts/collection_contracts.rb +0 -387
  92. data/lib/cuprum/collections/rspec/contracts/command_contracts.rb +0 -169
  93. data/lib/cuprum/collections/rspec/contracts/relation_contracts.rb +0 -1264
@@ -0,0 +1,1941 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/sleeping_king_studios/deferred'
4
+
5
+ require 'cuprum/collections/rspec/deferred/scope_examples'
6
+ require 'cuprum/collections/rspec/deferred/scopes'
7
+ require 'cuprum/collections/rspec/deferred/scopes/composition_examples'
8
+ require 'cuprum/collections/rspec/deferred/scopes/parser_examples'
9
+ require 'cuprum/collections/rspec/fixtures'
10
+
11
+ module Cuprum::Collections::RSpec::Deferred::Scopes
12
+ # Deferred examples for asserting on Criteria scopes.
13
+ module CriteriaExamples
14
+ include RSpec::SleepingKingStudios::Deferred::Provider
15
+ include Cuprum::Collections::RSpec::Deferred::ScopeExamples
16
+ include Cuprum::Collections::RSpec::Deferred::Scopes::CompositionExamples
17
+ include Cuprum::Collections::RSpec::Deferred::Scopes::ParserExamples
18
+
19
+ deferred_examples 'should implement the CriteriaScope methods' \
20
+ do |**deferred_options|
21
+ deferred_context 'when initialized with inverted: true' do
22
+ let(:constructor_options) { super().merge(inverted: true) }
23
+ end
24
+
25
+ deferred_context 'when the scope has multiple criteria' do
26
+ let(:criteria) do
27
+ operators = Cuprum::Collections::Queries::Operators
28
+
29
+ [
30
+ [
31
+ 'author',
32
+ operators::EQUAL,
33
+ 'Ursula K. LeGuin'
34
+ ],
35
+ [
36
+ 'published_at',
37
+ operators::LESS_THAN,
38
+ '1970-01-01'
39
+ ]
40
+ ]
41
+ end
42
+ end
43
+
44
+ deferred_context 'with criteria' do
45
+ let(:criteria) do
46
+ operators = Cuprum::Collections::Queries::Operators
47
+
48
+ [
49
+ ['title', operators::EQUAL, 'Gideon the Ninth'],
50
+ ['author', operators::EQUAL, 'Tamsyn Muir']
51
+ ]
52
+ end
53
+ end
54
+
55
+ let(:criteria) { [] }
56
+
57
+ include_deferred 'should implement the Scope methods', invertible: true
58
+
59
+ include_deferred 'should compose scopes as a CriteriaScope'
60
+
61
+ describe '.new' do
62
+ next if deferred_options.fetch(:skip_constructor, false)
63
+
64
+ it 'should define the constructor' do
65
+ expect(described_class)
66
+ .to be_constructible
67
+ .with(0).arguments
68
+ .and_keywords(:criteria, :inverted)
69
+ .and_any_keywords
70
+ end
71
+ end
72
+
73
+ describe '.build' do
74
+ let(:value) { { 'title' => 'The Word for World is Forest' } }
75
+
76
+ define_method :parse_criteria do |*args, &block|
77
+ return described_class.build(&block).criteria if args.empty?
78
+
79
+ described_class.build(args.first, &block).criteria
80
+ end
81
+
82
+ it 'should define class method' do
83
+ expect(described_class)
84
+ .to respond_to(:build)
85
+ .with(0..1).arguments
86
+ .and_a_block
87
+ end
88
+
89
+ it { expect(described_class.build(value)).to be_a described_class }
90
+
91
+ include_deferred 'should parse Scope criteria'
92
+ end
93
+
94
+ describe '.parse' do
95
+ define_method :parse_criteria do |*args, &block|
96
+ return described_class.parse(&block) if args.empty?
97
+
98
+ described_class.parse(args.first, &block)
99
+ end
100
+
101
+ it 'should define class method' do
102
+ expect(described_class)
103
+ .to respond_to(:parse)
104
+ .with(0..1).arguments
105
+ .and_a_block
106
+ end
107
+
108
+ include_deferred 'should parse Scope criteria'
109
+ end
110
+
111
+ describe '#==' do
112
+ next if deferred_options.fetch(:skip_equality, false)
113
+
114
+ describe 'with a scope with the same class' do
115
+ let(:other_criteria) { [] }
116
+ let(:other_inverted) { false }
117
+ let(:other) do
118
+ described_class.new(
119
+ criteria: other_criteria,
120
+ inverted: other_inverted
121
+ )
122
+ end
123
+
124
+ describe 'with empty criteria' do
125
+ let(:other_criteria) { [] }
126
+
127
+ it { expect(subject == other).to be true }
128
+
129
+ describe 'with inverted: true' do
130
+ let(:other_inverted) { true }
131
+
132
+ it { expect(subject == other).to be false }
133
+ end
134
+ end
135
+
136
+ describe 'with non-matching criteria' do
137
+ let(:other_criteria) do
138
+ operators = Cuprum::Collections::Queries::Operators
139
+
140
+ [
141
+ [
142
+ 'ok',
143
+ operators::EQUAL,
144
+ true
145
+ ]
146
+ ]
147
+ end
148
+
149
+ it { expect(subject == other).to be false }
150
+
151
+ describe 'with inverted: true' do
152
+ let(:other_inverted) { true }
153
+
154
+ it { expect(subject == other).to be false }
155
+ end
156
+ end
157
+
158
+ wrap_deferred 'with criteria' do
159
+ describe 'with empty criteria' do
160
+ let(:other_criteria) { [] }
161
+
162
+ it { expect(subject == other).to be false }
163
+
164
+ describe 'with inverted: true' do
165
+ let(:other_inverted) { true }
166
+
167
+ it { expect(subject == other).to be false }
168
+ end
169
+ end
170
+
171
+ describe 'with non-matching criteria' do
172
+ let(:other_criteria) do
173
+ operators = Cuprum::Collections::Queries::Operators
174
+
175
+ [
176
+ [
177
+ 'ok',
178
+ operators::EQUAL,
179
+ true
180
+ ]
181
+ ]
182
+ end
183
+
184
+ it { expect(subject == other).to be false }
185
+
186
+ describe 'with inverted: true' do
187
+ let(:other_inverted) { true }
188
+
189
+ it { expect(subject == other).to be false }
190
+ end
191
+ end
192
+
193
+ describe 'with matching criteria' do
194
+ let(:other_criteria) { subject.criteria }
195
+
196
+ it { expect(subject == other).to be true }
197
+
198
+ describe 'with inverted: true' do
199
+ let(:other_inverted) { true }
200
+
201
+ it { expect(subject == other).to be false }
202
+ end
203
+ end
204
+ end
205
+
206
+ wrap_deferred 'when initialized with inverted: true' do
207
+ describe 'with empty criteria' do
208
+ let(:other_criteria) { [] }
209
+
210
+ it { expect(subject == other).to be false }
211
+
212
+ describe 'with inverted: true' do
213
+ let(:other_inverted) { true }
214
+
215
+ it { expect(subject == other).to be true }
216
+ end
217
+ end
218
+
219
+ describe 'with non-matching criteria' do
220
+ let(:other_criteria) do
221
+ operators = Cuprum::Collections::Queries::Operators
222
+
223
+ [
224
+ [
225
+ 'ok',
226
+ operators::EQUAL,
227
+ true
228
+ ]
229
+ ]
230
+ end
231
+
232
+ it { expect(subject == other).to be false }
233
+
234
+ describe 'with inverted: true' do
235
+ let(:other_inverted) { true }
236
+
237
+ it { expect(subject == other).to be false }
238
+ end
239
+ end
240
+
241
+ wrap_deferred 'with criteria' do
242
+ describe 'with empty criteria' do
243
+ let(:other_criteria) { [] }
244
+
245
+ it { expect(subject == other).to be false }
246
+
247
+ describe 'with inverted: true' do
248
+ let(:other_inverted) { true }
249
+
250
+ it { expect(subject == other).to be false }
251
+ end
252
+ end
253
+
254
+ describe 'with non-matching criteria' do
255
+ let(:other_criteria) do
256
+ operators = Cuprum::Collections::Queries::Operators
257
+
258
+ [
259
+ [
260
+ 'ok',
261
+ operators::EQUAL,
262
+ true
263
+ ]
264
+ ]
265
+ end
266
+
267
+ it { expect(subject == other).to be false }
268
+
269
+ describe 'with inverted: true' do
270
+ let(:other_inverted) { true }
271
+
272
+ it { expect(subject == other).to be false }
273
+ end
274
+ end
275
+
276
+ describe 'with matching criteria' do
277
+ let(:other_criteria) { subject.criteria }
278
+
279
+ it { expect(subject == other).to be false }
280
+
281
+ describe 'with inverted: true' do
282
+ let(:other_inverted) { true }
283
+
284
+ it { expect(subject == other).to be true }
285
+ end
286
+ end
287
+ end
288
+ end
289
+ end
290
+
291
+ describe 'with a scope with the same type' do
292
+ let(:other_criteria) { [] }
293
+ let(:other_inverted) { false }
294
+ let(:other) do
295
+ described_class.new(
296
+ criteria: other_criteria,
297
+ inverted: other_inverted
298
+ )
299
+ end
300
+
301
+ example_class 'Spec::CustomScope',
302
+ Cuprum::Collections::Scopes::Base \
303
+ do |klass|
304
+ klass.include Cuprum::Collections::Scopes::Criteria
305
+ end
306
+
307
+ describe 'with empty criteria' do
308
+ let(:other_criteria) { [] }
309
+
310
+ it { expect(subject == other).to be true }
311
+
312
+ describe 'with inverted: true' do
313
+ let(:other_inverted) { true }
314
+
315
+ it { expect(subject == other).to be false }
316
+ end
317
+ end
318
+
319
+ describe 'with non-matching criteria' do
320
+ let(:other_criteria) do
321
+ operators = Cuprum::Collections::Queries::Operators
322
+
323
+ [
324
+ [
325
+ 'ok',
326
+ operators::EQUAL,
327
+ true
328
+ ]
329
+ ]
330
+ end
331
+
332
+ it { expect(subject == other).to be false }
333
+
334
+ describe 'with inverted: true' do
335
+ let(:other_inverted) { true }
336
+
337
+ it { expect(subject == other).to be false }
338
+ end
339
+ end
340
+
341
+ wrap_deferred 'with criteria' do
342
+ describe 'with empty criteria' do
343
+ let(:other_criteria) { [] }
344
+
345
+ it { expect(subject == other).to be false }
346
+
347
+ describe 'with inverted: true' do
348
+ let(:other_inverted) { true }
349
+
350
+ it { expect(subject == other).to be false }
351
+ end
352
+ end
353
+
354
+ describe 'with non-matching criteria' do
355
+ let(:other_criteria) do
356
+ operators = Cuprum::Collections::Queries::Operators
357
+
358
+ [
359
+ [
360
+ 'ok',
361
+ operators::EQUAL,
362
+ true
363
+ ]
364
+ ]
365
+ end
366
+
367
+ it { expect(subject == other).to be false }
368
+
369
+ describe 'with inverted: true' do
370
+ let(:other_inverted) { true }
371
+
372
+ it { expect(subject == other).to be false }
373
+ end
374
+ end
375
+
376
+ describe 'with matching criteria' do
377
+ let(:other_criteria) { subject.criteria }
378
+
379
+ it { expect(subject == other).to be true }
380
+
381
+ describe 'with inverted: true' do
382
+ let(:other_inverted) { true }
383
+
384
+ it { expect(subject == other).to be false }
385
+ end
386
+ end
387
+ end
388
+
389
+ wrap_deferred 'when initialized with inverted: true' do
390
+ describe 'with empty criteria' do
391
+ let(:other_criteria) { [] }
392
+
393
+ it { expect(subject == other).to be false }
394
+
395
+ describe 'with inverted: true' do
396
+ let(:other_inverted) { true }
397
+
398
+ it { expect(subject == other).to be true }
399
+ end
400
+ end
401
+
402
+ describe 'with non-matching criteria' do
403
+ let(:other_criteria) do
404
+ operators = Cuprum::Collections::Queries::Operators
405
+
406
+ [
407
+ [
408
+ 'ok',
409
+ operators::EQUAL,
410
+ true
411
+ ]
412
+ ]
413
+ end
414
+
415
+ it { expect(subject == other).to be false }
416
+
417
+ describe 'with inverted: true' do
418
+ let(:other_inverted) { true }
419
+
420
+ it { expect(subject == other).to be false }
421
+ end
422
+ end
423
+
424
+ wrap_deferred 'with criteria' do
425
+ describe 'with empty criteria' do
426
+ let(:other_criteria) { [] }
427
+
428
+ it { expect(subject == other).to be false }
429
+
430
+ describe 'with inverted: true' do
431
+ let(:other_inverted) { true }
432
+
433
+ it { expect(subject == other).to be false }
434
+ end
435
+ end
436
+
437
+ describe 'with non-matching criteria' do
438
+ let(:other_criteria) do
439
+ operators = Cuprum::Collections::Queries::Operators
440
+
441
+ [
442
+ [
443
+ 'ok',
444
+ operators::EQUAL,
445
+ true
446
+ ]
447
+ ]
448
+ end
449
+
450
+ it { expect(subject == other).to be false }
451
+
452
+ describe 'with inverted: true' do
453
+ let(:other_inverted) { true }
454
+
455
+ it { expect(subject == other).to be false }
456
+ end
457
+ end
458
+
459
+ describe 'with matching criteria' do
460
+ let(:other_criteria) { subject.criteria }
461
+
462
+ it { expect(subject == other).to be false }
463
+
464
+ describe 'with inverted: true' do
465
+ let(:other_inverted) { true }
466
+
467
+ it { expect(subject == other).to be true }
468
+ end
469
+ end
470
+ end
471
+ end
472
+ end
473
+ end
474
+
475
+ describe '#as_json' do
476
+ let(:expected) do
477
+ {
478
+ 'criteria' => subject.criteria,
479
+ 'inverted' => subject.inverted?,
480
+ 'type' => subject.type
481
+ }
482
+ end
483
+
484
+ it { expect(subject.as_json).to be == expected }
485
+
486
+ wrap_deferred 'when initialized with inverted: true' do
487
+ it { expect(subject.as_json).to be == expected }
488
+ end
489
+
490
+ wrap_deferred 'with criteria' do
491
+ it { expect(subject.as_json).to be == expected }
492
+
493
+ wrap_deferred 'when initialized with inverted: true' do
494
+ it { expect(subject.as_json).to be == expected }
495
+ end
496
+ end
497
+ end
498
+
499
+ describe '#call' do
500
+ next if deferred_options.fetch(:abstract, false)
501
+
502
+ it { expect(subject).to respond_to(:call) }
503
+
504
+ include_deferred 'should filter data by criteria'
505
+
506
+ context 'when the scope is inverted' do
507
+ subject { super().invert }
508
+
509
+ let(:matching_data) { data - filtered_data }
510
+
511
+ include_deferred 'should filter data by criteria',
512
+ ignore_invalid: true,
513
+ inverted: true
514
+ end
515
+ end
516
+
517
+ describe '#criteria' do
518
+ include_examples 'should define reader', :criteria, -> { criteria }
519
+
520
+ wrap_deferred 'with criteria' do
521
+ it { expect(subject.criteria).to be == criteria }
522
+ end
523
+ end
524
+
525
+ describe '#empty?' do
526
+ include_examples 'should define predicate', :empty?, true
527
+
528
+ wrap_deferred 'with criteria' do
529
+ it { expect(subject.empty?).to be false }
530
+ end
531
+ end
532
+
533
+ describe '#invert' do
534
+ shared_examples 'should invert the criteria' do
535
+ it { expect(copy.criteria).to be == [] }
536
+
537
+ context 'when the scope has an equality criterion' do
538
+ let(:criteria) do
539
+ described_class.parse({
540
+ 'title' => 'The Word for World is Forest'
541
+ })
542
+ end
543
+ let(:expected) do
544
+ operators = Cuprum::Collections::Queries::Operators
545
+
546
+ [
547
+ [
548
+ 'title',
549
+ operators::NOT_EQUAL,
550
+ 'The Word for World is Forest'
551
+ ]
552
+ ]
553
+ end
554
+
555
+ it { expect(copy.criteria).to be == expected }
556
+ end
557
+
558
+ context 'when the scope has a greater than criterion' do
559
+ let(:criteria) do
560
+ described_class.parse do |scope|
561
+ { 'published_at' => scope.greater_than('1972-03-13') }
562
+ end
563
+ end
564
+ let(:expected) do
565
+ operators = Cuprum::Collections::Queries::Operators
566
+
567
+ [
568
+ [
569
+ 'published_at',
570
+ operators::LESS_THAN_OR_EQUAL_TO,
571
+ '1972-03-13'
572
+ ]
573
+ ]
574
+ end
575
+
576
+ it { expect(copy.criteria).to be == expected }
577
+ end
578
+
579
+ context 'when the scope has a greater than or equal to criterion' do
580
+ let(:criteria) do
581
+ described_class.parse do |scope|
582
+ {
583
+ 'published_at' => scope.greater_than_or_equal_to(
584
+ '1972-03-13'
585
+ )
586
+ }
587
+ end
588
+ end
589
+ let(:expected) do
590
+ operators = Cuprum::Collections::Queries::Operators
591
+
592
+ [
593
+ [
594
+ 'published_at',
595
+ operators::LESS_THAN,
596
+ '1972-03-13'
597
+ ]
598
+ ]
599
+ end
600
+
601
+ it { expect(copy.criteria).to be == expected }
602
+ end
603
+
604
+ context 'when the scope has a less than criterion' do
605
+ let(:criteria) do
606
+ described_class.parse do |scope|
607
+ { 'published_at' => scope.less_than('1972-03-13') }
608
+ end
609
+ end
610
+ let(:expected) do
611
+ operators = Cuprum::Collections::Queries::Operators
612
+
613
+ [
614
+ [
615
+ 'published_at',
616
+ operators::GREATER_THAN_OR_EQUAL_TO,
617
+ '1972-03-13'
618
+ ]
619
+ ]
620
+ end
621
+
622
+ it { expect(copy.criteria).to be == expected }
623
+ end
624
+
625
+ context 'when the scope has a less than or equal to criterion' do
626
+ let(:criteria) do
627
+ described_class.parse do |scope|
628
+ {
629
+ 'published_at' => scope.less_than_or_equal_to(
630
+ '1972-03-13'
631
+ )
632
+ }
633
+ end
634
+ end
635
+ let(:expected) do
636
+ operators = Cuprum::Collections::Queries::Operators
637
+
638
+ [
639
+ [
640
+ 'published_at',
641
+ operators::GREATER_THAN,
642
+ '1972-03-13'
643
+ ]
644
+ ]
645
+ end
646
+
647
+ it { expect(copy.criteria).to be == expected }
648
+ end
649
+
650
+ context 'when the scope has a not equal criterion' do
651
+ let(:criteria) do
652
+ described_class.parse do |scope|
653
+ { 'author' => scope.not_equal('J.R.R. Tolkien') }
654
+ end
655
+ end
656
+ let(:expected) do
657
+ operators = Cuprum::Collections::Queries::Operators
658
+
659
+ [
660
+ [
661
+ 'author',
662
+ operators::EQUAL,
663
+ 'J.R.R. Tolkien'
664
+ ]
665
+ ]
666
+ end
667
+
668
+ it { expect(copy.criteria).to be == expected }
669
+ end
670
+
671
+ context 'when the scope has a not one of criterion' do
672
+ let(:criteria) do
673
+ described_class.parse do |scope|
674
+ titles = ['The Fellowship Of The Ring', 'The Two Towers']
675
+
676
+ { 'title' => scope.not_one_of(titles) }
677
+ end
678
+ end
679
+ let(:expected) do
680
+ operators = Cuprum::Collections::Queries::Operators
681
+
682
+ [
683
+ [
684
+ 'title',
685
+ operators::ONE_OF,
686
+ ['The Fellowship Of The Ring', 'The Two Towers']
687
+ ]
688
+ ]
689
+ end
690
+
691
+ it { expect(copy.criteria).to be == expected }
692
+ end
693
+
694
+ context 'when the scope has a one of criterion' do
695
+ let(:criteria) do
696
+ described_class.parse do |scope|
697
+ titles = ['The Fellowship Of The Ring', 'The Two Towers']
698
+
699
+ { 'title' => scope.one_of(titles) }
700
+ end
701
+ end
702
+ let(:expected) do
703
+ operators = Cuprum::Collections::Queries::Operators
704
+
705
+ [
706
+ [
707
+ 'title',
708
+ operators::NOT_ONE_OF,
709
+ ['The Fellowship Of The Ring', 'The Two Towers']
710
+ ]
711
+ ]
712
+ end
713
+
714
+ it { expect(copy.criteria).to be == expected }
715
+ end
716
+
717
+ context 'when the scope has multiple criteria' do
718
+ let(:criteria) do
719
+ described_class.parse do |scope|
720
+ titles = ['The Fellowship Of The Ring', 'The Two Towers']
721
+
722
+ {
723
+ 'author' => 'J.R.R. Tolkien',
724
+ 'title' => scope.not_one_of(titles)
725
+ }
726
+ end
727
+ end
728
+ let(:expected) do
729
+ operators = Cuprum::Collections::Queries::Operators
730
+
731
+ [
732
+ [
733
+ 'author',
734
+ operators::NOT_EQUAL,
735
+ 'J.R.R. Tolkien'
736
+ ],
737
+ [
738
+ 'title',
739
+ operators::ONE_OF,
740
+ ['The Fellowship Of The Ring', 'The Two Towers']
741
+ ]
742
+ ]
743
+ end
744
+
745
+ it { expect(copy.criteria).to be == expected }
746
+ end
747
+
748
+ context 'when initialized with uninvertible criteria' do
749
+ next if deferred_options.fetch(:ignore_uninvertible, false)
750
+
751
+ let(:criteria) do
752
+ [
753
+ [
754
+ 'title',
755
+ 'random',
756
+ nil
757
+ ]
758
+ ]
759
+ end
760
+ let(:error_class) do
761
+ Cuprum::Collections::Queries::UninvertibleOperatorException
762
+ end
763
+ let(:error_message) { 'uninvertible operator "random"' }
764
+
765
+ it 'should raise an exception' do
766
+ expect { subject.invert }
767
+ .to raise_error error_class, error_message
768
+ end
769
+ end
770
+ end
771
+
772
+ let(:copy) { subject.invert }
773
+
774
+ it { expect(copy).to be_a described_class }
775
+
776
+ it { expect(copy.inverted?).to be true }
777
+
778
+ include_examples 'should invert the criteria'
779
+
780
+ wrap_deferred 'when initialized with inverted: true' do
781
+ it { expect(copy).to be_a described_class }
782
+
783
+ it { expect(copy.inverted?).to be false }
784
+
785
+ include_examples 'should invert the criteria'
786
+ end
787
+ end
788
+
789
+ describe '#inverted?' do
790
+ include_examples 'should define predicate', :inverted?, false
791
+
792
+ wrap_deferred 'when initialized with inverted: true' do
793
+ it { expect(subject.inverted?).to be true }
794
+ end
795
+ end
796
+
797
+ describe '#type' do
798
+ include_examples 'should define reader', :type, :criteria
799
+ end
800
+
801
+ describe '#with_criteria' do
802
+ let(:new_criteria) { ['author', 'eq', 'Ursula K. LeGuin'] }
803
+
804
+ it { expect(subject).to respond_to(:with_criteria).with(1).argument }
805
+
806
+ it 'should return a scope' do
807
+ expect(subject.with_criteria(new_criteria)).to be_a described_class
808
+ end
809
+
810
+ it "should not change the original scope's criteria" do
811
+ expect { subject.with_criteria(new_criteria) }
812
+ .not_to change(subject, :criteria)
813
+ end
814
+
815
+ it "should set the copied scope's criteria" do
816
+ expect(subject.with_criteria(new_criteria).criteria)
817
+ .to be == new_criteria
818
+ end
819
+
820
+ it 'should return an uninverted scope' do
821
+ expect(subject.with_criteria(new_criteria).inverted?).to be false
822
+ end
823
+
824
+ wrap_deferred 'with criteria' do
825
+ it "should not change the original scope's criteria" do
826
+ expect { subject.with_criteria(new_criteria) }
827
+ .not_to change(subject, :criteria)
828
+ end
829
+
830
+ it "should set the copied scope's criteria" do
831
+ expect(subject.with_criteria(new_criteria).criteria)
832
+ .to be == new_criteria
833
+ end
834
+ end
835
+
836
+ wrap_deferred 'when initialized with inverted: true' do
837
+ it 'should return an inverted scope' do
838
+ expect(subject.with_criteria(new_criteria).inverted?).to be true
839
+ end
840
+ end
841
+ end
842
+ end
843
+
844
+ deferred_examples 'should compose scopes as a CriteriaScope' do
845
+ describe '#and' do
846
+ include_deferred 'with contexts for composable scopes'
847
+
848
+ describe 'with a block' do
849
+ let(:block) { ->(_) { { 'title' => 'A Wizard of Earthsea' } } }
850
+ let(:expected) do
851
+ operators = Cuprum::Collections::Queries::Operators
852
+ criteria = [
853
+ [
854
+ 'title',
855
+ operators::EQUAL,
856
+ 'A Wizard of Earthsea'
857
+ ]
858
+ ]
859
+
860
+ Cuprum::Collections::Scopes::CriteriaScope.new(
861
+ criteria: [*self.criteria, *criteria]
862
+ )
863
+ end
864
+
865
+ it { expect(subject.and(&block)).to be == expected }
866
+
867
+ wrap_deferred 'when the scope has multiple criteria' do
868
+ it { expect(subject.and(&block)).to be == expected }
869
+ end
870
+ end
871
+
872
+ describe 'with a hash' do
873
+ let(:value) { { 'title' => 'A Wizard of Earthsea' } }
874
+ let(:expected) do
875
+ operators = Cuprum::Collections::Queries::Operators
876
+ criteria = [
877
+ [
878
+ 'title',
879
+ operators::EQUAL,
880
+ 'A Wizard of Earthsea'
881
+ ]
882
+ ]
883
+
884
+ Cuprum::Collections::Scopes::CriteriaScope.new(
885
+ criteria: [*self.criteria, *criteria]
886
+ )
887
+ end
888
+
889
+ it { expect(subject.and(value)).to be == expected }
890
+
891
+ wrap_deferred 'when the scope has multiple criteria' do
892
+ it { expect(subject.and(value)).to be == expected }
893
+ end
894
+ end
895
+
896
+ wrap_deferred 'with an all scope' do
897
+ it { expect(subject.and(original)).to be == original }
898
+
899
+ wrap_deferred 'when the scope has multiple criteria' do
900
+ it { expect(subject.and(original)).to be subject }
901
+ end
902
+ end
903
+
904
+ wrap_deferred 'with a none scope' do
905
+ it { expect(subject.and(original)).to be == original }
906
+
907
+ wrap_deferred 'when the scope has multiple criteria' do
908
+ it { expect(subject.and(original)).to be == original }
909
+ end
910
+ end
911
+
912
+ wrap_deferred 'with an empty conjunction scope' do
913
+ it { expect(subject.and(original)).to be subject }
914
+
915
+ wrap_deferred 'when the scope has multiple criteria' do
916
+ it { expect(subject.and(original)).to be subject }
917
+ end
918
+ end
919
+
920
+ wrap_deferred 'with an empty criteria scope' do
921
+ it { expect(subject.and(original)).to be subject }
922
+
923
+ wrap_deferred 'when the scope has multiple criteria' do
924
+ it { expect(subject.and(original)).to be subject }
925
+ end
926
+ end
927
+
928
+ context 'with an empty inverted criteria scope' do
929
+ include_deferred 'with an empty criteria scope'
930
+
931
+ let(:inverted) { original.invert }
932
+
933
+ it { expect(subject.and(inverted)).to be subject }
934
+
935
+ wrap_deferred 'when the scope has multiple criteria' do
936
+ it { expect(subject.and(original)).to be subject }
937
+ end
938
+ end
939
+
940
+ wrap_deferred 'with an empty disjunction scope' do
941
+ it { expect(subject.and(original)).to be subject }
942
+
943
+ wrap_deferred 'when the scope has multiple criteria' do
944
+ it { expect(subject.and(original)).to be subject }
945
+ end
946
+ end
947
+
948
+ wrap_deferred 'with a non-empty conjunction scope' do
949
+ it { expect(subject.and(original)).to be == original }
950
+
951
+ wrap_deferred 'when the scope has multiple criteria' do
952
+ let(:expected) do
953
+ Cuprum::Collections::Scopes::ConjunctionScope.new(
954
+ scopes: [subject, *original.scopes]
955
+ )
956
+ end
957
+
958
+ it { expect(subject.and(original)).to be == expected }
959
+ end
960
+ end
961
+
962
+ wrap_deferred 'with a non-empty criteria scope' do
963
+ let(:expected) do
964
+ Cuprum::Collections::Scopes::CriteriaScope.new(
965
+ criteria: [*subject.criteria, *original.criteria]
966
+ )
967
+ end
968
+
969
+ it { expect(subject.and(original)).to be == expected }
970
+
971
+ wrap_deferred 'when the scope has multiple criteria' do
972
+ it { expect(subject.and(original)).to be == expected }
973
+ end
974
+ end
975
+
976
+ context 'with a non-empty inverted criteria scope' do
977
+ include_deferred 'with a non-empty criteria scope'
978
+
979
+ let(:inverted) { original.invert }
980
+
981
+ it { expect(subject.and(inverted)).to be == inverted }
982
+
983
+ wrap_deferred 'when the scope has multiple criteria' do
984
+ let(:expected) do
985
+ Cuprum::Collections::Scopes::ConjunctionScope.new(
986
+ scopes: [subject, inverted]
987
+ )
988
+ end
989
+
990
+ it { expect(subject.and(inverted)).to be == expected }
991
+ end
992
+ end
993
+
994
+ wrap_deferred 'with a non-empty disjunction scope' do
995
+ it { expect(subject.and(original)).to be == original }
996
+
997
+ wrap_deferred 'when the scope has multiple criteria' do
998
+ let(:expected) do
999
+ Cuprum::Collections::Scopes::ConjunctionScope.new(
1000
+ scopes: [subject, original]
1001
+ )
1002
+ end
1003
+
1004
+ it { expect(subject.and(original)).to be == expected }
1005
+ end
1006
+ end
1007
+
1008
+ wrap_deferred 'when initialized with inverted: true' do
1009
+ describe 'with a block' do
1010
+ let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } }
1011
+ let(:expected) do
1012
+ Cuprum::Collections::Scope.new(&block)
1013
+ end
1014
+
1015
+ it { expect(subject.and(&block)).to be == expected }
1016
+
1017
+ wrap_deferred 'when the scope has multiple criteria' do
1018
+ let(:expected) do
1019
+ wrapped = Cuprum::Collections::Scope.new(&block)
1020
+
1021
+ Cuprum::Collections::Scopes::ConjunctionScope.new(
1022
+ scopes: [subject, wrapped]
1023
+ )
1024
+ end
1025
+
1026
+ it { expect(subject.and(&block)).to be == expected }
1027
+ end
1028
+ end
1029
+
1030
+ describe 'with a hash' do
1031
+ let(:value) { { 'title' => 'A Wizard of Earthsea' } }
1032
+ let(:expected) do
1033
+ Cuprum::Collections::Scope.new(value)
1034
+ end
1035
+
1036
+ it { expect(subject.and(value)).to be == expected }
1037
+
1038
+ wrap_deferred 'when the scope has multiple criteria' do
1039
+ let(:expected) do
1040
+ wrapped = Cuprum::Collections::Scope.new(value)
1041
+
1042
+ Cuprum::Collections::Scopes::ConjunctionScope.new(
1043
+ scopes: [subject, wrapped]
1044
+ )
1045
+ end
1046
+
1047
+ it { expect(subject.and(value)).to be == expected }
1048
+ end
1049
+ end
1050
+
1051
+ wrap_deferred 'with an empty criteria scope' do
1052
+ it { expect(subject.and(original)).to be subject }
1053
+
1054
+ wrap_deferred 'when the scope has multiple criteria' do
1055
+ it { expect(subject.and(original)).to be subject }
1056
+ end
1057
+ end
1058
+
1059
+ context 'with an empty inverted criteria scope' do
1060
+ include_deferred 'with an empty criteria scope'
1061
+
1062
+ let(:inverted) { original.invert }
1063
+
1064
+ it { expect(subject.and(inverted)).to be subject }
1065
+
1066
+ wrap_deferred 'when the scope has multiple criteria' do
1067
+ it { expect(subject.and(inverted)).to be subject }
1068
+ end
1069
+ end
1070
+
1071
+ wrap_deferred 'with a non-empty criteria scope' do
1072
+ it { expect(subject.and(original)).to be == original }
1073
+
1074
+ wrap_deferred 'when the scope has multiple criteria' do
1075
+ let(:expected) do
1076
+ Cuprum::Collections::Scopes::ConjunctionScope.new(
1077
+ scopes: [subject, original]
1078
+ )
1079
+ end
1080
+
1081
+ it { expect(subject.and(original)).to be == expected }
1082
+ end
1083
+ end
1084
+
1085
+ context 'with a non-empty inverted criteria scope' do
1086
+ include_deferred 'with a non-empty criteria scope'
1087
+
1088
+ let(:inverted) { original.invert }
1089
+
1090
+ it { expect(subject.and(inverted)).to be == inverted }
1091
+
1092
+ wrap_deferred 'when the scope has multiple criteria' do
1093
+ let(:expected) do
1094
+ Cuprum::Collections::Scopes::ConjunctionScope.new(
1095
+ scopes: [subject, inverted]
1096
+ )
1097
+ end
1098
+
1099
+ it { expect(subject.and(inverted)).to be == expected }
1100
+ end
1101
+ end
1102
+ end
1103
+ end
1104
+
1105
+ describe '#not' do
1106
+ include_deferred 'with contexts for composable scopes'
1107
+
1108
+ describe 'with a block' do
1109
+ let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } }
1110
+ let(:expected) do
1111
+ Cuprum::Collections::Scope.new(&block).invert
1112
+ end
1113
+
1114
+ it { expect(subject.not(&block)).to be == expected }
1115
+
1116
+ wrap_deferred 'when the scope has multiple criteria' do
1117
+ let(:expected) do
1118
+ wrapped = Cuprum::Collections::Scope.new(&block)
1119
+
1120
+ Cuprum::Collections::Scopes::ConjunctionScope.new(
1121
+ scopes: [subject, wrapped.invert]
1122
+ )
1123
+ end
1124
+
1125
+ it { expect(subject.not(&block)).to be == expected }
1126
+ end
1127
+ end
1128
+
1129
+ describe 'with a hash' do
1130
+ let(:value) { { 'title' => 'A Wizard of Earthsea' } }
1131
+ let(:expected) do
1132
+ Cuprum::Collections::Scope.new(value).invert
1133
+ end
1134
+
1135
+ it { expect(subject.not(value)).to be == expected }
1136
+
1137
+ wrap_deferred 'when the scope has multiple criteria' do
1138
+ let(:expected) do
1139
+ wrapped = Cuprum::Collections::Scope.new(value)
1140
+
1141
+ Cuprum::Collections::Scopes::ConjunctionScope.new(
1142
+ scopes: [subject, wrapped.invert]
1143
+ )
1144
+ end
1145
+
1146
+ it { expect(subject.not(value)).to be == expected }
1147
+ end
1148
+ end
1149
+
1150
+ wrap_deferred 'with an all scope' do
1151
+ it { expect(subject.not(original)).to be == original.invert }
1152
+
1153
+ wrap_deferred 'when the scope has multiple criteria' do
1154
+ it { expect(subject.not(original)).to be == original.invert }
1155
+ end
1156
+ end
1157
+
1158
+ wrap_deferred 'with a none scope' do
1159
+ it { expect(subject.not(original)).to be == original.invert }
1160
+
1161
+ wrap_deferred 'when the scope has multiple criteria' do
1162
+ it { expect(subject.not(original)).to be subject }
1163
+ end
1164
+ end
1165
+
1166
+ wrap_deferred 'with an empty conjunction scope' do
1167
+ it { expect(subject.not(original)).to be subject }
1168
+
1169
+ wrap_deferred 'when the scope has multiple criteria' do
1170
+ it { expect(subject.not(original)).to be subject }
1171
+ end
1172
+ end
1173
+
1174
+ wrap_deferred 'with an empty criteria scope' do
1175
+ it { expect(subject.not(original)).to be subject }
1176
+
1177
+ wrap_deferred 'when the scope has multiple criteria' do
1178
+ it { expect(subject.not(original)).to be subject }
1179
+ end
1180
+ end
1181
+
1182
+ context 'with an empty inverted criteria scope' do
1183
+ include_deferred 'with an empty criteria scope'
1184
+
1185
+ let(:inverted) { original.invert }
1186
+
1187
+ it { expect(subject.not(inverted)).to be subject }
1188
+
1189
+ wrap_deferred 'when the scope has multiple criteria' do
1190
+ it { expect(subject.not(original)).to be subject }
1191
+ end
1192
+ end
1193
+
1194
+ wrap_deferred 'with an empty disjunction scope' do
1195
+ it { expect(subject.not(original)).to be subject }
1196
+
1197
+ wrap_deferred 'when the scope has multiple criteria' do
1198
+ it { expect(subject.not(original)).to be subject }
1199
+ end
1200
+ end
1201
+
1202
+ wrap_deferred 'with a non-empty conjunction scope' do
1203
+ it { expect(subject.not(original)).to be == original.invert }
1204
+
1205
+ wrap_deferred 'when the scope has multiple criteria' do
1206
+ let(:expected) do
1207
+ Cuprum::Collections::Scopes::ConjunctionScope.new(
1208
+ scopes: [subject, original.invert]
1209
+ )
1210
+ end
1211
+
1212
+ it { expect(subject.not(original)).to be == expected }
1213
+ end
1214
+ end
1215
+
1216
+ wrap_deferred 'with a non-empty criteria scope' do
1217
+ it { expect(subject.not(original)).to be == original.invert }
1218
+
1219
+ wrap_deferred 'when the scope has multiple criteria' do
1220
+ let(:expected) do
1221
+ Cuprum::Collections::Scopes::ConjunctionScope.new(
1222
+ scopes: [subject, original.invert]
1223
+ )
1224
+ end
1225
+
1226
+ it { expect(subject.not(original)).to be == expected }
1227
+ end
1228
+ end
1229
+
1230
+ context 'with a non-empty inverted criteria scope' do
1231
+ include_deferred 'with a non-empty criteria scope'
1232
+
1233
+ let(:inverted) { original.invert }
1234
+
1235
+ it { expect(subject.not(inverted)).to be == original }
1236
+
1237
+ wrap_deferred 'when the scope has multiple criteria' do
1238
+ let(:expected) do
1239
+ Cuprum::Collections::Scopes::CriteriaScope.new(
1240
+ criteria: [*subject.criteria, *original.criteria]
1241
+ )
1242
+ end
1243
+
1244
+ it { expect(subject.not(inverted)).to be == expected }
1245
+ end
1246
+ end
1247
+
1248
+ wrap_deferred 'with a non-empty disjunction scope' do
1249
+ it { expect(subject.not(original)).to be == original.invert }
1250
+
1251
+ wrap_deferred 'when the scope has multiple criteria' do
1252
+ let(:expected) do
1253
+ Cuprum::Collections::Scopes::ConjunctionScope.new(
1254
+ scopes: [subject, *original.invert.scopes]
1255
+ )
1256
+ end
1257
+
1258
+ it { expect(subject.not(original)).to be == expected }
1259
+ end
1260
+ end
1261
+
1262
+ wrap_deferred 'when initialized with inverted: true' do
1263
+ describe 'with a block' do
1264
+ let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } }
1265
+ let(:expected) do
1266
+ Cuprum::Collections::Scope.new(&block).invert
1267
+ end
1268
+
1269
+ it { expect(subject.not(&block)).to be == expected }
1270
+
1271
+ wrap_deferred 'when the scope has multiple criteria' do
1272
+ let(:expected) do
1273
+ wrapped = Cuprum::Collections::Scope.new(&block)
1274
+
1275
+ Cuprum::Collections::Scopes::ConjunctionScope.new(
1276
+ scopes: [subject, wrapped.invert]
1277
+ )
1278
+ end
1279
+
1280
+ it { expect(subject.not(&block)).to be == expected }
1281
+ end
1282
+ end
1283
+
1284
+ describe 'with a hash' do
1285
+ let(:value) { { 'title' => 'A Wizard of Earthsea' } }
1286
+ let(:expected) do
1287
+ Cuprum::Collections::Scope.new(value).invert
1288
+ end
1289
+
1290
+ it { expect(subject.not(value)).to be == expected }
1291
+
1292
+ wrap_deferred 'when the scope has multiple criteria' do
1293
+ let(:expected) do
1294
+ wrapped = Cuprum::Collections::Scope.new(value)
1295
+
1296
+ Cuprum::Collections::Scopes::ConjunctionScope.new(
1297
+ scopes: [subject, wrapped.invert]
1298
+ )
1299
+ end
1300
+
1301
+ it { expect(subject.not(value)).to be == expected }
1302
+ end
1303
+ end
1304
+
1305
+ wrap_deferred 'with an all scope' do
1306
+ it { expect(subject.not(original)).to be == original.invert }
1307
+
1308
+ wrap_deferred 'when the scope has multiple criteria' do
1309
+ it { expect(subject.not(original)).to be == original.invert }
1310
+ end
1311
+ end
1312
+
1313
+ wrap_deferred 'with a none scope' do
1314
+ it { expect(subject.not(original)).to be == original.invert }
1315
+
1316
+ wrap_deferred 'when the scope has multiple criteria' do
1317
+ it { expect(subject.not(original)).to be subject }
1318
+ end
1319
+ end
1320
+
1321
+ wrap_deferred 'with an empty conjunction scope' do
1322
+ it { expect(subject.not(original)).to be subject }
1323
+
1324
+ wrap_deferred 'when the scope has multiple criteria' do
1325
+ it { expect(subject.not(original)).to be subject }
1326
+ end
1327
+ end
1328
+
1329
+ wrap_deferred 'with an empty criteria scope' do
1330
+ it { expect(subject.not(original)).to be subject }
1331
+
1332
+ wrap_deferred 'when the scope has multiple criteria' do
1333
+ it { expect(subject.not(original)).to be subject }
1334
+ end
1335
+ end
1336
+
1337
+ context 'with an empty inverted criteria scope' do
1338
+ include_deferred 'with an empty criteria scope'
1339
+
1340
+ let(:inverted) { original.invert }
1341
+
1342
+ it { expect(subject.not(inverted)).to be subject }
1343
+
1344
+ wrap_deferred 'when the scope has multiple criteria' do
1345
+ it { expect(subject.not(original)).to be subject }
1346
+ end
1347
+ end
1348
+
1349
+ wrap_deferred 'with an empty disjunction scope' do
1350
+ it { expect(subject.not(original)).to be subject }
1351
+
1352
+ wrap_deferred 'when the scope has multiple criteria' do
1353
+ it { expect(subject.not(original)).to be subject }
1354
+ end
1355
+ end
1356
+
1357
+ wrap_deferred 'with a non-empty conjunction scope' do
1358
+ it { expect(subject.not(original)).to be == original.invert }
1359
+
1360
+ wrap_deferred 'when the scope has multiple criteria' do
1361
+ let(:expected) do
1362
+ Cuprum::Collections::Scopes::ConjunctionScope.new(
1363
+ scopes: [subject, original.invert]
1364
+ )
1365
+ end
1366
+
1367
+ it { expect(subject.not(original)).to be == expected }
1368
+ end
1369
+ end
1370
+
1371
+ wrap_deferred 'with a non-empty criteria scope' do
1372
+ it { expect(subject.not(original)).to be == original.invert }
1373
+
1374
+ wrap_deferred 'when the scope has multiple criteria' do
1375
+ let(:expected) do
1376
+ Cuprum::Collections::Scopes::ConjunctionScope.new(
1377
+ scopes: [subject, original.invert]
1378
+ )
1379
+ end
1380
+
1381
+ it { expect(subject.not(original)).to be == expected }
1382
+ end
1383
+ end
1384
+
1385
+ context 'with a non-empty inverted criteria scope' do
1386
+ include_deferred 'with a non-empty criteria scope'
1387
+
1388
+ let(:inverted) { original.invert }
1389
+
1390
+ it { expect(subject.not(inverted)).to be == original }
1391
+
1392
+ wrap_deferred 'when the scope has multiple criteria' do
1393
+ let(:expected) do
1394
+ Cuprum::Collections::Scopes::ConjunctionScope.new(
1395
+ scopes: [subject, original]
1396
+ )
1397
+ end
1398
+
1399
+ it { expect(subject.not(inverted)).to be == expected }
1400
+ end
1401
+ end
1402
+
1403
+ wrap_deferred 'with a non-empty disjunction scope' do
1404
+ it { expect(subject.not(original)).to be == original.invert }
1405
+
1406
+ wrap_deferred 'when the scope has multiple criteria' do
1407
+ let(:expected) do
1408
+ Cuprum::Collections::Scopes::ConjunctionScope.new(
1409
+ scopes: [subject, *original.invert.scopes]
1410
+ )
1411
+ end
1412
+
1413
+ it { expect(subject.not(original)).to be == expected }
1414
+ end
1415
+ end
1416
+ end
1417
+ end
1418
+
1419
+ describe '#or' do
1420
+ include_deferred 'with contexts for composable scopes'
1421
+
1422
+ describe 'with a block' do
1423
+ let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } }
1424
+ let(:expected) do
1425
+ Cuprum::Collections::Scope.new(&block)
1426
+ end
1427
+
1428
+ it { expect(subject.or(&block)).to be == expected }
1429
+
1430
+ wrap_deferred 'when the scope has multiple criteria' do
1431
+ let(:expected) do
1432
+ Cuprum::Collections::Scopes::DisjunctionScope.new(
1433
+ scopes: [subject, super()]
1434
+ )
1435
+ end
1436
+
1437
+ it { expect(subject.or(&block)).to be == expected }
1438
+ end
1439
+ end
1440
+
1441
+ describe 'with a hash' do
1442
+ let(:value) { { 'title' => 'A Wizard of Earthsea' } }
1443
+ let(:expected) do
1444
+ Cuprum::Collections::Scope.new(value)
1445
+ end
1446
+
1447
+ it { expect(subject.or(value)).to be == expected }
1448
+
1449
+ wrap_deferred 'when the scope has multiple criteria' do
1450
+ let(:expected) do
1451
+ Cuprum::Collections::Scopes::DisjunctionScope.new(
1452
+ scopes: [subject, super()]
1453
+ )
1454
+ end
1455
+
1456
+ it { expect(subject.or(value)).to be == expected }
1457
+ end
1458
+ end
1459
+
1460
+ wrap_deferred 'with an all scope' do
1461
+ it { expect(subject.or(original)).to be == original }
1462
+
1463
+ wrap_deferred 'when the scope has multiple criteria' do
1464
+ it { expect(subject.or(original)).to be == original }
1465
+ end
1466
+ end
1467
+
1468
+ wrap_deferred 'with a none scope' do
1469
+ it { expect(subject.or(original)).to be subject }
1470
+
1471
+ wrap_deferred 'when the scope has multiple criteria' do
1472
+ it { expect(subject.or(original)).to be subject }
1473
+ end
1474
+ end
1475
+
1476
+ wrap_deferred 'with an empty conjunction scope' do
1477
+ it { expect(subject.or(original)).to be subject }
1478
+
1479
+ wrap_deferred 'when the scope has multiple criteria' do
1480
+ it { expect(subject.or(original)).to be subject }
1481
+ end
1482
+ end
1483
+
1484
+ wrap_deferred 'with an empty criteria scope' do
1485
+ it { expect(subject.or(original)).to be subject }
1486
+
1487
+ wrap_deferred 'when the scope has multiple criteria' do
1488
+ it { expect(subject.or(original)).to be subject }
1489
+ end
1490
+ end
1491
+
1492
+ wrap_deferred 'with an empty disjunction scope' do
1493
+ it { expect(subject.or(original)).to be subject }
1494
+
1495
+ wrap_deferred 'when the scope has multiple criteria' do
1496
+ it { expect(subject.or(original)).to be subject }
1497
+ end
1498
+ end
1499
+
1500
+ wrap_deferred 'with a non-empty conjunction scope' do
1501
+ it { expect(subject.or(original)).to be == original }
1502
+
1503
+ wrap_deferred 'when the scope has multiple criteria' do
1504
+ let(:expected) do
1505
+ Cuprum::Collections::Scopes::DisjunctionScope.new(
1506
+ scopes: [subject, original]
1507
+ )
1508
+ end
1509
+
1510
+ it { expect(subject.or(original)).to be == expected }
1511
+ end
1512
+ end
1513
+
1514
+ wrap_deferred 'with a non-empty criteria scope' do
1515
+ it { expect(subject.or(original)).to be == original }
1516
+
1517
+ wrap_deferred 'when the scope has multiple criteria' do
1518
+ let(:expected) do
1519
+ Cuprum::Collections::Scopes::DisjunctionScope.new(
1520
+ scopes: [subject, original]
1521
+ )
1522
+ end
1523
+
1524
+ it { expect(subject.or(original)).to be == expected }
1525
+ end
1526
+ end
1527
+
1528
+ wrap_deferred 'with a non-empty disjunction scope' do
1529
+ it { expect(subject.or(original)).to be == original }
1530
+
1531
+ wrap_deferred 'when the scope has multiple criteria' do
1532
+ let(:expected) do
1533
+ Cuprum::Collections::Scopes::DisjunctionScope.new(
1534
+ scopes: [subject, *original.scopes]
1535
+ )
1536
+ end
1537
+
1538
+ it { expect(subject.or(original)).to be == expected }
1539
+ end
1540
+ end
1541
+ end
1542
+ end
1543
+
1544
+ # @option deferred_options ignore_invalid [Boolean] if true, skips tests for
1545
+ # handling invalid operators. Defaults to false.
1546
+ # @option deferred_options inverted [Boolean] if true, filters data that
1547
+ # does not match the criteria. Defaults to false.
1548
+ deferred_examples 'should filter data by criteria' do |**deferred_options|
1549
+ deferred_context 'with data' do
1550
+ let(:data) do
1551
+ Cuprum::Collections::RSpec::Fixtures::BOOKS_FIXTURES
1552
+ end
1553
+ end
1554
+
1555
+ let(:matching) { data }
1556
+ let(:expected) do
1557
+ deferred_options.fetch(:inverted, false) ? data - matching : matching
1558
+ end
1559
+
1560
+ context 'when the scope has no criteria' do
1561
+ describe 'with empty data' do
1562
+ let(:data) { [] }
1563
+
1564
+ it { expect(filtered_data).to be == [] }
1565
+ end
1566
+
1567
+ wrap_deferred 'with data' do
1568
+ let(:matching) { data }
1569
+
1570
+ it { expect(filtered_data).to match_array expected }
1571
+ end
1572
+ end
1573
+
1574
+ context 'when the scope has an equality criterion' do
1575
+ let(:criteria) do
1576
+ described_class.parse({ 'title' => 'The Word for World is Forest' })
1577
+ end
1578
+
1579
+ describe 'with empty data' do
1580
+ let(:data) { [] }
1581
+
1582
+ it { expect(filtered_data).to be == [] }
1583
+ end
1584
+
1585
+ wrap_deferred 'with data' do
1586
+ let(:matching) do
1587
+ data.select do |item|
1588
+ item['title'] == 'The Word for World is Forest'
1589
+ end
1590
+ end
1591
+
1592
+ it { expect(filtered_data).to match_array expected }
1593
+ end
1594
+ end
1595
+
1596
+ context 'when the scope has a greater than criterion' do
1597
+ let(:criteria) do
1598
+ described_class.parse do |scope|
1599
+ { 'published_at' => scope.greater_than('1972-03-13') }
1600
+ end
1601
+ end
1602
+
1603
+ describe 'with empty data' do
1604
+ let(:data) { [] }
1605
+
1606
+ it { expect(filtered_data).to be == [] }
1607
+ end
1608
+
1609
+ wrap_deferred 'with data' do
1610
+ let(:matching) do
1611
+ data.select do |item|
1612
+ item['published_at'] > '1972-03-13'
1613
+ end
1614
+ end
1615
+
1616
+ it { expect(filtered_data).to match_array expected }
1617
+ end
1618
+ end
1619
+
1620
+ context 'when the scope has a greater than or equal to criterion' do
1621
+ let(:criteria) do
1622
+ described_class.parse do |scope|
1623
+ { 'published_at' => scope.greater_than_or_equal_to('1972-03-13') }
1624
+ end
1625
+ end
1626
+
1627
+ describe 'with empty data' do
1628
+ let(:data) { [] }
1629
+
1630
+ it { expect(filtered_data).to be == [] }
1631
+ end
1632
+
1633
+ wrap_deferred 'with data' do
1634
+ let(:matching) do
1635
+ data.select do |item|
1636
+ item['published_at'] >= '1972-03-13'
1637
+ end
1638
+ end
1639
+
1640
+ it { expect(filtered_data).to match_array expected }
1641
+ end
1642
+ end
1643
+
1644
+ context 'when the scope has a less than criterion' do
1645
+ let(:criteria) do
1646
+ described_class.parse do |scope|
1647
+ { 'published_at' => scope.less_than('1972-03-13') }
1648
+ end
1649
+ end
1650
+
1651
+ describe 'with empty data' do
1652
+ let(:data) { [] }
1653
+
1654
+ it { expect(filtered_data).to be == [] }
1655
+ end
1656
+
1657
+ wrap_deferred 'with data' do
1658
+ let(:matching) do
1659
+ data.select do |item|
1660
+ item['published_at'] < '1972-03-13'
1661
+ end
1662
+ end
1663
+
1664
+ it { expect(filtered_data).to match_array expected }
1665
+ end
1666
+ end
1667
+
1668
+ context 'when the scope has a less than or equal to criterion' do
1669
+ let(:criteria) do
1670
+ described_class.parse do |scope|
1671
+ { 'published_at' => scope.less_than_or_equal_to('1972-03-13') }
1672
+ end
1673
+ end
1674
+
1675
+ describe 'with empty data' do
1676
+ let(:data) { [] }
1677
+
1678
+ it { expect(filtered_data).to be == [] }
1679
+ end
1680
+
1681
+ wrap_deferred 'with data' do
1682
+ let(:matching) do
1683
+ data.select do |item|
1684
+ item['published_at'] <= '1972-03-13'
1685
+ end
1686
+ end
1687
+
1688
+ it { expect(filtered_data).to match_array expected }
1689
+ end
1690
+ end
1691
+
1692
+ context 'when the scope has a not equal criterion' do
1693
+ let(:criteria) do
1694
+ described_class.parse do |scope|
1695
+ { 'author' => scope.not_equal('J.R.R. Tolkien') }
1696
+ end
1697
+ end
1698
+
1699
+ describe 'with empty data' do
1700
+ let(:data) { [] }
1701
+
1702
+ it { expect(filtered_data).to be == [] }
1703
+ end
1704
+
1705
+ wrap_deferred 'with data' do
1706
+ let(:matching) do
1707
+ data.reject do |item|
1708
+ item['author'] == 'J.R.R. Tolkien'
1709
+ end
1710
+ end
1711
+
1712
+ it { expect(filtered_data).to match_array expected }
1713
+ end
1714
+ end
1715
+
1716
+ context 'when the scope has a not null criterion' do
1717
+ let(:criteria) do
1718
+ described_class.parse do |scope|
1719
+ { 'series' => scope.not_null }
1720
+ end
1721
+ end
1722
+
1723
+ describe 'with empty data' do
1724
+ let(:data) { [] }
1725
+
1726
+ it { expect(filtered_data).to be == [] }
1727
+ end
1728
+
1729
+ wrap_deferred 'with data' do
1730
+ let(:matching) do
1731
+ data.reject do |item|
1732
+ item['series'].nil?
1733
+ end
1734
+ end
1735
+
1736
+ it { expect(filtered_data).to match_array expected }
1737
+ end
1738
+ end
1739
+
1740
+ context 'when the scope has a not one of criterion' do
1741
+ let(:criteria) do
1742
+ described_class.parse do |scope|
1743
+ titles = ['The Fellowship Of The Ring', 'The Two Towers']
1744
+
1745
+ { 'title' => scope.not_one_of(titles) }
1746
+ end
1747
+ end
1748
+
1749
+ describe 'with empty data' do
1750
+ let(:data) { [] }
1751
+
1752
+ it { expect(filtered_data).to be == [] }
1753
+ end
1754
+
1755
+ wrap_deferred 'with data' do
1756
+ let(:matching) do
1757
+ data.reject do |item|
1758
+ ['The Fellowship Of The Ring', 'The Two Towers']
1759
+ .include?(item['title'])
1760
+ end
1761
+ end
1762
+
1763
+ it { expect(filtered_data).to match_array expected }
1764
+ end
1765
+ end
1766
+
1767
+ context 'when the scope has a null criterion' do
1768
+ let(:criteria) do
1769
+ described_class.parse do |scope|
1770
+ { 'series' => scope.null }
1771
+ end
1772
+ end
1773
+
1774
+ describe 'with empty data' do
1775
+ let(:data) { [] }
1776
+
1777
+ it { expect(filtered_data).to be == [] }
1778
+ end
1779
+
1780
+ wrap_deferred 'with data' do
1781
+ let(:matching) do
1782
+ data.select do |item|
1783
+ item['series'].nil?
1784
+ end
1785
+ end
1786
+
1787
+ it { expect(filtered_data).to match_array expected }
1788
+ end
1789
+ end
1790
+
1791
+ context 'when the scope has a one of criterion' do
1792
+ let(:criteria) do
1793
+ described_class.parse do |scope|
1794
+ titles = ['The Fellowship Of The Ring', 'The Two Towers']
1795
+
1796
+ { 'title' => scope.one_of(titles) }
1797
+ end
1798
+ end
1799
+
1800
+ describe 'with empty data' do
1801
+ let(:data) { [] }
1802
+
1803
+ it { expect(filtered_data).to be == [] }
1804
+ end
1805
+
1806
+ wrap_deferred 'with data' do
1807
+ let(:matching) do
1808
+ data.select do |item|
1809
+ ['The Fellowship Of The Ring', 'The Two Towers']
1810
+ .include?(item['title'])
1811
+ end
1812
+ end
1813
+
1814
+ it { expect(filtered_data).to match_array expected }
1815
+ end
1816
+ end
1817
+
1818
+ context 'when the scope has mixed criteria' do
1819
+ let(:criteria) do
1820
+ described_class.parse do |scope|
1821
+ titles = ['The Fellowship Of The Ring', 'The Two Towers']
1822
+
1823
+ {
1824
+ 'author' => 'J.R.R. Tolkien',
1825
+ 'title' => scope.not_one_of(titles)
1826
+ }
1827
+ end
1828
+ end
1829
+
1830
+ describe 'with empty data' do
1831
+ let(:data) { [] }
1832
+
1833
+ it { expect(filtered_data).to be == [] }
1834
+ end
1835
+
1836
+ wrap_deferred 'with data' do
1837
+ let(:matching) do
1838
+ data
1839
+ .select { |item| item['author'] == 'J.R.R. Tolkien' }
1840
+ .reject do |item|
1841
+ ['The Fellowship Of The Ring', 'The Two Towers']
1842
+ .include?(item['title'])
1843
+ end
1844
+ end
1845
+
1846
+ it { expect(filtered_data).to match_array expected }
1847
+ end
1848
+ end
1849
+
1850
+ context 'when the scope has multiple matching criteria' do
1851
+ let(:criteria) do
1852
+ described_class.parse({
1853
+ 'author' => 'J.R.R. Tolkien',
1854
+ 'series' => 'The Lord of the Rings'
1855
+ })
1856
+ end
1857
+
1858
+ describe 'with empty data' do
1859
+ let(:data) { [] }
1860
+
1861
+ it { expect(filtered_data).to be == [] }
1862
+ end
1863
+
1864
+ wrap_deferred 'with data' do
1865
+ let(:matching) do
1866
+ data
1867
+ .select { |item| item['author'] == 'J.R.R. Tolkien' }
1868
+ .select { |item| item['series'] == 'The Lord of the Rings' }
1869
+ end
1870
+
1871
+ it { expect(filtered_data).to match_array expected }
1872
+ end
1873
+ end
1874
+
1875
+ context 'when the scope has non-overlapping criteria' do
1876
+ let(:criteria) do
1877
+ described_class.parse({
1878
+ 'author' => 'J.R.R. Tolkien',
1879
+ 'series' => 'Earthsea'
1880
+ })
1881
+ end
1882
+
1883
+ describe 'with empty data' do
1884
+ let(:data) { [] }
1885
+
1886
+ it { expect(filtered_data).to be == [] }
1887
+ end
1888
+
1889
+ wrap_deferred 'with data' do
1890
+ let(:matching) do
1891
+ data
1892
+ .select { |item| item['author'] == 'J.R.R. Tolkien' }
1893
+ .select { |item| item['series'] == 'Earthsea' }
1894
+ end
1895
+
1896
+ it { expect(filtered_data).to match_array expected }
1897
+ end
1898
+ end
1899
+
1900
+ context 'when the scope has invalid operators' do
1901
+ next if deferred_options.fetch(:ignore_invalid, false)
1902
+
1903
+ let(:criteria) { [['title', :random, nil]] }
1904
+ let(:error_class) do
1905
+ Cuprum::Collections::Queries::UnknownOperatorException
1906
+ end
1907
+ let(:error_message) do
1908
+ 'unknown operator "random"'
1909
+ end
1910
+
1911
+ describe 'with empty data' do
1912
+ let(:data) { [] }
1913
+
1914
+ it 'should return an empty result or raise an exception',
1915
+ :aggregate_failures \
1916
+ do
1917
+ # :nocov:
1918
+ begin
1919
+ actual = filtered_data
1920
+ rescue StandardError => exception
1921
+ expect(exception).to be_a error_class
1922
+ expect(exception.message).to be == error_message
1923
+
1924
+ next
1925
+ end
1926
+
1927
+ expect(actual).to be == []
1928
+ # :nocov:
1929
+ end
1930
+ end
1931
+
1932
+ wrap_deferred 'with data' do
1933
+ it 'should raise an exception' do
1934
+ expect { filtered_data }
1935
+ .to raise_error error_class, error_message
1936
+ end
1937
+ end
1938
+ end
1939
+ end
1940
+ end
1941
+ end