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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +145 -0
- data/DEVELOPMENT.md +20 -0
- data/README.md +356 -63
- data/lib/cuprum/rails/action.rb +32 -16
- data/lib/cuprum/rails/actions/create.rb +62 -15
- data/lib/cuprum/rails/actions/destroy.rb +23 -7
- data/lib/cuprum/rails/actions/edit.rb +23 -7
- data/lib/cuprum/rails/actions/index.rb +30 -10
- data/lib/cuprum/rails/actions/middleware/associations/cache.rb +112 -0
- data/lib/cuprum/rails/actions/middleware/associations/find.rb +23 -0
- data/lib/cuprum/rails/actions/middleware/associations/parent.rb +70 -0
- data/lib/cuprum/rails/actions/middleware/associations/query.rb +140 -0
- data/lib/cuprum/rails/actions/middleware/associations.rb +12 -0
- data/lib/cuprum/rails/actions/middleware/log_request.rb +126 -0
- data/lib/cuprum/rails/actions/middleware/log_result.rb +51 -0
- data/lib/cuprum/rails/actions/middleware/resources/find.rb +44 -0
- data/lib/cuprum/rails/actions/middleware/resources/query.rb +91 -0
- data/lib/cuprum/rails/actions/middleware/resources.rb +11 -0
- data/lib/cuprum/rails/actions/middleware.rb +13 -0
- data/lib/cuprum/rails/actions/new.rb +16 -4
- data/lib/cuprum/rails/actions/parameter_validation.rb +60 -0
- data/lib/cuprum/rails/actions/resource_action.rb +119 -42
- data/lib/cuprum/rails/actions/show.rb +23 -7
- data/lib/cuprum/rails/actions/update.rb +70 -22
- data/lib/cuprum/rails/actions.rb +11 -7
- data/lib/cuprum/rails/collection.rb +27 -47
- data/lib/cuprum/rails/command.rb +3 -1
- data/lib/cuprum/rails/commands/destroy_one.rb +10 -6
- data/lib/cuprum/rails/commands/find_many.rb +8 -1
- data/lib/cuprum/rails/commands/find_matching.rb +1 -1
- data/lib/cuprum/rails/commands/find_one.rb +8 -0
- data/lib/cuprum/rails/commands/insert_one.rb +17 -6
- data/lib/cuprum/rails/commands/update_one.rb +16 -5
- data/lib/cuprum/rails/constraints/parameters_contract.rb +14 -0
- data/lib/cuprum/rails/constraints.rb +10 -0
- data/lib/cuprum/rails/controller.rb +12 -2
- data/lib/cuprum/rails/controllers/action.rb +100 -0
- data/lib/cuprum/rails/controllers/class_methods/actions.rb +33 -7
- data/lib/cuprum/rails/controllers/class_methods/configuration.rb +36 -0
- data/lib/cuprum/rails/controllers/class_methods/middleware.rb +88 -0
- data/lib/cuprum/rails/controllers/class_methods/validations.rb +2 -2
- data/lib/cuprum/rails/controllers/configuration.rb +41 -1
- data/lib/cuprum/rails/controllers/middleware.rb +59 -0
- data/lib/cuprum/rails/controllers.rb +2 -0
- data/lib/cuprum/rails/errors/invalid_parameters.rb +55 -0
- data/lib/cuprum/rails/errors/invalid_statement.rb +11 -0
- data/lib/cuprum/rails/errors/missing_parameter.rb +42 -0
- data/lib/cuprum/rails/errors/resource_error.rb +46 -0
- data/lib/cuprum/rails/errors.rb +6 -1
- data/lib/cuprum/rails/map_errors.rb +29 -1
- data/lib/cuprum/rails/query.rb +1 -1
- data/lib/cuprum/rails/repository.rb +12 -25
- data/lib/cuprum/rails/request.rb +149 -60
- data/lib/cuprum/rails/resource.rb +119 -85
- data/lib/cuprum/rails/responders/base_responder.rb +78 -0
- data/lib/cuprum/rails/responders/html/plural_resource.rb +9 -39
- data/lib/cuprum/rails/responders/html/rendering.rb +81 -0
- data/lib/cuprum/rails/responders/html/resource.rb +107 -0
- data/lib/cuprum/rails/responders/html/singular_resource.rb +9 -38
- data/lib/cuprum/rails/responders/html.rb +2 -0
- data/lib/cuprum/rails/responders/html_responder.rb +8 -52
- data/lib/cuprum/rails/responders/json/resource.rb +3 -3
- data/lib/cuprum/rails/responders/json_responder.rb +31 -16
- data/lib/cuprum/rails/responders/matching.rb +29 -27
- data/lib/cuprum/rails/responders/serialization.rb +11 -9
- data/lib/cuprum/rails/responders.rb +1 -0
- data/lib/cuprum/rails/responses/head_response.rb +24 -0
- data/lib/cuprum/rails/responses/html/redirect_back_response.rb +55 -0
- data/lib/cuprum/rails/responses/html/redirect_response.rb +19 -4
- data/lib/cuprum/rails/responses/html/render_response.rb +17 -5
- data/lib/cuprum/rails/responses/html.rb +6 -2
- data/lib/cuprum/rails/responses.rb +1 -0
- data/lib/cuprum/rails/result.rb +36 -0
- data/lib/cuprum/rails/routes.rb +36 -23
- data/lib/cuprum/rails/rspec/contract_helpers.rb +57 -0
- data/lib/cuprum/rails/rspec/contracts/action_contracts.rb +754 -0
- data/lib/cuprum/rails/rspec/contracts/actions/create_contracts.rb +289 -0
- data/lib/cuprum/rails/rspec/contracts/actions/destroy_contracts.rb +164 -0
- data/lib/cuprum/rails/rspec/contracts/actions/edit_contracts.rb +73 -0
- data/lib/cuprum/rails/rspec/contracts/actions/index_contracts.rb +108 -0
- data/lib/cuprum/rails/rspec/contracts/actions/new_contracts.rb +111 -0
- data/lib/cuprum/rails/rspec/contracts/actions/show_contracts.rb +72 -0
- data/lib/cuprum/rails/rspec/contracts/actions/update_contracts.rb +263 -0
- data/lib/cuprum/rails/rspec/contracts/actions.rb +8 -0
- data/lib/cuprum/rails/rspec/contracts/command_contracts.rb +479 -0
- data/lib/cuprum/rails/rspec/contracts/responder_contracts.rb +232 -0
- data/lib/cuprum/rails/rspec/contracts/routes_contracts.rb +363 -0
- data/lib/cuprum/rails/rspec/contracts/serializers_contracts.rb +70 -0
- data/lib/cuprum/rails/rspec/contracts.rb +8 -0
- data/lib/cuprum/rails/rspec/matchers/be_a_result_matcher.rb +64 -0
- data/lib/cuprum/rails/rspec/matchers.rb +41 -0
- data/lib/cuprum/rails/serializers/base_serializer.rb +60 -0
- data/lib/cuprum/rails/serializers/context.rb +84 -0
- data/lib/cuprum/rails/serializers/json/active_record_serializer.rb +2 -2
- data/lib/cuprum/rails/serializers/json/array_serializer.rb +9 -8
- data/lib/cuprum/rails/serializers/json/attributes_serializer.rb +95 -172
- data/lib/cuprum/rails/serializers/json/error_serializer.rb +2 -2
- data/lib/cuprum/rails/serializers/json/hash_serializer.rb +9 -8
- data/lib/cuprum/rails/serializers/json/identity_serializer.rb +3 -3
- data/lib/cuprum/rails/serializers/json/properties_serializer.rb +252 -0
- data/lib/cuprum/rails/serializers/json.rb +2 -1
- data/lib/cuprum/rails/serializers.rb +3 -1
- data/lib/cuprum/rails/version.rb +1 -1
- data/lib/cuprum/rails.rb +19 -16
- metadata +73 -131
- data/lib/cuprum/rails/controller_action.rb +0 -121
- data/lib/cuprum/rails/errors/missing_parameters.rb +0 -33
- data/lib/cuprum/rails/errors/missing_primary_key.rb +0 -46
- data/lib/cuprum/rails/errors/undefined_permitted_attributes.rb +0 -34
- data/lib/cuprum/rails/rspec/command_contract.rb +0 -460
- data/lib/cuprum/rails/rspec/define_route_contract.rb +0 -84
- 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,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
|