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,429 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/rspec/contracts'
4
+ require 'cuprum/collections/rspec/contracts/relation_contracts'
5
+
6
+ module Cuprum::Collections::RSpec::Contracts
7
+ # Contracts for asserting on Collection objects.
8
+ module CollectionContracts
9
+ include Cuprum::Collections::RSpec::Contracts::RelationContracts
10
+
11
+ # Contract validating the behavior of a Collection.
12
+ module ShouldBeACollectionContract
13
+ extend RSpec::SleepingKingStudios::Contract
14
+
15
+ # @!method apply(example_group, **options)
16
+ # Adds the contract to the example group.
17
+ #
18
+ # @param example_group [RSpec::Core::ExampleGroup] the example group to
19
+ # which the contract is applied.
20
+ # @param options [Hash] additional options for the contract.
21
+ #
22
+ # @option options abstract [Boolean] if true, the collection is an
23
+ # abstract base class and does not define a query or commands.
24
+ # @option options default_entity_class [Class] the default entity class
25
+ # for the collection, if any.
26
+
27
+ contract do |**options|
28
+ shared_examples 'should define the command' \
29
+ do |command_name, command_class_name = nil|
30
+ next if options[:abstract]
31
+
32
+ tools = SleepingKingStudios::Tools::Toolbelt.instance
33
+ class_name = tools.str.camelize(command_name)
34
+ command_options = %i[
35
+ collection_name
36
+ member_name
37
+ primary_key_name
38
+ primary_key_type
39
+ ] + options.fetch(:command_options, []).map(&:intern)
40
+
41
+ describe "::#{class_name}" do
42
+ let(:constructor_options) { defined?(super()) ? super() : {} }
43
+ let(:command_class) do
44
+ command_class_name ||
45
+ "#{options[:commands_namespace]}::#{class_name}"
46
+ .then { |str| Object.const_get(str) }
47
+ end
48
+ let(:command) do
49
+ collection.const_get(class_name).new(**constructor_options)
50
+ end
51
+ let(:expected_options) do
52
+ Hash
53
+ .new { |_, key| collection.send(key) }
54
+ .merge(
55
+ collection_name: collection.name,
56
+ member_name: collection.singular_name
57
+ )
58
+ end
59
+
60
+ it { expect(collection).to define_constant(class_name) }
61
+
62
+ it { expect(collection.const_get(class_name)).to be_a Class }
63
+
64
+ it 'should be an instance of the command class' do
65
+ expect(collection.const_get(class_name)).to be < command_class
66
+ end
67
+
68
+ it { expect(command.options).to be >= {} }
69
+
70
+ command_options.each do |option_name|
71
+ it "should set the ##{option_name}" do
72
+ expect(command.send(option_name))
73
+ .to be == expected_options[option_name]
74
+ end
75
+ end
76
+
77
+ describe 'with options' do
78
+ let(:constructor_options) do
79
+ super().merge(
80
+ custom_option: 'value',
81
+ singular_name: 'tome'
82
+ )
83
+ end
84
+
85
+ it { expect(command.options).to be >= { custom_option: 'value' } }
86
+
87
+ command_options.each do |option_name|
88
+ it "should set the ##{option_name}" do
89
+ expect(command.send(option_name)).to(
90
+ be == expected_options[option_name]
91
+ )
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ describe "##{command_name}" do
98
+ let(:constructor_options) { defined?(super()) ? super() : {} }
99
+ let(:command) do
100
+ collection.send(command_name, **constructor_options)
101
+ end
102
+ let(:expected_options) do
103
+ Hash
104
+ .new { |_, key| collection.send(key) }
105
+ .merge(
106
+ collection_name: collection.name,
107
+ member_name: collection.singular_name
108
+ )
109
+ end
110
+
111
+ it 'should define the command' do
112
+ expect(collection)
113
+ .to respond_to(command_name)
114
+ .with(0).arguments
115
+ .and_any_keywords
116
+ end
117
+
118
+ it { expect(command).to be_a collection.const_get(class_name) }
119
+
120
+ command_options.each do |option_name|
121
+ it "should set the ##{option_name}" do
122
+ expect(command.send(option_name))
123
+ .to be == expected_options[option_name]
124
+ end
125
+ end
126
+
127
+ describe 'with options' do
128
+ let(:constructor_options) do
129
+ super().merge(
130
+ custom_option: 'value',
131
+ singular_name: 'tome'
132
+ )
133
+ end
134
+
135
+ it { expect(command.options).to be >= { custom_option: 'value' } }
136
+
137
+ command_options.each do |option_name|
138
+ it "should set the ##{option_name}" do
139
+ expect(command.send(option_name)).to(
140
+ be == expected_options[option_name]
141
+ )
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ include_contract 'should be a relation',
149
+ constructor: false,
150
+ default_entity_class: options[:default_entity_class]
151
+
152
+ include_contract 'should disambiguate parameter',
153
+ :name,
154
+ as: :collection_name
155
+
156
+ include_contract 'should disambiguate parameter',
157
+ :singular_name,
158
+ as: :member_name
159
+
160
+ include_contract 'should define primary keys'
161
+
162
+ include_examples 'should define the command', :assign_one
163
+
164
+ include_examples 'should define the command', :build_one
165
+
166
+ include_examples 'should define the command', :destroy_one
167
+
168
+ include_examples 'should define the command', :find_many
169
+
170
+ include_examples 'should define the command', :find_matching
171
+
172
+ include_examples 'should define the command', :find_one
173
+
174
+ include_examples 'should define the command', :insert_one
175
+
176
+ include_examples 'should define the command', :update_one
177
+
178
+ include_examples 'should define the command', :validate_one
179
+
180
+ describe '#==' do
181
+ let(:other_options) { { name: name } }
182
+ let(:other_collection) { described_class.new(**other_options) }
183
+
184
+ describe 'with nil' do
185
+ it { expect(collection == nil).to be false } # rubocop:disable Style/NilComparison
186
+ end
187
+
188
+ describe 'with an object' do
189
+ it { expect(collection == Object.new.freeze).to be false }
190
+ end
191
+
192
+ describe 'with a collection with non-matching properties' do
193
+ let(:other_options) { super().merge(custom_option: 'value') }
194
+
195
+ it { expect(collection == other_collection).to be false }
196
+ end
197
+
198
+ describe 'with a collection with matching properties' do
199
+ it { expect(collection == other_collection).to be true }
200
+ end
201
+
202
+ describe 'with another type of collection' do
203
+ let(:other_collection) do
204
+ Spec::OtherCollection.new(**other_options)
205
+ end
206
+
207
+ example_class 'Spec::OtherCollection',
208
+ Cuprum::Collections::Collection
209
+
210
+ it { expect(collection == other_collection).to be false }
211
+ end
212
+
213
+ context 'when initialized with options' do
214
+ let(:constructor_options) do
215
+ super().merge(
216
+ qualified_name: 'spec/scoped_books',
217
+ singular_name: 'grimoire'
218
+ )
219
+ end
220
+
221
+ describe 'with a collection with non-matching properties' do
222
+ it { expect(collection == other_collection).to be false }
223
+ end
224
+
225
+ describe 'with a collection with matching properties' do
226
+ let(:other_options) do
227
+ super().merge(
228
+ qualified_name: 'spec/scoped_books',
229
+ singular_name: 'grimoire'
230
+ )
231
+ end
232
+
233
+ it { expect(collection == other_collection).to be true }
234
+ end
235
+ end
236
+ end
237
+
238
+ describe '#count' do
239
+ it { expect(collection).to respond_to(:count).with(0).arguments }
240
+
241
+ it { expect(collection).to have_aliased_method(:count).as(:size) }
242
+
243
+ next if options[:abstract]
244
+
245
+ it { expect(collection.count).to be 0 }
246
+
247
+ wrap_context 'when the collection has many items' do
248
+ it { expect(collection.count).to be items.count }
249
+ end
250
+ end
251
+
252
+ describe '#matches?' do
253
+ def tools
254
+ SleepingKingStudios::Tools::Toolbelt.instance
255
+ end
256
+
257
+ it 'should define the method' do
258
+ expect(collection)
259
+ .to respond_to(:matches?)
260
+ .with(0).arguments
261
+ .and_any_keywords
262
+ end
263
+
264
+ describe 'with no options' do
265
+ it { expect(collection.matches?).to be true }
266
+ end
267
+
268
+ describe 'with non-matching entity class as a Class' do
269
+ let(:other_options) { { entity_class: Grimoire } }
270
+
271
+ it { expect(collection.matches?(**other_options)).to be false }
272
+ end
273
+
274
+ describe 'with non-matching entity class as a String' do
275
+ let(:other_options) { { entity_class: 'Grimoire' } }
276
+
277
+ it { expect(collection.matches?(**other_options)).to be false }
278
+ end
279
+
280
+ describe 'with non-matching name' do
281
+ it { expect(collection.matches?(name: 'grimoires')).to be false }
282
+ end
283
+
284
+ describe 'with non-matching primary key name' do
285
+ let(:other_options) { { primary_key_name: 'uuid' } }
286
+
287
+ it { expect(collection.matches?(**other_options)).to be false }
288
+ end
289
+
290
+ describe 'with non-matching primary key type' do
291
+ let(:other_options) { { primary_key_type: String } }
292
+
293
+ it { expect(collection.matches?(**other_options)).to be false }
294
+ end
295
+
296
+ describe 'with non-matching qualified name' do
297
+ let(:other_options) { { qualified_name: 'spec/scoped_books' } }
298
+
299
+ it { expect(collection.matches?(**other_options)).to be false }
300
+ end
301
+
302
+ describe 'with non-matching singular name' do
303
+ let(:other_options) { { singular_name: 'grimoire' } }
304
+
305
+ it { expect(collection.matches?(**other_options)).to be false }
306
+ end
307
+
308
+ describe 'with non-matching custom options' do
309
+ let(:other_options) { { custom_option: 'custom value' } }
310
+
311
+ it { expect(collection.matches?(**other_options)).to be false }
312
+ end
313
+
314
+ describe 'with partially-matching options' do
315
+ let(:other_options) do
316
+ {
317
+ name: name,
318
+ singular_name: 'grimoire'
319
+ }
320
+ end
321
+
322
+ it { expect(collection.matches?(**other_options)).to be false }
323
+ end
324
+
325
+ describe 'with matching entity class as a Class' do
326
+ let(:configured_entity_class) do
327
+ options.fetch(:default_entity_class, Book)
328
+ end
329
+ let(:other_options) { { entity_class: configured_entity_class } }
330
+
331
+ it { expect(collection.matches?(**other_options)).to be true }
332
+ end
333
+
334
+ describe 'with matching entity class as a String' do
335
+ let(:configured_entity_class) do
336
+ options.fetch(:default_entity_class, Book)
337
+ end
338
+ let(:other_options) do
339
+ { entity_class: configured_entity_class.to_s }
340
+ end
341
+
342
+ it { expect(collection.matches?(**other_options)).to be true }
343
+ end
344
+
345
+ describe 'with matching name' do
346
+ let(:other_options) { { collection_name: name } }
347
+
348
+ it { expect(collection.matches?(**other_options)).to be true }
349
+ end
350
+
351
+ describe 'with matching primary key name' do
352
+ let(:other_options) { { primary_key_name: 'id' } }
353
+
354
+ it { expect(collection.matches?(**other_options)).to be true }
355
+ end
356
+
357
+ describe 'with matching primary key type' do
358
+ let(:other_options) { { primary_key_type: Integer } }
359
+
360
+ it { expect(collection.matches?(**other_options)).to be true }
361
+ end
362
+
363
+ describe 'with matching qualified name' do
364
+ let(:other_options) { { qualified_name: name } }
365
+
366
+ it { expect(collection.matches?(**other_options)).to be true }
367
+ end
368
+
369
+ describe 'with matching singular name' do
370
+ let(:other_options) do
371
+ { singular_name: tools.str.singularize(name) }
372
+ end
373
+
374
+ it { expect(collection.matches?(**other_options)).to be true }
375
+ end
376
+
377
+ describe 'with multiple matching options' do
378
+ let(:other_options) do
379
+ {
380
+ collection_name: name,
381
+ primary_key_name: 'id',
382
+ qualified_name: name
383
+ }
384
+ end
385
+
386
+ it { expect(collection.matches?(**other_options)).to be true }
387
+ end
388
+ end
389
+
390
+ describe '#query' do
391
+ let(:error_message) do
392
+ "#{described_class.name} is an abstract class. Define a " \
393
+ 'repository subclass and implement the #query method.'
394
+ end
395
+ let(:default_order) { defined?(super()) ? super() : {} }
396
+ let(:query) { collection.query }
397
+
398
+ it { expect(collection).to respond_to(:query).with(0).arguments }
399
+
400
+ if options[:abstract]
401
+ it 'should raise an exception' do
402
+ expect { collection.query }
403
+ .to raise_error(
404
+ described_class::AbstractCollectionError,
405
+ error_message
406
+ )
407
+ end
408
+ else
409
+ it { expect(collection.query).to be_a query_class }
410
+
411
+ it 'should set the query options' do
412
+ query_options.each do |option, value|
413
+ expect(collection.query.send(option)).to be == value
414
+ end
415
+ end
416
+
417
+ it { expect(query.criteria).to be == [] }
418
+
419
+ it { expect(query.limit).to be nil }
420
+
421
+ it { expect(query.offset).to be nil }
422
+
423
+ it { expect(query.order).to be == default_order }
424
+ end
425
+ end
426
+ end
427
+ end
428
+ end
429
+ end