iknow_view_models 2.8.4
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 +7 -0
- data/.circleci/config.yml +115 -0
- data/.gitignore +36 -0
- data/.travis.yml +31 -0
- data/Appraisals +9 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +19 -0
- data/Rakefile +21 -0
- data/appveyor.yml +22 -0
- data/gemfiles/rails_5_2.gemfile +15 -0
- data/gemfiles/rails_6_0_beta.gemfile +15 -0
- data/iknow_view_models.gemspec +49 -0
- data/lib/iknow_view_models.rb +12 -0
- data/lib/iknow_view_models/railtie.rb +8 -0
- data/lib/iknow_view_models/version.rb +5 -0
- data/lib/view_model.rb +333 -0
- data/lib/view_model/access_control.rb +154 -0
- data/lib/view_model/access_control/composed.rb +216 -0
- data/lib/view_model/access_control/open.rb +13 -0
- data/lib/view_model/access_control/read_only.rb +13 -0
- data/lib/view_model/access_control/tree.rb +264 -0
- data/lib/view_model/access_control_error.rb +10 -0
- data/lib/view_model/active_record.rb +383 -0
- data/lib/view_model/active_record/association_data.rb +178 -0
- data/lib/view_model/active_record/association_manipulation.rb +389 -0
- data/lib/view_model/active_record/cache.rb +265 -0
- data/lib/view_model/active_record/cache/cacheable_view.rb +51 -0
- data/lib/view_model/active_record/cloner.rb +113 -0
- data/lib/view_model/active_record/collection_nested_controller.rb +100 -0
- data/lib/view_model/active_record/controller.rb +77 -0
- data/lib/view_model/active_record/controller_base.rb +185 -0
- data/lib/view_model/active_record/nested_controller_base.rb +93 -0
- data/lib/view_model/active_record/singular_nested_controller.rb +34 -0
- data/lib/view_model/active_record/update_context.rb +252 -0
- data/lib/view_model/active_record/update_data.rb +749 -0
- data/lib/view_model/active_record/update_operation.rb +810 -0
- data/lib/view_model/active_record/visitor.rb +77 -0
- data/lib/view_model/after_transaction_runner.rb +29 -0
- data/lib/view_model/callbacks.rb +219 -0
- data/lib/view_model/changes.rb +62 -0
- data/lib/view_model/config.rb +29 -0
- data/lib/view_model/controller.rb +142 -0
- data/lib/view_model/deserialization_error.rb +437 -0
- data/lib/view_model/deserialize_context.rb +16 -0
- data/lib/view_model/error.rb +191 -0
- data/lib/view_model/error_view.rb +35 -0
- data/lib/view_model/record.rb +367 -0
- data/lib/view_model/record/attribute_data.rb +48 -0
- data/lib/view_model/reference.rb +31 -0
- data/lib/view_model/references.rb +48 -0
- data/lib/view_model/registry.rb +73 -0
- data/lib/view_model/schemas.rb +45 -0
- data/lib/view_model/serialization_error.rb +10 -0
- data/lib/view_model/serialize_context.rb +118 -0
- data/lib/view_model/test_helpers.rb +103 -0
- data/lib/view_model/test_helpers/arvm_builder.rb +111 -0
- data/lib/view_model/traversal_context.rb +126 -0
- data/lib/view_model/utils.rb +24 -0
- data/lib/view_model/utils/collections.rb +49 -0
- data/test/helpers/arvm_test_models.rb +59 -0
- data/test/helpers/arvm_test_utilities.rb +187 -0
- data/test/helpers/callback_tracer.rb +27 -0
- data/test/helpers/controller_test_helpers.rb +270 -0
- data/test/helpers/match_enumerator.rb +58 -0
- data/test/helpers/query_logging.rb +71 -0
- data/test/helpers/test_access_control.rb +56 -0
- data/test/helpers/viewmodel_spec_helpers.rb +326 -0
- data/test/unit/view_model/access_control_test.rb +769 -0
- data/test/unit/view_model/active_record/alias_test.rb +35 -0
- data/test/unit/view_model/active_record/belongs_to_test.rb +376 -0
- data/test/unit/view_model/active_record/cache_test.rb +351 -0
- data/test/unit/view_model/active_record/cloner_test.rb +313 -0
- data/test/unit/view_model/active_record/controller_test.rb +561 -0
- data/test/unit/view_model/active_record/counter_test.rb +80 -0
- data/test/unit/view_model/active_record/customization_test.rb +388 -0
- data/test/unit/view_model/active_record/has_many_test.rb +957 -0
- data/test/unit/view_model/active_record/has_many_through_poly_test.rb +269 -0
- data/test/unit/view_model/active_record/has_many_through_test.rb +736 -0
- data/test/unit/view_model/active_record/has_one_test.rb +334 -0
- data/test/unit/view_model/active_record/namespacing_test.rb +75 -0
- data/test/unit/view_model/active_record/optional_attribute_view_test.rb +58 -0
- data/test/unit/view_model/active_record/poly_test.rb +320 -0
- data/test/unit/view_model/active_record/shared_test.rb +285 -0
- data/test/unit/view_model/active_record/version_test.rb +121 -0
- data/test/unit/view_model/active_record_test.rb +542 -0
- data/test/unit/view_model/callbacks_test.rb +582 -0
- data/test/unit/view_model/deserialization_error/unique_violation_test.rb +73 -0
- data/test/unit/view_model/record_test.rb +524 -0
- data/test/unit/view_model/traversal_context_test.rb +371 -0
- data/test/unit/view_model_test.rb +62 -0
- metadata +490 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ViewModel
|
|
4
|
+
class DeserializationError < ViewModel::AbstractErrorWithBlame
|
|
5
|
+
status 500
|
|
6
|
+
|
|
7
|
+
def code
|
|
8
|
+
"DeserializationError.#{self.class.name.demodulize}"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
protected
|
|
12
|
+
|
|
13
|
+
def viewmodel_class
|
|
14
|
+
first = nodes.first.viewmodel_class
|
|
15
|
+
unless nodes.all? { |n| n.viewmodel_class == first }
|
|
16
|
+
raise ArgumentError.new("All nodes must be of the same type for #{self.class.name}")
|
|
17
|
+
end
|
|
18
|
+
first
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# A collection of DeserializationErrors
|
|
22
|
+
class Collection < ViewModel::AbstractErrorCollection
|
|
23
|
+
title "Error(s) occurred during deserialization"
|
|
24
|
+
code "DeserializationError.Collection"
|
|
25
|
+
|
|
26
|
+
def detail
|
|
27
|
+
"Error(s) occurred during deserialization: #{cause_details}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The client has provided a syntactically or structurally incoherent
|
|
32
|
+
# request.
|
|
33
|
+
class InvalidRequest < DeserializationError
|
|
34
|
+
# Abstract
|
|
35
|
+
status 400
|
|
36
|
+
title "Invalid request"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# There has been an unexpected internal failure of the ViewModel library.
|
|
40
|
+
class Internal < DeserializationError
|
|
41
|
+
status 500
|
|
42
|
+
attr_reader :detail
|
|
43
|
+
|
|
44
|
+
def initialize(detail, nodes = [])
|
|
45
|
+
@detail = detail
|
|
46
|
+
super(nodes)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class InvalidStructure < InvalidRequest
|
|
51
|
+
attr_reader :detail
|
|
52
|
+
|
|
53
|
+
def initialize(detail, nodes = [])
|
|
54
|
+
@detail = detail
|
|
55
|
+
super(nodes)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class InvalidSyntax < InvalidRequest
|
|
60
|
+
attr_reader :detail
|
|
61
|
+
|
|
62
|
+
def initialize(detail, nodes = [])
|
|
63
|
+
@detail = detail
|
|
64
|
+
super(nodes)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# A view included a invalid shared reference
|
|
69
|
+
class InvalidSharedReference < InvalidRequest
|
|
70
|
+
attr_reader :reference
|
|
71
|
+
|
|
72
|
+
def initialize(reference, node)
|
|
73
|
+
@reference = reference
|
|
74
|
+
super([node])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def detail
|
|
78
|
+
"Could not find shared reference with key '#{reference}'"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def meta
|
|
82
|
+
super.merge(reference: reference)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# A view was of an unknown type
|
|
87
|
+
class UnknownView < InvalidRequest
|
|
88
|
+
attr_reader :type
|
|
89
|
+
|
|
90
|
+
def initialize(type)
|
|
91
|
+
@type = type
|
|
92
|
+
super([])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def detail
|
|
96
|
+
"ViewModel class for view name '#{type}' could not be found"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def meta
|
|
100
|
+
super.merge(type: type)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# A view included an unknown attribute
|
|
105
|
+
class UnknownAttribute < InvalidRequest
|
|
106
|
+
attr_reader :attribute
|
|
107
|
+
|
|
108
|
+
def initialize(attribute, node)
|
|
109
|
+
@attribute = attribute
|
|
110
|
+
super([node])
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def detail
|
|
114
|
+
"Unknown attribute/association #{attribute} in viewmodel '#{viewmodel_class.view_name}'"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def meta
|
|
118
|
+
super.merge(attribute: attribute)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# A view included an unexpected schema version for the corresponding
|
|
123
|
+
# viewmodel.
|
|
124
|
+
class SchemaVersionMismatch < InvalidRequest
|
|
125
|
+
attr_reader :viewmodel_class, :schema_version
|
|
126
|
+
|
|
127
|
+
def initialize(viewmodel_class, schema_version, nodes)
|
|
128
|
+
@viewmodel_class = viewmodel_class
|
|
129
|
+
@schema_version = schema_version
|
|
130
|
+
super(nodes)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def detail
|
|
134
|
+
"Mismatched schema version for type #{viewmodel_class.view_name}, "\
|
|
135
|
+
"expected #{viewmodel_class.schema_version}, received #{schema_version}."
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def meta
|
|
139
|
+
super.merge(expected: viewmodel_class.schema_version,
|
|
140
|
+
received: schema_version)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# The target of an association was not a valid view type for that
|
|
145
|
+
# association.
|
|
146
|
+
class InvalidAssociationType < InvalidRequest
|
|
147
|
+
attr_reader :association, :target_type
|
|
148
|
+
def initialize(association, target_type, node)
|
|
149
|
+
@association = association
|
|
150
|
+
@target_type = target_type
|
|
151
|
+
super([node])
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def detail
|
|
155
|
+
"Invalid target viewmodel type '#{target_type}' for association '#{association}'"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def meta
|
|
159
|
+
super.merge(association: association,
|
|
160
|
+
target_type: target_type)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
class InvalidViewType < InvalidRequest
|
|
165
|
+
attr_reader :expected_type
|
|
166
|
+
|
|
167
|
+
def initialize(expected_type, node)
|
|
168
|
+
@expected_type = expected_type
|
|
169
|
+
super(node)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def detail
|
|
173
|
+
"Cannot deserialize inappropriate view type, expected '#{expected_type}' or an alias"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def meta
|
|
177
|
+
super.merge(expected_type: expected_type)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Attempted to load persisted viewmodels by id, but they were not available
|
|
182
|
+
class NotFound < DeserializationError
|
|
183
|
+
status 404
|
|
184
|
+
|
|
185
|
+
def detail
|
|
186
|
+
model_ids = nodes.map(&:model_id)
|
|
187
|
+
"Couldn't find #{viewmodel_class.view_name}(s) with id(s)=#{model_ids.inspect}"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
class AssociatedNotFound < NotFound
|
|
192
|
+
attr_reader :missing_nodes, :association
|
|
193
|
+
|
|
194
|
+
def initialize(association, missing_nodes, blame_nodes)
|
|
195
|
+
@association = association
|
|
196
|
+
@missing_nodes = Array.wrap(missing_nodes)
|
|
197
|
+
super(blame_nodes)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def detail
|
|
201
|
+
errors = missing_nodes.map(&:to_s).join(", ")
|
|
202
|
+
"Couldn't find requested member node(s) in association '#{association}': "\
|
|
203
|
+
"#{errors}"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def meta
|
|
207
|
+
super.merge(association: association,
|
|
208
|
+
missing_nodes: format_references(missing_nodes))
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
class DuplicateNodes < InvalidRequest
|
|
213
|
+
attr_reader :type
|
|
214
|
+
|
|
215
|
+
def initialize(type, nodes)
|
|
216
|
+
@type = type
|
|
217
|
+
super(nodes)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def detail
|
|
221
|
+
"Duplicate views for the same '#{type}' specified: "+ nodes.map(&:to_s).join(", ")
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def meta
|
|
225
|
+
super.merge(type: type)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
class ParentNotFound < NotFound
|
|
230
|
+
def detail
|
|
231
|
+
"Could not resolve release from previous parent for the following owned viewmodel(s): " +
|
|
232
|
+
nodes.map(&:to_s).join(", ")
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
class ReadOnlyAttribute < DeserializationError
|
|
237
|
+
status 400
|
|
238
|
+
attr_reader :attribute
|
|
239
|
+
|
|
240
|
+
def initialize(attribute, node)
|
|
241
|
+
@attribute = attribute
|
|
242
|
+
super([node])
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def detail
|
|
246
|
+
"Cannot edit read only attribute '#{attribute}'"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def meta
|
|
250
|
+
super.merge(attribute: attribute)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
class ReadOnlyType < DeserializationError
|
|
255
|
+
status 400
|
|
256
|
+
detail "Deserialization not defined for view type"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
class InvalidAttributeType < InvalidRequest
|
|
260
|
+
attr_reader :attribute, :expected_type, :provided_type
|
|
261
|
+
|
|
262
|
+
def initialize(attribute, expected_type, provided_type, node)
|
|
263
|
+
@attribute = attribute
|
|
264
|
+
@expected_type = expected_type
|
|
265
|
+
@provided_type = provided_type
|
|
266
|
+
super([node])
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def detail
|
|
270
|
+
"Expected '#{attribute}' to be of type '#{expected_type}', was '#{provided_type}'"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def meta
|
|
274
|
+
super.merge(attribute: attribute,
|
|
275
|
+
expected_type: expected_type,
|
|
276
|
+
provided_type: provided_type)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
class InvalidParentEdit < DeserializationError
|
|
281
|
+
def initialize(changes, node)
|
|
282
|
+
@changes = changes
|
|
283
|
+
super([node])
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
detail 'Illegal edit to parent during external association update'
|
|
287
|
+
|
|
288
|
+
def meta
|
|
289
|
+
super.merge(changes: @changes.to_h)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Optimistic lock failure updating nodes
|
|
294
|
+
class LockFailure < DeserializationError
|
|
295
|
+
status 400
|
|
296
|
+
|
|
297
|
+
def detail
|
|
298
|
+
errors = nodes.map(&:to_s).join(", ")
|
|
299
|
+
"Optimistic lock failure updating nodes: #{errors}"
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
class DatabaseConstraint < DeserializationError
|
|
304
|
+
status 400
|
|
305
|
+
attr_reader :detail
|
|
306
|
+
|
|
307
|
+
def initialize(detail, nodes = [])
|
|
308
|
+
@detail = detail
|
|
309
|
+
super(nodes)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Database constraint errors are pretty opaque and stringly typed. We can
|
|
313
|
+
# do our best to parse out what metadata we can from the error, and fall
|
|
314
|
+
# back when we can't.
|
|
315
|
+
def self.from_exception(exception, nodes = [])
|
|
316
|
+
case exception.cause
|
|
317
|
+
when PG::UniqueViolation
|
|
318
|
+
UniqueViolation.from_postgres_error(exception.cause, nodes)
|
|
319
|
+
else
|
|
320
|
+
self.new(exception.message, nodes)
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
class UniqueViolation < DeserializationError
|
|
326
|
+
status 400
|
|
327
|
+
attr_reader :detail, :constraint, :columns, :values
|
|
328
|
+
|
|
329
|
+
PG_ERROR_FIELD_CONSTRAINT_NAME = 'n'.ord # Not exposed in pg gem
|
|
330
|
+
def self.from_postgres_error(err, nodes)
|
|
331
|
+
result = err.result
|
|
332
|
+
constraint = result.error_field(PG_ERROR_FIELD_CONSTRAINT_NAME)
|
|
333
|
+
message_detail = result.error_field(PG::Result::PG_DIAG_MESSAGE_DETAIL)
|
|
334
|
+
|
|
335
|
+
columns, values = parse_message_detail(message_detail)
|
|
336
|
+
|
|
337
|
+
unless columns
|
|
338
|
+
# Couldn't parse the detail message, fall back on an unparsed error
|
|
339
|
+
return DatabaseConstraint.new(err.message, nodes)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
self.new(err.message, constraint, columns, values, nodes)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
class << self
|
|
346
|
+
DETAIL_PREFIX = 'Key ('
|
|
347
|
+
DETAIL_SUFFIX = ') already exists.'
|
|
348
|
+
DETAIL_INFIX = ')=('
|
|
349
|
+
def parse_message_detail(detail)
|
|
350
|
+
stream = detail.dup
|
|
351
|
+
|
|
352
|
+
return nil unless stream.delete_prefix!(DETAIL_PREFIX)
|
|
353
|
+
return nil unless stream.delete_suffix!(DETAIL_SUFFIX)
|
|
354
|
+
|
|
355
|
+
# The message should start with an identifier list: pop off identifier
|
|
356
|
+
# tokens while we can.
|
|
357
|
+
identifiers = []
|
|
358
|
+
|
|
359
|
+
identifier = parse_identifier(stream)
|
|
360
|
+
return nil unless identifier
|
|
361
|
+
|
|
362
|
+
identifiers << identifier
|
|
363
|
+
|
|
364
|
+
while stream.delete_prefix!(', ')
|
|
365
|
+
identifier = parse_identifier(stream)
|
|
366
|
+
return nil unless identifier
|
|
367
|
+
|
|
368
|
+
identifiers << identifier
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# The message should now contain ")=(" followed by the (unparseable)
|
|
372
|
+
# value list.
|
|
373
|
+
return nil unless stream.delete_prefix!(DETAIL_INFIX)
|
|
374
|
+
|
|
375
|
+
[identifiers, stream]
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
private
|
|
379
|
+
|
|
380
|
+
QUOTED_IDENTIFIER = /\A"(?:[^"]|"")+"/
|
|
381
|
+
UNQUOTED_IDENTIFIER = /\A(?:\p{Alpha}|_)(?:\p{Alnum}|_)*/
|
|
382
|
+
def parse_identifier(stream)
|
|
383
|
+
if (identifier = stream.slice!(UNQUOTED_IDENTIFIER))
|
|
384
|
+
identifier
|
|
385
|
+
elsif (quoted_identifier = stream.slice!(QUOTED_IDENTIFIER))
|
|
386
|
+
quoted_identifier[1..-2].gsub('""', '"')
|
|
387
|
+
else
|
|
388
|
+
nil
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def initialize(detail, constraint, columns, values, nodes = [])
|
|
394
|
+
@detail = detail
|
|
395
|
+
@constraint = constraint
|
|
396
|
+
@columns = columns
|
|
397
|
+
@values = values
|
|
398
|
+
super(nodes)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def meta
|
|
402
|
+
super.merge(constraint: @constraint, columns: @columns, values: @values)
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
class Validation < DeserializationError
|
|
407
|
+
status 400
|
|
408
|
+
attr_reader :attribute, :reason, :details
|
|
409
|
+
|
|
410
|
+
def initialize(attribute, reason, details, node)
|
|
411
|
+
@attribute = attribute
|
|
412
|
+
@reason = reason
|
|
413
|
+
@details = details
|
|
414
|
+
super([node])
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def detail
|
|
418
|
+
"Validation failed: '#{attribute}' #{reason}"
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def meta
|
|
422
|
+
super.merge(attribute: attribute, message: reason, details: details)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Return Validation errors for each error in the the provided
|
|
426
|
+
# ActiveModel::Errors, wrapped in a Collection if necessary.
|
|
427
|
+
def self.from_active_model(errors, node)
|
|
428
|
+
causes = errors.messages.each_key.flat_map do |attr|
|
|
429
|
+
errors.messages[attr].zip(errors.details[attr]).map do |message, details|
|
|
430
|
+
self.new(attr.to_s, message, details, node)
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
Collection.for_errors(causes)
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'view_model/traversal_context'
|
|
2
|
+
|
|
3
|
+
class ViewModel::DeserializeContext < ViewModel::TraversalContext
|
|
4
|
+
class SharedContext < ViewModel::TraversalContext::SharedContext
|
|
5
|
+
# During deserialization, collects a tree of viewmodel association names that
|
|
6
|
+
# were updated. Used to ensure that updated associations are always included
|
|
7
|
+
# in response serialization after deserialization, even if hidden by default.
|
|
8
|
+
attr_accessor :updated_associations
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.shared_context_class
|
|
12
|
+
SharedContext
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
delegate :updated_associations, :"updated_associations=", to: :shared_context
|
|
16
|
+
end
|