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,363 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/sleeping_king_studios/contract'
4
+
5
+ require 'cuprum/rails/rspec/contracts'
6
+
7
+ module Cuprum::Rails::RSpec::Contracts
8
+ # Namespace for RSpec contracts for Routes objects.
9
+ module RoutesContracts
10
+ # Contract asserting that the given collection route helper is defined.
11
+ module ShouldDefineCollectionRouteContract
12
+ extend RSpec::SleepingKingStudios::Contract
13
+
14
+ # @!method apply(example_group, constructor_keywords: [])
15
+ # Adds the contract to the example group.
16
+ #
17
+ # @param example_group [RSpec::Core::ExampleGroup] the example group to
18
+ # which the contract is applied.
19
+ # @param action_name [String, Symbol] the name of the route action.
20
+ # @param path [String] the expected path for the route helper.
21
+ # @param options [Hash] additional options for the contract.
22
+ #
23
+ # @option options wildcards [Hash{String=>String}] the required
24
+ # wildcards for generating the route.
25
+ contract do |action_name:, path:, **options|
26
+ options[:wildcards] = options.fetch(:wildcards, {}).stringify_keys
27
+ expected_wildcards =
28
+ path.split('/').select { |str| str.start_with?(':') }
29
+
30
+ describe "##{action_name}_path" do
31
+ let(:method_name) { "#{action_name}_path" }
32
+ let(:wildcards) { options[:wildcards] }
33
+ let(:expected) do
34
+ expected_wildcards.reduce(path) do |resolved, key|
35
+ resolved.sub(key, resolve_wildcard(key))
36
+ end
37
+ end
38
+ let(:error_class) do
39
+ Cuprum::Rails::Routes::MissingWildcardError
40
+ end
41
+ let(:error_message) do
42
+ "missing wildcard #{expected_wildcards.first}"
43
+ end
44
+
45
+ def resolve_wildcard(key)
46
+ key = key.sub(/\A:/, '')
47
+ value =
48
+ wildcards.fetch(key) { wildcards.fetch(key.sub(/_id\z/, '')) }
49
+
50
+ return value.to_s unless value.class.respond_to?(:primary_key)
51
+
52
+ primary_key = value.class.primary_key
53
+
54
+ value[primary_key].to_s
55
+ end
56
+
57
+ it 'should define the helper method' do
58
+ expect(subject)
59
+ .to respond_to(method_name)
60
+ .with(0).arguments
61
+ .and_any_keywords
62
+ end
63
+
64
+ if expected_wildcards.empty?
65
+ it { expect(subject.send(method_name)).to be == expected }
66
+ else
67
+ it 'should raise an exception' do
68
+ expect { subject.send(method_name) }
69
+ .to raise_error(error_class, error_message)
70
+ end
71
+
72
+ expected_wildcards.each do |key|
73
+ describe "with wildcards: missing #{key}" do
74
+ let(:wildcards) do
75
+ wildcard = key[1..]
76
+
77
+ super().except(wildcard, wildcard[...-3])
78
+ end
79
+ let(:error_message) { "missing wildcard #{key}" }
80
+
81
+ it 'should raise an exception' do
82
+ expect { subject.send(method_name, **wildcards) }
83
+ .to raise_error(error_class, error_message)
84
+ end
85
+ end
86
+
87
+ context "when the routes defines wildcards: missing #{key}" do
88
+ let(:wildcards) do
89
+ wildcard = key[1..]
90
+
91
+ super().except(wildcard, wildcard[...-3])
92
+ end
93
+ let(:error_message) { "missing wildcard #{key}" }
94
+
95
+ it 'should raise an exception' do
96
+ expect { subject.with_wildcards(wildcards).send(method_name) }
97
+ .to raise_error(error_class, error_message)
98
+ end
99
+ end
100
+ end
101
+
102
+ describe 'with wildcards: matching wildcards' do
103
+ it 'should generate the path' do
104
+ expect(subject.send(method_name, **wildcards)).to be == expected
105
+ end
106
+ end
107
+
108
+ context 'when the routes defines wildcards: matching wildcards' do
109
+ it 'should generate the path' do
110
+ expect(subject.with_wildcards(wildcards).send(method_name))
111
+ .to be == expected
112
+ end
113
+
114
+ describe 'with wildcards: value' do
115
+ let(:other_wildcards) do
116
+ expected_wildcards.each.with_index.to_h
117
+ end
118
+
119
+ it 'should generate the path' do
120
+ expect(
121
+ subject
122
+ .with_wildcards(other_wildcards)
123
+ .send(method_name, **wildcards)
124
+ )
125
+ .to be == expected
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ describe 'with wildcards: extra wildcards' do
132
+ let(:wildcards) { super().merge('other_id' => 'value') }
133
+
134
+ it 'should generate the path' do
135
+ expect(subject.send(method_name, **wildcards)).to be == expected
136
+ end
137
+ end
138
+
139
+ context 'when the routes defines wildcards: extra wildcards' do
140
+ let(:wildcards) { super().merge('other_id' => 'value') }
141
+
142
+ it 'should generate the path' do
143
+ expect(subject.with_wildcards(wildcards).send(method_name))
144
+ .to be == expected
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ # Contract asserting that the given member route helper is defined.
152
+ module ShouldDefineMemberRouteContract
153
+ extend RSpec::SleepingKingStudios::Contract
154
+
155
+ # @!method apply(example_group, constructor_keywords: [])
156
+ # Adds the contract to the example group.
157
+ #
158
+ # @param example_group [RSpec::Core::ExampleGroup] the example group to
159
+ # which the contract is applied.
160
+ # @param action_name [String, Symbol] the name of the route action.
161
+ # @param path [String] the expected path for the route helper.
162
+ # @param options [Hash] additional options for the contract.
163
+ #
164
+ # @option options wildcards [Hash{String=>String}] the required
165
+ # wildcards for generating the route.
166
+ contract do |action_name:, path:, **options|
167
+ options[:wildcards] = options.fetch(:wildcards, {}).stringify_keys
168
+ expected_wildcards =
169
+ path.split('/').select { |str| str.start_with?(':') && str != ':id' }
170
+
171
+ describe "##{action_name}_path" do
172
+ let(:configured_wildcards) { options[:wildcards] }
173
+ let(:method_name) { "#{action_name}_path" }
174
+ let(:wildcards) { options[:wildcards] }
175
+ let(:entity_value) { options[:wildcards].fetch('id') }
176
+ let(:expected) do
177
+ [':id', *expected_wildcards].reduce(path) do |resolved, key|
178
+ resolved.sub(key, resolve_wildcard(key))
179
+ end
180
+ end
181
+ let(:error_class) do
182
+ Cuprum::Rails::Routes::MissingWildcardError
183
+ end
184
+ let(:error_message) do
185
+ "missing wildcard #{expected_wildcards.first}"
186
+ end
187
+
188
+ def resolve_wildcard(key)
189
+ key = key.sub(/\A:/, '')
190
+ value =
191
+ configured_wildcards
192
+ .stringify_keys
193
+ .fetch(key) { wildcards.fetch(key.sub(/_id\z/, '')) }
194
+
195
+ return value.to_s unless value.class.respond_to?(:primary_key)
196
+
197
+ primary_key = value.class.primary_key
198
+
199
+ value[primary_key].to_s
200
+ end
201
+
202
+ it 'should define the helper method' do
203
+ expect(subject)
204
+ .to respond_to(method_name)
205
+ .with(0..1).arguments
206
+ .and_any_keywords
207
+ end
208
+
209
+ if expected_wildcards.empty?
210
+ it 'should raise an exception' do
211
+ expect { subject.send(method_name) }
212
+ .to raise_error(error_class, 'missing wildcard :id')
213
+ end
214
+
215
+ describe 'with entity: value' do
216
+ it 'should generate the path' do
217
+ expect(subject.send(method_name, entity_value))
218
+ .to be == expected
219
+ end
220
+ end
221
+
222
+ describe 'with wildcards: matching wildcards' do
223
+ it 'should generate the path' do
224
+ expect(subject.send(method_name, **wildcards))
225
+ .to be == expected
226
+ end
227
+
228
+ describe 'with entity: value' do
229
+ let(:other_wildcards) do
230
+ [':id', expected_wildcards].each.with_index.to_h
231
+ end
232
+
233
+ it 'should generate the path' do
234
+ expect(
235
+ subject.send(method_name, entity_value, **other_wildcards)
236
+ )
237
+ .to be == expected
238
+ end
239
+ end
240
+ end
241
+
242
+ context 'when the routes defines wildcards: matching wildcards' do
243
+ it 'should generate the path' do
244
+ expect(subject.with_wildcards(**wildcards).send(method_name))
245
+ .to be == expected
246
+ end
247
+
248
+ describe 'with entity: value' do
249
+ let(:other_wildcards) do
250
+ [':id', expected_wildcards].each.with_index.to_h
251
+ end
252
+
253
+ it 'should generate the path' do
254
+ expect(
255
+ subject
256
+ .with_wildcards(**other_wildcards)
257
+ .send(method_name, entity_value)
258
+ )
259
+ .to be == expected
260
+ end
261
+ end
262
+ end
263
+ else
264
+ it 'should raise an exception' do
265
+ expect { subject.send(method_name) }
266
+ .to raise_error(error_class, error_message)
267
+ end
268
+
269
+ expected_wildcards.each do |key|
270
+ describe "with wildcards: missing #{key}" do
271
+ let(:wildcards) do
272
+ wildcard = key[1..]
273
+
274
+ super().except(wildcard, wildcard[...-3])
275
+ end
276
+ let(:error_message) { "missing wildcard #{key}" }
277
+
278
+ it 'should raise an exception' do
279
+ expect { subject.send(method_name, **wildcards) }
280
+ .to raise_error(error_class, error_message)
281
+ end
282
+ end
283
+
284
+ context "when the routes defines wildcards: missing #{key}" do
285
+ let(:wildcards) do
286
+ wildcard = key[1..]
287
+
288
+ super().except(wildcard, wildcard[...-3])
289
+ end
290
+ let(:error_message) { "missing wildcard #{key}" }
291
+
292
+ it 'should raise an exception' do
293
+ expect { subject.with_wildcards(wildcards).send(method_name) }
294
+ .to raise_error(error_class, error_message)
295
+ end
296
+ end
297
+ end
298
+
299
+ describe 'with entity: value' do
300
+ it 'should raise an exception' do
301
+ expect { subject.send(method_name) }
302
+ .to raise_error(error_class, error_message)
303
+ end
304
+
305
+ describe 'with wildcards: matching wildcards' do
306
+ let(:wildcards) { super().except('id') }
307
+
308
+ it 'should generate the path' do
309
+ expect(subject.send(method_name, entity_value, **wildcards))
310
+ .to be == expected
311
+ end
312
+ end
313
+
314
+ context 'when the routes defines wildcards: matching wildcards' do
315
+ let(:wildcards) { super().except('id') }
316
+
317
+ it 'should generate the path' do
318
+ expect(
319
+ subject
320
+ .with_wildcards(wildcards)
321
+ .send(method_name, entity_value)
322
+ )
323
+ .to be == expected
324
+ end
325
+ end
326
+ end
327
+
328
+ describe 'with wildcards: matching wildcards' do
329
+ it 'should generate the path' do
330
+ expect(subject.send(method_name, **wildcards))
331
+ .to be == expected
332
+ end
333
+ end
334
+
335
+ context 'when the routes defines wildcards: matching wildcards' do
336
+ it 'should generate the path' do
337
+ expect(subject.with_wildcards(wildcards).send(method_name))
338
+ .to be == expected
339
+ end
340
+ end
341
+ end
342
+
343
+ describe 'with wildcards: extra wildcards' do
344
+ let(:wildcards) { super().merge('other_id' => 'value') }
345
+
346
+ it 'should generate the path' do
347
+ expect(subject.send(method_name, **wildcards)).to be == expected
348
+ end
349
+ end
350
+
351
+ context 'when the routes defines wildcards: extra wildcards' do
352
+ let(:wildcards) { super().merge('other_id' => 'value') }
353
+
354
+ it 'should generate the path' do
355
+ expect(subject.with_wildcards(wildcards).send(method_name))
356
+ .to be == expected
357
+ end
358
+ end
359
+ end
360
+ end
361
+ end
362
+ end
363
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/sleeping_king_studios/contract'
4
+
5
+ require 'cuprum/rails/rspec/contracts'
6
+
7
+ module Cuprum::Rails::RSpec::Contracts
8
+ # Namespace for contracts that specify serialization behavior.
9
+ module SerializersContracts
10
+ # Contract specifying that a serializer serializes the expected properties.
11
+ module ShouldSerializeAttributesContract
12
+ extend RSpec::SleepingKingStudios::Contract
13
+
14
+ # @method apply(example_group, *attribute_names, **attribute_values)
15
+ # Adds the contract to the example group.
16
+ #
17
+ # @param example_group [RSpec::Core::ExampleGroup] The example group to
18
+ # which the contract is applied.
19
+ # @param attribute_names [Array] The names of the attributes to
20
+ # serialize. The value for each attribute should match the value of
21
+ # the attribute on the original object.
22
+ # @param attribute_values [Hash] The names and values of attributes to
23
+ # serialize. The value of the serialized attribute should match the
24
+ # given value.
25
+
26
+ contract do |*attribute_names, **attribute_values|
27
+ let(:serializers) do
28
+ return super() if defined?(super())
29
+
30
+ Cuprum::Rails::Serializers::Json.default_serializers
31
+ end
32
+ let(:context) do
33
+ return super() if defined?(super())
34
+
35
+ Cuprum::Rails::Serializers::Context.new(serializers: serializers)
36
+ end
37
+ let(:expected_attributes) do
38
+ tools = SleepingKingStudios::Tools::Toolbelt.instance
39
+
40
+ attribute_names
41
+ .to_h do |attr_name|
42
+ [attr_name, context.serialize(object.send(attr_name))]
43
+ end # rubocop:disable Style/MultilineBlockChain
44
+ .merge(attribute_values)
45
+ .then { |hsh| tools.hash_tools.convert_keys_to_strings(hsh) }
46
+ end
47
+ let(:serialized) { serializer.call(object, context: context) }
48
+
49
+ it 'should serialize the expected attributes' do
50
+ expect(serialized.keys).to contain_exactly(*expected_attributes.keys)
51
+ end
52
+
53
+ attribute_names.each do |attr_name|
54
+ it "should serialize the #{attr_name.inspect} attribute" do
55
+ expect(serialized[attr_name.to_s])
56
+ .to be == expected_attributes[attr_name.to_s]
57
+ end
58
+ end
59
+
60
+ attribute_values.each do |attr_name, attr_value|
61
+ it "should serialize the #{attr_name.inspect} attribute" do
62
+ attr_value = instance_exec(&attr_value) if attr_value.is_a?(Proc)
63
+
64
+ expect(serialized[attr_name.to_s]).to be == attr_value
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/rspec'
4
+
5
+ module Cuprum::Rails::RSpec
6
+ # Namespace for RSpec contract objects.
7
+ module Contracts; end
8
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/rspec/matchers'
4
+
5
+ module Cuprum::Rails::RSpec::Matchers
6
+ # Asserts the actual object is a result object with the specified properties.
7
+ #
8
+ # @see Cuprum::RSpec::BeAResultMatcher.
9
+ class BeAResultMatcher < Cuprum::RSpec::BeAResultMatcher
10
+ # @param expected_class [Class] the expected class of result. Defaults to
11
+ # Cuprum::Result.
12
+ def initialize(expected_class = nil)
13
+ super
14
+
15
+ @expected_metadata = DEFAULT_VALUE
16
+ end
17
+
18
+ # Sets a metadata expectation on the matcher.
19
+ #
20
+ # Calls to #matches? will fail unless the result responds to #metadata? and
21
+ # has the specified metadata.
22
+ def with_metadata(metadata)
23
+ @expected_metadata = metadata
24
+
25
+ self
26
+ end
27
+ alias and_metadata with_metadata
28
+
29
+ private
30
+
31
+ attr_reader :expected_metadata
32
+
33
+ def expected_metadata?
34
+ expected_metadata != DEFAULT_VALUE
35
+ end
36
+
37
+ def expected_properties
38
+ return super unless expected_metadata?
39
+
40
+ super.merge('metadata' => expected_metadata)
41
+ end
42
+
43
+ def metadata_failure_message
44
+ return '' if metadata_matches?
45
+
46
+ unless actual.respond_to?(:metadata)
47
+ return "\n actual does not respond to #metadata"
48
+ end
49
+
50
+ "#{pad_key('expected metadata')}#{inspect_expected(expected_metadata)}" \
51
+ "#{pad_key('actual metadata')}#{result.metadata.inspect}"
52
+ end
53
+
54
+ def metadata_matches?
55
+ return @metadata_matches unless @metadata_matches.nil?
56
+
57
+ return @metadata_matches unless expected_metadata?
58
+
59
+ return @metadata_matches = false unless actual.respond_to?(:metadata)
60
+
61
+ @metadata_matches = compare_items(expected_metadata, result.metadata)
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/rspec'
4
+
5
+ module Cuprum::Rails::RSpec
6
+ # Namespace for custom RSpec matchers.
7
+ module Matchers
8
+ autoload :BeAResultMatcher,
9
+ 'cuprum/rails/rspec/matchers/be_a_result_matcher'
10
+
11
+ # Asserts that the object is a result with status: :failure.
12
+ #
13
+ # @param expected_class [Class] the expected class of result. Defaults to
14
+ # Cuprum::Result.
15
+ #
16
+ # @return [Cuprum::Rails::RSpec::Matchers::BeAResultMatcher] the generated
17
+ # matcher.
18
+ def be_a_failing_result(expected_class = nil)
19
+ be_a_result(expected_class).with_status(:failure)
20
+ end
21
+
22
+ # Asserts that the object is a Cuprum::Result with status: :success.
23
+ #
24
+ # @param expected_class [Class] the expected class of result. Defaults to
25
+ # Cuprum::Result.
26
+ #
27
+ # @return [Cuprum::Rails::RSpec::Matchers::BeAResultMatcher] the generated
28
+ # matcher.
29
+ def be_a_passing_result(expected_class = nil)
30
+ be_a_result(expected_class).with_status(:success).and_error(nil)
31
+ end
32
+
33
+ # Asserts that the object is a Cuprum::Result.
34
+ #
35
+ # @return [Cuprum::Rails::RSpec::Matchers::BeAResultMatcher] the generated
36
+ # matcher.
37
+ def be_a_result(expected_class = nil)
38
+ Cuprum::Rails::RSpec::Matchers::BeAResultMatcher.new(expected_class)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/serializers'
4
+
5
+ module Cuprum::Rails::Serializers
6
+ # Converts objects or data structures based on configured serializers.
7
+ class BaseSerializer
8
+ # Error class used when a serializer calls itself.
9
+ class RecursiveSerializerError < StandardError; end
10
+
11
+ # Error class used when there is no matching serializer for the object.
12
+ class UndefinedSerializerError < StandardError; end
13
+
14
+ # @return [Cuprum::Rails::Serializers::Serializer] a cached instance
15
+ # of the serializer.
16
+ def self.instance
17
+ @instance ||= new
18
+ end
19
+
20
+ # Converts the object to a serialized representation.
21
+ #
22
+ # First, #call finds the best serializer from the :serializers Hash. This is
23
+ # done by walking up the object class's ancestors to find the closest
24
+ # ancestor which is a key in the :serializers Hash. The corresponding value
25
+ # is then called with the object.
26
+ #
27
+ # @param object [Object] The object to serialize.
28
+ # @param context [Cuprum::Rails::Serializers::Context] The serialization
29
+ # context, which includes the configured serializers for attributes or
30
+ # collection items.
31
+ #
32
+ # @return [Object] a serialized representation of the object.
33
+ #
34
+ # @raise RecursiveSerializerError if the serializer would create an infinite
35
+ # loop, e.g. by calling itself.
36
+ # @raise UndefinedSerializerError if there is no matching serializer for
37
+ # the object.
38
+ def call(object, context:)
39
+ handle_recursion!(object, context: context)
40
+
41
+ context.serialize(object)
42
+ end
43
+
44
+ private
45
+
46
+ def allow_recursion?
47
+ false
48
+ end
49
+
50
+ def handle_recursion!(object, context:)
51
+ return if allow_recursion?
52
+
53
+ return unless context.serializer_for(object).instance_of?(self.class)
54
+
55
+ raise RecursiveSerializerError,
56
+ "invalid serializer for #{object.class.name} - recursive calls to " \
57
+ "#{self.class.name}#call"
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/serializers'
4
+
5
+ module Cuprum::Rails::Serializers
6
+ # Encapsulates and applies configuration for performing serialization.
7
+ class Context
8
+ # Error class used when there is no matching serializer for the object.
9
+ class UndefinedSerializerError < StandardError; end
10
+
11
+ # @param serializers [Hash<Class, Object>] The configured serializers for
12
+ # different object types.
13
+ def initialize(serializers:)
14
+ @serializers = serializers
15
+ end
16
+
17
+ # @return [Hash<Class, Object>] The configured serializers for different
18
+ # object types.
19
+ attr_reader :serializers
20
+
21
+ # Finds and calls the configured serializer for the given object.
22
+ #
23
+ # @param object [Object] The object to serialize.
24
+ #
25
+ # @return [Object] the serialized representation of the object.
26
+ #
27
+ # @raise [UndefinedSerializerError] if there is no configured serializer for
28
+ # the object.
29
+ #
30
+ # @see #serializer_for
31
+ def serialize(object)
32
+ serializer_for(object).call(object, context: self)
33
+ end
34
+
35
+ # Finds and initializes the configured serializer for the given object.
36
+ #
37
+ # If the configured serializer is a Class and responds to the .instance
38
+ # class method, then #serializer_for will return the value of .instance. If
39
+ # the configured serializer is a Class but does not respond to .instance, a
40
+ # new instance of the serializer class will be created using .new and
41
+ # returned. If the configured serializer is not a Class, #serializer_for
42
+ # will return the configured serializer.
43
+ #
44
+ # The return value is cached across multiple calls to #serializer_for.
45
+ #
46
+ # @param object [Object] The object to serialize.
47
+ #
48
+ # @return [Object] the serializer instance for that object.
49
+ #
50
+ # @raise [UndefinedSerializerError] if there is no configured serializer for
51
+ # the object.
52
+ def serializer_for(object)
53
+ (@cached_serializers ||= {})[object.class] ||= find_serializer_for(object)
54
+ end
55
+
56
+ private
57
+
58
+ def find_serializer_class_for(object)
59
+ object.class.ancestors.each do |ancestor|
60
+ configured = serializers[ancestor]
61
+
62
+ return configured if configured
63
+ end
64
+
65
+ nil
66
+ end
67
+
68
+ def find_serializer_for(object)
69
+ configured = find_serializer_class_for(object)
70
+
71
+ if configured.nil?
72
+ raise UndefinedSerializerError,
73
+ "no serializer defined for #{object.class.name}",
74
+ caller(1..-1)
75
+ end
76
+
77
+ return configured unless configured.is_a?(Class)
78
+
79
+ return configured.instance if configured.respond_to?(:instance)
80
+
81
+ configured.new
82
+ end
83
+ end
84
+ end