cuprum-collections 0.5.1 → 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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/lib/cuprum/collections/adaptable/collection.rb +18 -0
  4. data/lib/cuprum/collections/adaptable/command.rb +22 -0
  5. data/lib/cuprum/collections/adaptable/commands/abstract_assign_one.rb +27 -0
  6. data/lib/cuprum/collections/adaptable/commands/abstract_build_one.rb +25 -0
  7. data/lib/cuprum/collections/adaptable/commands/abstract_validate_one.rb +35 -0
  8. data/lib/cuprum/collections/adaptable/commands.rb +15 -0
  9. data/lib/cuprum/collections/adaptable/query.rb +64 -0
  10. data/lib/cuprum/collections/adaptable.rb +13 -0
  11. data/lib/cuprum/collections/adapter.rb +300 -0
  12. data/lib/cuprum/collections/adapters/data_adapter.rb +82 -0
  13. data/lib/cuprum/collections/adapters/entity_adapter.rb +76 -0
  14. data/lib/cuprum/collections/adapters/hash_adapter.rb +48 -0
  15. data/lib/cuprum/collections/adapters.rb +14 -0
  16. data/lib/cuprum/collections/basic/collection.rb +2 -20
  17. data/lib/cuprum/collections/basic/commands/destroy_one.rb +1 -1
  18. data/lib/cuprum/collections/basic/commands/find_many.rb +0 -31
  19. data/lib/cuprum/collections/basic/commands/find_matching.rb +0 -94
  20. data/lib/cuprum/collections/basic/commands/find_one.rb +0 -18
  21. data/lib/cuprum/collections/basic/commands/insert_one.rb +1 -1
  22. data/lib/cuprum/collections/basic/commands/update_one.rb +1 -1
  23. data/lib/cuprum/collections/basic/scopes/criteria_scope.rb +36 -21
  24. data/lib/cuprum/collections/basic.rb +6 -5
  25. data/lib/cuprum/collections/collection.rb +6 -0
  26. data/lib/cuprum/collections/collection_command.rb +1 -1
  27. data/lib/cuprum/collections/commands/abstract_find_many.rb +40 -3
  28. data/lib/cuprum/collections/commands/abstract_find_matching.rb +102 -0
  29. data/lib/cuprum/collections/commands/abstract_find_one.rb +23 -1
  30. data/lib/cuprum/collections/commands/associations/find_many.rb +1 -3
  31. data/lib/cuprum/collections/commands/associations/require_many.rb +1 -1
  32. data/lib/cuprum/collections/commands/find_one_matching.rb +10 -10
  33. data/lib/cuprum/collections/commands/query_command.rb +6 -4
  34. data/lib/cuprum/collections/commands/upsert.rb +0 -2
  35. data/lib/cuprum/collections/constraints/order/attributes_array.rb +5 -4
  36. data/lib/cuprum/collections/constraints/order/attributes_hash.rb +5 -4
  37. data/lib/cuprum/collections/constraints/order/sort_direction.rb +2 -2
  38. data/lib/cuprum/collections/constraints/ordering.rb +11 -9
  39. data/lib/cuprum/collections/constraints/query_hash.rb +2 -2
  40. data/lib/cuprum/collections/errors/abstract_find_error.rb +101 -23
  41. data/lib/cuprum/collections/errors/extra_attributes.rb +3 -3
  42. data/lib/cuprum/collections/errors/failed_validation.rb +3 -3
  43. data/lib/cuprum/collections/errors/missing_default_contract.rb +12 -4
  44. data/lib/cuprum/collections/queries.rb +4 -0
  45. data/lib/cuprum/collections/relation.rb +0 -2
  46. data/lib/cuprum/collections/relations/parameters.rb +120 -68
  47. data/lib/cuprum/collections/repository.rb +71 -6
  48. data/lib/cuprum/collections/rspec/contracts/query_contracts.rb +23 -4
  49. data/lib/cuprum/collections/rspec/contracts/repository_contracts.rb +18 -0
  50. data/lib/cuprum/collections/rspec/contracts/scope_contracts.rb +51 -0
  51. data/lib/cuprum/collections/rspec/contracts/scopes/builder_contracts.rb +10 -0
  52. data/lib/cuprum/collections/rspec/contracts/scopes/composition_contracts.rb +8 -0
  53. data/lib/cuprum/collections/rspec/contracts/scopes/criteria_contracts.rb +18 -366
  54. data/lib/cuprum/collections/rspec/contracts/scopes/logical_contracts.rb +30 -0
  55. data/lib/cuprum/collections/rspec/contracts/scopes.rb +2 -0
  56. data/lib/cuprum/collections/rspec/contracts.rb +2 -10
  57. data/lib/cuprum/collections/rspec/deferred/adapter_examples.rb +1077 -0
  58. data/lib/cuprum/collections/rspec/deferred/collection_examples.rb +27 -7
  59. data/lib/cuprum/collections/rspec/deferred/commands/assign_one_examples.rb +4 -4
  60. data/lib/cuprum/collections/rspec/deferred/commands/build_one_examples.rb +2 -2
  61. data/lib/cuprum/collections/rspec/deferred/commands/destroy_one_examples.rb +2 -2
  62. data/lib/cuprum/collections/rspec/deferred/commands/find_many_examples.rb +5 -5
  63. data/lib/cuprum/collections/rspec/deferred/commands/find_matching_examples.rb +45 -12
  64. data/lib/cuprum/collections/rspec/deferred/commands/find_one_examples.rb +2 -2
  65. data/lib/cuprum/collections/rspec/deferred/commands/insert_one_examples.rb +1 -1
  66. data/lib/cuprum/collections/rspec/deferred/commands/update_one_examples.rb +1 -1
  67. data/lib/cuprum/collections/rspec/deferred/query_examples.rb +930 -0
  68. data/lib/cuprum/collections/rspec/deferred/relation_examples.rb +48 -17
  69. data/lib/cuprum/collections/rspec/deferred/repository_examples.rb +961 -0
  70. data/lib/cuprum/collections/rspec/deferred/scope_examples.rb +598 -0
  71. data/lib/cuprum/collections/rspec/deferred/scopes/all_examples.rb +391 -0
  72. data/lib/cuprum/collections/rspec/deferred/scopes/builder_examples.rb +857 -0
  73. data/lib/cuprum/collections/rspec/deferred/scopes/composition_examples.rb +93 -0
  74. data/lib/cuprum/collections/rspec/deferred/scopes/conjunction_examples.rb +438 -0
  75. data/lib/cuprum/collections/rspec/deferred/scopes/criteria_examples.rb +1941 -0
  76. data/lib/cuprum/collections/rspec/deferred/scopes/disjunction_examples.rb +415 -0
  77. data/lib/cuprum/collections/rspec/deferred/scopes/none_examples.rb +385 -0
  78. data/lib/cuprum/collections/rspec/deferred/scopes/parser_examples.rb +740 -0
  79. data/lib/cuprum/collections/rspec/deferred/scopes.rb +8 -0
  80. data/lib/cuprum/collections/scope.rb +2 -2
  81. data/lib/cuprum/collections/scopes/container.rb +5 -4
  82. data/lib/cuprum/collections/scopes/criteria/parser.rb +24 -48
  83. data/lib/cuprum/collections/scopes/criteria.rb +7 -6
  84. data/lib/cuprum/collections/version.rb +4 -4
  85. data/lib/cuprum/collections.rb +5 -1
  86. metadata +47 -11
  87. data/lib/cuprum/collections/rspec/contracts/association_contracts.rb +0 -2127
  88. data/lib/cuprum/collections/rspec/contracts/basic.rb +0 -11
  89. data/lib/cuprum/collections/rspec/contracts/collection_contracts.rb +0 -387
  90. data/lib/cuprum/collections/rspec/contracts/command_contracts.rb +0 -169
  91. data/lib/cuprum/collections/rspec/contracts/relation_contracts.rb +0 -1264
@@ -0,0 +1,930 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/rspec/deferred'
4
+
5
+ module Cuprum::Collections::RSpec::Deferred
6
+ # Deferred examples for testing queries.
7
+ module QueryExamples
8
+ include RSpec::SleepingKingStudios::Deferred::Provider
9
+
10
+ BOOKS_FIXTURES = Cuprum::Collections::RSpec::Fixtures::BOOKS_FIXTURES
11
+ private_constant :BOOKS_FIXTURES
12
+
13
+ OPERATORS = Cuprum::Collections::Queries::Operators
14
+ private_constant :OPERATORS
15
+
16
+ deferred_examples 'should be a Query' do |**deferred_options|
17
+ shared_context 'when initialized with a Hash' do
18
+ let(:initial_scope) do
19
+ { 'author' => 'Ursula K. LeGuin' }
20
+ end
21
+ let(:filtered_data) do
22
+ super().select { |item| item['author'] == 'Ursula K. LeGuin' }
23
+ end
24
+ end
25
+
26
+ shared_context 'when initialized with a Proc' do
27
+ let(:initial_scope) do
28
+ ->(scope) { { 'published_at' => scope.less_than('1973-01-01') } }
29
+ end
30
+ let(:filtered_data) do
31
+ super().select { |item| item['published_at'] < '1973-01-01' }
32
+ end
33
+ end
34
+
35
+ shared_context 'when initialized with a scope' do
36
+ let(:initial_scope) do
37
+ Cuprum::Collections::Scope.new do |scope|
38
+ { 'published_at' => scope.less_than('1973-01-01') }
39
+ end
40
+ end
41
+ let(:filtered_data) do
42
+ super().select { |item| item['published_at'] < '1973-01-01' }
43
+ end
44
+ end
45
+
46
+ shared_context 'when the query has composed filters' do
47
+ let(:scoped_query) do
48
+ super()
49
+ .where { { author: 'Ursula K. LeGuin' } }
50
+ .where { |scope| { series: scope.not_equal('Earthsea') } }
51
+ end
52
+ let(:filtered_data) do
53
+ super()
54
+ .select { |item| item['author'] == 'Ursula K. LeGuin' }
55
+ .reject { |item| item['series'] == 'Earthsea' }
56
+ end
57
+ end
58
+
59
+ let(:filter) { nil }
60
+ let(:limit) { nil }
61
+ let(:offset) { nil }
62
+ let(:order) { nil }
63
+ let(:scoped_query) do
64
+ scoped =
65
+ if filter.is_a?(Proc)
66
+ subject.where(&filter)
67
+ elsif filter
68
+ subject.where(filter)
69
+ else
70
+ subject
71
+ end
72
+ scoped = scoped.limit(limit) if limit
73
+ scoped = scoped.offset(offset) if offset
74
+ scoped = scoped.order(order) if order
75
+
76
+ scoped
77
+ end
78
+
79
+ it 'should be enumerable' do
80
+ expect(described_class).to be < Enumerable
81
+ end
82
+
83
+ describe '#count' do
84
+ it { expect(query).to respond_to(:count).with(0).arguments }
85
+
86
+ next if deferred_options.fetch(:abstract, false)
87
+
88
+ it { expect(query.count).to be 0 }
89
+
90
+ context 'when the collection data changes' do
91
+ let(:item) { BOOKS_FIXTURES.first }
92
+
93
+ before(:example) do
94
+ query.count # Cache query results.
95
+
96
+ add_item_to_collection(item)
97
+ end
98
+
99
+ it { expect(query.count).to be 0 }
100
+ end
101
+
102
+ context 'when the collection has many items' do
103
+ let(:data) { BOOKS_FIXTURES }
104
+
105
+ include_deferred 'should query the collection', ignore_order: true \
106
+ do
107
+ it { expect(scoped_query.count).to be == expected_data.count }
108
+
109
+ wrap_context 'when the query has composed filters' do
110
+ it { expect(scoped_query.count).to be == expected_data.count }
111
+ end
112
+
113
+ wrap_context 'when initialized with a Hash' do
114
+ it { expect(scoped_query.count).to be == expected_data.count }
115
+
116
+ wrap_context 'when the query has composed filters' do
117
+ it { expect(scoped_query.count).to be == expected_data.count }
118
+ end
119
+ end
120
+
121
+ wrap_context 'when initialized with a Proc' do
122
+ it { expect(scoped_query.count).to be == expected_data.count }
123
+
124
+ wrap_context 'when the query has composed filters' do
125
+ it { expect(scoped_query.count).to be == expected_data.count }
126
+ end
127
+ end
128
+
129
+ wrap_context 'when initialized with a scope' do
130
+ it { expect(scoped_query.count).to be == expected_data.count }
131
+
132
+ wrap_context 'when the query has composed filters' do
133
+ it { expect(scoped_query.count).to be == expected_data.count }
134
+ end
135
+ end
136
+ end
137
+
138
+ context 'when the collection data changes' do
139
+ let(:data) { BOOKS_FIXTURES[0...-1] }
140
+ let(:item) { BOOKS_FIXTURES.last }
141
+
142
+ before(:example) do
143
+ query.count # Cache query results.
144
+
145
+ add_item_to_collection(item)
146
+ end
147
+
148
+ it { expect(query.count).to be == expected_data.count }
149
+ end
150
+ end
151
+ end
152
+
153
+ describe '#each' do
154
+ shared_examples 'should enumerate the matching data' do
155
+ describe 'with no arguments' do
156
+ it { expect(scoped_query.each).to be_a Enumerator }
157
+
158
+ it { expect(scoped_query.each.count).to be == expected_data.size }
159
+
160
+ it { expect(scoped_query.each.to_a).to deep_match expected_data }
161
+ end
162
+
163
+ describe 'with a block' do
164
+ it 'should yield each matching item' do
165
+ expect { |block| scoped_query.each(&block) }
166
+ .to yield_successive_args(*expected_data)
167
+ end
168
+ end
169
+ end
170
+
171
+ next if deferred_options.fetch(:abstract, false)
172
+
173
+ it { expect(query).to respond_to(:each).with(0).arguments }
174
+
175
+ include_examples 'should enumerate the matching data'
176
+
177
+ context 'when the collection data changes' do
178
+ let(:item) { BOOKS_FIXTURES.first }
179
+
180
+ before(:example) do
181
+ query.each {} # Cache query results.
182
+
183
+ add_item_to_collection(item)
184
+ end
185
+
186
+ include_examples 'should enumerate the matching data'
187
+ end
188
+
189
+ context 'when the collection has many items' do
190
+ let(:data) { BOOKS_FIXTURES }
191
+
192
+ include_deferred 'should query the collection' do
193
+ include_examples 'should enumerate the matching data'
194
+
195
+ wrap_context 'when the query has composed filters' do
196
+ include_examples 'should enumerate the matching data'
197
+ end
198
+
199
+ wrap_context 'when initialized with a scope' do
200
+ include_examples 'should enumerate the matching data'
201
+
202
+ wrap_context 'when the query has composed filters' do
203
+ include_examples 'should enumerate the matching data'
204
+ end
205
+ end
206
+ end
207
+
208
+ context 'when the collection data changes' do
209
+ let(:data) { BOOKS_FIXTURES[0...-1] }
210
+ let(:item) { BOOKS_FIXTURES.last }
211
+
212
+ before(:example) do
213
+ query.each {} # Cache query results.
214
+
215
+ add_item_to_collection(item)
216
+ end
217
+
218
+ include_examples 'should enumerate the matching data'
219
+ end
220
+ end
221
+ end
222
+
223
+ describe '#exists?' do
224
+ shared_examples 'should check the existence of matching data' do
225
+ let(:data) { [] }
226
+ let(:expected_data) { defined?(super()) ? super() : data }
227
+
228
+ it { expect(query.exists?).to be == !expected_data.empty? }
229
+ end
230
+
231
+ next if deferred_options.fetch(:abstract, false)
232
+
233
+ include_examples 'should define predicate', :exists?
234
+
235
+ include_examples 'should check the existence of matching data'
236
+
237
+ context 'when the collection has many items' do
238
+ let(:data) { BOOKS_FIXTURES }
239
+
240
+ include_deferred 'should query the collection', ignore_order: true \
241
+ do
242
+ include_examples 'should check the existence of matching data'
243
+
244
+ wrap_context 'when the query has composed filters' do
245
+ include_examples 'should check the existence of matching data'
246
+ end
247
+
248
+ wrap_context 'when initialized with a scope' do
249
+ include_examples 'should check the existence of matching data'
250
+
251
+ wrap_context 'when the query has composed filters' do
252
+ include_examples 'should check the existence of matching data'
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
258
+
259
+ describe '#limit' do
260
+ it { expect(query).to respond_to(:limit).with(0..1).arguments }
261
+
262
+ describe 'with no arguments' do
263
+ it { expect(query.limit).to be nil }
264
+ end
265
+
266
+ describe 'with nil' do
267
+ let(:error_message) { 'limit must be a non-negative integer' }
268
+
269
+ it 'should raise an exception' do
270
+ expect { query.limit nil }
271
+ .to raise_error ArgumentError, error_message
272
+ end
273
+ end
274
+
275
+ describe 'with an object' do
276
+ let(:error_message) { 'limit must be a non-negative integer' }
277
+
278
+ it 'should raise an exception' do
279
+ expect { query.limit Object.new.freeze }
280
+ .to raise_error ArgumentError, error_message
281
+ end
282
+ end
283
+
284
+ describe 'with a negative integer' do
285
+ let(:error_message) { 'limit must be a non-negative integer' }
286
+
287
+ it 'should raise an exception' do
288
+ expect { query.limit(-1) }
289
+ .to raise_error ArgumentError, error_message
290
+ end
291
+ end
292
+
293
+ describe 'with zero' do
294
+ it { expect(query.limit(0)).to be_a described_class }
295
+
296
+ it { expect(query.limit(0)).not_to be query }
297
+
298
+ it { expect(query.limit(0).limit).to be 0 }
299
+ end
300
+
301
+ describe 'with a positive integer' do
302
+ it { expect(query.limit(3)).to be_a described_class }
303
+
304
+ it { expect(query.limit(3)).not_to be query }
305
+
306
+ it { expect(query.limit(3).limit).to be 3 }
307
+ end
308
+ end
309
+
310
+ describe '#offset' do
311
+ it { expect(query).to respond_to(:offset).with(0..1).argument }
312
+
313
+ describe 'with no arguments' do
314
+ it { expect(query.offset).to be nil }
315
+ end
316
+
317
+ describe 'with nil' do
318
+ let(:error_message) { 'offset must be a non-negative integer' }
319
+
320
+ it 'should raise an exception' do
321
+ expect { query.offset nil }
322
+ .to raise_error ArgumentError, error_message
323
+ end
324
+ end
325
+
326
+ describe 'with an object' do
327
+ let(:error_message) { 'offset must be a non-negative integer' }
328
+
329
+ it 'should raise an exception' do
330
+ expect { query.offset Object.new.freeze }
331
+ .to raise_error ArgumentError, error_message
332
+ end
333
+ end
334
+
335
+ describe 'with a negative integer' do
336
+ let(:error_message) { 'offset must be a non-negative integer' }
337
+
338
+ it 'should raise an exception' do
339
+ expect { query.offset(-1) }
340
+ .to raise_error ArgumentError, error_message
341
+ end
342
+ end
343
+
344
+ describe 'with zero' do
345
+ it { expect(query.offset(0)).to be_a described_class }
346
+
347
+ it { expect(query.offset(0)).not_to be query }
348
+
349
+ it { expect(query.offset(0).offset).to be 0 }
350
+ end
351
+
352
+ describe 'with a positive integer' do
353
+ it { expect(query.offset(3)).to be_a described_class }
354
+
355
+ it { expect(query.offset(3)).not_to be query }
356
+
357
+ it { expect(query.offset(3).offset).to be 3 }
358
+ end
359
+ end
360
+
361
+ describe '#order' do
362
+ let(:default_order) { defined?(super()) ? super() : {} }
363
+ let(:error_message) do
364
+ 'order must be a list of attribute names and/or a hash of ' \
365
+ 'attribute names with values :asc or :desc'
366
+ end
367
+
368
+ it 'should define the method' do
369
+ expect(query)
370
+ .to respond_to(:order)
371
+ .with(0).arguments
372
+ .and_unlimited_arguments
373
+ end
374
+
375
+ it { expect(query).to have_aliased_method(:order).as(:order_by) }
376
+
377
+ describe 'with no arguments' do
378
+ it { expect(query.order).to be == default_order }
379
+ end
380
+
381
+ describe 'with a hash with invalid keys' do
382
+ it 'should raise an exception' do
383
+ expect { query.order({ nil => :asc }) }
384
+ .to raise_error ArgumentError, error_message
385
+ end
386
+ end
387
+
388
+ describe 'with a hash with empty string keys' do
389
+ it 'should raise an exception' do
390
+ expect { query.order({ '' => :asc }) }
391
+ .to raise_error ArgumentError, error_message
392
+ end
393
+ end
394
+
395
+ describe 'with a hash with empty symbol keys' do
396
+ it 'should raise an exception' do
397
+ expect { query.order({ '': :asc }) }
398
+ .to raise_error ArgumentError, error_message
399
+ end
400
+ end
401
+
402
+ describe 'with a hash with nil value' do
403
+ it 'should raise an exception' do
404
+ expect { query.order({ title: nil }) }
405
+ .to raise_error ArgumentError, error_message
406
+ end
407
+ end
408
+
409
+ describe 'with a hash with object value' do
410
+ it 'should raise an exception' do
411
+ expect { query.order({ title: Object.new.freeze }) }
412
+ .to raise_error ArgumentError, error_message
413
+ end
414
+ end
415
+
416
+ describe 'with a hash with empty value' do
417
+ it 'should raise an exception' do
418
+ expect { query.order({ title: '' }) }
419
+ .to raise_error ArgumentError, error_message
420
+ end
421
+ end
422
+
423
+ describe 'with a hash with invalid value' do
424
+ it 'should raise an exception' do
425
+ expect { query.order({ title: 'wibbly' }) }
426
+ .to raise_error ArgumentError, error_message
427
+ end
428
+ end
429
+
430
+ describe 'with a valid ordering' do
431
+ let(:expected) do
432
+ { title: :asc }
433
+ end
434
+
435
+ it { expect(query.order(:title)).to be_a described_class }
436
+
437
+ it { expect(query.order(:title)).not_to be query }
438
+
439
+ it { expect(query.order(:title).order).to be == expected }
440
+ end
441
+ end
442
+
443
+ describe '#reset' do
444
+ let(:data) { [] }
445
+ let(:matching_data) { data }
446
+ let(:expected_data) do
447
+ defined?(super()) ? super() : matching_data
448
+ end
449
+
450
+ it { expect(query).to respond_to(:reset).with(0).arguments }
451
+
452
+ it { expect(query.reset).to be_a query.class }
453
+
454
+ it { expect(query.reset).not_to be query }
455
+
456
+ next if deferred_options.fetch(:abstract, false)
457
+
458
+ it { expect(query.reset.to_a).to be == query.to_a }
459
+
460
+ context 'when the collection data changes' do
461
+ let(:item) { BOOKS_FIXTURES.first }
462
+ let(:matching_data) { [item] }
463
+
464
+ before(:example) do
465
+ query.to_a # Cache query results.
466
+
467
+ add_item_to_collection(item)
468
+ end
469
+
470
+ it { expect(query.reset.count).to be expected_data.size }
471
+
472
+ it { expect(query.reset.to_a).to deep_match expected_data }
473
+ end
474
+
475
+ context 'when the collection has many items' do
476
+ let(:data) { BOOKS_FIXTURES }
477
+
478
+ it { expect(query.reset).to be_a query.class }
479
+
480
+ it { expect(query.reset).not_to be query }
481
+
482
+ it { expect(query.reset.to_a).to be == query.to_a }
483
+
484
+ context 'when the collection data changes' do
485
+ let(:data) { BOOKS_FIXTURES[0...-1] }
486
+ let(:item) { BOOKS_FIXTURES.last }
487
+ let(:matching_data) { [*data, item] }
488
+
489
+ before(:example) do
490
+ query.to_a # Cache query results.
491
+
492
+ add_item_to_collection(item)
493
+ end
494
+
495
+ it { expect(query.reset.count).to be expected_data.size }
496
+
497
+ it { expect(query.reset.to_a).to deep_match expected_data }
498
+ end
499
+ end
500
+ end
501
+
502
+ describe '#scope' do
503
+ include_examples 'should define reader', :scope
504
+
505
+ it { expect(query.scope).to be_a Cuprum::Collections::Scopes::Base }
506
+
507
+ it { expect(query.scope.type).to be :all }
508
+
509
+ wrap_context 'when initialized with a scope' do
510
+ let(:expected) do
511
+ Cuprum::Collections::Scope.new do |scope|
512
+ {
513
+ 'published_at' => scope.less_than('1973-01-01')
514
+ }
515
+ end
516
+ end
517
+
518
+ it { expect(scoped_query.scope).to be == expected }
519
+
520
+ wrap_context 'when the query has composed filters' do
521
+ let(:expected) do
522
+ Cuprum::Collections::Scope.new do |scope|
523
+ {
524
+ 'published_at' => scope.less_than('1973-01-01'),
525
+ 'author' => 'Ursula K. LeGuin',
526
+ 'series' => scope.not_equal('Earthsea')
527
+ }
528
+ end
529
+ end
530
+
531
+ it { expect(scoped_query.scope).to be == expected }
532
+ end
533
+ end
534
+
535
+ wrap_context 'when the query has composed filters' do
536
+ let(:expected) do
537
+ Cuprum::Collections::Scope.new do |scope|
538
+ {
539
+ 'author' => 'Ursula K. LeGuin',
540
+ 'series' => scope.not_equal('Earthsea')
541
+ }
542
+ end
543
+ end
544
+
545
+ it { expect(scoped_query.scope).to be == expected }
546
+ end
547
+ end
548
+
549
+ describe '#to_a' do
550
+ let(:data) { [] }
551
+ let(:queried_data) { scoped_query.to_a }
552
+ let(:expected_data) { defined?(super()) ? super() : data }
553
+
554
+ it { expect(query).to respond_to(:to_a).with(0).arguments }
555
+
556
+ next if deferred_options.fetch(:abstract, false)
557
+
558
+ it { expect(queried_data).to be == [] }
559
+
560
+ context 'when the collection data changes' do
561
+ let(:item) { BOOKS_FIXTURES.first }
562
+
563
+ before(:example) do
564
+ scoped_query.to_a # Cache query results.
565
+
566
+ add_item_to_collection(item)
567
+ end
568
+
569
+ it { expect(queried_data).to be == [] }
570
+ end
571
+
572
+ context 'when the collection has many items' do
573
+ let(:data) { BOOKS_FIXTURES }
574
+
575
+ include_deferred 'should query the collection' do
576
+ it { expect(queried_data).to be == expected_data }
577
+
578
+ wrap_context 'when the query has composed filters' do
579
+ it { expect(queried_data).to be == expected_data }
580
+ end
581
+
582
+ wrap_context 'when initialized with a scope' do
583
+ it { expect(queried_data).to be == expected_data }
584
+
585
+ wrap_context 'when the query has composed filters' do
586
+ it { expect(queried_data).to be == expected_data }
587
+ end
588
+ end
589
+ end
590
+
591
+ context 'when the collection data changes' do
592
+ let(:data) { BOOKS_FIXTURES[0...-1] }
593
+ let(:item) { BOOKS_FIXTURES.last }
594
+
595
+ before(:example) do
596
+ scoped_query.to_a # Cache query results.
597
+
598
+ add_item_to_collection(item)
599
+ end
600
+
601
+ it { expect(queried_data).to deep_match expected_data }
602
+ end
603
+ end
604
+ end
605
+
606
+ describe '#where' do
607
+ let(:block) { -> { { title: 'Gideon the Ninth' } } }
608
+
609
+ it 'should define the method' do
610
+ expect(subject)
611
+ .to respond_to(:where)
612
+ .with(0..1).arguments
613
+ .and_a_block
614
+ end
615
+
616
+ it { expect(subject.where(&block)).to be_a described_class }
617
+
618
+ it { expect(subject.where(&block)).not_to be subject }
619
+
620
+ it 'should set the scope' do
621
+ expect(subject.where(&block).scope)
622
+ .to be_a Cuprum::Collections::Scopes::Base
623
+ end
624
+
625
+ it 'should not change the original query scope' do
626
+ expect { subject.where(&block) }
627
+ .not_to change(subject, :scope)
628
+ end
629
+
630
+ context 'when the query does not have a scope' do
631
+ let(:expected) do
632
+ Cuprum::Collections::Scope.new({ 'title' => 'Gideon the Ninth' })
633
+ end
634
+
635
+ describe 'with a block' do
636
+ let(:block) { -> { { 'title' => 'Gideon the Ninth' } } }
637
+
638
+ it { expect(subject.where(&block).scope).to be == expected }
639
+ end
640
+
641
+ describe 'with a hash' do
642
+ let(:value) { { 'title' => 'Gideon the Ninth' } }
643
+
644
+ it { expect(subject.where(value).scope).to be == expected }
645
+ end
646
+
647
+ describe 'with a basic scope' do
648
+ let(:value) do
649
+ Cuprum::Collections::Scope
650
+ .new({ 'title' => 'Gideon the Ninth' })
651
+ end
652
+
653
+ it { expect(subject.where(value).scope).to be == value }
654
+ end
655
+
656
+ describe 'with a complex scope' do
657
+ let(:value) do
658
+ Cuprum::Collections::Scope
659
+ .new({ 'title' => 'Gideon the Ninth' })
660
+ .or({ 'title' => 'Harrow the Ninth' })
661
+ end
662
+
663
+ it { expect(subject.where(value).scope).to be == value }
664
+ end
665
+ end
666
+
667
+ context 'when the query has a scope' do
668
+ let(:initial_scope) do
669
+ Cuprum::Collections::Scope.new({ 'author' => 'Tamsyn Muir' })
670
+ end
671
+ let(:expected) do
672
+ operators = Cuprum::Collections::Queries::Operators
673
+
674
+ [
675
+ [
676
+ 'author',
677
+ operators::EQUAL,
678
+ 'Tamsyn Muir'
679
+ ],
680
+ [
681
+ 'title',
682
+ operators::EQUAL,
683
+ 'Gideon the Ninth'
684
+ ]
685
+ ]
686
+ end
687
+
688
+ describe 'with a block' do
689
+ let(:block) { -> { { 'title' => 'Gideon the Ninth' } } }
690
+ let(:scope) { subject.where(&block).scope }
691
+
692
+ it { expect(scope).to be_a Cuprum::Collections::Scopes::Base }
693
+
694
+ it { expect(scope.type).to be :criteria }
695
+
696
+ it { expect(scope.criteria).to be == expected }
697
+ end
698
+
699
+ describe 'with a value' do
700
+ let(:value) { { 'title' => 'Gideon the Ninth' } }
701
+ let(:scope) { subject.where(value).scope }
702
+
703
+ it { expect(scope).to be_a Cuprum::Collections::Scopes::Base }
704
+
705
+ it { expect(scope.type).to be :criteria }
706
+
707
+ it { expect(scope.criteria).to be == expected }
708
+ end
709
+
710
+ describe 'with a basic scope' do
711
+ let(:value) do
712
+ Cuprum::Collections::Scope
713
+ .new({ 'title' => 'Gideon the Ninth' })
714
+ end
715
+ let(:scope) { subject.where(value).scope }
716
+
717
+ it { expect(scope).to be_a Cuprum::Collections::Scopes::Base }
718
+
719
+ it { expect(scope.type).to be :criteria }
720
+
721
+ it { expect(scope.criteria).to be == expected }
722
+ end
723
+
724
+ describe 'with a complex scope' do
725
+ let(:value) do
726
+ Cuprum::Collections::Scope
727
+ .new({ 'title' => 'Gideon the Ninth' })
728
+ .or({ 'title' => 'Harrow the Ninth' })
729
+ end
730
+ let(:scope) { subject.where(value).scope }
731
+ let(:outer) { scope.scopes.last }
732
+ let(:expected) do
733
+ operators = Cuprum::Collections::Queries::Operators
734
+
735
+ [
736
+ [
737
+ 'author',
738
+ operators::EQUAL,
739
+ 'Tamsyn Muir'
740
+ ]
741
+ ]
742
+ end
743
+ let(:expected_first) do
744
+ operators = Cuprum::Collections::Queries::Operators
745
+
746
+ [
747
+ [
748
+ 'title',
749
+ operators::EQUAL,
750
+ 'Gideon the Ninth'
751
+ ]
752
+ ]
753
+ end
754
+ let(:expected_second) do
755
+ operators = Cuprum::Collections::Queries::Operators
756
+
757
+ [
758
+ [
759
+ 'title',
760
+ operators::EQUAL,
761
+ 'Harrow the Ninth'
762
+ ]
763
+ ]
764
+ end
765
+
766
+ it { expect(scope).to be_a Cuprum::Collections::Scopes::Base }
767
+
768
+ it { expect(scope.type).to be :conjunction }
769
+
770
+ it { expect(scope.scopes.size).to be 2 }
771
+
772
+ it { expect(scope.scopes.first.type).to be :criteria }
773
+
774
+ it { expect(scope.scopes.first.criteria).to be == expected }
775
+
776
+ it { expect(outer).to be_a Cuprum::Collections::Scopes::Base }
777
+
778
+ it { expect(outer.type).to be :disjunction }
779
+
780
+ it { expect(outer.scopes.size).to be 2 }
781
+
782
+ it { expect(outer.scopes.first.criteria).to be == expected_first }
783
+
784
+ it { expect(outer.scopes.last.criteria).to be == expected_second }
785
+ end
786
+ end
787
+ end
788
+ end
789
+
790
+ deferred_examples 'should query the collection' \
791
+ do |**deferred_options, &examples|
792
+ shared_examples 'should query the collection' do
793
+ # :nocov:
794
+ if examples
795
+ instance_exec(&examples)
796
+ else
797
+ it { expect(queried_data).to be == expected_data }
798
+ end
799
+ # :nocov:
800
+ end
801
+
802
+ shared_examples 'should apply limit and offset' do
803
+ include_examples 'should query the collection'
804
+
805
+ context 'with limit: value' do
806
+ let(:limit) { 3 }
807
+
808
+ include_examples 'should query the collection'
809
+ end
810
+
811
+ context 'with offset: value' do
812
+ let(:offset) { 2 }
813
+
814
+ include_examples 'should query the collection'
815
+ end
816
+
817
+ describe 'with limit: value and offset: value' do
818
+ let(:limit) { 3 }
819
+ let(:offset) { 2 }
820
+
821
+ include_examples 'should query the collection'
822
+ end
823
+ end
824
+
825
+ shared_examples 'should order the results' do
826
+ include_examples 'should apply limit and offset'
827
+
828
+ next if deferred_options.fetch(:ignore_order, false)
829
+
830
+ describe 'with a simple ordering' do
831
+ let(:order) { 'title' }
832
+ let(:ordered_data) do
833
+ filtered_data.sort_by { |item| item['title'] }
834
+ end
835
+
836
+ include_examples 'should apply limit and offset'
837
+ end
838
+
839
+ describe 'with a complex ordering' do
840
+ let(:order) do
841
+ {
842
+ 'author' => :desc,
843
+ 'published_at' => :asc
844
+ }
845
+ end
846
+ let(:ordered_data) do
847
+ filtered_data.sort do |u, v|
848
+ compare = u['author'] <=> v['author']
849
+
850
+ next -compare unless compare.zero?
851
+
852
+ u['published_at'] <=> v['published_at']
853
+ end
854
+ end
855
+
856
+ include_examples 'should apply limit and offset'
857
+ end
858
+ end
859
+
860
+ let(:filter) { defined?(super()) ? super() : nil }
861
+ let(:limit) { defined?(super()) ? super() : nil }
862
+ let(:offset) { defined?(super()) ? super() : nil }
863
+ let(:mapped_data) { defined?(super()) ? super() : data }
864
+ let(:filtered_data) { mapped_data }
865
+ let(:ordered_data) do
866
+ return super() if defined?(super())
867
+
868
+ attr_name = defined?(default_order) ? default_order : 'id'
869
+
870
+ filtered_data.sort_by { |item| item[attr_name] }
871
+ end
872
+ let(:matching_data) do
873
+ data = ordered_data
874
+ data = data[offset..] || [] if offset
875
+ data = data[...limit] || [] if limit
876
+
877
+ data
878
+ end
879
+ let(:expected_data) do
880
+ defined?(super()) ? super() : matching_data
881
+ end
882
+
883
+ include_examples 'should order the results'
884
+
885
+ describe 'with a block filter' do
886
+ let(:filter) { -> { { 'author' => 'Ursula K. LeGuin' } } }
887
+ let(:filtered_data) do
888
+ super().select { |item| item['author'] == 'Ursula K. LeGuin' }
889
+ end
890
+
891
+ include_examples 'should order the results'
892
+ end
893
+
894
+ describe 'with a hash filter' do
895
+ let(:filter) { { 'author' => 'Ursula K. LeGuin' } }
896
+ let(:filtered_data) do
897
+ super().select { |item| item['author'] == 'Ursula K. LeGuin' }
898
+ end
899
+
900
+ include_examples 'should order the results'
901
+ end
902
+
903
+ describe 'with a basic scope filter' do
904
+ let(:filter) do
905
+ Cuprum::Collections::Scope.new({ 'author' => 'Ursula K. LeGuin' })
906
+ end
907
+ let(:filtered_data) do
908
+ super().select { |item| item['author'] == 'Ursula K. LeGuin' }
909
+ end
910
+
911
+ include_examples 'should order the results'
912
+ end
913
+
914
+ describe 'with a complex scope filter' do
915
+ let(:filter) do
916
+ Cuprum::Collections::Scope
917
+ .new({ 'author' => 'Ursula K. LeGuin' })
918
+ .or({ 'series' => nil })
919
+ end
920
+ let(:filtered_data) do
921
+ super().select do |item|
922
+ item['author'] == 'Ursula K. LeGuin' || item['series'].nil?
923
+ end
924
+ end
925
+
926
+ include_examples 'should order the results'
927
+ end
928
+ end
929
+ end
930
+ end