cuprum-collections 0.5.1 → 0.6.0.rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/lib/cuprum/collections/adaptable/collection.rb +18 -0
  4. data/lib/cuprum/collections/adaptable/command.rb +22 -0
  5. data/lib/cuprum/collections/adaptable/commands/abstract_assign_one.rb +27 -0
  6. data/lib/cuprum/collections/adaptable/commands/abstract_build_one.rb +25 -0
  7. data/lib/cuprum/collections/adaptable/commands/abstract_validate_one.rb +35 -0
  8. data/lib/cuprum/collections/adaptable/commands.rb +15 -0
  9. data/lib/cuprum/collections/adaptable/query.rb +64 -0
  10. data/lib/cuprum/collections/adaptable.rb +13 -0
  11. data/lib/cuprum/collections/adapter.rb +300 -0
  12. data/lib/cuprum/collections/adapters/data_adapter.rb +82 -0
  13. data/lib/cuprum/collections/adapters/entity_adapter.rb +76 -0
  14. data/lib/cuprum/collections/adapters/hash_adapter.rb +48 -0
  15. data/lib/cuprum/collections/adapters.rb +14 -0
  16. data/lib/cuprum/collections/basic/collection.rb +2 -20
  17. data/lib/cuprum/collections/basic/commands/destroy_one.rb +1 -1
  18. data/lib/cuprum/collections/basic/commands/find_many.rb +0 -31
  19. data/lib/cuprum/collections/basic/commands/find_matching.rb +0 -94
  20. data/lib/cuprum/collections/basic/commands/find_one.rb +0 -18
  21. data/lib/cuprum/collections/basic/commands/insert_one.rb +1 -1
  22. data/lib/cuprum/collections/basic/commands/update_one.rb +1 -1
  23. data/lib/cuprum/collections/basic/scopes/criteria_scope.rb +36 -21
  24. data/lib/cuprum/collections/basic.rb +6 -5
  25. data/lib/cuprum/collections/collection.rb +6 -0
  26. data/lib/cuprum/collections/collection_command.rb +1 -1
  27. data/lib/cuprum/collections/commands/abstract_find_many.rb +40 -3
  28. data/lib/cuprum/collections/commands/abstract_find_matching.rb +102 -0
  29. data/lib/cuprum/collections/commands/abstract_find_one.rb +23 -1
  30. data/lib/cuprum/collections/commands/associations/find_many.rb +1 -3
  31. data/lib/cuprum/collections/commands/associations/require_many.rb +1 -1
  32. data/lib/cuprum/collections/commands/find_one_matching.rb +10 -10
  33. data/lib/cuprum/collections/commands/query_command.rb +6 -4
  34. data/lib/cuprum/collections/commands/upsert.rb +0 -2
  35. data/lib/cuprum/collections/constraints/order/attributes_array.rb +5 -4
  36. data/lib/cuprum/collections/constraints/order/attributes_hash.rb +5 -4
  37. data/lib/cuprum/collections/constraints/order/sort_direction.rb +2 -2
  38. data/lib/cuprum/collections/constraints/ordering.rb +11 -9
  39. data/lib/cuprum/collections/constraints/query_hash.rb +2 -2
  40. data/lib/cuprum/collections/errors/abstract_find_error.rb +101 -23
  41. data/lib/cuprum/collections/errors/extra_attributes.rb +3 -3
  42. data/lib/cuprum/collections/errors/failed_validation.rb +3 -3
  43. data/lib/cuprum/collections/errors/missing_default_contract.rb +12 -4
  44. data/lib/cuprum/collections/queries.rb +4 -0
  45. data/lib/cuprum/collections/relation.rb +0 -2
  46. data/lib/cuprum/collections/relations/parameters.rb +120 -68
  47. data/lib/cuprum/collections/repository.rb +71 -6
  48. data/lib/cuprum/collections/rspec/contracts/query_contracts.rb +23 -4
  49. data/lib/cuprum/collections/rspec/contracts/repository_contracts.rb +18 -0
  50. data/lib/cuprum/collections/rspec/contracts/scope_contracts.rb +51 -0
  51. data/lib/cuprum/collections/rspec/contracts/scopes/builder_contracts.rb +10 -0
  52. data/lib/cuprum/collections/rspec/contracts/scopes/composition_contracts.rb +8 -0
  53. data/lib/cuprum/collections/rspec/contracts/scopes/criteria_contracts.rb +18 -366
  54. data/lib/cuprum/collections/rspec/contracts/scopes/logical_contracts.rb +30 -0
  55. data/lib/cuprum/collections/rspec/contracts/scopes.rb +2 -0
  56. data/lib/cuprum/collections/rspec/contracts.rb +2 -10
  57. data/lib/cuprum/collections/rspec/deferred/adapter_examples.rb +1077 -0
  58. data/lib/cuprum/collections/rspec/deferred/collection_examples.rb +27 -7
  59. data/lib/cuprum/collections/rspec/deferred/commands/assign_one_examples.rb +4 -4
  60. data/lib/cuprum/collections/rspec/deferred/commands/build_one_examples.rb +2 -2
  61. data/lib/cuprum/collections/rspec/deferred/commands/destroy_one_examples.rb +2 -2
  62. data/lib/cuprum/collections/rspec/deferred/commands/find_many_examples.rb +5 -5
  63. data/lib/cuprum/collections/rspec/deferred/commands/find_matching_examples.rb +45 -12
  64. data/lib/cuprum/collections/rspec/deferred/commands/find_one_examples.rb +2 -2
  65. data/lib/cuprum/collections/rspec/deferred/commands/insert_one_examples.rb +1 -1
  66. data/lib/cuprum/collections/rspec/deferred/commands/update_one_examples.rb +1 -1
  67. data/lib/cuprum/collections/rspec/deferred/query_examples.rb +930 -0
  68. data/lib/cuprum/collections/rspec/deferred/relation_examples.rb +48 -17
  69. data/lib/cuprum/collections/rspec/deferred/repository_examples.rb +961 -0
  70. data/lib/cuprum/collections/rspec/deferred/scope_examples.rb +598 -0
  71. data/lib/cuprum/collections/rspec/deferred/scopes/all_examples.rb +391 -0
  72. data/lib/cuprum/collections/rspec/deferred/scopes/builder_examples.rb +857 -0
  73. data/lib/cuprum/collections/rspec/deferred/scopes/composition_examples.rb +93 -0
  74. data/lib/cuprum/collections/rspec/deferred/scopes/conjunction_examples.rb +438 -0
  75. data/lib/cuprum/collections/rspec/deferred/scopes/criteria_examples.rb +1941 -0
  76. data/lib/cuprum/collections/rspec/deferred/scopes/disjunction_examples.rb +415 -0
  77. data/lib/cuprum/collections/rspec/deferred/scopes/none_examples.rb +385 -0
  78. data/lib/cuprum/collections/rspec/deferred/scopes/parser_examples.rb +740 -0
  79. data/lib/cuprum/collections/rspec/deferred/scopes.rb +8 -0
  80. data/lib/cuprum/collections/scope.rb +2 -2
  81. data/lib/cuprum/collections/scopes/container.rb +5 -4
  82. data/lib/cuprum/collections/scopes/criteria/parser.rb +24 -48
  83. data/lib/cuprum/collections/scopes/criteria.rb +7 -6
  84. data/lib/cuprum/collections/version.rb +4 -4
  85. data/lib/cuprum/collections.rb +5 -1
  86. metadata +47 -11
  87. data/lib/cuprum/collections/rspec/contracts/association_contracts.rb +0 -2127
  88. data/lib/cuprum/collections/rspec/contracts/basic.rb +0 -11
  89. data/lib/cuprum/collections/rspec/contracts/collection_contracts.rb +0 -387
  90. data/lib/cuprum/collections/rspec/contracts/command_contracts.rb +0 -169
  91. data/lib/cuprum/collections/rspec/contracts/relation_contracts.rb +0 -1264
@@ -0,0 +1,961 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/sleeping_king_studios/deferred'
4
+ require 'rspec/sleeping_king_studios/matchers/core/have_aliased_method'
5
+
6
+ require 'cuprum/collections/rspec/deferred'
7
+
8
+ module Cuprum::Collections::RSpec::Deferred
9
+ # Deferred examples for validating Repository implementations.
10
+ module RepositoryExamples
11
+ include RSpec::SleepingKingStudios::Deferred::Provider
12
+
13
+ # Initializes the repository with collection data.
14
+ #
15
+ # The including example group must define a #build_collection(**options)
16
+ # method, which returns a valid collection instance for the repository.
17
+ #
18
+ # The default collections generated can be overriden by defining the
19
+ # #configured_collections memoized helper in the including example group.
20
+ deferred_context 'when the repository has many collections' do
21
+ include RSpec::SleepingKingStudios::Deferred::Dependencies
22
+
23
+ depends_on :build_collection,
24
+ 'builds a valid collection for the repository'
25
+
26
+ let(:configured_collections) do
27
+ next super() if defined?(super())
28
+
29
+ [
30
+ { name: 'authors' },
31
+ { name: 'books', qualified_name: 'sources/books' },
32
+ { name: 'publishers' }
33
+ ]
34
+ end
35
+ let(:collections) do
36
+ configured_collections.to_h do |options|
37
+ collection = build_collection(**options)
38
+
39
+ [collection.qualified_name, collection]
40
+ end
41
+ end
42
+
43
+ before(:example) do
44
+ collections.each_value { |collection| repository << collection }
45
+ end
46
+ end
47
+
48
+ # Validates that the described class implements the Repository interface.
49
+ #
50
+ # The including example group must define a #build_collection(**options)
51
+ # method, which returns a valid collection instance for the repository.
52
+ deferred_examples 'should be a Repository' do |**deferred_options|
53
+ deferred_examples 'should create the collection' do
54
+ let(:configured_collection_class) do
55
+ return super() if defined?(super())
56
+
57
+ configured = deferred_options[:collection_class]
58
+
59
+ # :nocov:
60
+ configured = Object.const_get(configured) if configured.is_a?(String)
61
+ # :nocov:
62
+
63
+ configured
64
+ end
65
+ let(:configured_entity_class) do
66
+ return super() if defined?(super())
67
+
68
+ # :nocov:
69
+ expected =
70
+ if collection_options.key?(:entity_class)
71
+ collection_options[:entity_class]
72
+ elsif deferred_options.key?(:entity_class)
73
+ deferred_options[:entity_class]
74
+ else
75
+ qualified_name
76
+ .split('/')
77
+ .then { |ary| [*ary[0...-1], tools.str.singularize(ary[-1])] }
78
+ .map { |str| tools.str.camelize(str) }
79
+ .join('::')
80
+ end
81
+ # :nocov:
82
+ expected = Object.const_get(expected) if expected.is_a?(String)
83
+
84
+ expected
85
+ end
86
+ let(:configured_member_name) do
87
+ return super() if defined?(super())
88
+
89
+ tools.str.singularize(collection_name.to_s.split('/').last)
90
+ end
91
+
92
+ def tools
93
+ SleepingKingStudios::Tools::Toolbelt.instance
94
+ end
95
+
96
+ it 'should create the collection' do
97
+ create_collection(safe: false)
98
+
99
+ expect(repository.key?(qualified_name)).to be true
100
+ end
101
+
102
+ it 'should return the collection' do
103
+ collection = create_collection(safe: false)
104
+
105
+ expect(collection).to be repository[qualified_name]
106
+ end
107
+
108
+ it { expect(collection).to be_a configured_collection_class }
109
+
110
+ it 'should set the entity class' do
111
+ expect(collection.entity_class).to be == configured_entity_class
112
+ end
113
+
114
+ it 'should set the collection name' do
115
+ expect(collection.name).to be == collection_name.to_s
116
+ end
117
+
118
+ it 'should set the member name' do
119
+ expect(collection.singular_name).to be == configured_member_name
120
+ end
121
+
122
+ it 'should set the qualified name' do
123
+ expect(collection.qualified_name).to be == qualified_name
124
+ end
125
+
126
+ it 'should set the collection options' do
127
+ expect(collection).to have_attributes(
128
+ primary_key_name:,
129
+ primary_key_type:
130
+ )
131
+ end
132
+ end
133
+
134
+ let(:valid_collection) do
135
+ next super() if defined?(super())
136
+
137
+ build_collection(name: 'widgets', qualified_name: 'scope/widgets')
138
+ end
139
+
140
+ describe '.new' do
141
+ it 'should yield the repository' do
142
+ yielded = nil
143
+
144
+ repository = described_class.new { |value| yielded = value }
145
+
146
+ expect(yielded).to be repository
147
+ end
148
+ end
149
+
150
+ describe '#[]' do
151
+ let(:error_class) do
152
+ described_class::UndefinedCollectionError
153
+ end
154
+ let(:error_message) do
155
+ "repository does not define collection #{collection_name.inspect}"
156
+ end
157
+
158
+ it { expect(repository).to respond_to(:[]).with(1).argument }
159
+
160
+ describe 'with nil' do
161
+ let(:collection_name) { nil }
162
+
163
+ it 'should raise an exception' do
164
+ expect { repository[collection_name] }
165
+ .to raise_error(error_class, error_message)
166
+ end
167
+ end
168
+
169
+ describe 'with an object' do
170
+ let(:collection_name) { Object.new.freeze }
171
+
172
+ it 'should raise an exception' do
173
+ expect { repository[collection_name] }
174
+ .to raise_error(error_class, error_message)
175
+ end
176
+ end
177
+
178
+ describe 'with an invalid string' do
179
+ let(:collection_name) { 'invalid_name' }
180
+
181
+ it 'should raise an exception' do
182
+ expect { repository[collection_name] }
183
+ .to raise_error(error_class, error_message)
184
+ end
185
+ end
186
+
187
+ describe 'with an invalid symbol' do
188
+ let(:collection_name) { :invalid_name }
189
+
190
+ it 'should raise an exception' do
191
+ expect { repository[collection_name] }
192
+ .to raise_error(error_class, error_message)
193
+ end
194
+ end
195
+
196
+ wrap_deferred 'when the repository has many collections' do
197
+ describe 'with an invalid string' do
198
+ let(:collection_name) { 'invalid_name' }
199
+
200
+ it 'should raise an exception' do
201
+ expect { repository[collection_name] }
202
+ .to raise_error(error_class, error_message)
203
+ end
204
+ end
205
+
206
+ describe 'with an invalid symbol' do
207
+ let(:collection_name) { :invalid_name }
208
+
209
+ it 'should raise an exception' do
210
+ expect { repository[collection_name] }
211
+ .to raise_error(error_class, error_message)
212
+ end
213
+ end
214
+
215
+ describe 'with a valid string' do
216
+ let(:collection) { collections.values.first }
217
+ let(:collection_name) { collections.keys.first }
218
+
219
+ it { expect(repository[collection_name]).to be collection }
220
+ end
221
+
222
+ describe 'with a valid symbol' do
223
+ let(:collection) { collections.values.first }
224
+ let(:collection_name) { collections.keys.first.intern }
225
+
226
+ it { expect(repository[collection_name]).to be collection }
227
+ end
228
+ end
229
+ end
230
+
231
+ describe '#add' do
232
+ let(:error_class) do
233
+ described_class::InvalidCollectionError
234
+ end
235
+ let(:error_message) do
236
+ "#{collection.inspect} is not a valid collection"
237
+ end
238
+
239
+ it 'should define the method' do
240
+ expect(repository)
241
+ .to respond_to(:add)
242
+ .with(1).argument
243
+ .and_keywords(:force)
244
+ end
245
+
246
+ it 'should alias #add as #<<' do
247
+ expect(repository.method(:<<)).to be == repository.method(:add)
248
+ end
249
+
250
+ describe 'with nil' do
251
+ let(:collection) { nil }
252
+
253
+ it 'should raise an exception' do
254
+ expect { repository.add(collection) }
255
+ .to raise_error(error_class, error_message)
256
+ end
257
+ end
258
+
259
+ describe 'with an object' do
260
+ let(:collection) { Object.new.freeze }
261
+
262
+ it 'should raise an exception' do
263
+ expect { repository.add(collection) }
264
+ .to raise_error(error_class, error_message)
265
+ end
266
+ end
267
+
268
+ describe 'with a collection' do
269
+ it { expect(repository.add(valid_collection)).to be repository }
270
+
271
+ it 'should add the collection to the repository' do
272
+ repository.add(valid_collection)
273
+
274
+ expect(repository[valid_collection.qualified_name])
275
+ .to be valid_collection
276
+ end
277
+
278
+ describe 'with force: true' do
279
+ it 'should add the collection to the repository' do
280
+ repository.add(valid_collection, force: true)
281
+
282
+ expect(repository[valid_collection.qualified_name])
283
+ .to be valid_collection
284
+ end
285
+ end
286
+
287
+ context 'when the collection already exists' do
288
+ let(:error_message) do
289
+ "collection #{valid_collection.qualified_name} already exists"
290
+ end
291
+
292
+ before(:example) do
293
+ allow(repository)
294
+ .to receive(:key?)
295
+ .with(valid_collection.qualified_name)
296
+ .and_return(true)
297
+ end
298
+
299
+ it 'should raise an exception' do
300
+ expect { repository.add(valid_collection) }
301
+ .to raise_error(
302
+ described_class::DuplicateCollectionError,
303
+ error_message
304
+ )
305
+ end
306
+
307
+ it 'should not update the repository' do
308
+ begin
309
+ repository.add(valid_collection)
310
+ rescue described_class::DuplicateCollectionError
311
+ # Do nothing.
312
+ end
313
+
314
+ expect { repository[valid_collection.qualified_name] }
315
+ .to raise_error(
316
+ described_class::UndefinedCollectionError,
317
+ 'repository does not define collection ' \
318
+ "#{valid_collection.qualified_name.inspect}"
319
+ )
320
+ end
321
+
322
+ describe 'with force: true' do
323
+ it 'should add the collection to the repository' do
324
+ repository.add(valid_collection, force: true)
325
+
326
+ expect(repository[valid_collection.qualified_name])
327
+ .to be valid_collection
328
+ end
329
+ end
330
+ end
331
+ end
332
+
333
+ context 'when the repository is frozen' do
334
+ let(:error_message) do
335
+ "can't modify frozen #{described_class.name}"
336
+ end
337
+
338
+ before(:example) { repository.freeze }
339
+
340
+ it 'should raise an exception' do
341
+ expect { repository.add(valid_collection) }
342
+ .to raise_error FrozenError, error_message
343
+ end
344
+ end
345
+ end
346
+
347
+ describe '#create' do
348
+ let(:collection_name) { 'books' }
349
+ let(:qualified_name) { collection_name.to_s }
350
+ let(:primary_key_name) { 'id' }
351
+ let(:primary_key_type) { Integer }
352
+ let(:collection_options) { {} }
353
+ let(:collection) do
354
+ create_collection
355
+
356
+ repository[qualified_name]
357
+ end
358
+ let(:error_message) do
359
+ "#{described_class.name} is an abstract class. Define a " \
360
+ 'repository subclass and implement the #build_collection method.'
361
+ end
362
+
363
+ define_method(:create_collection) \
364
+ do |force: false, safe: true, **options|
365
+ if safe
366
+ begin
367
+ repository.create(force:, **collection_options, **options)
368
+ rescue StandardError
369
+ # Do nothing.
370
+ end
371
+ else
372
+ repository.create(force:, **collection_options, **options)
373
+ end
374
+ end
375
+
376
+ it 'should define the method' do
377
+ expect(repository)
378
+ .to respond_to(:create)
379
+ .with(0).arguments
380
+ .and_keywords(:collection_name, :entity_class, :force)
381
+ .and_any_keywords
382
+ end
383
+
384
+ if deferred_options.fetch(:abstract, false)
385
+ it 'should raise an exception' do
386
+ expect { create_collection(safe: false) }
387
+ .to raise_error(
388
+ described_class::AbstractRepositoryError,
389
+ error_message
390
+ )
391
+ end
392
+
393
+ next
394
+ end
395
+
396
+ describe 'with entity_class: a Class' do
397
+ let(:entity_class) { Book }
398
+ let(:collection_options) do
399
+ super().merge(entity_class:)
400
+ end
401
+
402
+ include_deferred 'should create the collection'
403
+ end
404
+
405
+ describe 'with entity_class: a String' do
406
+ let(:entity_class) { 'Book' }
407
+ let(:collection_options) do
408
+ super().merge(entity_class:)
409
+ end
410
+
411
+ include_deferred 'should create the collection'
412
+ end
413
+
414
+ describe 'with name: a String' do
415
+ let(:collection_name) { 'books' }
416
+ let(:collection_options) do
417
+ super().merge(name: collection_name)
418
+ end
419
+
420
+ include_deferred 'should create the collection'
421
+ end
422
+
423
+ describe 'with name: a Symbol' do
424
+ let(:collection_name) { :books }
425
+ let(:collection_options) do
426
+ super().merge(name: collection_name)
427
+ end
428
+
429
+ include_deferred 'should create the collection'
430
+ end
431
+
432
+ describe 'with collection options' do
433
+ let(:primary_key_name) { 'uuid' }
434
+ let(:primary_key_type) { String }
435
+ let(:collection_options) do
436
+ super().merge(
437
+ name: collection_name,
438
+ primary_key_name:,
439
+ primary_key_type:
440
+ )
441
+ end
442
+
443
+ include_deferred 'should create the collection'
444
+ end
445
+
446
+ context 'when the collection already exists' do
447
+ let(:collection_name) { 'books' }
448
+ let(:collection_options) do
449
+ super().merge(name: collection_name)
450
+ end
451
+ let(:error_message) do
452
+ "collection #{qualified_name} already exists"
453
+ end
454
+
455
+ before { create_collection(old: true) }
456
+
457
+ it 'should raise an exception' do
458
+ expect { create_collection(safe: false) }
459
+ .to raise_error(
460
+ described_class::DuplicateCollectionError,
461
+ error_message
462
+ )
463
+ end
464
+
465
+ it 'should not update the repository' do
466
+ create_collection(old: false)
467
+
468
+ collection = repository[qualified_name]
469
+
470
+ expect(collection.options[:old]).to be true
471
+ end
472
+
473
+ describe 'with force: true' do
474
+ it 'should update the repository' do
475
+ create_collection(force: true, old: false)
476
+
477
+ collection = repository[qualified_name]
478
+
479
+ expect(collection.options[:old]).to be false
480
+ end
481
+ end
482
+ end
483
+
484
+ context 'when the repository is frozen' do
485
+ let(:collection_name) { 'books' }
486
+ let(:collection_options) do
487
+ super().merge(name: collection_name)
488
+ end
489
+ let(:error_message) do
490
+ "can't modify frozen #{described_class.name}"
491
+ end
492
+
493
+ before(:example) { repository.freeze }
494
+
495
+ it 'should raise an exception' do
496
+ expect { create_collection(safe: false) }
497
+ .to raise_error FrozenError, error_message
498
+ end
499
+ end
500
+ end
501
+
502
+ describe '#find' do
503
+ shared_examples 'should find the matching collection' do
504
+ context 'when the collection does not exist' do
505
+ let(:error_message) do
506
+ "repository does not define collection #{qualified_name.inspect}"
507
+ end
508
+
509
+ it 'should raise an exception' do
510
+ expect { repository.find(**collection_options) }.to raise_error(
511
+ described_class::UndefinedCollectionError,
512
+ error_message
513
+ )
514
+ end
515
+ end
516
+
517
+ next if deferred_options.fetch(:abstract, false)
518
+
519
+ context 'when the collection exists' do
520
+ include_deferred 'when the repository has many collections'
521
+
522
+ let(:collection) { collections.values.first }
523
+ let(:entity_class) { collection.entity_class }
524
+ let(:name) { collection.name }
525
+ let(:qualified_name) { collection.qualified_name }
526
+
527
+ it 'should find the collection' do
528
+ expect(repository.find(**collection_options)).to be == collection
529
+ end
530
+ end
531
+ end
532
+
533
+ let(:name) { 'books' }
534
+ let(:qualified_name) { name.to_s }
535
+ let(:collection_options) { {} }
536
+
537
+ it 'should define the method' do
538
+ expect(repository)
539
+ .to respond_to(:find)
540
+ .with(0).arguments
541
+ .and_any_keywords
542
+ end
543
+
544
+ describe 'with no parameters' do
545
+ let(:error_message) { "name or entity class can't be blank" }
546
+
547
+ it 'should raise an exception' do
548
+ expect { repository.find }
549
+ .to raise_error ArgumentError, error_message
550
+ end
551
+ end
552
+
553
+ if deferred_options.fetch(:find_by_entity_class, true)
554
+ describe 'with entity_class: a Class' do
555
+ let(:entity_class) { Book }
556
+ let(:collection_options) { super().merge(entity_class:) }
557
+
558
+ include_examples 'should find the matching collection'
559
+ end
560
+
561
+ describe 'with entity_class: a String' do
562
+ let(:entity_class) { 'Book' }
563
+ let(:collection_options) { super().merge(entity_class:) }
564
+
565
+ include_examples 'should find the matching collection'
566
+ end
567
+ end
568
+
569
+ describe 'with name: a String' do
570
+ let(:collection_options) { super().merge(name: name.to_s) }
571
+
572
+ include_examples 'should find the matching collection'
573
+ end
574
+
575
+ describe 'with name: a Symbol' do
576
+ let(:collection_options) { super().merge(name: name.intern) }
577
+
578
+ include_examples 'should find the matching collection'
579
+ end
580
+
581
+ describe 'with qualified_name: a String' do
582
+ let(:collection_options) do
583
+ super().merge(qualified_name: qualified_name.to_s)
584
+ end
585
+
586
+ include_examples 'should find the matching collection'
587
+ end
588
+
589
+ describe 'with qualified_name: a Symbol' do
590
+ let(:collection_options) do
591
+ super().merge(qualified_name: qualified_name.intern)
592
+ end
593
+
594
+ include_examples 'should find the matching collection'
595
+ end
596
+
597
+ describe 'with multiple parameters' do
598
+ let(:entity_class) { Book }
599
+ let(:collection_options) do
600
+ super().merge(entity_class:, qualified_name:)
601
+ end
602
+
603
+ context 'when the collection does not exist' do
604
+ let(:error_message) do
605
+ "repository does not define collection #{qualified_name.inspect}"
606
+ end
607
+
608
+ it 'should raise an exception' do
609
+ expect { repository.find(**collection_options) }.to raise_error(
610
+ described_class::UndefinedCollectionError,
611
+ error_message
612
+ )
613
+ end
614
+ end
615
+
616
+ next if deferred_options.fetch(:abstract, false)
617
+
618
+ context 'when a partially-matching collection exists' do
619
+ include_deferred 'when the repository has many collections'
620
+
621
+ let(:collection) { collections.values.first }
622
+ let(:entity_class) { Spec::EntityClass }
623
+ let(:qualified_name) { collection.qualified_name }
624
+ let(:error_message) do
625
+ <<~TEXT.strip
626
+ collection "#{collection.qualified_name}" exists but does not match:
627
+
628
+ expected: #{{ entity_class: Spec::EntityClass }.inspect}
629
+ actual: #{{ entity_class: collection.entity_class }.inspect}
630
+ TEXT
631
+ end
632
+
633
+ example_class 'Spec::EntityClass'
634
+
635
+ it 'should raise an exception' do
636
+ expect { repository.find(**collection_options) }.to raise_error(
637
+ described_class::DuplicateCollectionError,
638
+ error_message
639
+ )
640
+ end
641
+ end
642
+
643
+ context 'when a matching collection exists' do
644
+ include_deferred 'when the repository has many collections'
645
+
646
+ let(:collection) { collections.values.first }
647
+ let(:entity_class) { collection.entity_class }
648
+ let(:qualified_name) { collection.qualified_name }
649
+
650
+ it 'should find the collection' do
651
+ expect(repository.find(**collection_options)).to be == collection
652
+ end
653
+ end
654
+ end
655
+ end
656
+
657
+ describe '#find_or_create' do
658
+ let(:collection_name) { 'books' }
659
+ let(:qualified_name) { collection_name.to_s }
660
+ let(:primary_key_name) { 'id' }
661
+ let(:primary_key_type) { Integer }
662
+ let(:collection_options) { {} }
663
+ let(:collection) do
664
+ create_collection
665
+
666
+ repository[qualified_name]
667
+ end
668
+ let(:error_message) do
669
+ "#{described_class.name} is an abstract class. Define a " \
670
+ 'repository subclass and implement the #build_collection method.'
671
+ end
672
+
673
+ before(:example) do
674
+ allow(SleepingKingStudios::Tools::Toolbelt.instance.core_tools)
675
+ .to receive(:deprecate)
676
+ end
677
+
678
+ define_method :create_collection do |safe: true, **options|
679
+ if safe
680
+ begin
681
+ repository.find_or_create(**collection_options, **options)
682
+ rescue StandardError
683
+ # Do nothing.
684
+ end
685
+ else
686
+ repository.find_or_create(**collection_options, **options)
687
+ end
688
+ end
689
+
690
+ it 'should define the method' do
691
+ expect(repository)
692
+ .to respond_to(:find_or_create)
693
+ .with(0).arguments
694
+ .and_keywords(:entity_class)
695
+ .and_any_keywords
696
+ end
697
+
698
+ if deferred_options.fetch(:abstract, false)
699
+ let(:collection_options) { { name: collection_name } }
700
+
701
+ it 'should raise an exception' do
702
+ expect { create_collection(safe: false) }
703
+ .to raise_error(
704
+ described_class::AbstractRepositoryError,
705
+ error_message
706
+ )
707
+ end
708
+
709
+ next
710
+ end
711
+
712
+ it 'should print a deprecation warning' do
713
+ repository.find_or_create(qualified_name:)
714
+
715
+ expect(SleepingKingStudios::Tools::Toolbelt.instance.core_tools)
716
+ .to have_received(:deprecate)
717
+ .with(
718
+ "#{described_class.name}#find_or_create()",
719
+ message: 'Use #create or #find method.'
720
+ )
721
+ end
722
+
723
+ describe 'with entity_class: a Class' do
724
+ let(:entity_class) { Book }
725
+ let(:collection_options) do
726
+ super().merge(entity_class:)
727
+ end
728
+
729
+ include_deferred 'should create the collection'
730
+ end
731
+
732
+ describe 'with entity_class: a String' do
733
+ let(:entity_class) { Book }
734
+ let(:collection_options) do
735
+ super().merge(entity_class:)
736
+ end
737
+
738
+ include_deferred 'should create the collection'
739
+ end
740
+
741
+ describe 'with name: a String' do
742
+ let(:collection_name) { 'books' }
743
+ let(:collection_options) do
744
+ super().merge(name: collection_name)
745
+ end
746
+
747
+ include_deferred 'should create the collection'
748
+ end
749
+
750
+ describe 'with name: a Symbol' do
751
+ let(:collection_name) { :books }
752
+ let(:collection_options) do
753
+ super().merge(name: collection_name)
754
+ end
755
+
756
+ include_deferred 'should create the collection'
757
+ end
758
+
759
+ describe 'with collection options' do
760
+ let(:primary_key_name) { 'uuid' }
761
+ let(:primary_key_type) { String }
762
+ let(:qualified_name) { 'spec/scoped_books' }
763
+ let(:collection_options) do
764
+ super().merge(
765
+ name: collection_name,
766
+ primary_key_name:,
767
+ primary_key_type:,
768
+ qualified_name:
769
+ )
770
+ end
771
+
772
+ include_deferred 'should create the collection'
773
+ end
774
+
775
+ context 'when the collection already exists' do
776
+ let(:collection_name) { 'books' }
777
+ let(:collection_options) do
778
+ super().merge(name: collection_name)
779
+ end
780
+ let(:error_message) do
781
+ "collection #{qualified_name} already exists"
782
+ end
783
+
784
+ before { create_collection(old: true) }
785
+
786
+ describe 'with non-matching options' do
787
+ it 'should raise an exception' do
788
+ expect { create_collection(old: false, safe: false) }
789
+ .to raise_error(
790
+ described_class::DuplicateCollectionError,
791
+ error_message
792
+ )
793
+ end
794
+
795
+ it 'should not update the repository' do
796
+ create_collection(old: false)
797
+
798
+ collection = repository[qualified_name]
799
+
800
+ expect(collection.options[:old]).to be true
801
+ end
802
+ end
803
+
804
+ describe 'with matching options' do
805
+ it 'should return the collection' do
806
+ collection = create_collection(old: true)
807
+
808
+ expect(collection.options[:old]).to be true
809
+ end
810
+ end
811
+ end
812
+
813
+ context 'when the repository is frozen' do
814
+ let(:collection_name) { 'books' }
815
+ let(:collection_options) do
816
+ super().merge(name: collection_name)
817
+ end
818
+ let(:error_message) do
819
+ "can't modify frozen #{described_class.name}"
820
+ end
821
+
822
+ before(:example) { repository.freeze }
823
+
824
+ it 'should raise an exception' do
825
+ expect { create_collection(safe: false) }
826
+ .to raise_error FrozenError, error_message
827
+ end
828
+ end
829
+ end
830
+
831
+ describe '#key?' do
832
+ it { expect(repository).to respond_to(:key?).with(1).argument }
833
+
834
+ it { expect(repository.key?(nil)).to be false }
835
+
836
+ it { expect(repository.key?(Object.new.freeze)).to be false }
837
+
838
+ it { expect(repository.key?('invalid_name')).to be false }
839
+
840
+ it { expect(repository.key?(:invalid_name)).to be false }
841
+
842
+ wrap_deferred 'when the repository has many collections' do
843
+ it { expect(repository.key?('invalid_name')).to be false }
844
+
845
+ it { expect(repository.key?(:invalid_name)).to be false }
846
+
847
+ it { expect(repository.key?(collections.keys.first)).to be true }
848
+
849
+ it 'should include the key' do
850
+ expect(repository.key?(collections.keys.first.intern)).to be true
851
+ end
852
+ end
853
+ end
854
+
855
+ describe '#keys' do
856
+ include_examples 'should define reader', :keys, []
857
+
858
+ wrap_deferred 'when the repository has many collections' do
859
+ it { expect(repository.keys).to be == collections.keys }
860
+ end
861
+ end
862
+
863
+ describe '#remove' do
864
+ let(:qualified_name) { 'books' }
865
+
866
+ it 'should define the method' do
867
+ expect(repository)
868
+ .to respond_to(:remove)
869
+ .with(0).arguments
870
+ .and_keywords(:qualified_name)
871
+ end
872
+
873
+ describe 'with qualified_name: a String' do
874
+ let(:qualified_name) { super().to_s }
875
+
876
+ context 'when the collection does not exist' do
877
+ let(:error_message) do
878
+ "repository does not define collection #{qualified_name.inspect}"
879
+ end
880
+
881
+ it 'should raise an exception' do
882
+ expect { repository.remove(qualified_name:) }.to raise_error(
883
+ described_class::UndefinedCollectionError,
884
+ error_message
885
+ )
886
+ end
887
+ end
888
+
889
+ next if deferred_options.fetch(:abstract, false)
890
+
891
+ context 'when the collection exists' do
892
+ include_deferred 'when the repository has many collections'
893
+
894
+ let(:collection) { collections.values.first }
895
+ let(:qualified_name) { collection.qualified_name.to_s }
896
+
897
+ it 'should return the collection' do
898
+ expect(repository.remove(qualified_name:)).to be == collection
899
+ end
900
+
901
+ it 'should remove the collection from the repository' do
902
+ expect { repository.remove(qualified_name:) }
903
+ .to change(repository, :keys)
904
+ .to(satisfy { |keys| !keys.include?(qualified_name.to_s) })
905
+ end
906
+ end
907
+ end
908
+
909
+ describe 'with qualified_name: a Symbol' do
910
+ let(:qualified_name) { super().intern }
911
+
912
+ context 'when the collection does not exist' do
913
+ let(:error_message) do
914
+ 'repository does not define collection ' \
915
+ "#{qualified_name.to_s.inspect}"
916
+ end
917
+
918
+ it 'should raise an exception' do
919
+ expect { repository.remove(qualified_name:) }.to raise_error(
920
+ described_class::UndefinedCollectionError,
921
+ error_message
922
+ )
923
+ end
924
+ end
925
+
926
+ next if deferred_options.fetch(:abstract, false)
927
+
928
+ context 'when the collection exists' do
929
+ include_deferred 'when the repository has many collections'
930
+
931
+ let(:collection) { collections.values.first }
932
+ let(:qualified_name) { collection.qualified_name.intern }
933
+
934
+ it 'should return the collection' do
935
+ expect(repository.remove(qualified_name:)).to be == collection
936
+ end
937
+
938
+ it 'should remove the collection from the repository' do
939
+ expect { repository.remove(qualified_name:) }
940
+ .to change(repository, :keys)
941
+ .to(satisfy { |keys| !keys.include?(qualified_name.to_s) })
942
+ end
943
+ end
944
+ end
945
+
946
+ context 'when the repository is frozen' do
947
+ let(:error_message) do
948
+ "can't modify frozen #{described_class.name}"
949
+ end
950
+
951
+ before(:example) { repository.freeze }
952
+
953
+ it 'should raise an exception' do
954
+ expect { repository.remove(qualified_name:) }
955
+ .to raise_error FrozenError, error_message
956
+ end
957
+ end
958
+ end
959
+ end
960
+ end
961
+ end