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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/DEVELOPMENT.md +2 -2
- data/README.md +13 -11
- data/lib/cuprum/collections/association.rb +256 -0
- data/lib/cuprum/collections/associations/belongs_to.rb +32 -0
- data/lib/cuprum/collections/associations/has_many.rb +23 -0
- data/lib/cuprum/collections/associations/has_one.rb +23 -0
- data/lib/cuprum/collections/associations.rb +10 -0
- data/lib/cuprum/collections/basic/collection.rb +39 -74
- data/lib/cuprum/collections/basic/commands/find_many.rb +1 -1
- data/lib/cuprum/collections/basic/commands/find_matching.rb +1 -1
- data/lib/cuprum/collections/basic/repository.rb +9 -33
- data/lib/cuprum/collections/basic.rb +1 -0
- data/lib/cuprum/collections/collection.rb +154 -0
- data/lib/cuprum/collections/commands/associations/find_many.rb +161 -0
- data/lib/cuprum/collections/commands/associations/require_many.rb +48 -0
- data/lib/cuprum/collections/commands/associations.rb +13 -0
- data/lib/cuprum/collections/commands/find_one_matching.rb +1 -1
- data/lib/cuprum/collections/commands.rb +1 -0
- data/lib/cuprum/collections/errors/abstract_find_error.rb +1 -1
- data/lib/cuprum/collections/relation.rb +401 -0
- data/lib/cuprum/collections/repository.rb +71 -4
- data/lib/cuprum/collections/resource.rb +65 -0
- data/lib/cuprum/collections/rspec/contracts/association_contracts.rb +2137 -0
- data/lib/cuprum/collections/rspec/contracts/basic/command_contracts.rb +484 -0
- data/lib/cuprum/collections/rspec/contracts/basic.rb +11 -0
- data/lib/cuprum/collections/rspec/contracts/collection_contracts.rb +429 -0
- data/lib/cuprum/collections/rspec/contracts/command_contracts.rb +1462 -0
- data/lib/cuprum/collections/rspec/contracts/query_contracts.rb +1093 -0
- data/lib/cuprum/collections/rspec/contracts/relation_contracts.rb +1381 -0
- data/lib/cuprum/collections/rspec/contracts/repository_contracts.rb +605 -0
- data/lib/cuprum/collections/rspec/contracts.rb +23 -0
- data/lib/cuprum/collections/rspec/fixtures.rb +85 -82
- data/lib/cuprum/collections/rspec.rb +4 -1
- data/lib/cuprum/collections/version.rb +1 -1
- data/lib/cuprum/collections.rb +9 -4
- metadata +23 -19
- data/lib/cuprum/collections/base.rb +0 -11
- data/lib/cuprum/collections/basic/rspec/command_contract.rb +0 -392
- data/lib/cuprum/collections/rspec/assign_one_command_contract.rb +0 -168
- data/lib/cuprum/collections/rspec/build_one_command_contract.rb +0 -93
- data/lib/cuprum/collections/rspec/collection_contract.rb +0 -190
- data/lib/cuprum/collections/rspec/destroy_one_command_contract.rb +0 -108
- data/lib/cuprum/collections/rspec/find_many_command_contract.rb +0 -407
- data/lib/cuprum/collections/rspec/find_matching_command_contract.rb +0 -194
- data/lib/cuprum/collections/rspec/find_one_command_contract.rb +0 -157
- data/lib/cuprum/collections/rspec/insert_one_command_contract.rb +0 -84
- data/lib/cuprum/collections/rspec/query_builder_contract.rb +0 -92
- data/lib/cuprum/collections/rspec/query_contract.rb +0 -650
- data/lib/cuprum/collections/rspec/querying_contract.rb +0 -298
- data/lib/cuprum/collections/rspec/repository_contract.rb +0 -235
- data/lib/cuprum/collections/rspec/update_one_command_contract.rb +0 -80
- 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
|