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.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +115 -0
  3. data/.gitignore +36 -0
  4. data/.travis.yml +31 -0
  5. data/Appraisals +9 -0
  6. data/Gemfile +19 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +19 -0
  9. data/Rakefile +21 -0
  10. data/appveyor.yml +22 -0
  11. data/gemfiles/rails_5_2.gemfile +15 -0
  12. data/gemfiles/rails_6_0_beta.gemfile +15 -0
  13. data/iknow_view_models.gemspec +49 -0
  14. data/lib/iknow_view_models.rb +12 -0
  15. data/lib/iknow_view_models/railtie.rb +8 -0
  16. data/lib/iknow_view_models/version.rb +5 -0
  17. data/lib/view_model.rb +333 -0
  18. data/lib/view_model/access_control.rb +154 -0
  19. data/lib/view_model/access_control/composed.rb +216 -0
  20. data/lib/view_model/access_control/open.rb +13 -0
  21. data/lib/view_model/access_control/read_only.rb +13 -0
  22. data/lib/view_model/access_control/tree.rb +264 -0
  23. data/lib/view_model/access_control_error.rb +10 -0
  24. data/lib/view_model/active_record.rb +383 -0
  25. data/lib/view_model/active_record/association_data.rb +178 -0
  26. data/lib/view_model/active_record/association_manipulation.rb +389 -0
  27. data/lib/view_model/active_record/cache.rb +265 -0
  28. data/lib/view_model/active_record/cache/cacheable_view.rb +51 -0
  29. data/lib/view_model/active_record/cloner.rb +113 -0
  30. data/lib/view_model/active_record/collection_nested_controller.rb +100 -0
  31. data/lib/view_model/active_record/controller.rb +77 -0
  32. data/lib/view_model/active_record/controller_base.rb +185 -0
  33. data/lib/view_model/active_record/nested_controller_base.rb +93 -0
  34. data/lib/view_model/active_record/singular_nested_controller.rb +34 -0
  35. data/lib/view_model/active_record/update_context.rb +252 -0
  36. data/lib/view_model/active_record/update_data.rb +749 -0
  37. data/lib/view_model/active_record/update_operation.rb +810 -0
  38. data/lib/view_model/active_record/visitor.rb +77 -0
  39. data/lib/view_model/after_transaction_runner.rb +29 -0
  40. data/lib/view_model/callbacks.rb +219 -0
  41. data/lib/view_model/changes.rb +62 -0
  42. data/lib/view_model/config.rb +29 -0
  43. data/lib/view_model/controller.rb +142 -0
  44. data/lib/view_model/deserialization_error.rb +437 -0
  45. data/lib/view_model/deserialize_context.rb +16 -0
  46. data/lib/view_model/error.rb +191 -0
  47. data/lib/view_model/error_view.rb +35 -0
  48. data/lib/view_model/record.rb +367 -0
  49. data/lib/view_model/record/attribute_data.rb +48 -0
  50. data/lib/view_model/reference.rb +31 -0
  51. data/lib/view_model/references.rb +48 -0
  52. data/lib/view_model/registry.rb +73 -0
  53. data/lib/view_model/schemas.rb +45 -0
  54. data/lib/view_model/serialization_error.rb +10 -0
  55. data/lib/view_model/serialize_context.rb +118 -0
  56. data/lib/view_model/test_helpers.rb +103 -0
  57. data/lib/view_model/test_helpers/arvm_builder.rb +111 -0
  58. data/lib/view_model/traversal_context.rb +126 -0
  59. data/lib/view_model/utils.rb +24 -0
  60. data/lib/view_model/utils/collections.rb +49 -0
  61. data/test/helpers/arvm_test_models.rb +59 -0
  62. data/test/helpers/arvm_test_utilities.rb +187 -0
  63. data/test/helpers/callback_tracer.rb +27 -0
  64. data/test/helpers/controller_test_helpers.rb +270 -0
  65. data/test/helpers/match_enumerator.rb +58 -0
  66. data/test/helpers/query_logging.rb +71 -0
  67. data/test/helpers/test_access_control.rb +56 -0
  68. data/test/helpers/viewmodel_spec_helpers.rb +326 -0
  69. data/test/unit/view_model/access_control_test.rb +769 -0
  70. data/test/unit/view_model/active_record/alias_test.rb +35 -0
  71. data/test/unit/view_model/active_record/belongs_to_test.rb +376 -0
  72. data/test/unit/view_model/active_record/cache_test.rb +351 -0
  73. data/test/unit/view_model/active_record/cloner_test.rb +313 -0
  74. data/test/unit/view_model/active_record/controller_test.rb +561 -0
  75. data/test/unit/view_model/active_record/counter_test.rb +80 -0
  76. data/test/unit/view_model/active_record/customization_test.rb +388 -0
  77. data/test/unit/view_model/active_record/has_many_test.rb +957 -0
  78. data/test/unit/view_model/active_record/has_many_through_poly_test.rb +269 -0
  79. data/test/unit/view_model/active_record/has_many_through_test.rb +736 -0
  80. data/test/unit/view_model/active_record/has_one_test.rb +334 -0
  81. data/test/unit/view_model/active_record/namespacing_test.rb +75 -0
  82. data/test/unit/view_model/active_record/optional_attribute_view_test.rb +58 -0
  83. data/test/unit/view_model/active_record/poly_test.rb +320 -0
  84. data/test/unit/view_model/active_record/shared_test.rb +285 -0
  85. data/test/unit/view_model/active_record/version_test.rb +121 -0
  86. data/test/unit/view_model/active_record_test.rb +542 -0
  87. data/test/unit/view_model/callbacks_test.rb +582 -0
  88. data/test/unit/view_model/deserialization_error/unique_violation_test.rb +73 -0
  89. data/test/unit/view_model/record_test.rb +524 -0
  90. data/test/unit/view_model/traversal_context_test.rb +371 -0
  91. data/test/unit/view_model_test.rb +62 -0
  92. metadata +490 -0
@@ -0,0 +1,191 @@
1
+ # Abstract base for renderable errors in ViewModel-based APIs. Errors of this
2
+ # type will be caught by ViewModel controllers and rendered in a standard format
3
+ # by ViewModel::ErrorView, which loosely follows errors in JSON-API.
4
+ class ViewModel::AbstractError < StandardError
5
+ class << self
6
+ # Brief DSL for quickly defining constant attribute values in subclasses
7
+ [:detail, :status, :title, :code].each do |attribute|
8
+ define_method(attribute) do |x|
9
+ define_method(attribute){ x }
10
+ end
11
+ end
12
+ end
13
+
14
+ def initialize
15
+ # `detail` is used to provide the exception message. However, it's not safe
16
+ # to just override StandardError's `message` or `to_s` to call `detail`,
17
+ # since some of Ruby's C implementation of Exceptions internally ignores
18
+ # these methods and fetches the invisible internal `idMesg` attribute
19
+ # instead. (!)
20
+ #
21
+ # This means that all fields necessary to derive the detail message must be
22
+ # initialized before calling super().
23
+ super(detail)
24
+ end
25
+
26
+ # Human-readable reason for use displaying this error.
27
+ def detail
28
+ "ViewModel::AbstractError"
29
+ end
30
+
31
+ # HTTP status code most appropriate for this error
32
+ def status
33
+ 500
34
+ end
35
+
36
+ # Human-readable title for displaying this error
37
+ def title
38
+ nil
39
+ end
40
+
41
+ # Unique symbol identifying this error type
42
+ def code
43
+ "ViewModel.AbstractError"
44
+ end
45
+
46
+ # Additional information specific to this error type.
47
+ def meta
48
+ {}
49
+ end
50
+
51
+ # Some types of error may be aggregations over multiple causes
52
+ def aggregation?
53
+ false
54
+ end
55
+
56
+ # If so, the causes of this error (as AbstractErrors)
57
+ def causes
58
+ nil
59
+ end
60
+
61
+ # The exception responsible for this error. In most cases, that should be this
62
+ # object, but sometimes an Error may be used to wrap an external exception.
63
+ def exception
64
+ self
65
+ end
66
+
67
+ def view
68
+ ViewModel::ErrorView.new(self)
69
+ end
70
+
71
+ def to_s
72
+ detail
73
+ end
74
+
75
+ protected
76
+
77
+
78
+
79
+ def format_references(viewmodel_refs)
80
+ viewmodel_refs.map do |viewmodel_ref|
81
+ format_reference(viewmodel_ref)
82
+ end
83
+ end
84
+
85
+ def format_reference(viewmodel_ref)
86
+ {
87
+ ViewModel::TYPE_ATTRIBUTE => viewmodel_ref.viewmodel_class.view_name,
88
+ ViewModel::ID_ATTRIBUTE => viewmodel_ref.model_id
89
+ }
90
+ end
91
+ end
92
+
93
+ # For errors associated with specific viewmodel nodes, include metadata
94
+ # describing the node to blame.
95
+ class ViewModel::AbstractErrorWithBlame < ViewModel::AbstractError
96
+ attr_reader :nodes
97
+
98
+ def initialize(blame_nodes)
99
+ @nodes = Array.wrap(blame_nodes)
100
+ unless @nodes.all? { |n| n.is_a?(ViewModel::Reference) }
101
+ raise ArgumentError.new("#{self.class.name}: 'blame_nodes' must all be of type ViewModel::Reference")
102
+ end
103
+ super()
104
+ end
105
+
106
+ def meta
107
+ {
108
+ nodes: format_references(nodes)
109
+ }
110
+ end
111
+ end
112
+
113
+ # Abstract collection of errors
114
+ class ViewModel::AbstractErrorCollection < ViewModel::AbstractError
115
+ attr_reader :causes
116
+
117
+ def initialize(causes)
118
+ @causes = Array.wrap(causes)
119
+ unless @causes.present?
120
+ raise ArgumentError.new("A collection must have at least one cause")
121
+ end
122
+ super()
123
+ end
124
+
125
+ def status
126
+ causes.inject(causes.first.status) do |status, cause|
127
+ if status == cause.status
128
+ status
129
+ else
130
+ 400
131
+ end
132
+ end
133
+ end
134
+
135
+ def detail
136
+ "ViewModel::AbstractErrors: #{cause_details}"
137
+ end
138
+
139
+ def aggregation?
140
+ true
141
+ end
142
+
143
+ def self.for_errors(errors)
144
+ if errors.size == 1
145
+ errors.first
146
+ else
147
+ self.new(errors)
148
+ end
149
+ end
150
+
151
+ protected
152
+
153
+ def cause_details
154
+ causes.map(&:detail).join("; ")
155
+ end
156
+ end
157
+
158
+ # Error type to wrap an arbitrary exception as a renderable ViewModel::AbstractError
159
+ class ViewModel::WrappedExceptionError < ViewModel::AbstractError
160
+ attr_reader :exception, :status
161
+
162
+ def initialize(exception, status, code)
163
+ @exception = exception
164
+ @status = status
165
+ @code = code
166
+ super()
167
+ end
168
+
169
+ def detail
170
+ exception.message
171
+ end
172
+
173
+ def code
174
+ @code || "Exception.#{exception.class.name}"
175
+ end
176
+ end
177
+
178
+ # Implementation of ViewModel::AbstractError with constructor parameters for
179
+ # each error data field.
180
+ class ViewModel::Error < ViewModel::AbstractError
181
+ attr_reader :detail, :status, :title, :code, :meta
182
+
183
+ def initialize(status: 400, detail: "ViewModel Error", title: nil, code: nil, meta: {})
184
+ @detail = detail
185
+ @status = status
186
+ @title = title
187
+ @code = code
188
+ @meta = meta
189
+ super()
190
+ end
191
+ end
@@ -0,0 +1,35 @@
1
+ require 'view_model/record'
2
+
3
+ # ViewModel for rendering ViewModel::AbstractErrors
4
+ class ViewModel::ErrorView < ViewModel::Record
5
+ self.model_class = ViewModel::AbstractError
6
+ self.view_name = 'Error'
7
+
8
+ class ExceptionDetailView < ::ViewModel
9
+ attributes :exception
10
+ def serialize_view(json, serialize_context: nil)
11
+ json.set! :class, exception.class.name
12
+ json.backtrace exception.backtrace
13
+ if cause = exception.cause
14
+ json.cause do
15
+ json.set! :class, cause.class.name
16
+ json.backtrace cause.backtrace
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ attributes :status, :detail, :title, :code, :meta
23
+ attribute :causes, array: true, using: self
24
+ attribute :exception, using: ExceptionDetailView
25
+
26
+ # Ruby exceptions should never be serialized in production
27
+ def serialize_exception(json, serialize_context:)
28
+ super if ViewModel::Config.show_cause_in_error_view
29
+ end
30
+
31
+ # Only serialize causes for aggregation errors.
32
+ def serialize_causes(*)
33
+ super if model.aggregation?
34
+ end
35
+ end
@@ -0,0 +1,367 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Abstract ViewModel type for serializing a subset of attributes from a record.
4
+ # A record viewmodel wraps a single underlying model, exposing a fixed set of
5
+ # real or calculated attributes.
6
+ class ViewModel::Record < ViewModel
7
+ # All ViewModel::Records have the same underlying ViewModel attribute: the
8
+ # record model they back on to. We want this to be inherited by subclasses, so
9
+ # we override ViewModel's :_attributes to close over it.
10
+ attr_accessor :model
11
+
12
+ require 'view_model/record/attribute_data'
13
+
14
+ class << self
15
+ attr_reader :_members
16
+ attr_accessor :abstract_class, :unregistered
17
+
18
+ def inherited(subclass)
19
+ super
20
+ subclass.initialize_as_viewmodel_record
21
+ ViewModel::Registry.register(subclass)
22
+ end
23
+
24
+ def initialize_as_viewmodel_record
25
+ @_members = {}
26
+ @abstract_class = false
27
+ @unregistered = false
28
+
29
+ @generated_accessor_module = Module.new
30
+ include @generated_accessor_module
31
+ end
32
+
33
+ # Should this class be registered in the viewmodel registry
34
+ def should_register?
35
+ !abstract_class && !unregistered
36
+ end
37
+
38
+ # Specifies an attribute from the model to be serialized in this view
39
+ def attribute(attr, as: nil, read_only: false, write_once: false, using: nil, format: nil, array: false, optional: false)
40
+ model_attribute_name = attr.to_s
41
+ vm_attribute_name = (as || attr).to_s
42
+
43
+ if using && format
44
+ raise ArgumentError.new("Only one of :using and :format may be specified")
45
+ end
46
+ if using && !(using.is_a?(Class) && using < ViewModel)
47
+ raise ArgumentError.new("Invalid 'using:' viewmodel: not a viewmodel class")
48
+ end
49
+ if format && !format.respond_to?(:dump) && !format.respond_to?(:load)
50
+ raise ArgumentError.new("Invalid 'format:' serializer: must respond to :dump and :load")
51
+ end
52
+
53
+ attr_data = AttributeData.new(vm_attribute_name, model_attribute_name, using, format,
54
+ array, optional, read_only, write_once)
55
+ _members[vm_attribute_name] = attr_data
56
+
57
+ @generated_accessor_module.module_eval do
58
+ define_method vm_attribute_name do
59
+ _get_attribute(attr_data)
60
+ end
61
+
62
+ define_method "serialize_#{vm_attribute_name}" do |json, serialize_context: self.class.new_serialize_context|
63
+ _serialize_attribute(attr_data, json, serialize_context: serialize_context)
64
+ end
65
+
66
+ define_method "deserialize_#{vm_attribute_name}" do |value, references: {}, deserialize_context: self.class.new_deserialize_context|
67
+ _deserialize_attribute(attr_data, value, references: references, deserialize_context: deserialize_context)
68
+ end
69
+ end
70
+ end
71
+
72
+ def deserialize_from_view(view_hashes, references: {}, deserialize_context: new_deserialize_context)
73
+ ViewModel::Utils.map_one_or_many(view_hashes) do |view_hash|
74
+ view_hash = view_hash.dup
75
+ metadata = ViewModel.extract_viewmodel_metadata(view_hash)
76
+
77
+ unless self.view_name == metadata.view_name || self.view_aliases.include?(metadata.view_name)
78
+ raise ViewModel::DeserializationError::InvalidViewType.new(
79
+ self.view_name,
80
+ ViewModel::Reference.new(self, metadata.id))
81
+ end
82
+
83
+ if metadata.schema_version && !self.accepts_schema_version?(metadata.schema_version)
84
+ raise ViewModel::DeserializationError::SchemaVersionMismatch.new(
85
+ self, version, ViewModel::Reference.new(self, metadata.id))
86
+ end
87
+
88
+ viewmodel = resolve_viewmodel(metadata, view_hash, deserialize_context: deserialize_context)
89
+
90
+ deserialize_members_from_view(viewmodel, view_hash, references: references, deserialize_context: deserialize_context)
91
+
92
+ viewmodel
93
+ end
94
+ end
95
+
96
+ def deserialize_members_from_view(viewmodel, view_hash, references:, deserialize_context:)
97
+ super do |hook_control|
98
+ final_changes = viewmodel.clear_changes!
99
+
100
+ if final_changes.changed?
101
+ deserialize_context.run_callback(ViewModel::Callbacks::Hook::OnChange, viewmodel, changes: final_changes)
102
+ end
103
+
104
+ hook_control.record_changes(final_changes)
105
+ end
106
+ end
107
+
108
+ def resolve_viewmodel(metadata, view_hash, deserialize_context:)
109
+ self.for_new_model
110
+ end
111
+
112
+ def for_new_model(*model_args)
113
+ self.new(model_class.new(*model_args)).tap { |v| v.model_is_new! }
114
+ end
115
+
116
+ # Returns the AR model class wrapped by this viewmodel. If this has not been
117
+ # set via `model_class_name=`, attempt to automatically resolve based on the
118
+ # name of this viewmodel.
119
+ def model_class
120
+ unless instance_variable_defined?(:@model_class)
121
+ # try to auto-detect the model class based on our name
122
+ self.model_class_name =
123
+ ViewModel::Registry.infer_model_class_name(self.view_name)
124
+ end
125
+
126
+ @model_class
127
+ end
128
+
129
+ def member_names
130
+ self._members.keys
131
+ end
132
+
133
+ private
134
+
135
+ # Set the record type to be wrapped by this viewmodel
136
+ def model_class_name=(name)
137
+ name = name.to_s
138
+
139
+ type = name.safe_constantize
140
+
141
+ if type.nil?
142
+ raise ArgumentError.new("Could not find model class with name '#{name}'")
143
+ end
144
+
145
+ self.model_class = type
146
+ end
147
+
148
+ # Set the record type to be wrapped by this viewmodel
149
+ def model_class=(type)
150
+ if instance_variable_defined?(:@model_class)
151
+ raise ArgumentError.new("Model class for ViewModel '#{self.name}' already set")
152
+ end
153
+
154
+ @model_class = type
155
+ end
156
+ end
157
+
158
+ delegate :model_class, to: 'self.class'
159
+
160
+ attr_reader :changed_attributes, :previous_changes
161
+
162
+ def initialize(model)
163
+ unless model.is_a?(model_class)
164
+ raise ArgumentError.new("'#{model.inspect}' is not an instance of #{model_class.name}")
165
+ end
166
+
167
+ self.model = model
168
+
169
+ @new_model = false
170
+ @changed_attributes = []
171
+ @changed_children = false
172
+ end
173
+
174
+ # VM::Record identity matches the identity of its model. If the model has a
175
+ # stable identity, use it, otherwise fall back to its object_id.
176
+ def id
177
+ if stable_id?
178
+ model.id
179
+ else
180
+ model.object_id
181
+ end
182
+ end
183
+
184
+ def stable_id?
185
+ model.respond_to?(:id)
186
+ end
187
+
188
+ def new_model?
189
+ @new_model
190
+ end
191
+
192
+ def changed_children?
193
+ @changed_children
194
+ end
195
+
196
+ def serialize_view(json, serialize_context: self.class.new_serialize_context)
197
+ json.set!(ViewModel::ID_ATTRIBUTE, model.id) if model.respond_to?(:id)
198
+ json.set!(ViewModel::TYPE_ATTRIBUTE, self.view_name)
199
+ json.set!(ViewModel::VERSION_ATTRIBUTE, self.class.schema_version)
200
+
201
+ serialize_members(json, serialize_context: serialize_context)
202
+ end
203
+
204
+ def serialize_members(json, serialize_context:)
205
+ self.class._members.each do |member_name, member_data|
206
+ next unless serialize_context.includes_member?(member_name, !member_data.optional?)
207
+
208
+ self.public_send("serialize_#{member_name}", json, serialize_context: serialize_context)
209
+ end
210
+ end
211
+
212
+ # Check that the model backing this view is consistent, for example by calling
213
+ # AR validations. Default implementation handles ActiveModel::Validations, may
214
+ # be overridden by subclasses for other types of validation. Must raise
215
+ # DeserializationError::Validation if invalid.
216
+ def validate!
217
+ if model_class < ActiveModel::Validations && !model.valid?
218
+ raise ViewModel::DeserializationError::Validation.from_active_model(model.errors, self.blame_reference)
219
+ end
220
+ end
221
+
222
+ def model_is_new!
223
+ @new_model = true
224
+ end
225
+
226
+ def attribute_changed!(attr_name)
227
+ @changed_attributes << attr_name.to_s
228
+ end
229
+
230
+ def children_changed!
231
+ @changed_children = true
232
+ end
233
+
234
+ def changes
235
+ ViewModel::Changes.new(
236
+ new: new_model?,
237
+ changed_attributes: changed_attributes,
238
+ changed_children: changed_children?)
239
+ end
240
+
241
+ def clear_changes!
242
+ @previous_changes = changes
243
+ @new_model = false
244
+ @changed_attributes = []
245
+ @changed_children = false
246
+ previous_changes
247
+ end
248
+
249
+ # Use ActiveRecord style identity for viewmodels. This allows serialization to
250
+ # generate a references section by keying on the viewmodel itself.
251
+ def hash
252
+ [self.class, self.model].hash
253
+ end
254
+
255
+ def ==(other)
256
+ self.class == other.class && self.model == other.model
257
+ end
258
+
259
+ alias eql? ==
260
+
261
+ self.abstract_class = true
262
+
263
+ private
264
+
265
+ def _get_attribute(attr_data)
266
+ value = model.public_send(attr_data.model_attr_name)
267
+
268
+ if attr_data.using_viewmodel? && !value.nil?
269
+ # Where an attribute uses a viewmodel, the associated viewmodel type is
270
+ # significant and may have behaviour: like with VM::ActiveRecord
271
+ # associations it's useful to return the value wrapped in its viewmodel
272
+ # type even when not serializing.
273
+ value = attr_data.map_value(value) do |v|
274
+ attr_data.attribute_viewmodel.new(v)
275
+ end
276
+ end
277
+
278
+ value
279
+ end
280
+
281
+ def _serialize_attribute(attr_data, json, serialize_context:)
282
+ vm_attr_name = attr_data.name
283
+
284
+ value = self.public_send(vm_attr_name)
285
+
286
+ if attr_data.using_serializer? && !value.nil?
287
+ # Where an attribute uses a low level serializer (rather than another
288
+ # viewmodel), it's only desired for converting the value to and from wire
289
+ # format, so conversion is deferred to serialization time.
290
+ value = attr_data.map_value(value) do |v|
291
+ begin
292
+ attr_data.attribute_serializer.dump(v, json: true)
293
+ rescue IknowParams::Serializer::DumpError => ex
294
+ raise ViewModel::SerializationError.new(
295
+ "Could not serialize invalid value '#{vm_attr_name}': #{ex.message}")
296
+ end
297
+ end
298
+ end
299
+
300
+ json.set! vm_attr_name do
301
+ serialize_context = self.context_for_child(vm_attr_name, context: serialize_context) if attr_data.using_viewmodel?
302
+ self.class.serialize(value, json, serialize_context: serialize_context)
303
+ end
304
+ end
305
+
306
+ def _deserialize_attribute(attr_data, serialized_value, references:, deserialize_context:)
307
+ vm_attr_name = attr_data.name
308
+
309
+ if attr_data.array? && !serialized_value.nil?
310
+ expect_type!(vm_attr_name, Array, serialized_value)
311
+ end
312
+
313
+ value =
314
+ case
315
+ when serialized_value.nil?
316
+ serialized_value
317
+ when attr_data.using_viewmodel?
318
+ ctx = self.context_for_child(vm_attr_name, context: deserialize_context)
319
+ attr_data.map_value(serialized_value) do |sv|
320
+ attr_data.attribute_viewmodel.deserialize_from_view(sv, references: references, deserialize_context: ctx)
321
+ end
322
+ when attr_data.using_serializer?
323
+ attr_data.map_value(serialized_value) do |sv|
324
+ begin
325
+ attr_data.attribute_serializer.load(sv)
326
+ rescue IknowParams::Serializer::LoadError => ex
327
+ reason = "could not be deserialized because #{ex.message}"
328
+ raise ViewModel::DeserializationError::Validation.new(vm_attr_name, reason, {}, blame_reference)
329
+ end
330
+ end
331
+ else
332
+ serialized_value
333
+ end
334
+
335
+ # Detect changes with ==. In the case of `using_viewmodel?`, this compares viewmodels or arrays of viewmodels.
336
+ if value != self.public_send(vm_attr_name)
337
+ if attr_data.read_only? && !(attr_data.write_once? && new_model?)
338
+ raise ViewModel::DeserializationError::ReadOnlyAttribute.new(vm_attr_name, blame_reference)
339
+ end
340
+
341
+ attribute_changed!(vm_attr_name)
342
+
343
+ if attr_data.using_viewmodel? && !value.nil?
344
+ # Extract model from target viewmodel(s) to attach to our model
345
+ value = attr_data.map_value(value) { |vm| vm.model }
346
+ end
347
+
348
+ model.public_send("#{attr_data.model_attr_name}=", value)
349
+ end
350
+
351
+ if attr_data.using_viewmodel? &&
352
+ Array.wrap(value).any? { |v| v.respond_to?(:previous_changes) && v.previous_changes.changed_tree? }
353
+ self.children_changed!
354
+ end
355
+ end
356
+
357
+ # Helper for type-checking input in hand-rolled deserialization: raises
358
+ # DeserializationError unless the serialized value is of the provided type.
359
+ def expect_type!(attribute, type, serialized_value)
360
+ unless serialized_value.is_a?(type)
361
+ raise ViewModel::DeserializationError::InvalidAttributeType.new(attribute.to_s,
362
+ type.name,
363
+ serialized_value.class.name,
364
+ blame_reference)
365
+ end
366
+ end
367
+ end