cuprum-collections 0.3.0 → 0.4.0.rc.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/DEVELOPMENT.md +2 -2
  4. data/README.md +13 -11
  5. data/lib/cuprum/collections/association.rb +256 -0
  6. data/lib/cuprum/collections/associations/belongs_to.rb +32 -0
  7. data/lib/cuprum/collections/associations/has_many.rb +23 -0
  8. data/lib/cuprum/collections/associations/has_one.rb +23 -0
  9. data/lib/cuprum/collections/associations.rb +10 -0
  10. data/lib/cuprum/collections/basic/collection.rb +39 -74
  11. data/lib/cuprum/collections/basic/commands/find_many.rb +1 -1
  12. data/lib/cuprum/collections/basic/commands/find_matching.rb +1 -1
  13. data/lib/cuprum/collections/basic/repository.rb +9 -33
  14. data/lib/cuprum/collections/basic.rb +1 -0
  15. data/lib/cuprum/collections/collection.rb +154 -0
  16. data/lib/cuprum/collections/commands/associations/find_many.rb +161 -0
  17. data/lib/cuprum/collections/commands/associations/require_many.rb +48 -0
  18. data/lib/cuprum/collections/commands/associations.rb +13 -0
  19. data/lib/cuprum/collections/commands/find_one_matching.rb +1 -1
  20. data/lib/cuprum/collections/commands.rb +1 -0
  21. data/lib/cuprum/collections/errors/abstract_find_error.rb +1 -1
  22. data/lib/cuprum/collections/relation.rb +401 -0
  23. data/lib/cuprum/collections/repository.rb +71 -4
  24. data/lib/cuprum/collections/resource.rb +65 -0
  25. data/lib/cuprum/collections/rspec/contracts/association_contracts.rb +2137 -0
  26. data/lib/cuprum/collections/rspec/contracts/basic/command_contracts.rb +484 -0
  27. data/lib/cuprum/collections/rspec/contracts/basic.rb +11 -0
  28. data/lib/cuprum/collections/rspec/contracts/collection_contracts.rb +429 -0
  29. data/lib/cuprum/collections/rspec/contracts/command_contracts.rb +1462 -0
  30. data/lib/cuprum/collections/rspec/contracts/query_contracts.rb +1093 -0
  31. data/lib/cuprum/collections/rspec/contracts/relation_contracts.rb +1381 -0
  32. data/lib/cuprum/collections/rspec/contracts/repository_contracts.rb +605 -0
  33. data/lib/cuprum/collections/rspec/contracts.rb +23 -0
  34. data/lib/cuprum/collections/rspec/fixtures.rb +85 -82
  35. data/lib/cuprum/collections/rspec.rb +4 -1
  36. data/lib/cuprum/collections/version.rb +3 -3
  37. data/lib/cuprum/collections.rb +9 -4
  38. metadata +25 -21
  39. data/lib/cuprum/collections/base.rb +0 -11
  40. data/lib/cuprum/collections/basic/rspec/command_contract.rb +0 -392
  41. data/lib/cuprum/collections/rspec/assign_one_command_contract.rb +0 -168
  42. data/lib/cuprum/collections/rspec/build_one_command_contract.rb +0 -93
  43. data/lib/cuprum/collections/rspec/collection_contract.rb +0 -190
  44. data/lib/cuprum/collections/rspec/destroy_one_command_contract.rb +0 -108
  45. data/lib/cuprum/collections/rspec/find_many_command_contract.rb +0 -407
  46. data/lib/cuprum/collections/rspec/find_matching_command_contract.rb +0 -194
  47. data/lib/cuprum/collections/rspec/find_one_command_contract.rb +0 -157
  48. data/lib/cuprum/collections/rspec/insert_one_command_contract.rb +0 -84
  49. data/lib/cuprum/collections/rspec/query_builder_contract.rb +0 -92
  50. data/lib/cuprum/collections/rspec/query_contract.rb +0 -650
  51. data/lib/cuprum/collections/rspec/querying_contract.rb +0 -298
  52. data/lib/cuprum/collections/rspec/repository_contract.rb +0 -235
  53. data/lib/cuprum/collections/rspec/update_one_command_contract.rb +0 -80
  54. data/lib/cuprum/collections/rspec/validate_one_command_contract.rb +0 -96
@@ -0,0 +1,1462 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/rspec/contracts'
4
+
5
+ module Cuprum::Collections::RSpec::Contracts
6
+ # Contracts for asserting on Command objects.
7
+ module CommandContracts
8
+ # Contract validating the behavior of an AssignOne command implementation.
9
+ module ShouldBeAnAssignOneCommandContract
10
+ extend RSpec::SleepingKingStudios::Contract
11
+
12
+ # @!method apply(example_group, allow_extra_attributes:)
13
+ # Adds the contract to the example group.
14
+ #
15
+ # @param example_group [RSpec::Core::ExampleGroup] the example group to
16
+ # which the contract is applied.
17
+ # @param allow_extra_attributes [Boolean] if false, the command should
18
+ # fail if given attributes not defined for the entity.
19
+ contract do |allow_extra_attributes:|
20
+ describe '#call' do
21
+ shared_examples 'should assign the attributes' do
22
+ it { expect(result).to be_a_passing_result }
23
+
24
+ it { expect(result.value).to be_a entity.class }
25
+
26
+ it { expect(result.value).to be == expected_value }
27
+ end
28
+
29
+ let(:attributes) { {} }
30
+ let(:result) do
31
+ command.call(attributes: attributes, entity: entity)
32
+ end
33
+ let(:expected_attributes) do
34
+ initial_attributes.merge(attributes)
35
+ end
36
+ let(:expected_value) do
37
+ defined?(super()) ? super() : expected_attributes
38
+ end
39
+
40
+ it 'should validate the :attributes keyword' do
41
+ expect(command)
42
+ .to validate_parameter(:call, :attributes)
43
+ .using_constraint(
44
+ Stannum::Constraints::Types::HashWithIndifferentKeys.new
45
+ )
46
+ end
47
+
48
+ it 'should validate the :entity keyword' do
49
+ expect(command)
50
+ .to validate_parameter(:call, :entity)
51
+ .using_constraint(entity_type)
52
+ .with_parameters(attributes: {}, entity: nil)
53
+ end
54
+
55
+ describe 'with an empty attributes hash' do
56
+ let(:attributes) { {} }
57
+
58
+ include_examples 'should assign the attributes'
59
+ end
60
+
61
+ describe 'with an attributes hash with partial attributes' do
62
+ let(:attributes) { { title: 'Gideon the Ninth' } }
63
+
64
+ include_examples 'should assign the attributes'
65
+ end
66
+
67
+ describe 'with an attributes hash with full attributes' do
68
+ let(:attributes) do
69
+ {
70
+ title: 'Gideon the Ninth',
71
+ author: 'Tamsyn Muir',
72
+ series: 'The Locked Tomb',
73
+ category: 'Horror'
74
+ }
75
+ end
76
+
77
+ include_examples 'should assign the attributes'
78
+ end
79
+
80
+ describe 'with an attributes hash with extra attributes' do
81
+ let(:attributes) do
82
+ {
83
+ title: 'The Book of Lost Tales',
84
+ audiobook: true
85
+ }
86
+ end
87
+
88
+ if allow_extra_attributes
89
+ include_examples 'should assign the attributes'
90
+ else
91
+ # :nocov:
92
+ let(:valid_attributes) do
93
+ defined?(super()) ? super() : expected_attributes.keys
94
+ end
95
+ let(:expected_error) do
96
+ Cuprum::Collections::Errors::ExtraAttributes.new(
97
+ entity_class: entity.class,
98
+ extra_attributes: %w[audiobook],
99
+ valid_attributes: valid_attributes
100
+ )
101
+ end
102
+
103
+ it 'should return a failing result' do
104
+ expect(result).to be_a_failing_result.with_error(expected_error)
105
+ end
106
+ # :nocov:
107
+ end
108
+ end
109
+
110
+ context 'when the entity has existing attributes' do
111
+ let(:initial_attributes) do
112
+ # :nocov:
113
+ if defined?(super())
114
+ super().merge(fixtures_data.first)
115
+ else
116
+ fixtures_data.first
117
+ end
118
+ # :nocov:
119
+ end
120
+
121
+ describe 'with an empty attributes hash' do
122
+ let(:attributes) { {} }
123
+
124
+ include_examples 'should assign the attributes'
125
+ end
126
+
127
+ describe 'with an attributes hash with partial attributes' do
128
+ let(:attributes) { { title: 'Gideon the Ninth' } }
129
+
130
+ include_examples 'should assign the attributes'
131
+ end
132
+
133
+ describe 'with an attributes hash with full attributes' do
134
+ let(:attributes) do
135
+ {
136
+ title: 'Gideon the Ninth',
137
+ author: 'Tamsyn Muir',
138
+ series: 'The Locked Tomb',
139
+ category: 'Horror'
140
+ }
141
+ end
142
+
143
+ include_examples 'should assign the attributes'
144
+ end
145
+
146
+ describe 'with an attributes hash with extra attributes' do
147
+ let(:attributes) do
148
+ {
149
+ title: 'The Book of Lost Tales',
150
+ audiobook: true
151
+ }
152
+ end
153
+
154
+ if allow_extra_attributes
155
+ include_examples 'should assign the attributes'
156
+ else
157
+ # :nocov:
158
+ let(:valid_attributes) do
159
+ defined?(super()) ? super() : expected_attributes.keys
160
+ end
161
+ let(:expected_error) do
162
+ Cuprum::Collections::Errors::ExtraAttributes.new(
163
+ entity_class: entity.class,
164
+ extra_attributes: %w[audiobook],
165
+ valid_attributes: valid_attributes
166
+ )
167
+ end
168
+
169
+ it 'should return a failing result' do
170
+ expect(result)
171
+ .to be_a_failing_result
172
+ .with_error(expected_error)
173
+ end
174
+ # :nocov:
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ # Contract validating the behavior of a Build command implementation.
183
+ module ShouldBeABuildOneCommandContract
184
+ extend RSpec::SleepingKingStudios::Contract
185
+
186
+ # @!method apply(example_group, allow_extra_attributes:)
187
+ # Adds the contract to the example group.
188
+ #
189
+ # @param example_group [RSpec::Core::ExampleGroup] the example group to
190
+ # which the contract is applied.
191
+ # @param allow_extra_attributes [Boolean] if false, the command should
192
+ # fail if given attributes not defined for the entity.
193
+ contract do |allow_extra_attributes:|
194
+ include Stannum::RSpec::Matchers
195
+
196
+ describe '#call' do
197
+ shared_examples 'should build the entity' do
198
+ it { expect(result).to be_a_passing_result }
199
+
200
+ it { expect(result.value).to be == expected_value }
201
+ end
202
+
203
+ let(:attributes) { {} }
204
+ let(:result) { command.call(attributes: attributes) }
205
+ let(:expected_attributes) do
206
+ attributes
207
+ end
208
+ let(:expected_value) do
209
+ defined?(super()) ? super() : attributes
210
+ end
211
+
212
+ it 'should validate the :attributes keyword' do
213
+ expect(command)
214
+ .to validate_parameter(:call, :attributes)
215
+ .using_constraint(
216
+ Stannum::Constraints::Types::HashWithIndifferentKeys.new
217
+ )
218
+ end
219
+
220
+ describe 'with an empty attributes hash' do
221
+ let(:attributes) { {} }
222
+
223
+ include_examples 'should build the entity'
224
+ end
225
+
226
+ describe 'with an attributes hash with partial attributes' do
227
+ let(:attributes) { { title: 'Gideon the Ninth' } }
228
+
229
+ include_examples 'should build the entity'
230
+ end
231
+
232
+ describe 'with an attributes hash with full attributes' do
233
+ let(:attributes) do
234
+ {
235
+ title: 'Gideon the Ninth',
236
+ author: 'Tamsyn Muir',
237
+ series: 'The Locked Tomb',
238
+ category: 'Horror'
239
+ }
240
+ end
241
+
242
+ include_examples 'should build the entity'
243
+ end
244
+
245
+ describe 'with an attributes hash with extra attributes' do
246
+ let(:attributes) do
247
+ {
248
+ title: 'The Book of Lost Tales',
249
+ audiobook: true
250
+ }
251
+ end
252
+
253
+ if allow_extra_attributes
254
+ include_examples 'should build the entity'
255
+ else
256
+ # :nocov:
257
+ let(:valid_attributes) do
258
+ defined?(super()) ? super() : expected_attributes.keys
259
+ end
260
+ let(:expected_error) do
261
+ Cuprum::Collections::Errors::ExtraAttributes.new(
262
+ entity_class: entity_type,
263
+ extra_attributes: %w[audiobook],
264
+ valid_attributes: valid_attributes
265
+ )
266
+ end
267
+
268
+ it 'should return a failing result' do
269
+ expect(result).to be_a_failing_result.with_error(expected_error)
270
+ end
271
+ # :nocov:
272
+ end
273
+ end
274
+ end
275
+ end
276
+ end
277
+
278
+ # Contract validating the behavior of a FindOne command implementation.
279
+ module ShouldBeADestroyOneCommandContract
280
+ extend RSpec::SleepingKingStudios::Contract
281
+
282
+ # @!method apply(example_group)
283
+ # Adds the contract to the example group.
284
+ #
285
+ # @param example_group [RSpec::Core::ExampleGroup] the example group to
286
+ # which the contract is applied.
287
+ contract do
288
+ describe '#call' do
289
+ let(:mapped_data) do
290
+ defined?(super()) ? super() : data
291
+ end
292
+ let(:primary_key_name) { defined?(super()) ? super() : 'id' }
293
+ let(:primary_key_type) { defined?(super()) ? super() : Integer }
294
+ let(:invalid_primary_key_value) do
295
+ defined?(super()) ? super() : 100
296
+ end
297
+ let(:valid_primary_key_value) do
298
+ defined?(super()) ? super() : 0
299
+ end
300
+
301
+ it 'should validate the :primary_key keyword' do
302
+ expect(command)
303
+ .to validate_parameter(:call, :primary_key)
304
+ .using_constraint(primary_key_type)
305
+ end
306
+
307
+ describe 'with an invalid primary key' do
308
+ let(:primary_key) { invalid_primary_key_value }
309
+ let(:expected_error) do
310
+ Cuprum::Collections::Errors::NotFound.new(
311
+ attribute_name: primary_key_name,
312
+ attribute_value: primary_key,
313
+ collection_name: collection_name,
314
+ primary_key: true
315
+ )
316
+ end
317
+
318
+ it 'should return a failing result' do
319
+ expect(command.call(primary_key: primary_key))
320
+ .to be_a_failing_result
321
+ .with_error(expected_error)
322
+ end
323
+
324
+ it 'should not remove an entity from the collection' do
325
+ expect { command.call(primary_key: primary_key) }
326
+ .not_to(change { query.reset.count })
327
+ end
328
+ end
329
+
330
+ context 'when the collection has many items' do
331
+ let(:data) { fixtures_data }
332
+ let(:matching_data) do
333
+ mapped_data.find do |item|
334
+ item[primary_key_name.to_s] == primary_key
335
+ end
336
+ end
337
+ let!(:expected_data) do
338
+ defined?(super()) ? super() : matching_data
339
+ end
340
+
341
+ describe 'with an invalid primary key' do
342
+ let(:primary_key) { invalid_primary_key_value }
343
+ let(:expected_error) do
344
+ Cuprum::Collections::Errors::NotFound.new(
345
+ attribute_name: primary_key_name,
346
+ attribute_value: primary_key,
347
+ collection_name: collection_name,
348
+ primary_key: true
349
+ )
350
+ end
351
+
352
+ it 'should return a failing result' do
353
+ expect(command.call(primary_key: primary_key))
354
+ .to be_a_failing_result
355
+ .with_error(expected_error)
356
+ end
357
+
358
+ it 'should not remove an entity from the collection' do
359
+ expect { command.call(primary_key: primary_key) }
360
+ .not_to(change { query.reset.count })
361
+ end
362
+ end
363
+
364
+ describe 'with a valid primary key' do
365
+ let(:primary_key) { valid_primary_key_value }
366
+
367
+ it 'should return a passing result' do
368
+ expect(command.call(primary_key: primary_key))
369
+ .to be_a_passing_result
370
+ .with_value(expected_data)
371
+ end
372
+
373
+ it 'should remove an entity from the collection' do
374
+ expect { command.call(primary_key: primary_key) }
375
+ .to(
376
+ change { query.reset.count }.by(-1)
377
+ )
378
+ end
379
+
380
+ it 'should remove the entity from the collection' do
381
+ command.call(primary_key: primary_key)
382
+
383
+ expect(query.map { |item| item[primary_key_name.to_s] })
384
+ .not_to include primary_key
385
+ end
386
+ end
387
+ end
388
+ end
389
+ end
390
+ end
391
+
392
+ # Contract validating the behavior of a FindMany command implementation.
393
+ module ShouldBeAFindManyCommandContract
394
+ extend RSpec::SleepingKingStudios::Contract
395
+
396
+ # @!method apply(example_group)
397
+ # Adds the contract to the example group.
398
+ #
399
+ # @param example_group [RSpec::Core::ExampleGroup] the example group to
400
+ # which the contract is applied.
401
+ contract do
402
+ describe '#call' do
403
+ let(:mapped_data) do
404
+ defined?(super()) ? super() : data
405
+ end
406
+ let(:primary_key_name) { defined?(super()) ? super() : 'id' }
407
+ let(:primary_key_type) { defined?(super()) ? super() : Integer }
408
+ let(:primary_keys_contract) do
409
+ Stannum::Constraints::Types::ArrayType
410
+ .new(item_type: primary_key_type)
411
+ end
412
+ let(:invalid_primary_key_values) do
413
+ defined?(super()) ? super() : [100, 101, 102]
414
+ end
415
+ let(:valid_primary_key_values) do
416
+ defined?(super()) ? super() : [0, 1, 2]
417
+ end
418
+
419
+ it 'should validate the :allow_partial keyword' do
420
+ expect(command)
421
+ .to validate_parameter(:call, :allow_partial)
422
+ .using_constraint(Stannum::Constraints::Boolean.new)
423
+ end
424
+
425
+ it 'should validate the :envelope keyword' do
426
+ expect(command)
427
+ .to validate_parameter(:call, :envelope)
428
+ .using_constraint(Stannum::Constraints::Boolean.new)
429
+ end
430
+
431
+ it 'should validate the :primary_keys keyword' do
432
+ expect(command)
433
+ .to validate_parameter(:call, :primary_keys)
434
+ .using_constraint(Array)
435
+ end
436
+
437
+ it 'should validate the :primary_keys keyword items' do
438
+ expect(command)
439
+ .to validate_parameter(:call, :primary_keys)
440
+ .with_value([nil])
441
+ .using_constraint(primary_keys_contract)
442
+ end
443
+
444
+ it 'should validate the :scope keyword' do
445
+ expect(command)
446
+ .to validate_parameter(:call, :scope)
447
+ .using_constraint(
448
+ Stannum::Constraints::Type.new(query.class, optional: true)
449
+ )
450
+ .with_value(Object.new.freeze)
451
+ end
452
+
453
+ describe 'with an array of invalid primary keys' do
454
+ let(:primary_keys) { invalid_primary_key_values }
455
+ let(:expected_error) do
456
+ Cuprum::Errors::MultipleErrors.new(
457
+ errors: primary_keys.map do |primary_key|
458
+ Cuprum::Collections::Errors::NotFound.new(
459
+ attribute_name: primary_key_name,
460
+ attribute_value: primary_key,
461
+ collection_name: command.collection_name,
462
+ primary_key: true
463
+ )
464
+ end
465
+ )
466
+ end
467
+
468
+ it 'should return a failing result' do
469
+ expect(command.call(primary_keys: primary_keys))
470
+ .to be_a_failing_result
471
+ .with_error(expected_error)
472
+ end
473
+ end
474
+
475
+ context 'when the collection has many items' do
476
+ let(:data) { fixtures_data }
477
+ let(:matching_data) do
478
+ primary_keys
479
+ .map do |key|
480
+ mapped_data.find { |item| item[primary_key_name.to_s] == key }
481
+ end
482
+ end
483
+ let(:expected_data) do
484
+ defined?(super()) ? super() : matching_data
485
+ end
486
+
487
+ describe 'with an array of invalid primary keys' do
488
+ let(:primary_keys) { invalid_primary_key_values }
489
+ let(:expected_error) do
490
+ Cuprum::Errors::MultipleErrors.new(
491
+ errors: primary_keys.map do |primary_key|
492
+ Cuprum::Collections::Errors::NotFound.new(
493
+ attribute_name: primary_key_name,
494
+ attribute_value: primary_key,
495
+ collection_name: command.collection_name,
496
+ primary_key: true
497
+ )
498
+ end
499
+ )
500
+ end
501
+
502
+ it 'should return a failing result' do
503
+ expect(command.call(primary_keys: primary_keys))
504
+ .to be_a_failing_result
505
+ .with_error(expected_error)
506
+ end
507
+ end
508
+
509
+ describe 'with a partially valid array of primary keys' do
510
+ let(:primary_keys) do
511
+ invalid_primary_key_values + valid_primary_key_values
512
+ end
513
+ let(:expected_error) do
514
+ Cuprum::Errors::MultipleErrors.new(
515
+ errors: primary_keys.map do |primary_key|
516
+ unless invalid_primary_key_values.include?(primary_key)
517
+ next nil
518
+ end
519
+
520
+ Cuprum::Collections::Errors::NotFound.new(
521
+ attribute_name: primary_key_name,
522
+ attribute_value: primary_key,
523
+ collection_name: command.collection_name,
524
+ primary_key: true
525
+ )
526
+ end
527
+ )
528
+ end
529
+
530
+ it 'should return a failing result' do
531
+ expect(command.call(primary_keys: primary_keys))
532
+ .to be_a_failing_result
533
+ .with_error(expected_error)
534
+ end
535
+ end
536
+
537
+ describe 'with a valid array of primary keys' do
538
+ let(:primary_keys) { valid_primary_key_values }
539
+
540
+ it 'should return a passing result' do
541
+ expect(command.call(primary_keys: primary_keys))
542
+ .to be_a_passing_result
543
+ .with_value(expected_data)
544
+ end
545
+
546
+ describe 'with an ordered array of primary keys' do
547
+ let(:primary_keys) { valid_primary_key_values.reverse }
548
+
549
+ it 'should return a passing result' do
550
+ expect(command.call(primary_keys: primary_keys))
551
+ .to be_a_passing_result
552
+ .with_value(expected_data)
553
+ end
554
+ end
555
+ end
556
+
557
+ describe 'with allow_partial: true' do
558
+ describe 'with an array of invalid primary keys' do
559
+ let(:primary_keys) { invalid_primary_key_values }
560
+ let(:expected_error) do
561
+ Cuprum::Errors::MultipleErrors.new(
562
+ errors: invalid_primary_key_values.map do |primary_key|
563
+ Cuprum::Collections::Errors::NotFound.new(
564
+ attribute_name: primary_key_name,
565
+ attribute_value: primary_key,
566
+ collection_name: command.collection_name,
567
+ primary_key: true
568
+ )
569
+ end
570
+ )
571
+ end
572
+
573
+ it 'should return a failing result' do
574
+ expect(command.call(primary_keys: primary_keys))
575
+ .to be_a_failing_result
576
+ .with_error(expected_error)
577
+ end
578
+ end
579
+
580
+ describe 'with a partially valid array of primary keys' do
581
+ let(:primary_keys) do
582
+ invalid_primary_key_values + valid_primary_key_values
583
+ end
584
+ let(:expected_error) do
585
+ Cuprum::Errors::MultipleErrors.new(
586
+ errors: primary_keys.map do |primary_key|
587
+ unless invalid_primary_key_values.include?(primary_key)
588
+ next nil
589
+ end
590
+
591
+ Cuprum::Collections::Errors::NotFound.new(
592
+ attribute_name: primary_key_name,
593
+ attribute_value: primary_key,
594
+ collection_name: command.collection_name,
595
+ primary_key: true
596
+ )
597
+ end
598
+ )
599
+ end
600
+
601
+ it 'should return a passing result' do
602
+ expect(
603
+ command.call(
604
+ primary_keys: primary_keys,
605
+ allow_partial: true
606
+ )
607
+ )
608
+ .to be_a_passing_result
609
+ .with_value(expected_data)
610
+ .and_error(expected_error)
611
+ end
612
+ end
613
+
614
+ describe 'with a valid array of primary keys' do
615
+ let(:primary_keys) { valid_primary_key_values }
616
+
617
+ it 'should return a passing result' do
618
+ expect(
619
+ command.call(
620
+ primary_keys: primary_keys,
621
+ allow_partial: true
622
+ )
623
+ )
624
+ .to be_a_passing_result
625
+ .with_value(expected_data)
626
+ end
627
+
628
+ describe 'with an ordered array of primary keys' do
629
+ let(:primary_keys) { valid_primary_key_values.reverse }
630
+
631
+ it 'should return a passing result' do
632
+ expect(
633
+ command.call(
634
+ primary_keys: primary_keys,
635
+ allow_partial: true
636
+ )
637
+ )
638
+ .to be_a_passing_result
639
+ .with_value(expected_data)
640
+ end
641
+ end
642
+ end
643
+ end
644
+
645
+ describe 'with envelope: true' do
646
+ describe 'with a valid array of primary keys' do
647
+ let(:primary_keys) { valid_primary_key_values }
648
+
649
+ it 'should return a passing result' do
650
+ expect(
651
+ command.call(primary_keys: primary_keys, envelope: true)
652
+ )
653
+ .to be_a_passing_result
654
+ .with_value({ collection_name => expected_data })
655
+ end
656
+
657
+ describe 'with an ordered array of primary keys' do
658
+ let(:primary_keys) { valid_primary_key_values.reverse }
659
+
660
+ it 'should return a passing result' do
661
+ expect(
662
+ command.call(primary_keys: primary_keys, envelope: true)
663
+ )
664
+ .to be_a_passing_result
665
+ .with_value({ collection_name => expected_data })
666
+ end
667
+ end
668
+ end
669
+ end
670
+
671
+ describe 'with scope: query' do
672
+ let(:scope_filter) { -> { {} } }
673
+
674
+ describe 'with an array of invalid primary keys' do
675
+ let(:primary_keys) { invalid_primary_key_values }
676
+ let(:expected_error) do
677
+ Cuprum::Errors::MultipleErrors.new(
678
+ errors: primary_keys.map do |primary_key|
679
+ Cuprum::Collections::Errors::NotFound.new(
680
+ attribute_name: primary_key_name,
681
+ attribute_value: primary_key,
682
+ collection_name: command.collection_name,
683
+ primary_key: true
684
+ )
685
+ end
686
+ )
687
+ end
688
+
689
+ it 'should return a failing result' do
690
+ expect(command.call(primary_keys: primary_keys, scope: scope))
691
+ .to be_a_failing_result
692
+ .with_error(expected_error)
693
+ end
694
+ end
695
+
696
+ describe 'with a scope that does not match any keys' do
697
+ let(:scope_filter) { -> { { author: 'Ursula K. LeGuin' } } }
698
+
699
+ describe 'with a valid array of primary keys' do
700
+ let(:primary_keys) { valid_primary_key_values }
701
+ let(:expected_error) do
702
+ Cuprum::Errors::MultipleErrors.new(
703
+ errors: primary_keys.map do |primary_key|
704
+ Cuprum::Collections::Errors::NotFound.new(
705
+ attribute_name: primary_key_name,
706
+ attribute_value: primary_key,
707
+ collection_name: command.collection_name,
708
+ primary_key: true
709
+ )
710
+ end
711
+ )
712
+ end
713
+
714
+ it 'should return a failing result' do
715
+ expect(
716
+ command.call(primary_keys: primary_keys, scope: scope)
717
+ )
718
+ .to be_a_failing_result
719
+ .with_error(expected_error)
720
+ end
721
+ end
722
+ end
723
+
724
+ describe 'with a scope that matches some keys' do
725
+ let(:scope_filter) { -> { { series: nil } } }
726
+ let(:matching_data) do
727
+ super().map do |item|
728
+ next nil unless item['series'].nil?
729
+
730
+ item
731
+ end
732
+ end
733
+
734
+ describe 'with a valid array of primary keys' do
735
+ let(:primary_keys) { valid_primary_key_values }
736
+ let(:expected_error) do
737
+ found_keys =
738
+ matching_data
739
+ .compact
740
+ .map { |item| item[primary_key_name.to_s] }
741
+
742
+ Cuprum::Errors::MultipleErrors.new(
743
+ errors: primary_keys.map do |primary_key|
744
+ next if found_keys.include?(primary_key)
745
+
746
+ Cuprum::Collections::Errors::NotFound.new(
747
+ attribute_name: primary_key_name,
748
+ attribute_value: primary_key,
749
+ collection_name: command.collection_name,
750
+ primary_key: true
751
+ )
752
+ end
753
+ )
754
+ end
755
+
756
+ it 'should return a failing result' do
757
+ expect(
758
+ command.call(primary_keys: primary_keys, scope: scope)
759
+ )
760
+ .to be_a_failing_result
761
+ .with_error(expected_error)
762
+ end
763
+ end
764
+
765
+ describe 'with allow_partial: true' do
766
+ describe 'with a valid array of primary keys' do
767
+ let(:primary_keys) { valid_primary_key_values }
768
+ let(:expected_error) do
769
+ found_keys =
770
+ matching_data
771
+ .compact
772
+ .map { |item| item[primary_key_name.to_s] }
773
+
774
+ Cuprum::Errors::MultipleErrors.new(
775
+ errors: primary_keys.map do |primary_key|
776
+ next if found_keys.include?(primary_key)
777
+
778
+ Cuprum::Collections::Errors::NotFound.new(
779
+ attribute_name: primary_key_name,
780
+ attribute_value: primary_key,
781
+ collection_name: command.collection_name,
782
+ primary_key: true
783
+ )
784
+ end
785
+ )
786
+ end
787
+
788
+ it 'should return a passing result' do
789
+ expect(
790
+ command.call(
791
+ allow_partial: true,
792
+ primary_keys: primary_keys,
793
+ scope: scope
794
+ )
795
+ )
796
+ .to be_a_passing_result
797
+ .with_value(expected_data)
798
+ .and_error(expected_error)
799
+ end
800
+ end
801
+ end
802
+ end
803
+
804
+ describe 'with a scope that matches all keys' do
805
+ let(:scope_filter) { -> { { author: 'J.R.R. Tolkien' } } }
806
+
807
+ describe 'with a valid array of primary keys' do
808
+ let(:primary_keys) { valid_primary_key_values }
809
+
810
+ it 'should return a passing result' do
811
+ expect(command.call(primary_keys: primary_keys))
812
+ .to be_a_passing_result
813
+ .with_value(expected_data)
814
+ end
815
+ end
816
+ end
817
+ end
818
+ end
819
+ end
820
+ end
821
+ end
822
+
823
+ # Contract validating the behavior of a FindMatching command implementation.
824
+ module ShouldBeAFindMatchingCommandContract
825
+ extend RSpec::SleepingKingStudios::Contract
826
+
827
+ # @!method apply(example_group)
828
+ # Adds the contract to the example group.
829
+ #
830
+ # @param example_group [RSpec::Core::ExampleGroup] the example group to
831
+ # which the contract is applied.
832
+ contract do
833
+ include Stannum::RSpec::Matchers
834
+ include Cuprum::Collections::RSpec::Contracts::QueryContracts
835
+
836
+ describe '#call' do
837
+ include_contract 'with query contexts'
838
+
839
+ shared_examples 'should return the matching items' do
840
+ it { expect(result).to be_a_passing_result }
841
+
842
+ it { expect(result.value).to be_a Enumerator }
843
+
844
+ it { expect(result.value.to_a).to be == expected_data }
845
+ end
846
+
847
+ shared_examples 'should return the wrapped items' do
848
+ it { expect(result).to be_a_passing_result }
849
+
850
+ it { expect(result.value).to be_a Hash }
851
+
852
+ it { expect(result.value.keys).to be == [collection_name] }
853
+
854
+ it { expect(result.value[collection_name]).to be == expected_data }
855
+ end
856
+
857
+ let(:options) do
858
+ opts = {}
859
+
860
+ opts[:limit] = limit if limit
861
+ opts[:offset] = offset if offset
862
+ opts[:order] = order if order
863
+ opts[:where] = filter unless filter.nil? || filter.is_a?(Proc)
864
+
865
+ opts
866
+ end
867
+ let(:block) { filter.is_a?(Proc) ? filter : nil }
868
+ let(:result) { command.call(**options, &block) }
869
+ let(:data) { [] }
870
+ let(:matching_data) { data }
871
+ let(:expected_data) do
872
+ defined?(super()) ? super() : matching_data
873
+ end
874
+
875
+ it 'should validate the :envelope keyword' do
876
+ expect(command)
877
+ .to validate_parameter(:call, :envelope)
878
+ .using_constraint(Stannum::Constraints::Boolean.new)
879
+ end
880
+
881
+ it 'should validate the :limit keyword' do
882
+ expect(command)
883
+ .to validate_parameter(:call, :limit)
884
+ .with_value(Object.new)
885
+ .using_constraint(Integer, required: false)
886
+ end
887
+
888
+ it 'should validate the :offset keyword' do
889
+ expect(command)
890
+ .to validate_parameter(:call, :offset)
891
+ .with_value(Object.new)
892
+ .using_constraint(Integer, required: false)
893
+ end
894
+
895
+ it 'should validate the :order keyword' do
896
+ constraint = Cuprum::Collections::Constraints::Ordering.new
897
+
898
+ expect(command)
899
+ .to validate_parameter(:call, :order)
900
+ .with_value(Object.new)
901
+ .using_constraint(constraint, required: false)
902
+ end
903
+
904
+ it 'should validate the :scope keyword' do
905
+ expect(command)
906
+ .to validate_parameter(:call, :scope)
907
+ .using_constraint(
908
+ Stannum::Constraints::Type.new(query.class, optional: true)
909
+ )
910
+ .with_value(Object.new.freeze)
911
+ end
912
+
913
+ it 'should validate the :where keyword' do
914
+ expect(command).to validate_parameter(:call, :where)
915
+ end
916
+
917
+ include_examples 'should return the matching items'
918
+
919
+ include_contract 'should perform queries',
920
+ block: lambda {
921
+ include_examples 'should return the matching items'
922
+ }
923
+
924
+ describe 'with an invalid filter block' do
925
+ let(:block) { -> {} }
926
+ let(:expected_error) do
927
+ an_instance_of(Cuprum::Collections::Errors::InvalidQuery)
928
+ end
929
+
930
+ it 'should return a failing result' do
931
+ expect(result).to be_a_failing_result.with_error(expected_error)
932
+ end
933
+ end
934
+
935
+ describe 'with envelope: true' do
936
+ let(:options) { super().merge(envelope: true) }
937
+
938
+ include_examples 'should return the wrapped items'
939
+
940
+ include_contract 'should perform queries',
941
+ block: lambda {
942
+ include_examples 'should return the wrapped items'
943
+ }
944
+ end
945
+
946
+ context 'when the collection has many items' do
947
+ let(:data) { fixtures_data }
948
+
949
+ include_examples 'should return the matching items'
950
+
951
+ include_contract 'should perform queries',
952
+ block: lambda {
953
+ include_examples 'should return the matching items'
954
+ }
955
+
956
+ describe 'with envelope: true' do
957
+ let(:options) { super().merge(envelope: true) }
958
+
959
+ include_examples 'should return the wrapped items'
960
+
961
+ include_contract 'should perform queries',
962
+ block: lambda {
963
+ include_examples 'should return the wrapped items'
964
+ }
965
+ end
966
+
967
+ describe 'with scope: query' do
968
+ let(:scope_filter) { -> { {} } }
969
+ let(:options) { super().merge(scope: scope) }
970
+
971
+ describe 'with a scope that does not match any values' do
972
+ let(:scope_filter) { -> { { series: 'Mistborn' } } }
973
+ let(:matching_data) { [] }
974
+
975
+ include_examples 'should return the matching items'
976
+ end
977
+
978
+ describe 'with a scope that matches some values' do
979
+ let(:scope_filter) { -> { { series: nil } } }
980
+ let(:matching_data) do
981
+ super().select { |item| item['series'].nil? }
982
+ end
983
+
984
+ include_examples 'should return the matching items'
985
+
986
+ describe 'with a where filter' do
987
+ let(:filter) { -> { { author: 'Ursula K. LeGuin' } } }
988
+ let(:options) { super().merge(where: filter) }
989
+ let(:matching_data) do
990
+ super()
991
+ .select { |item| item['author'] == 'Ursula K. LeGuin' }
992
+ end
993
+
994
+ include_examples 'should return the matching items'
995
+ end
996
+ end
997
+
998
+ describe 'with a scope that matches all values' do
999
+ let(:scope_filter) { -> { { id: not_equal(nil) } } }
1000
+
1001
+ include_examples 'should return the matching items'
1002
+
1003
+ describe 'with a where filter' do
1004
+ let(:filter) { -> { { author: 'Ursula K. LeGuin' } } }
1005
+ let(:options) { super().merge(where: filter) }
1006
+ let(:matching_data) do
1007
+ super()
1008
+ .select { |item| item['author'] == 'Ursula K. LeGuin' }
1009
+ end
1010
+
1011
+ include_examples 'should return the matching items'
1012
+ end
1013
+ end
1014
+ end
1015
+ end
1016
+ end
1017
+ end
1018
+ end
1019
+
1020
+ # Contract validating the behavior of a FindOne command implementation.
1021
+ module ShouldBeAFindOneCommandContract
1022
+ extend RSpec::SleepingKingStudios::Contract
1023
+
1024
+ # @!method apply(example_group)
1025
+ # Adds the contract to the example group.
1026
+ #
1027
+ # @param example_group [RSpec::Core::ExampleGroup] the example group to
1028
+ # which the contract is applied.
1029
+ contract do
1030
+ describe '#call' do
1031
+ let(:mapped_data) do
1032
+ defined?(super()) ? super() : data
1033
+ end
1034
+ let(:primary_key_name) { defined?(super()) ? super() : 'id' }
1035
+ let(:primary_key_type) { defined?(super()) ? super() : Integer }
1036
+ let(:invalid_primary_key_value) do
1037
+ defined?(super()) ? super() : 100
1038
+ end
1039
+ let(:valid_primary_key_value) do
1040
+ defined?(super()) ? super() : 0
1041
+ end
1042
+
1043
+ def tools
1044
+ SleepingKingStudios::Tools::Toolbelt.instance
1045
+ end
1046
+
1047
+ it 'should validate the :envelope keyword' do
1048
+ expect(command)
1049
+ .to validate_parameter(:call, :envelope)
1050
+ .using_constraint(Stannum::Constraints::Boolean.new)
1051
+ end
1052
+
1053
+ it 'should validate the :primary_key keyword' do
1054
+ expect(command)
1055
+ .to validate_parameter(:call, :primary_key)
1056
+ .using_constraint(primary_key_type)
1057
+ end
1058
+
1059
+ it 'should validate the :scope keyword' do
1060
+ expect(command)
1061
+ .to validate_parameter(:call, :scope)
1062
+ .using_constraint(
1063
+ Stannum::Constraints::Type.new(query.class, optional: true)
1064
+ )
1065
+ .with_value(Object.new.freeze)
1066
+ end
1067
+
1068
+ describe 'with an invalid primary key' do
1069
+ let(:primary_key) { invalid_primary_key_value }
1070
+ let(:expected_error) do
1071
+ Cuprum::Collections::Errors::NotFound.new(
1072
+ attribute_name: primary_key_name,
1073
+ attribute_value: primary_key,
1074
+ collection_name: command.collection_name,
1075
+ primary_key: true
1076
+ )
1077
+ end
1078
+
1079
+ it 'should return a failing result' do
1080
+ expect(command.call(primary_key: primary_key))
1081
+ .to be_a_failing_result
1082
+ .with_error(expected_error)
1083
+ end
1084
+ end
1085
+
1086
+ context 'when the collection has many items' do
1087
+ let(:data) { fixtures_data }
1088
+ let(:matching_data) do
1089
+ mapped_data
1090
+ .find { |item| item[primary_key_name.to_s] == primary_key }
1091
+ end
1092
+ let(:expected_data) do
1093
+ defined?(super()) ? super() : matching_data
1094
+ end
1095
+
1096
+ describe 'with an invalid primary key' do
1097
+ let(:primary_key) { invalid_primary_key_value }
1098
+ let(:expected_error) do
1099
+ Cuprum::Collections::Errors::NotFound.new(
1100
+ attribute_name: primary_key_name,
1101
+ attribute_value: primary_key,
1102
+ collection_name: command.collection_name,
1103
+ primary_key: true
1104
+ )
1105
+ end
1106
+
1107
+ it 'should return a failing result' do
1108
+ expect(command.call(primary_key: primary_key))
1109
+ .to be_a_failing_result
1110
+ .with_error(expected_error)
1111
+ end
1112
+ end
1113
+
1114
+ describe 'with a valid primary key' do
1115
+ let(:primary_key) { valid_primary_key_value }
1116
+
1117
+ it 'should return a passing result' do
1118
+ expect(command.call(primary_key: primary_key))
1119
+ .to be_a_passing_result
1120
+ .with_value(expected_data)
1121
+ end
1122
+ end
1123
+
1124
+ describe 'with envelope: true' do
1125
+ let(:member_name) { tools.str.singularize(collection_name) }
1126
+
1127
+ describe 'with a valid primary key' do
1128
+ let(:primary_key) { valid_primary_key_value }
1129
+
1130
+ it 'should return a passing result' do
1131
+ expect(command.call(primary_key: primary_key, envelope: true))
1132
+ .to be_a_passing_result
1133
+ .with_value({ member_name => expected_data })
1134
+ end
1135
+ end
1136
+ end
1137
+
1138
+ describe 'with scope: query' do
1139
+ let(:scope_filter) { -> { {} } }
1140
+
1141
+ describe 'with a scope that does not match the key' do
1142
+ let(:scope_filter) { -> { { author: 'Ursula K. LeGuin' } } }
1143
+
1144
+ describe 'with an valid primary key' do
1145
+ let(:primary_key) { valid_primary_key_value }
1146
+ let(:expected_error) do
1147
+ Cuprum::Collections::Errors::NotFound.new(
1148
+ attribute_name: primary_key_name,
1149
+ attribute_value: primary_key,
1150
+ collection_name: command.collection_name,
1151
+ primary_key: true
1152
+ )
1153
+ end
1154
+
1155
+ it 'should return a failing result' do
1156
+ expect(command.call(primary_key: primary_key, scope: scope))
1157
+ .to be_a_failing_result
1158
+ .with_error(expected_error)
1159
+ end
1160
+ end
1161
+ end
1162
+
1163
+ describe 'with a scope that matches the key' do
1164
+ let(:scope_filter) { -> { { author: 'J.R.R. Tolkien' } } }
1165
+
1166
+ describe 'with a valid primary key' do
1167
+ let(:primary_key) { valid_primary_key_value }
1168
+
1169
+ it 'should return a passing result' do
1170
+ expect(command.call(primary_key: primary_key))
1171
+ .to be_a_passing_result
1172
+ .with_value(expected_data)
1173
+ end
1174
+ end
1175
+ end
1176
+ end
1177
+ end
1178
+ end
1179
+ end
1180
+ end
1181
+
1182
+ # Contract validating the behavior of an InsertOne command implementation.
1183
+ module ShouldBeAnInsertOneCommandContract
1184
+ extend RSpec::SleepingKingStudios::Contract
1185
+
1186
+ # @!method apply(example_group)
1187
+ # Adds the contract to the example group.
1188
+ #
1189
+ # @param example_group [RSpec::Core::ExampleGroup] the example group to
1190
+ # which the contract is applied.
1191
+ contract do
1192
+ describe '#call' do
1193
+ let(:matching_data) { attributes }
1194
+ let(:expected_data) do
1195
+ defined?(super()) ? super() : matching_data
1196
+ end
1197
+ let(:primary_key_name) do
1198
+ defined?(super()) ? super() : 'id'
1199
+ end
1200
+ let(:primary_key_type) do
1201
+ defined?(super()) ? super() : Integer
1202
+ end
1203
+ let(:scoped) do
1204
+ key = primary_key_name
1205
+ value = entity[primary_key_name.to_s]
1206
+
1207
+ query.where { { key => value } }
1208
+ end
1209
+
1210
+ it 'should validate the :entity keyword' do
1211
+ expect(command)
1212
+ .to validate_parameter(:call, :entity)
1213
+ .using_constraint(entity_type)
1214
+ end
1215
+
1216
+ context 'when the item does not exist in the collection' do
1217
+ it 'should return a passing result' do
1218
+ expect(command.call(entity: entity))
1219
+ .to be_a_passing_result
1220
+ .with_value(be == expected_data)
1221
+ end
1222
+
1223
+ it 'should append an item to the collection' do
1224
+ expect { command.call(entity: entity) }
1225
+ .to(
1226
+ change { query.reset.count }
1227
+ .by(1)
1228
+ )
1229
+ end
1230
+
1231
+ it 'should add the entity to the collection' do
1232
+ expect { command.call(entity: entity) }
1233
+ .to change(scoped, :exists?)
1234
+ .to be true
1235
+ end
1236
+
1237
+ it 'should set the attributes' do
1238
+ command.call(entity: entity)
1239
+
1240
+ expect(scoped.to_a.first).to be == expected_data
1241
+ end
1242
+ end
1243
+
1244
+ context 'when the item exists in the collection' do
1245
+ let(:data) { fixtures_data }
1246
+ let(:expected_error) do
1247
+ Cuprum::Collections::Errors::AlreadyExists.new(
1248
+ attribute_name: primary_key_name,
1249
+ attribute_value: attributes.fetch(
1250
+ primary_key_name.to_s,
1251
+ attributes[primary_key_name.intern]
1252
+ ),
1253
+ collection_name: collection_name,
1254
+ primary_key: true
1255
+ )
1256
+ end
1257
+
1258
+ it 'should return a failing result' do
1259
+ expect(command.call(entity: entity))
1260
+ .to be_a_failing_result
1261
+ .with_error(expected_error)
1262
+ end
1263
+
1264
+ it 'should not append an item to the collection' do
1265
+ expect { command.call(entity: entity) }
1266
+ .not_to(change { query.reset.count })
1267
+ end
1268
+ end
1269
+ end
1270
+ end
1271
+ end
1272
+
1273
+ # Contract validating the behavior of an UpdateOne command implementation.
1274
+ module ShouldBeAnUpdateOneCommandContract
1275
+ extend RSpec::SleepingKingStudios::Contract
1276
+
1277
+ # @!method apply(example_group)
1278
+ # Adds the contract to the example group.
1279
+ #
1280
+ # @param example_group [RSpec::Core::ExampleGroup] the example group to
1281
+ # which the contract is applied.
1282
+ contract do
1283
+ describe '#call' do
1284
+ let(:mapped_data) do
1285
+ defined?(super()) ? super() : data
1286
+ end
1287
+ let(:matching_data) { attributes }
1288
+ let(:expected_data) do
1289
+ defined?(super()) ? super() : matching_data
1290
+ end
1291
+ let(:primary_key_name) do
1292
+ defined?(super()) ? super() : 'id'
1293
+ end
1294
+ let(:scoped) do
1295
+ key = primary_key_name
1296
+ value = entity[primary_key_name.to_s]
1297
+
1298
+ query.where { { key => value } }
1299
+ end
1300
+
1301
+ it 'should validate the :entity keyword' do
1302
+ expect(command)
1303
+ .to validate_parameter(:call, :entity)
1304
+ .using_constraint(entity_type)
1305
+ end
1306
+
1307
+ context 'when the item does not exist in the collection' do
1308
+ let(:expected_error) do
1309
+ Cuprum::Collections::Errors::NotFound.new(
1310
+ attribute_name: primary_key_name,
1311
+ attribute_value: attributes.fetch(
1312
+ primary_key_name.to_s,
1313
+ attributes[primary_key_name.intern]
1314
+ ),
1315
+ collection_name: collection_name,
1316
+ primary_key: true
1317
+ )
1318
+ end
1319
+ let(:matching_data) { mapped_data.first }
1320
+
1321
+ it 'should return a failing result' do
1322
+ expect(command.call(entity: entity))
1323
+ .to be_a_failing_result
1324
+ .with_error(expected_error)
1325
+ end
1326
+
1327
+ it 'should not append an item to the collection' do
1328
+ expect { command.call(entity: entity) }
1329
+ .not_to(change { query.reset.count })
1330
+ end
1331
+ end
1332
+
1333
+ context 'when the item exists in the collection' do
1334
+ let(:data) { fixtures_data }
1335
+ let(:matching_data) do
1336
+ mapped_data.first.merge(super())
1337
+ end
1338
+
1339
+ it 'should return a passing result' do
1340
+ expect(command.call(entity: entity))
1341
+ .to be_a_passing_result
1342
+ .with_value(be == expected_data)
1343
+ end
1344
+
1345
+ it 'should not append an item to the collection' do
1346
+ expect { command.call(entity: entity) }
1347
+ .not_to(change { query.reset.count })
1348
+ end
1349
+
1350
+ it 'should set the attributes' do
1351
+ command.call(entity: entity)
1352
+
1353
+ expect(scoped.to_a.first).to be == expected_data
1354
+ end
1355
+ end
1356
+ end
1357
+ end
1358
+ end
1359
+
1360
+ # Contract validating the behavior of a ValidateOne command implementation.
1361
+ module ShouldBeAValidateOneCommandContract
1362
+ extend RSpec::SleepingKingStudios::Contract
1363
+
1364
+ # @!method apply(example_group, default_contract:)
1365
+ # Adds the contract to the example group.
1366
+ #
1367
+ # @param default_contract [Boolean] if true, the command defines a
1368
+ # default contract.
1369
+ # @param example_group [RSpec::Core::ExampleGroup] the example group to
1370
+ # which the contract is applied.
1371
+ contract do |default_contract:|
1372
+ describe '#call' do
1373
+ it 'should validate the :contract keyword' do
1374
+ expect(command)
1375
+ .to validate_parameter(:call, :contract)
1376
+ .with_value(Object.new.freeze)
1377
+ .using_constraint(Stannum::Constraints::Base, optional: true)
1378
+ end
1379
+
1380
+ it 'should validate the :entity keyword' do
1381
+ expect(command)
1382
+ .to validate_parameter(:call, :entity)
1383
+ .with_value(Object.new.freeze)
1384
+ .using_constraint(entity_type)
1385
+ end
1386
+
1387
+ describe 'with contract: nil' do
1388
+ if default_contract
1389
+ context 'when the entity does not match the default contract' do
1390
+ let(:attributes) { invalid_default_attributes }
1391
+ let(:expected_error) do
1392
+ Cuprum::Collections::Errors::FailedValidation.new(
1393
+ entity_class: entity.class,
1394
+ errors: expected_errors
1395
+ )
1396
+ end
1397
+
1398
+ it 'should return a failing result' do
1399
+ expect(command.call(entity: entity))
1400
+ .to be_a_failing_result
1401
+ .with_error(expected_error)
1402
+ end
1403
+ end
1404
+
1405
+ context 'when the entity matches the default contract' do
1406
+ let(:attributes) { valid_default_attributes }
1407
+
1408
+ it 'should return a passing result' do
1409
+ expect(command.call(entity: entity))
1410
+ .to be_a_passing_result
1411
+ .with_value(entity)
1412
+ end
1413
+ end
1414
+ else
1415
+ let(:attributes) { valid_attributes }
1416
+ let(:expected_error) do
1417
+ Cuprum::Collections::Errors::MissingDefaultContract.new(
1418
+ entity_class: entity.class
1419
+ )
1420
+ end
1421
+
1422
+ it 'should return a failing result' do
1423
+ expect(command.call(entity: entity))
1424
+ .to be_a_failing_result
1425
+ .with_error(expected_error)
1426
+ end
1427
+ end
1428
+ end
1429
+
1430
+ describe 'with contract: value' do
1431
+ context 'when the entity does not match the contract' do
1432
+ let(:attributes) { invalid_attributes }
1433
+ let(:errors) { contract.errors_for(entity) }
1434
+ let(:expected_error) do
1435
+ Cuprum::Collections::Errors::FailedValidation.new(
1436
+ entity_class: entity.class,
1437
+ errors: errors
1438
+ )
1439
+ end
1440
+
1441
+ it 'should return a failing result' do
1442
+ expect(command.call(contract: contract, entity: entity))
1443
+ .to be_a_failing_result
1444
+ .with_error(expected_error)
1445
+ end
1446
+ end
1447
+
1448
+ context 'when the entity matches the contract' do
1449
+ let(:attributes) { valid_attributes }
1450
+
1451
+ it 'should return a passing result' do
1452
+ expect(command.call(contract: contract, entity: entity))
1453
+ .to be_a_passing_result
1454
+ .with_value(entity)
1455
+ end
1456
+ end
1457
+ end
1458
+ end
1459
+ end
1460
+ end
1461
+ end
1462
+ end