cuprum-rails 0.1.0 → 0.2.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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +145 -0
  3. data/DEVELOPMENT.md +20 -0
  4. data/README.md +356 -63
  5. data/lib/cuprum/rails/action.rb +32 -16
  6. data/lib/cuprum/rails/actions/create.rb +62 -15
  7. data/lib/cuprum/rails/actions/destroy.rb +23 -7
  8. data/lib/cuprum/rails/actions/edit.rb +23 -7
  9. data/lib/cuprum/rails/actions/index.rb +30 -10
  10. data/lib/cuprum/rails/actions/middleware/associations/cache.rb +112 -0
  11. data/lib/cuprum/rails/actions/middleware/associations/find.rb +23 -0
  12. data/lib/cuprum/rails/actions/middleware/associations/parent.rb +70 -0
  13. data/lib/cuprum/rails/actions/middleware/associations/query.rb +140 -0
  14. data/lib/cuprum/rails/actions/middleware/associations.rb +12 -0
  15. data/lib/cuprum/rails/actions/middleware/log_request.rb +126 -0
  16. data/lib/cuprum/rails/actions/middleware/log_result.rb +51 -0
  17. data/lib/cuprum/rails/actions/middleware/resources/find.rb +44 -0
  18. data/lib/cuprum/rails/actions/middleware/resources/query.rb +91 -0
  19. data/lib/cuprum/rails/actions/middleware/resources.rb +11 -0
  20. data/lib/cuprum/rails/actions/middleware.rb +13 -0
  21. data/lib/cuprum/rails/actions/new.rb +16 -4
  22. data/lib/cuprum/rails/actions/parameter_validation.rb +60 -0
  23. data/lib/cuprum/rails/actions/resource_action.rb +119 -42
  24. data/lib/cuprum/rails/actions/show.rb +23 -7
  25. data/lib/cuprum/rails/actions/update.rb +70 -22
  26. data/lib/cuprum/rails/actions.rb +11 -7
  27. data/lib/cuprum/rails/collection.rb +27 -47
  28. data/lib/cuprum/rails/command.rb +3 -1
  29. data/lib/cuprum/rails/commands/destroy_one.rb +10 -6
  30. data/lib/cuprum/rails/commands/find_many.rb +8 -1
  31. data/lib/cuprum/rails/commands/find_matching.rb +1 -1
  32. data/lib/cuprum/rails/commands/find_one.rb +8 -0
  33. data/lib/cuprum/rails/commands/insert_one.rb +17 -6
  34. data/lib/cuprum/rails/commands/update_one.rb +16 -5
  35. data/lib/cuprum/rails/constraints/parameters_contract.rb +14 -0
  36. data/lib/cuprum/rails/constraints.rb +10 -0
  37. data/lib/cuprum/rails/controller.rb +12 -2
  38. data/lib/cuprum/rails/controllers/action.rb +100 -0
  39. data/lib/cuprum/rails/controllers/class_methods/actions.rb +33 -7
  40. data/lib/cuprum/rails/controllers/class_methods/configuration.rb +36 -0
  41. data/lib/cuprum/rails/controllers/class_methods/middleware.rb +88 -0
  42. data/lib/cuprum/rails/controllers/class_methods/validations.rb +2 -2
  43. data/lib/cuprum/rails/controllers/configuration.rb +41 -1
  44. data/lib/cuprum/rails/controllers/middleware.rb +59 -0
  45. data/lib/cuprum/rails/controllers.rb +2 -0
  46. data/lib/cuprum/rails/errors/invalid_parameters.rb +55 -0
  47. data/lib/cuprum/rails/errors/invalid_statement.rb +11 -0
  48. data/lib/cuprum/rails/errors/missing_parameter.rb +42 -0
  49. data/lib/cuprum/rails/errors/resource_error.rb +46 -0
  50. data/lib/cuprum/rails/errors.rb +6 -1
  51. data/lib/cuprum/rails/map_errors.rb +29 -1
  52. data/lib/cuprum/rails/query.rb +1 -1
  53. data/lib/cuprum/rails/repository.rb +12 -25
  54. data/lib/cuprum/rails/request.rb +149 -60
  55. data/lib/cuprum/rails/resource.rb +119 -85
  56. data/lib/cuprum/rails/responders/base_responder.rb +78 -0
  57. data/lib/cuprum/rails/responders/html/plural_resource.rb +9 -39
  58. data/lib/cuprum/rails/responders/html/rendering.rb +81 -0
  59. data/lib/cuprum/rails/responders/html/resource.rb +107 -0
  60. data/lib/cuprum/rails/responders/html/singular_resource.rb +9 -38
  61. data/lib/cuprum/rails/responders/html.rb +2 -0
  62. data/lib/cuprum/rails/responders/html_responder.rb +8 -52
  63. data/lib/cuprum/rails/responders/json/resource.rb +3 -3
  64. data/lib/cuprum/rails/responders/json_responder.rb +31 -16
  65. data/lib/cuprum/rails/responders/matching.rb +29 -27
  66. data/lib/cuprum/rails/responders/serialization.rb +11 -9
  67. data/lib/cuprum/rails/responders.rb +1 -0
  68. data/lib/cuprum/rails/responses/head_response.rb +24 -0
  69. data/lib/cuprum/rails/responses/html/redirect_back_response.rb +55 -0
  70. data/lib/cuprum/rails/responses/html/redirect_response.rb +19 -4
  71. data/lib/cuprum/rails/responses/html/render_response.rb +17 -5
  72. data/lib/cuprum/rails/responses/html.rb +6 -2
  73. data/lib/cuprum/rails/responses.rb +1 -0
  74. data/lib/cuprum/rails/result.rb +36 -0
  75. data/lib/cuprum/rails/routes.rb +36 -23
  76. data/lib/cuprum/rails/rspec/contract_helpers.rb +57 -0
  77. data/lib/cuprum/rails/rspec/contracts/action_contracts.rb +754 -0
  78. data/lib/cuprum/rails/rspec/contracts/actions/create_contracts.rb +289 -0
  79. data/lib/cuprum/rails/rspec/contracts/actions/destroy_contracts.rb +164 -0
  80. data/lib/cuprum/rails/rspec/contracts/actions/edit_contracts.rb +73 -0
  81. data/lib/cuprum/rails/rspec/contracts/actions/index_contracts.rb +108 -0
  82. data/lib/cuprum/rails/rspec/contracts/actions/new_contracts.rb +111 -0
  83. data/lib/cuprum/rails/rspec/contracts/actions/show_contracts.rb +72 -0
  84. data/lib/cuprum/rails/rspec/contracts/actions/update_contracts.rb +263 -0
  85. data/lib/cuprum/rails/rspec/contracts/actions.rb +8 -0
  86. data/lib/cuprum/rails/rspec/contracts/command_contracts.rb +479 -0
  87. data/lib/cuprum/rails/rspec/contracts/responder_contracts.rb +232 -0
  88. data/lib/cuprum/rails/rspec/contracts/routes_contracts.rb +363 -0
  89. data/lib/cuprum/rails/rspec/contracts/serializers_contracts.rb +70 -0
  90. data/lib/cuprum/rails/rspec/contracts.rb +8 -0
  91. data/lib/cuprum/rails/rspec/matchers/be_a_result_matcher.rb +64 -0
  92. data/lib/cuprum/rails/rspec/matchers.rb +41 -0
  93. data/lib/cuprum/rails/serializers/base_serializer.rb +60 -0
  94. data/lib/cuprum/rails/serializers/context.rb +84 -0
  95. data/lib/cuprum/rails/serializers/json/active_record_serializer.rb +2 -2
  96. data/lib/cuprum/rails/serializers/json/array_serializer.rb +9 -8
  97. data/lib/cuprum/rails/serializers/json/attributes_serializer.rb +95 -172
  98. data/lib/cuprum/rails/serializers/json/error_serializer.rb +2 -2
  99. data/lib/cuprum/rails/serializers/json/hash_serializer.rb +9 -8
  100. data/lib/cuprum/rails/serializers/json/identity_serializer.rb +3 -3
  101. data/lib/cuprum/rails/serializers/json/properties_serializer.rb +252 -0
  102. data/lib/cuprum/rails/serializers/json.rb +2 -1
  103. data/lib/cuprum/rails/serializers.rb +3 -1
  104. data/lib/cuprum/rails/version.rb +1 -1
  105. data/lib/cuprum/rails.rb +19 -16
  106. metadata +73 -131
  107. data/lib/cuprum/rails/controller_action.rb +0 -121
  108. data/lib/cuprum/rails/errors/missing_parameters.rb +0 -33
  109. data/lib/cuprum/rails/errors/missing_primary_key.rb +0 -46
  110. data/lib/cuprum/rails/errors/undefined_permitted_attributes.rb +0 -34
  111. data/lib/cuprum/rails/rspec/command_contract.rb +0 -460
  112. data/lib/cuprum/rails/rspec/define_route_contract.rb +0 -84
  113. data/lib/cuprum/rails/serializers/json/serializer.rb +0 -66
@@ -0,0 +1,754 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/errors/not_found'
4
+ require 'cuprum/collections/repository'
5
+ require 'rspec/sleeping_king_studios/contract'
6
+
7
+ require 'cuprum/rails/map_errors'
8
+ require 'cuprum/rails/rspec/contract_helpers'
9
+ require 'cuprum/rails/rspec/contracts'
10
+
11
+ module Cuprum::Rails::RSpec::Contracts
12
+ # Namespace for RSpec action contracts, which validate action implementations.
13
+ module ActionContracts
14
+ # Contract validating the interface for an action.
15
+ module ShouldBeAnActionContract
16
+ extend RSpec::SleepingKingStudios::Contract
17
+
18
+ # @!method apply(example_group, **options)
19
+ # Adds the contract to the example group.
20
+ #
21
+ # @param example_group [RSpec::Core::ExampleGroup] the example group to
22
+ # which the contract is applied.
23
+ # @param options [Hash] additional options for the contract.
24
+ #
25
+ # @option options required_keywords [Array[Symbol]] additional keywords
26
+ # required by the #call method.
27
+ contract do |**options|
28
+ describe '.new' do
29
+ it { expect(described_class).to respond_to(:new).with(0).arguments }
30
+ end
31
+
32
+ describe '#call' do
33
+ let(:expected_keywords) do
34
+ %i[repository request] + options.fetch(:required_keywords, [])
35
+ end
36
+
37
+ it 'should define the method' do
38
+ expect(action)
39
+ .to be_callable
40
+ .with(0).arguments
41
+ .and_keywords(*expected_keywords)
42
+ .and_any_keywords
43
+ end
44
+ end
45
+
46
+ describe '#options' do
47
+ include_examples 'should define reader', :options
48
+ end
49
+
50
+ describe '#params' do
51
+ include_examples 'should define reader', :params
52
+ end
53
+
54
+ describe '#repository' do
55
+ include_examples 'should define reader', :repository
56
+ end
57
+
58
+ describe '#request' do
59
+ include_examples 'should define reader', :request
60
+ end
61
+ end
62
+ end
63
+
64
+ # Contract validating the interface for a resourceful action.
65
+ module ShouldBeAResourceActionContract
66
+ extend RSpec::SleepingKingStudios::Contract
67
+
68
+ # @!method apply(example_group, **options)
69
+ # Adds the contract to the example group.
70
+ #
71
+ # @param example_group [RSpec::Core::ExampleGroup] the example group to
72
+ # which the contract is applied.
73
+ # @param options [Hash] additional options for the conrtact.
74
+ #
75
+ # @option options collection_class [String, Class] the expected class
76
+ # for the resource collection.
77
+ # @option options require_permitted_attributes [Boolean] if true, should
78
+ # require the resource to define permitted attributes as a non-empty
79
+ # Array.
80
+ # @option options required_keywords [Array[Symbol]] additional keywords
81
+ # required by the #call method.
82
+
83
+ contract do |**options|
84
+ include Cuprum::Rails::RSpec::Contracts::ActionContracts
85
+
86
+ let(:configured_params) do
87
+ return params if defined?(params)
88
+
89
+ {}
90
+ end
91
+ let(:configured_repository) do
92
+ return repository if defined?(repository)
93
+
94
+ Cuprum::Rails::Repository.new
95
+ end
96
+ let(:configured_request) do
97
+ return request if defined?(request)
98
+
99
+ Cuprum::Rails::Request.new(params: configured_params)
100
+ end
101
+ let(:configured_resource) do
102
+ return resource if defined?(resource)
103
+
104
+ # :nocov:
105
+ Cuprum::Rails::Resource.new(entity_class: Book)
106
+ # :nocov:
107
+ end
108
+ let(:configured_action_options) do
109
+ return action_options if defined?(action_options)
110
+
111
+ {
112
+ repository: configured_repository,
113
+ resource: configured_resource
114
+ }
115
+ end
116
+
117
+ define_method(:call_action) do
118
+ action.call(request: configured_request, **configured_action_options)
119
+ end
120
+
121
+ include_contract 'should be an action',
122
+ required_keywords: [:resource, *options.fetch(:required_keywords, [])]
123
+
124
+ describe '#call' do
125
+ next unless options[:require_permitted_attributes]
126
+
127
+ describe 'with a permitted_attributes: nil' do
128
+ let(:resource) do
129
+ Cuprum::Rails::Resource.new(
130
+ name: 'books',
131
+ permitted_attributes: nil
132
+ )
133
+ end
134
+ let(:expected_error) do
135
+ Cuprum::Rails::Errors::ResourceError.new(
136
+ message: "permitted attributes can't be blank",
137
+ resource: configured_resource
138
+ )
139
+ end
140
+
141
+ it 'should return a failing result' do
142
+ expect(call_action)
143
+ .to be_a_failing_result
144
+ .with_error(expected_error)
145
+ end
146
+ end
147
+
148
+ describe 'with a permitted_attributes: an empty Array' do
149
+ let(:resource) do
150
+ Cuprum::Rails::Resource.new(
151
+ name: 'books',
152
+ permitted_attributes: []
153
+ )
154
+ end
155
+ let(:expected_error) do
156
+ Cuprum::Rails::Errors::ResourceError.new(
157
+ message: "permitted attributes can't be blank",
158
+ resource: configured_resource
159
+ )
160
+ end
161
+
162
+ it 'should return a failing result' do
163
+ expect(call_action)
164
+ .to be_a_failing_result
165
+ .with_error(expected_error)
166
+ end
167
+ end
168
+ end
169
+
170
+ describe '#collection' do
171
+ let(:expected_collection_class) do
172
+ next super() if defined?(super())
173
+
174
+ options
175
+ .fetch(:collection_class, Cuprum::Collections::Collection)
176
+ .then { |obj| obj.is_a?(String) ? obj.constantize : obj }
177
+ end
178
+
179
+ before(:example) { call_action }
180
+
181
+ include_examples 'should define reader', :collection
182
+
183
+ it { expect(action.collection).to be_a expected_collection_class }
184
+
185
+ it 'should set the collection name' do
186
+ expect(action.collection.name)
187
+ .to be == resource.name
188
+ end
189
+
190
+ it 'should set the entity class' do
191
+ expect(action.collection.entity_class)
192
+ .to be == resource.entity_class
193
+ end
194
+
195
+ context 'when the repository defines a matching collection' do
196
+ let!(:existing_collection) do
197
+ configured_repository.find_or_create(
198
+ qualified_name: resource.qualified_name
199
+ )
200
+ end
201
+
202
+ it { expect(action.collection).to be existing_collection }
203
+ end
204
+
205
+ context 'when there is a partially matching collection' do
206
+ let(:configured_repository) do
207
+ repository = super()
208
+
209
+ repository.find_or_create(
210
+ entity_class: resource.entity_class,
211
+ name: 'other_collection',
212
+ qualified_name: resource.qualified_name
213
+ )
214
+
215
+ repository
216
+ end
217
+ let!(:existing_collection) do
218
+ configured_repository[resource.qualified_name]
219
+ end
220
+
221
+ it { expect(action.collection).to be existing_collection }
222
+ end
223
+ end
224
+
225
+ describe '#resource' do
226
+ include_examples 'should define reader', :resource
227
+
228
+ context 'when called with a resource' do
229
+ before(:example) { call_action }
230
+
231
+ it { expect(action.resource).to be == configured_resource }
232
+ end
233
+ end
234
+
235
+ describe '#resource_id' do
236
+ include_examples 'should define reader', :resource_id
237
+
238
+ context 'when called with a resource' do
239
+ let(:params) { {} }
240
+ let(:request) { Cuprum::Rails::Request.new(params: params) }
241
+
242
+ before(:example) { call_action }
243
+
244
+ context 'when the parameters do not include a primary key' do
245
+ let(:params) { {} }
246
+
247
+ it { expect(action.resource_id).to be nil }
248
+ end
249
+
250
+ context 'when the :id parameter is set' do
251
+ let(:primary_key_value) { 0 }
252
+ let(:params) { { 'id' => primary_key_value } }
253
+
254
+ it { expect(action.resource_id).to be primary_key_value }
255
+ end
256
+ end
257
+ end
258
+
259
+ describe '#resource_params' do
260
+ include_examples 'should define reader', :resource_params
261
+
262
+ context 'when called with a resource' do
263
+ let(:params) { {} }
264
+ let(:request) { Cuprum::Rails::Request.new(params: params) }
265
+
266
+ before(:example) { call_action }
267
+
268
+ context 'when the parameters do not include params for the ' \
269
+ 'resource' \
270
+ do
271
+ let(:params) { {} }
272
+
273
+ it { expect(action.resource_params).to be == {} }
274
+ end
275
+
276
+ context 'when the params for the resource are empty' do
277
+ let(:params) { { resource.singular_name => {} } }
278
+
279
+ it { expect(action.resource_params).to be == {} }
280
+ end
281
+
282
+ context 'when the parameter for the resource is not a Hash' do
283
+ let(:params) { { resource.singular_name => 'invalid' } }
284
+
285
+ it { expect(action.resource_params).to be == 'invalid' }
286
+ end
287
+
288
+ context 'when the parameters include the params for resource' do
289
+ let(:params) do
290
+ resource_params =
291
+ configured_resource
292
+ .permitted_attributes
293
+ .then { |ary| ary || [] }
294
+ .to_h { |attr_name| [attr_name.to_s, "#{attr_name} value"] }
295
+
296
+ {
297
+ configured_resource.singular_name => resource_params
298
+ }
299
+ end
300
+ let(:expected) do
301
+ params[configured_resource.singular_name]
302
+ end
303
+
304
+ it { expect(action.resource_params).to be == expected }
305
+ end
306
+ end
307
+ end
308
+
309
+ describe '#transaction' do
310
+ let(:transaction_class) { resource.entity_class }
311
+
312
+ before(:example) { call_action }
313
+
314
+ it 'should define the private method' do
315
+ expect(action).to respond_to(:transaction, true).with(0).arguments
316
+ end
317
+
318
+ it 'should yield the block' do
319
+ expect { |block| action.send(:transaction, &block) }
320
+ .to yield_control
321
+ end
322
+
323
+ it 'should wrap the block in a transaction' do
324
+ in_transaction = false
325
+
326
+ allow(transaction_class).to receive(:transaction) do |&block|
327
+ in_transaction = true
328
+
329
+ block.call
330
+
331
+ in_transaction = false
332
+ end
333
+
334
+ action.send(:transaction) do
335
+ expect(in_transaction).to be true
336
+ end
337
+ end
338
+
339
+ context 'when the block contains a failing step' do
340
+ let(:expected_error) do
341
+ Cuprum::Error.new(message: 'Something went wrong.')
342
+ end
343
+
344
+ before(:example) do
345
+ action.define_singleton_method(:failing_step) do
346
+ error = Cuprum::Error.new(message: 'Something went wrong.')
347
+
348
+ step { failure(error) }
349
+ end
350
+ end
351
+
352
+ it 'should return the failing result' do
353
+ expect(action.send(:transaction) { action.failing_step })
354
+ .to be_a_failing_result
355
+ .with_error(expected_error)
356
+ end
357
+
358
+ it 'should roll back the transaction' do
359
+ rollback = false
360
+
361
+ allow(transaction_class).to receive(:transaction) do |&block|
362
+ block.call
363
+ rescue ActiveRecord::Rollback
364
+ rollback = true
365
+ end
366
+
367
+ action.send(:transaction) { action.failing_step }
368
+
369
+ expect(rollback).to be true
370
+ end
371
+ end
372
+ end
373
+ end
374
+ end
375
+
376
+ # Contract asserting the action finds and returns the requested entity.
377
+ module ShouldFindTheEntityContract
378
+ extend RSpec::SleepingKingStudios::Contract
379
+
380
+ # @method apply(example_group, existing_entity:, **options, &block)
381
+ # Adds the contract to the example group.
382
+ #
383
+ # @param example_group [RSpec::Core::ExampleGroup] The example group to
384
+ # which the contract is applied.
385
+ # @param existing_entity [Object] The existing entity to destroy.
386
+ #
387
+ # @option options [Hash<String>] expected_value The expected value for
388
+ # the passing result. Defaults to a Hash with the destroyed entity.
389
+ # @option options [Hash<String>] params The parameters used to build the
390
+ # request. Defaults to the id of the entity.
391
+ #
392
+ # @yield Additional configuration or examples.
393
+
394
+ contract do |existing_entity:, **options, &block|
395
+ describe '#call' do
396
+ include Cuprum::Rails::RSpec::ContractHelpers
397
+
398
+ context 'when the entity exists' do
399
+ let(:request) do
400
+ Cuprum::Rails::Request.new(params: configured_params)
401
+ end
402
+ let(:configured_existing_entity) do
403
+ option_with_default(existing_entity)
404
+ end
405
+ let(:configured_params) do
406
+ resource_id =
407
+ configured_existing_entity[configured_resource.primary_key]
408
+
409
+ option_with_default(
410
+ options[:params],
411
+ default: { 'id' => resource_id }
412
+ )
413
+ end
414
+ let(:configured_expected_value) do
415
+ resource_name = configured_resource.singular_name
416
+
417
+ option_with_default(
418
+ options[:expected_value],
419
+ default: { resource_name => configured_existing_entity }
420
+ )
421
+ end
422
+
423
+ it 'should return a passing result' do
424
+ expect(call_action)
425
+ .to be_a_passing_result
426
+ .with_value(configured_expected_value)
427
+ end
428
+
429
+ instance_exec(&block) if block
430
+ end
431
+ end
432
+ end
433
+ end
434
+
435
+ # Contract asserting the action requires a valid entity.
436
+ module ShouldRequireExistingEntityContract
437
+ extend RSpec::SleepingKingStudios::Contract
438
+
439
+ # @!method apply(example_group, **options)
440
+ # Adds the contract to the example group.
441
+ #
442
+ # @param example_group [RSpec::Core::ExampleGroup] The example group to
443
+ # which the contract is applied.
444
+ #
445
+ # @option options [Hash<String>] params The parameters used to build the
446
+ # request. Defaults to the id of the entity.
447
+ # @option options [Object] primary_key_value The value of the primary
448
+ # key for the missing entity.
449
+ #
450
+ # @yield Additional configuration or examples.
451
+
452
+ contract do |**options, &block|
453
+ describe '#call' do
454
+ include Cuprum::Rails::RSpec::ContractHelpers
455
+
456
+ context 'when the entity does not exist' do
457
+ let(:request) do
458
+ Cuprum::Rails::Request.new(params: configured_params)
459
+ end
460
+ let(:configured_primary_key_value) do
461
+ option_with_default(
462
+ options[:primary_key_value],
463
+ default: 0
464
+ )
465
+ end
466
+ let(:configured_params) do
467
+ option_with_default(
468
+ options[:params],
469
+ default: {}
470
+ )
471
+ .merge({ 'id' => configured_primary_key_value })
472
+ end
473
+ let(:expected_error) do
474
+ Cuprum::Collections::Errors::NotFound.new(
475
+ attribute_name: configured_resource.primary_key.to_s,
476
+ attribute_value: configured_primary_key_value,
477
+ collection_name: configured_resource.name,
478
+ primary_key: true
479
+ )
480
+ end
481
+
482
+ before(:example) do
483
+ primary_key_name = configured_resource.primary_key
484
+
485
+ resource
486
+ .entity_class
487
+ .where(primary_key_name => configured_primary_key_value)
488
+ .destroy_all
489
+ end
490
+
491
+ it 'should return a failing result' do
492
+ expect(call_action)
493
+ .to be_a_failing_result
494
+ .with_error(expected_error)
495
+ end
496
+
497
+ instance_exec(&block) if block
498
+ end
499
+ end
500
+ end
501
+ end
502
+
503
+ # Contract asserting the action requires resource parameters.
504
+ module ShouldRequireParametersContract
505
+ extend RSpec::SleepingKingStudios::Contract
506
+
507
+ # @!method apply(example_group)
508
+ # Adds the contract to the example group.
509
+ #
510
+ # @param example_group [RSpec::Core::ExampleGroup] The example group to
511
+ # which the contract is applied.
512
+ #
513
+ # @option options [Hash<String>] params The parameters used to build the
514
+ # request. Defaults to an empty Hash.
515
+ #
516
+ # @yield Additional configuration or examples.
517
+
518
+ contract do |**options, &block|
519
+ describe '#call' do
520
+ include Cuprum::Rails::RSpec::ContractHelpers
521
+
522
+ context 'when the parameters do not include params for the resource' \
523
+ do
524
+ let(:request) do
525
+ Cuprum::Rails::Request.new(params: configured_params)
526
+ end
527
+ let(:configured_params) do
528
+ option_with_default(options[:params], default: {})
529
+ .dup
530
+ .tap do |hsh|
531
+ hsh.delete(configured_resource.singular_name)
532
+ end
533
+ end
534
+ let(:configured_expected_error) do
535
+ errors = Stannum::Errors.new.tap do |err|
536
+ err[configured_resource.singular_name]
537
+ .add(Stannum::Constraints::Presence::TYPE)
538
+ end
539
+
540
+ Cuprum::Rails::Errors::InvalidParameters.new(errors: errors)
541
+ end
542
+
543
+ it 'should return a failing result' do
544
+ expect(call_action)
545
+ .to be_a_failing_result
546
+ .with_error(configured_expected_error)
547
+ end
548
+
549
+ instance_exec(&block) if block
550
+ end
551
+
552
+ context 'when the resource parameters are not a Hash' do
553
+ let(:request) do
554
+ Cuprum::Rails::Request.new(params: configured_params)
555
+ end
556
+ let(:configured_params) do
557
+ option_with_default(options[:params], default: {})
558
+ .merge(configured_resource.singular_name => 'invalid')
559
+ end
560
+ let(:configured_expected_error) do
561
+ errors = Stannum::Errors.new.tap do |err|
562
+ err[configured_resource.singular_name].add(
563
+ Stannum::Constraints::Type::TYPE,
564
+ allow_empty: true,
565
+ required: true,
566
+ type: Hash
567
+ )
568
+ end
569
+
570
+ Cuprum::Rails::Errors::InvalidParameters.new(errors: errors)
571
+ end
572
+
573
+ it 'should return a failing result' do
574
+ expect(call_action)
575
+ .to be_a_failing_result
576
+ .with_error(configured_expected_error)
577
+ end
578
+
579
+ instance_exec(&block) if block
580
+ end
581
+ end
582
+ end
583
+ end
584
+
585
+ # Contract asserting the action requires a primary key.
586
+ module ShouldRequirePrimaryKeyContract
587
+ extend RSpec::SleepingKingStudios::Contract
588
+
589
+ # @!method apply(example_group, **options, &block)
590
+ # Adds the contract to the example group.
591
+ #
592
+ # @param example_group [RSpec::Core::ExampleGroup] The example group to
593
+ # which the contract is applied.
594
+ #
595
+ # @option options [Hash<String>] params The parameters used to build the
596
+ # request. Defaults to an empty Hash.
597
+ #
598
+ # @yield Additional configuration or examples.
599
+
600
+ contract do |**options, &block|
601
+ describe '#call' do
602
+ include Cuprum::Rails::RSpec::ContractHelpers
603
+
604
+ context 'when the parameters do not include a primary key' do
605
+ let(:request) do
606
+ Cuprum::Rails::Request.new(params: configured_params)
607
+ end
608
+ let(:configured_params) do
609
+ option_with_default(options[:params], default: {})
610
+ .dup
611
+ .tap { |hsh| hsh.delete('id') }
612
+ end
613
+ let(:configured_expected_error) do
614
+ errors = Stannum::Errors.new.tap do |err|
615
+ err['id'].add(Stannum::Constraints::Presence::TYPE)
616
+ end
617
+
618
+ Cuprum::Rails::Errors::InvalidParameters.new(errors: errors)
619
+ end
620
+
621
+ it 'should return a failing result' do
622
+ expect(call_action)
623
+ .to be_a_failing_result
624
+ .with_error(configured_expected_error)
625
+ end
626
+
627
+ instance_exec(&block) if block
628
+ end
629
+ end
630
+ end
631
+ end
632
+
633
+ # Contract asserting the action validates the created or updated entity.
634
+ module ShouldValidateAttributesContract
635
+ extend RSpec::SleepingKingStudios::Contract
636
+
637
+ # @!method apply(example_group, invalid_attributes:, expected_attributes: nil, **options)
638
+ # Adds the contract to the example group.
639
+ #
640
+ # @param example_group [RSpec::Core::ExampleGroup] The example group to
641
+ # which the contract is applied.
642
+ # @param invalid_attributes [Hash<String>] A set of attributes that will
643
+ # fail validation.
644
+ #
645
+ # @option options [Object] existing_entity The existing entity, if any.
646
+ # @option options [Hash<String>] expected_attributes The expected
647
+ # attributes for the returned object. Defaults to the value of
648
+ # invalid_attributes.
649
+ # @option options [Hash<String>] params The parameters used to build the
650
+ # request. Defaults to the given attributes.
651
+ #
652
+ # @yield Additional configuration or examples.
653
+
654
+ contract do |invalid_attributes:, **options, &block|
655
+ describe '#call' do
656
+ include Cuprum::Rails::RSpec::ContractHelpers
657
+
658
+ context 'when the resource params fail validation' do
659
+ let(:request) do
660
+ Cuprum::Rails::Request.new(params: configured_params)
661
+ end
662
+ let(:configured_invalid_attributes) do
663
+ option_with_default(invalid_attributes)
664
+ end
665
+ let(:configured_params) do
666
+ resource_name = configured_resource.singular_name
667
+
668
+ option_with_default(
669
+ options[:params],
670
+ default: {}
671
+ ).merge({ resource_name => configured_invalid_attributes })
672
+ end
673
+ let(:configured_existing_entity) do
674
+ option_with_default(options[:existing_entity])
675
+ end
676
+ let(:configured_expected_attributes) do
677
+ option_with_default(
678
+ options[:expected_attributes],
679
+ default: (configured_existing_entity&.attributes || {}).merge(
680
+ configured_invalid_attributes
681
+ )
682
+ )
683
+ end
684
+ let(:configured_expected_entity) do
685
+ if configured_existing_entity
686
+ repository
687
+ .find_or_create(
688
+ qualified_name: resource.qualified_name
689
+ )
690
+ .assign_one
691
+ .call(
692
+ attributes: configured_invalid_attributes,
693
+ entity: configured_existing_entity.clone
694
+ )
695
+ .value
696
+ .tap(&:valid?)
697
+ else
698
+ action
699
+ .resource
700
+ .entity_class
701
+ .new(configured_expected_attributes)
702
+ .tap(&:valid?)
703
+ end
704
+ end
705
+ let(:configured_expected_value) do
706
+ matcher =
707
+ be_a(configured_expected_entity.class)
708
+ .and(have_attributes(configured_expected_entity.attributes))
709
+ option_with_default(
710
+ options[:expected_value],
711
+ default: {
712
+ configured_resource.singular_name => matcher
713
+ }
714
+ )
715
+ end
716
+ let(:configured_expected_error) do
717
+ errors =
718
+ Cuprum::Rails::MapErrors
719
+ .instance
720
+ .call(native_errors: configured_expected_entity.errors)
721
+
722
+ Cuprum::Collections::Errors::FailedValidation.new(
723
+ entity_class: configured_resource.entity_class,
724
+ errors: scope_validation_errors(errors)
725
+ )
726
+ end
727
+
728
+ def scope_validation_errors(errors)
729
+ mapped_errors = Stannum::Errors.new
730
+ resource_name = configured_resource.singular_name
731
+
732
+ errors.each do |err|
733
+ mapped_errors
734
+ .dig(resource_name, *err[:path].map(&:to_s))
735
+ .add(err[:type], message: err[:message], **err[:data])
736
+ end
737
+
738
+ mapped_errors
739
+ end
740
+
741
+ it 'should return a failing result' do
742
+ expect(call_action)
743
+ .to be_a_failing_result
744
+ .with_value(deep_match(configured_expected_value))
745
+ .and_error(configured_expected_error)
746
+ end
747
+
748
+ instance_exec(&block) if block
749
+ end
750
+ end
751
+ end
752
+ end
753
+ end
754
+ end