cuprum-collections 0.3.0.rc.0 → 0.4.0.rc.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -4
  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 +2 -2
  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