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,605 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/rspec/contracts'
4
+
5
+ module Cuprum::Collections::RSpec::Contracts
6
+ # Contracts for asserting on Repository objects.
7
+ module RepositoryContracts
8
+ # Contract validating the behavior of a Repository.
9
+ module ShouldBeARepositoryContract
10
+ extend RSpec::SleepingKingStudios::Contract
11
+
12
+ # @!method apply(example_group, abstract:, **options)
13
+ # Adds the contract to the example group.
14
+ #
15
+ # @param abstract [Boolean] if true, the repository is abstract and does
16
+ # not define certain methods. Defaults to false.
17
+ # @param example_group [RSpec::Core::ExampleGroup] the example group to
18
+ # which the contract is applied.
19
+ # @param options [Hash] additional options for the contract.
20
+ #
21
+ # @option options collection_class [Class, String] the expected class
22
+ # for created collections.
23
+ # @option options entity_class [Class, String] the expected entity
24
+ # class.
25
+ contract do |abstract: false, **options|
26
+ shared_examples 'should create the collection' do
27
+ let(:configured_collection_class) do
28
+ return super() if defined?(super())
29
+
30
+ configured = options[:collection_class]
31
+
32
+ # :nocov:
33
+ if configured.is_a?(String)
34
+ configured = Object.const_get(configured)
35
+ end
36
+ # :nocov:
37
+
38
+ configured
39
+ end
40
+ let(:configured_entity_class) do
41
+ return super() if defined?(super())
42
+
43
+ # :nocov:
44
+ expected =
45
+ if collection_options.key?(:entity_class)
46
+ collection_options[:entity_class]
47
+ elsif options.key?(:entity_class)
48
+ options[:entity_class]
49
+ else
50
+ qualified_name
51
+ .split('/')
52
+ .then { |ary| [*ary[0...-1], tools.str.singularize(ary[-1])] }
53
+ .map { |str| tools.str.camelize(str) }
54
+ .join('::')
55
+ end
56
+ # :nocov:
57
+ expected = Object.const_get(expected) if expected.is_a?(String)
58
+
59
+ expected
60
+ end
61
+ let(:configured_member_name) do
62
+ return super() if defined?(super())
63
+
64
+ tools.str.singularize(collection_name.to_s.split('/').last)
65
+ end
66
+
67
+ def tools
68
+ SleepingKingStudios::Tools::Toolbelt.instance
69
+ end
70
+
71
+ it 'should create the collection' do
72
+ create_collection(safe: false)
73
+
74
+ expect(repository.key?(qualified_name)).to be true
75
+ end
76
+
77
+ it 'should return the collection' do
78
+ collection = create_collection(safe: false)
79
+
80
+ expect(collection).to be repository[qualified_name]
81
+ end
82
+
83
+ it { expect(collection).to be_a configured_collection_class }
84
+
85
+ it 'should set the entity class' do
86
+ expect(collection.entity_class).to be == configured_entity_class
87
+ end
88
+
89
+ it 'should set the collection name' do
90
+ expect(collection.name).to be == collection_name.to_s
91
+ end
92
+
93
+ it 'should set the member name' do
94
+ expect(collection.singular_name).to be == configured_member_name
95
+ end
96
+
97
+ it 'should set the qualified name' do
98
+ expect(collection.qualified_name).to be == qualified_name
99
+ end
100
+
101
+ it 'should set the collection options' do
102
+ expect(collection).to have_attributes(
103
+ primary_key_name: primary_key_name,
104
+ primary_key_type: primary_key_type
105
+ )
106
+ end
107
+ end
108
+
109
+ describe '#[]' do
110
+ let(:error_class) do
111
+ described_class::UndefinedCollectionError
112
+ end
113
+ let(:error_message) do
114
+ "repository does not define collection #{collection_name.inspect}"
115
+ end
116
+
117
+ it { expect(repository).to respond_to(:[]).with(1).argument }
118
+
119
+ describe 'with nil' do
120
+ let(:collection_name) { nil }
121
+
122
+ it 'should raise an exception' do
123
+ expect { repository[collection_name] }
124
+ .to raise_error(error_class, error_message)
125
+ end
126
+ end
127
+
128
+ describe 'with an object' do
129
+ let(:collection_name) { Object.new.freeze }
130
+
131
+ it 'should raise an exception' do
132
+ expect { repository[collection_name] }
133
+ .to raise_error(error_class, error_message)
134
+ end
135
+ end
136
+
137
+ describe 'with an invalid string' do
138
+ let(:collection_name) { 'invalid_name' }
139
+
140
+ it 'should raise an exception' do
141
+ expect { repository[collection_name] }
142
+ .to raise_error(error_class, error_message)
143
+ end
144
+ end
145
+
146
+ describe 'with an invalid symbol' do
147
+ let(:collection_name) { :invalid_name }
148
+
149
+ it 'should raise an exception' do
150
+ expect { repository[collection_name] }
151
+ .to raise_error(error_class, error_message)
152
+ end
153
+ end
154
+
155
+ wrap_context 'when the repository has many collections' do
156
+ describe 'with an invalid string' do
157
+ let(:collection_name) { 'invalid_name' }
158
+
159
+ it 'should raise an exception' do
160
+ expect { repository[collection_name] }
161
+ .to raise_error(error_class, error_message)
162
+ end
163
+ end
164
+
165
+ describe 'with an invalid symbol' do
166
+ let(:collection_name) { :invalid_name }
167
+
168
+ it 'should raise an exception' do
169
+ expect { repository[collection_name] }
170
+ .to raise_error(error_class, error_message)
171
+ end
172
+ end
173
+
174
+ describe 'with a valid string' do
175
+ let(:collection) { collections.values.first }
176
+ let(:collection_name) { collections.keys.first }
177
+
178
+ it { expect(repository[collection_name]).to be collection }
179
+ end
180
+
181
+ describe 'with a valid symbol' do
182
+ let(:collection) { collections.values.first }
183
+ let(:collection_name) { collections.keys.first.intern }
184
+
185
+ it { expect(repository[collection_name]).to be collection }
186
+ end
187
+ end
188
+ end
189
+
190
+ describe '#add' do
191
+ let(:error_class) do
192
+ described_class::InvalidCollectionError
193
+ end
194
+ let(:error_message) do
195
+ "#{collection.inspect} is not a valid collection"
196
+ end
197
+
198
+ it 'should define the method' do
199
+ expect(repository)
200
+ .to respond_to(:add)
201
+ .with(1).argument
202
+ .and_keywords(:force)
203
+ end
204
+
205
+ it 'should alias #add as #<<' do
206
+ expect(repository.method(:<<)).to be == repository.method(:add)
207
+ end
208
+
209
+ describe 'with nil' do
210
+ let(:collection) { nil }
211
+
212
+ it 'should raise an exception' do
213
+ expect { repository.add(collection) }
214
+ .to raise_error(error_class, error_message)
215
+ end
216
+ end
217
+
218
+ describe 'with an object' do
219
+ let(:collection) { Object.new.freeze }
220
+
221
+ it 'should raise an exception' do
222
+ expect { repository.add(collection) }
223
+ .to raise_error(error_class, error_message)
224
+ end
225
+ end
226
+
227
+ describe 'with a collection' do
228
+ it { expect(repository.add(example_collection)).to be repository }
229
+
230
+ it 'should add the collection to the repository' do
231
+ repository.add(example_collection)
232
+
233
+ expect(repository[example_collection.qualified_name])
234
+ .to be example_collection
235
+ end
236
+
237
+ describe 'with force: true' do
238
+ it 'should add the collection to the repository' do
239
+ repository.add(example_collection, force: true)
240
+
241
+ expect(repository[example_collection.qualified_name])
242
+ .to be example_collection
243
+ end
244
+ end
245
+
246
+ context 'when the collection already exists' do
247
+ let(:error_message) do
248
+ "collection #{example_collection.qualified_name} already exists"
249
+ end
250
+
251
+ before(:example) do
252
+ allow(repository)
253
+ .to receive(:key?)
254
+ .with(example_collection.qualified_name)
255
+ .and_return(true)
256
+ end
257
+
258
+ it 'should raise an exception' do
259
+ expect { repository.add(example_collection) }
260
+ .to raise_error(
261
+ described_class::DuplicateCollectionError,
262
+ error_message
263
+ )
264
+ end
265
+
266
+ it 'should not update the repository' do
267
+ begin
268
+ repository.add(example_collection)
269
+ rescue described_class::DuplicateCollectionError
270
+ # Do nothing.
271
+ end
272
+
273
+ expect { repository[example_collection.qualified_name] }
274
+ .to raise_error(
275
+ described_class::UndefinedCollectionError,
276
+ 'repository does not define collection ' \
277
+ "#{example_collection.qualified_name.inspect}"
278
+ )
279
+ end
280
+
281
+ describe 'with force: true' do
282
+ it 'should add the collection to the repository' do
283
+ repository.add(example_collection, force: true)
284
+
285
+ expect(repository[example_collection.qualified_name])
286
+ .to be example_collection
287
+ end
288
+ end
289
+ end
290
+ end
291
+ end
292
+
293
+ describe '#create' do
294
+ let(:collection_name) { 'books' }
295
+ let(:qualified_name) { collection_name.to_s }
296
+ let(:primary_key_name) { 'id' }
297
+ let(:primary_key_type) { Integer }
298
+ let(:collection_options) { {} }
299
+ let(:collection) do
300
+ create_collection
301
+
302
+ repository[qualified_name]
303
+ end
304
+ let(:error_message) do
305
+ "#{described_class.name} is an abstract class. Define a " \
306
+ 'repository subclass and implement the #build_collection method.'
307
+ end
308
+
309
+ def create_collection(force: false, safe: true, **options)
310
+ if safe
311
+ begin
312
+ repository.create(force: force, **collection_options, **options)
313
+ rescue StandardError
314
+ # Do nothing.
315
+ end
316
+ else
317
+ repository.create(force: force, **collection_options, **options)
318
+ end
319
+ end
320
+
321
+ it 'should define the method' do
322
+ expect(repository)
323
+ .to respond_to(:create)
324
+ .with(0).arguments
325
+ .and_keywords(:collection_name, :entity_class, :force)
326
+ .and_any_keywords
327
+ end
328
+
329
+ if abstract
330
+ it 'should raise an exception' do
331
+ expect { create_collection(safe: false) }
332
+ .to raise_error(
333
+ described_class::AbstractRepositoryError,
334
+ error_message
335
+ )
336
+ end
337
+
338
+ next
339
+ end
340
+
341
+ describe 'with entity_class: a Class' do
342
+ let(:entity_class) { Book }
343
+ let(:collection_options) do
344
+ super().merge(entity_class: entity_class)
345
+ end
346
+
347
+ include_examples 'should create the collection'
348
+ end
349
+
350
+ describe 'with entity_class: a String' do
351
+ let(:entity_class) { 'Book' }
352
+ let(:collection_options) do
353
+ super().merge(entity_class: entity_class)
354
+ end
355
+
356
+ include_examples 'should create the collection'
357
+ end
358
+
359
+ describe 'with name: a String' do
360
+ let(:collection_name) { 'books' }
361
+ let(:collection_options) do
362
+ super().merge(name: collection_name)
363
+ end
364
+
365
+ include_examples 'should create the collection'
366
+ end
367
+
368
+ describe 'with name: a Symbol' do
369
+ let(:collection_name) { :books }
370
+ let(:collection_options) do
371
+ super().merge(name: collection_name)
372
+ end
373
+
374
+ include_examples 'should create the collection'
375
+ end
376
+
377
+ describe 'with collection options' do
378
+ let(:primary_key_name) { 'uuid' }
379
+ let(:primary_key_type) { String }
380
+ let(:collection_options) do
381
+ super().merge(
382
+ name: collection_name,
383
+ primary_key_name: primary_key_name,
384
+ primary_key_type: primary_key_type
385
+ )
386
+ end
387
+
388
+ include_examples 'should create the collection'
389
+ end
390
+
391
+ context 'when the collection already exists' do
392
+ let(:collection_name) { 'books' }
393
+ let(:collection_options) do
394
+ super().merge(name: collection_name)
395
+ end
396
+ let(:error_message) do
397
+ "collection #{qualified_name} already exists"
398
+ end
399
+
400
+ before { create_collection(old: true) }
401
+
402
+ it 'should raise an exception' do
403
+ expect { create_collection(safe: false) }
404
+ .to raise_error(
405
+ described_class::DuplicateCollectionError,
406
+ error_message
407
+ )
408
+ end
409
+
410
+ it 'should not update the repository' do
411
+ create_collection(old: false)
412
+
413
+ collection = repository[qualified_name]
414
+
415
+ expect(collection.options[:old]).to be true
416
+ end
417
+
418
+ describe 'with force: true' do
419
+ it 'should update the repository' do
420
+ create_collection(force: true, old: false)
421
+
422
+ collection = repository[qualified_name]
423
+
424
+ expect(collection.options[:old]).to be false
425
+ end
426
+ end
427
+ end
428
+ end
429
+
430
+ describe '#find_or_create' do
431
+ let(:collection_name) { 'books' }
432
+ let(:qualified_name) { collection_name.to_s }
433
+ let(:primary_key_name) { 'id' }
434
+ let(:primary_key_type) { Integer }
435
+ let(:collection_options) { {} }
436
+ let(:collection) do
437
+ create_collection
438
+
439
+ repository[qualified_name]
440
+ end
441
+ let(:error_message) do
442
+ "#{described_class.name} is an abstract class. Define a " \
443
+ 'repository subclass and implement the #build_collection method.'
444
+ end
445
+
446
+ def create_collection(safe: true, **options)
447
+ if safe
448
+ begin
449
+ repository.find_or_create(**collection_options, **options)
450
+ rescue StandardError
451
+ # Do nothing.
452
+ end
453
+ else
454
+ repository.find_or_create(**collection_options, **options)
455
+ end
456
+ end
457
+
458
+ it 'should define the method' do
459
+ expect(repository)
460
+ .to respond_to(:find_or_create)
461
+ .with(0).arguments
462
+ .and_keywords(:entity_class)
463
+ .and_any_keywords
464
+ end
465
+
466
+ if abstract
467
+ let(:collection_options) { { name: collection_name } }
468
+
469
+ it 'should raise an exception' do
470
+ expect { create_collection(safe: false) }
471
+ .to raise_error(
472
+ described_class::AbstractRepositoryError,
473
+ error_message
474
+ )
475
+ end
476
+
477
+ next
478
+ end
479
+
480
+ describe 'with entity_class: a Class' do
481
+ let(:entity_class) { Book }
482
+ let(:collection_options) do
483
+ super().merge(entity_class: entity_class)
484
+ end
485
+
486
+ include_examples 'should create the collection'
487
+ end
488
+
489
+ describe 'with entity_class: a String' do
490
+ let(:entity_class) { Book }
491
+ let(:collection_options) do
492
+ super().merge(entity_class: entity_class)
493
+ end
494
+
495
+ include_examples 'should create the collection'
496
+ end
497
+
498
+ describe 'with name: a String' do
499
+ let(:collection_name) { 'books' }
500
+ let(:collection_options) do
501
+ super().merge(name: collection_name)
502
+ end
503
+
504
+ include_examples 'should create the collection'
505
+ end
506
+
507
+ describe 'with name: a Symbol' do
508
+ let(:collection_name) { :books }
509
+ let(:collection_options) do
510
+ super().merge(name: collection_name)
511
+ end
512
+
513
+ include_examples 'should create the collection'
514
+ end
515
+
516
+ describe 'with collection options' do
517
+ let(:primary_key_name) { 'uuid' }
518
+ let(:primary_key_type) { String }
519
+ let(:qualified_name) { 'spec/scoped_books' }
520
+ let(:collection_options) do
521
+ super().merge(
522
+ name: collection_name,
523
+ primary_key_name: primary_key_name,
524
+ primary_key_type: primary_key_type,
525
+ qualified_name: qualified_name
526
+ )
527
+ end
528
+
529
+ include_examples 'should create the collection'
530
+ end
531
+
532
+ context 'when the collection already exists' do
533
+ let(:collection_name) { 'books' }
534
+ let(:collection_options) do
535
+ super().merge(name: collection_name)
536
+ end
537
+ let(:error_message) do
538
+ "collection #{qualified_name} already exists"
539
+ end
540
+
541
+ before { create_collection(old: true) }
542
+
543
+ describe 'with non-matching options' do
544
+ it 'should raise an exception' do
545
+ expect { create_collection(old: false, safe: false) }
546
+ .to raise_error(
547
+ described_class::DuplicateCollectionError,
548
+ error_message
549
+ )
550
+ end
551
+
552
+ it 'should not update the repository' do
553
+ create_collection(old: false)
554
+
555
+ collection = repository[qualified_name]
556
+
557
+ expect(collection.options[:old]).to be true
558
+ end
559
+ end
560
+
561
+ describe 'with matching options' do
562
+ it 'should return the collection' do
563
+ collection = create_collection(old: true)
564
+
565
+ expect(collection.options[:old]).to be true
566
+ end
567
+ end
568
+ end
569
+ end
570
+
571
+ describe '#key?' do
572
+ it { expect(repository).to respond_to(:key?).with(1).argument }
573
+
574
+ it { expect(repository.key?(nil)).to be false }
575
+
576
+ it { expect(repository.key?(Object.new.freeze)).to be false }
577
+
578
+ it { expect(repository.key?('invalid_name')).to be false }
579
+
580
+ it { expect(repository.key?(:invalid_name)).to be false }
581
+
582
+ wrap_context 'when the repository has many collections' do
583
+ it { expect(repository.key?('invalid_name')).to be false }
584
+
585
+ it { expect(repository.key?(:invalid_name)).to be false }
586
+
587
+ it { expect(repository.key?(collections.keys.first)).to be true }
588
+
589
+ it 'should include the key' do
590
+ expect(repository.key?(collections.keys.first.intern)).to be true
591
+ end
592
+ end
593
+ end
594
+
595
+ describe '#keys' do
596
+ include_examples 'should define reader', :keys, []
597
+
598
+ wrap_context 'when the repository has many collections' do
599
+ it { expect(repository.keys).to be == collections.keys }
600
+ end
601
+ end
602
+ end
603
+ end
604
+ end
605
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/rspec'
4
+
5
+ module Cuprum::Collections::RSpec
6
+ # Namespace for RSpec contract objects.
7
+ module Contracts
8
+ autoload :AssociationContracts,
9
+ 'cuprum/collections/rspec/contracts/association_contracts'
10
+ autoload :Basic,
11
+ 'cuprum/collections/rspec/contracts/basic'
12
+ autoload :CollectionContracts,
13
+ 'cuprum/collections/rspec/contracts/collection_contracts'
14
+ autoload :CommandContracts,
15
+ 'cuprum/collections/rspec/contracts/command_contracts'
16
+ autoload :QueryContracts,
17
+ 'cuprum/collections/rspec/contracts/query_contracts'
18
+ autoload :RelationContracts,
19
+ 'cuprum/collections/rspec/contracts/relation_contracts'
20
+ autoload :RepositoryContracts,
21
+ 'cuprum/collections/rspec/contracts/repository_contracts'
22
+ end
23
+ end