cuprum-collections 0.3.0 → 0.4.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 (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 +1 -1
  37. data/lib/cuprum/collections.rb +9 -4
  38. metadata +23 -19
  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