cuprum-collections 0.3.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 +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 +3 -3
  37. data/lib/cuprum/collections.rb +9 -4
  38. metadata +25 -21
  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