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,810 @@
|
|
|
1
|
+
require "renum"
|
|
2
|
+
|
|
3
|
+
# Partially parsed tree of user-specified update hashes, created during deserialization.
|
|
4
|
+
class ViewModel::ActiveRecord
|
|
5
|
+
using ViewModel::Utils::Collections
|
|
6
|
+
|
|
7
|
+
class UpdateOperation
|
|
8
|
+
# inverse association and record to update a change in parent from a child
|
|
9
|
+
ParentData = Struct.new(:association_reflection, :viewmodel)
|
|
10
|
+
|
|
11
|
+
enum :RunState, [:Pending, :Running, :Run]
|
|
12
|
+
|
|
13
|
+
attr_accessor :viewmodel,
|
|
14
|
+
:update_data,
|
|
15
|
+
:points_to, # AssociationData => UpdateOperation (returns single new viewmodel to update fkey)
|
|
16
|
+
:pointed_to, # AssociationData => UpdateOperation(s) (returns viewmodel(s) with which to update assoc cache)
|
|
17
|
+
:reparent_to, # If node needs to update its pointer to a new parent, ParentData for the parent
|
|
18
|
+
:reposition_to, # if this node participates in a list under its parent, what should its position be?
|
|
19
|
+
:released_children # Set of children that have been released
|
|
20
|
+
|
|
21
|
+
delegate :attributes, to: :update_data
|
|
22
|
+
|
|
23
|
+
def initialize(viewmodel, update_data, reparent_to: nil, reposition_to: nil)
|
|
24
|
+
self.viewmodel = viewmodel
|
|
25
|
+
self.update_data = update_data
|
|
26
|
+
self.points_to = {}
|
|
27
|
+
self.pointed_to = {}
|
|
28
|
+
self.reparent_to = reparent_to
|
|
29
|
+
self.reposition_to = reposition_to
|
|
30
|
+
self.released_children = []
|
|
31
|
+
|
|
32
|
+
@run_state = RunState::Pending
|
|
33
|
+
@changed_associations = []
|
|
34
|
+
@built = false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def viewmodel_reference
|
|
38
|
+
unless viewmodel.model.new_record?
|
|
39
|
+
viewmodel.to_reference
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def deferred?
|
|
44
|
+
viewmodel.nil?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def built?
|
|
48
|
+
@built
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Evaluate a built update tree, applying and saving changes to the models.
|
|
52
|
+
def run!(deserialize_context:)
|
|
53
|
+
raise ViewModel::DeserializationError::Internal.new("Internal error: UpdateOperation run before build") unless built?
|
|
54
|
+
|
|
55
|
+
case @run_state
|
|
56
|
+
when RunState::Running
|
|
57
|
+
raise ViewModel::DeserializationError::Internal.new("Internal error: Cycle found in running UpdateOperation")
|
|
58
|
+
when RunState::Run
|
|
59
|
+
return viewmodel
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
@run_state = RunState::Running
|
|
63
|
+
|
|
64
|
+
model = viewmodel.model
|
|
65
|
+
|
|
66
|
+
debug_name = "#{model.class.name}:#{model.id || '<new>'}"
|
|
67
|
+
debug "-> #{debug_name}: Entering"
|
|
68
|
+
|
|
69
|
+
model.class.transaction do
|
|
70
|
+
# Run context and viewmodel hooks
|
|
71
|
+
ViewModel::Callbacks.wrap_deserialize(viewmodel, deserialize_context: deserialize_context) do |hook_control|
|
|
72
|
+
# update parent association
|
|
73
|
+
if reparent_to.present?
|
|
74
|
+
debug "-> #{debug_name}: Updating parent pointer to '#{reparent_to.viewmodel.class.view_name}:#{reparent_to.viewmodel.id}'"
|
|
75
|
+
association = model.association(reparent_to.association_reflection.name)
|
|
76
|
+
association.writer(reparent_to.viewmodel.model)
|
|
77
|
+
debug "<- #{debug_name}: Updated parent pointer"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# update position
|
|
81
|
+
if reposition_to.present?
|
|
82
|
+
debug "-> #{debug_name}: Updating position to #{reposition_to}"
|
|
83
|
+
viewmodel._list_attribute = reposition_to
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# update user-specified attributes
|
|
87
|
+
valid_members = viewmodel.class._members.keys.map(&:to_s).to_set
|
|
88
|
+
bad_keys = attributes.keys.reject { |k| valid_members.include?(k) }
|
|
89
|
+
if bad_keys.present?
|
|
90
|
+
causes = bad_keys.map { |k| ViewModel::DeserializationError::UnknownAttribute.new(k, blame_reference) }
|
|
91
|
+
raise ViewModel::DeserializationError::Collection.for_errors(causes)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
attributes.each do |attr_name, serialized_value|
|
|
95
|
+
# Note that the VM::AR deserialization tree asserts ownership over any
|
|
96
|
+
# references it's provided, and so they're intentionally not passed on
|
|
97
|
+
# to attribute deserialization for use by their `using:` viewmodels. A
|
|
98
|
+
# (better?) alternative would be to provide them as reference-only
|
|
99
|
+
# hashes, to indicate that no modification can be permitted.
|
|
100
|
+
viewmodel.public_send("deserialize_#{attr_name}", serialized_value,
|
|
101
|
+
references: {},
|
|
102
|
+
deserialize_context: deserialize_context)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Update points-to associations before save
|
|
106
|
+
points_to.each do |association_data, child_operation|
|
|
107
|
+
reflection = association_data.direct_reflection
|
|
108
|
+
debug "-> #{debug_name}: Updating points-to association '#{reflection.name}'"
|
|
109
|
+
|
|
110
|
+
association = model.association(reflection.name)
|
|
111
|
+
new_target =
|
|
112
|
+
if child_operation
|
|
113
|
+
child_ctx = viewmodel.context_for_child(association_data.association_name, context: deserialize_context)
|
|
114
|
+
child_viewmodel = child_operation.run!(deserialize_context: child_ctx)
|
|
115
|
+
if !association_data.shared? && child_viewmodel.previous_changes.changed_tree?
|
|
116
|
+
viewmodel.children_changed!
|
|
117
|
+
end
|
|
118
|
+
child_viewmodel.model
|
|
119
|
+
end
|
|
120
|
+
association.writer(new_target)
|
|
121
|
+
debug "<- #{debug_name}: Updated points-to association '#{reflection.name}'"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# validate
|
|
125
|
+
deserialize_context.run_callback(ViewModel::Callbacks::Hook::BeforeValidate, viewmodel)
|
|
126
|
+
viewmodel.validate!
|
|
127
|
+
|
|
128
|
+
# Save if the model has been altered. Covers not only models with
|
|
129
|
+
# view changes but also lock version assertions.
|
|
130
|
+
if viewmodel.model.changed?
|
|
131
|
+
debug "-> #{debug_name}: Saving"
|
|
132
|
+
begin
|
|
133
|
+
model.save!
|
|
134
|
+
rescue ::ActiveRecord::RecordInvalid => ex
|
|
135
|
+
raise ViewModel::DeserializationError::Validation.from_active_model(ex.errors, blame_reference)
|
|
136
|
+
rescue ::ActiveRecord::StaleObjectError => _ex
|
|
137
|
+
raise ViewModel::DeserializationError::LockFailure.new(blame_reference)
|
|
138
|
+
end
|
|
139
|
+
debug "<- #{debug_name}: Saved"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Update association cache of pointed-from associations after save: the
|
|
143
|
+
# child update will have saved the pointer.
|
|
144
|
+
pointed_to.each do |association_data, child_operation|
|
|
145
|
+
reflection = association_data.direct_reflection
|
|
146
|
+
|
|
147
|
+
debug "-> #{debug_name}: Updating pointed-to association '#{reflection.name}'"
|
|
148
|
+
|
|
149
|
+
association = model.association(reflection.name)
|
|
150
|
+
child_ctx = viewmodel.context_for_child(association_data.association_name, context: deserialize_context)
|
|
151
|
+
|
|
152
|
+
new_target =
|
|
153
|
+
if child_operation
|
|
154
|
+
ViewModel::Utils.map_one_or_many(child_operation) do |op|
|
|
155
|
+
child_viewmodel = op.run!(deserialize_context: child_ctx)
|
|
156
|
+
if !association_data.shared? && child_viewmodel.previous_changes.changed_tree?
|
|
157
|
+
viewmodel.children_changed!
|
|
158
|
+
end
|
|
159
|
+
child_viewmodel.model
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
association.target = new_target
|
|
164
|
+
|
|
165
|
+
debug "<- #{debug_name}: Updated pointed-to association '#{reflection.name}'"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
if self.released_children.present?
|
|
169
|
+
# Released children that were not reclaimed by other parents during the
|
|
170
|
+
# build phase will be deleted: check access control.
|
|
171
|
+
debug "-> #{debug_name}: Checking released children permissions"
|
|
172
|
+
self.released_children.reject(&:claimed?).each do |released_child|
|
|
173
|
+
debug "-> #{debug_name}: Checking #{released_child.viewmodel.to_reference}"
|
|
174
|
+
child_vm = released_child.viewmodel
|
|
175
|
+
child_association_data = released_child.association_data
|
|
176
|
+
child_ctx = viewmodel.context_for_child(child_association_data.association_name, context: deserialize_context)
|
|
177
|
+
|
|
178
|
+
ViewModel::Callbacks.wrap_deserialize(child_vm, deserialize_context: child_ctx) do |child_hook_control|
|
|
179
|
+
changes = ViewModel::Changes.new(deleted: true)
|
|
180
|
+
child_ctx.run_callback(ViewModel::Callbacks::Hook::OnChange,
|
|
181
|
+
child_vm,
|
|
182
|
+
changes: changes)
|
|
183
|
+
child_hook_control.record_changes(changes)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
viewmodel.children_changed! unless child_association_data.shared?
|
|
187
|
+
end
|
|
188
|
+
debug "<- #{debug_name}: Finished checking released children permissions"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
final_changes = viewmodel.clear_changes!
|
|
192
|
+
|
|
193
|
+
if final_changes.changed?
|
|
194
|
+
# Now that the change has been fully attempted, call the OnChange
|
|
195
|
+
# hook if local changes were made
|
|
196
|
+
deserialize_context.run_callback(ViewModel::Callbacks::Hook::OnChange, viewmodel, changes: final_changes)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
hook_control.record_changes(final_changes)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
debug "<- #{debug_name}: Leaving"
|
|
204
|
+
|
|
205
|
+
@run_state = RunState::Run
|
|
206
|
+
viewmodel
|
|
207
|
+
rescue ::ActiveRecord::StatementInvalid, ::ActiveRecord::InvalidForeignKey, ::ActiveRecord::RecordNotSaved => ex
|
|
208
|
+
raise ViewModel::DeserializationError::DatabaseConstraint.from_exception(ex, blame_reference)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Recursively builds UpdateOperations for the associations in our UpdateData
|
|
212
|
+
def build!(update_context)
|
|
213
|
+
raise ViewModel::DeserializationError::Internal.new("Internal error: UpdateOperation cannot build a deferred update") if deferred?
|
|
214
|
+
return self if built?
|
|
215
|
+
|
|
216
|
+
update_data.associations.each do |association_name, association_update_data|
|
|
217
|
+
association_data = self.viewmodel.class._association_data(association_name)
|
|
218
|
+
update =
|
|
219
|
+
if association_data.collection?
|
|
220
|
+
build_updates_for_collection_association(association_data, association_update_data, update_context)
|
|
221
|
+
else
|
|
222
|
+
build_update_for_single_association(association_data, association_update_data, update_context)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
add_update(association_data, update)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
update_data.referenced_associations.each do |association_name, reference_string|
|
|
229
|
+
association_data = self.viewmodel.class._association_data(association_name)
|
|
230
|
+
|
|
231
|
+
update =
|
|
232
|
+
if association_data.through?
|
|
233
|
+
build_updates_for_collection_referenced_association(association_data, reference_string, update_context)
|
|
234
|
+
else
|
|
235
|
+
build_update_for_single_referenced_association(association_data, reference_string, update_context)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
add_update(association_data, update)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
@built = true
|
|
242
|
+
self
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def add_update(association_data, update)
|
|
246
|
+
target =
|
|
247
|
+
case association_data.pointer_location
|
|
248
|
+
when :remote; pointed_to
|
|
249
|
+
when :local; points_to
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
target[association_data] = update
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
private
|
|
256
|
+
|
|
257
|
+
def build_update_for_single_referenced_association(association_data, reference_string, update_context)
|
|
258
|
+
# TODO intern loads for shared items so we only load them once
|
|
259
|
+
model = self.viewmodel.model
|
|
260
|
+
previous_child_viewmodel = model.public_send(association_data.direct_reflection.name).try do |previous_child_model|
|
|
261
|
+
vm_class = association_data.viewmodel_class_for_model!(previous_child_model.class)
|
|
262
|
+
vm_class.new(previous_child_model)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
if reference_string.nil?
|
|
266
|
+
referred_update = nil
|
|
267
|
+
referred_viewmodel = nil
|
|
268
|
+
else
|
|
269
|
+
referred_update = update_context.resolve_reference(reference_string, blame_reference)
|
|
270
|
+
referred_viewmodel = referred_update.viewmodel
|
|
271
|
+
|
|
272
|
+
unless association_data.accepts?(referred_viewmodel.class)
|
|
273
|
+
raise ViewModel::DeserializationError::InvalidAssociationType.new(
|
|
274
|
+
association_data.association_name.to_s,
|
|
275
|
+
referred_viewmodel.class.view_name,
|
|
276
|
+
blame_reference)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
referred_update.build!(update_context)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
if previous_child_viewmodel != referred_viewmodel
|
|
283
|
+
viewmodel.association_changed!(association_data.association_name)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
referred_update
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Resolve or construct viewmodels for incoming update data. Where a child
|
|
290
|
+
# hash references an existing model not currently attached to this parent,
|
|
291
|
+
# it must be found before recursing into that child. If the model is
|
|
292
|
+
# available in released models we can take it and recurse, otherwise we must
|
|
293
|
+
# return a ViewModel::Reference to be added to the worklist for deferred
|
|
294
|
+
# resolution.
|
|
295
|
+
def resolve_child_viewmodels(association_data, update_datas, previous_child_viewmodels, update_context)
|
|
296
|
+
if self.viewmodel.respond_to?(:"resolve_#{association_data.direct_reflection.name}")
|
|
297
|
+
return self.viewmodel.public_send(:"resolve_#{association_data.direct_reflection.name}", update_datas, previous_child_viewmodels)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
previous_child_viewmodels = Array.wrap(previous_child_viewmodels)
|
|
301
|
+
|
|
302
|
+
previous_by_key = previous_child_viewmodels.index_by do |vm|
|
|
303
|
+
vm.to_reference
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
ViewModel::Utils.map_one_or_many(update_datas) do |update_data|
|
|
307
|
+
child_viewmodel_class = update_data.viewmodel_class
|
|
308
|
+
key = ViewModel::Reference.new(child_viewmodel_class, update_data.id)
|
|
309
|
+
|
|
310
|
+
case
|
|
311
|
+
when update_data.new?
|
|
312
|
+
child_viewmodel_class.for_new_model(id: update_data.id)
|
|
313
|
+
when existing_child = previous_by_key[key]
|
|
314
|
+
existing_child
|
|
315
|
+
when taken_child = update_context.try_take_released_viewmodel(key)
|
|
316
|
+
taken_child
|
|
317
|
+
else
|
|
318
|
+
# Refers to child that hasn't yet been seen: create a deferred update.
|
|
319
|
+
key
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def build_update_for_single_association(association_data, association_update_data, update_context)
|
|
325
|
+
model = self.viewmodel.model
|
|
326
|
+
|
|
327
|
+
previous_child_viewmodel = model.public_send(association_data.direct_reflection.name).try do |previous_child_model|
|
|
328
|
+
vm_class = association_data.viewmodel_class_for_model!(previous_child_model.class)
|
|
329
|
+
vm_class.new(previous_child_model)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
if previous_child_viewmodel.present?
|
|
333
|
+
# Clear the cached association so that AR's save behaviour doesn't
|
|
334
|
+
# conflict with our explicit parent updates. If we still have a child
|
|
335
|
+
# after the update, we'll either call `Association#writer` or manually
|
|
336
|
+
# fix the target cache after recursing in run!(). If we don't, we promise
|
|
337
|
+
# that the child will no longer be attached in the database, so the new
|
|
338
|
+
# cached data of nil will be correct.
|
|
339
|
+
clear_association_cache(model, association_data.direct_reflection)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
child_viewmodel =
|
|
343
|
+
if association_update_data.present?
|
|
344
|
+
resolve_child_viewmodels(association_data, association_update_data, previous_child_viewmodel, update_context)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
if previous_child_viewmodel != child_viewmodel
|
|
348
|
+
viewmodel.association_changed!(association_data.association_name)
|
|
349
|
+
# free previous child if present
|
|
350
|
+
if previous_child_viewmodel.present?
|
|
351
|
+
if association_data.pointer_location == :local
|
|
352
|
+
# When we free a child that's pointed to from its old parent, we need to
|
|
353
|
+
# clear the cached association to that old parent. If we don't do this,
|
|
354
|
+
# then if the child gets claimed by a new parent and `save!`ed, AR will
|
|
355
|
+
# re-establish the link from the old parent in the cache.
|
|
356
|
+
|
|
357
|
+
# Ideally we want
|
|
358
|
+
# model.association(...).inverse_reflection_for(previous_child_model), but
|
|
359
|
+
# that's private.
|
|
360
|
+
|
|
361
|
+
inverse_reflection =
|
|
362
|
+
if association_data.direct_reflection.polymorphic?
|
|
363
|
+
association_data.direct_reflection.polymorphic_inverse_of(previous_child_viewmodel.model.class)
|
|
364
|
+
else
|
|
365
|
+
association_data.direct_reflection.inverse_of
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
if inverse_reflection.present?
|
|
369
|
+
clear_association_cache(previous_child_viewmodel.model, inverse_reflection)
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
release_viewmodel(previous_child_viewmodel, association_data, update_context)
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Construct and return update for new child viewmodel
|
|
378
|
+
if child_viewmodel.present?
|
|
379
|
+
# If the association's pointer is in the child, need to provide it with a
|
|
380
|
+
# ParentData to update
|
|
381
|
+
parent_data =
|
|
382
|
+
if association_data.pointer_location == :remote
|
|
383
|
+
ParentData.new(association_data.direct_reflection.inverse_of, viewmodel)
|
|
384
|
+
else
|
|
385
|
+
nil
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
case child_viewmodel
|
|
389
|
+
when ViewModel::Reference # deferred
|
|
390
|
+
vm_ref = child_viewmodel
|
|
391
|
+
update_context.new_deferred_update(vm_ref, association_update_data, reparent_to: parent_data)
|
|
392
|
+
else
|
|
393
|
+
update_context.new_update(child_viewmodel, association_update_data, reparent_to: parent_data).build!(update_context)
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def build_updates_for_collection_association(association_data, association_update, update_context)
|
|
399
|
+
model = self.viewmodel.model
|
|
400
|
+
|
|
401
|
+
# reference back to this model, so we can set the link while updating the children
|
|
402
|
+
parent_data = ParentData.new(association_data.direct_reflection.inverse_of, viewmodel)
|
|
403
|
+
|
|
404
|
+
# load children already attached to this model
|
|
405
|
+
child_viewmodel_class = association_data.viewmodel_class
|
|
406
|
+
previous_child_viewmodels =
|
|
407
|
+
model.public_send(association_data.direct_reflection.name).map do |child_model|
|
|
408
|
+
child_viewmodel_class.new(child_model)
|
|
409
|
+
end
|
|
410
|
+
if child_viewmodel_class._list_member?
|
|
411
|
+
previous_child_viewmodels.sort_by!(&:_list_attribute)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
if previous_child_viewmodels.present?
|
|
415
|
+
# Clear the cached association so that AR's save behaviour doesn't
|
|
416
|
+
# conflict with our explicit parent updates. If we still have children
|
|
417
|
+
# after the update, we'll reset the target cache after recursing in
|
|
418
|
+
# run(). If not, the empty array we cache here will be correct, because
|
|
419
|
+
# previous children will be deleted or have had their parent pointers
|
|
420
|
+
# updated.
|
|
421
|
+
clear_association_cache(model, association_data.direct_reflection)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
child_datas =
|
|
425
|
+
case association_update
|
|
426
|
+
when OwnedCollectionUpdate::Replace
|
|
427
|
+
association_update.update_datas
|
|
428
|
+
|
|
429
|
+
when OwnedCollectionUpdate::Functional
|
|
430
|
+
child_datas =
|
|
431
|
+
previous_child_viewmodels.map do |previous_child_viewmodel|
|
|
432
|
+
UpdateData.empty_update_for(previous_child_viewmodel)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
association_update.check_for_duplicates!(update_context, self.viewmodel.blame_reference)
|
|
436
|
+
|
|
437
|
+
association_update.actions.each do |fupdate|
|
|
438
|
+
case fupdate
|
|
439
|
+
when FunctionalUpdate::Append
|
|
440
|
+
if fupdate.before || fupdate.after
|
|
441
|
+
moved_refs = fupdate.contents.map(&:viewmodel_reference).to_set
|
|
442
|
+
child_datas = child_datas.reject { |child| moved_refs.include?(child.viewmodel_reference) }
|
|
443
|
+
|
|
444
|
+
ref = fupdate.before || fupdate.after
|
|
445
|
+
index = child_datas.find_index { |cd| cd.viewmodel_reference == ref }
|
|
446
|
+
unless index
|
|
447
|
+
raise ViewModel::DeserializationError::AssociatedNotFound.new(
|
|
448
|
+
association_data.association_name.to_s, ref, blame_reference)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
index += 1 if fupdate.after
|
|
452
|
+
child_datas.insert(index, *fupdate.contents)
|
|
453
|
+
|
|
454
|
+
else
|
|
455
|
+
child_datas.concat(fupdate.contents)
|
|
456
|
+
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
when FunctionalUpdate::Remove
|
|
460
|
+
removed_refs = fupdate.removed_vm_refs.to_set
|
|
461
|
+
child_datas.reject! { |child_data| removed_refs.include?(child_data.viewmodel_reference) }
|
|
462
|
+
|
|
463
|
+
when FunctionalUpdate::Update
|
|
464
|
+
# Already guaranteed that each ref has a single data attached
|
|
465
|
+
new_datas = fupdate.contents.index_by(&:viewmodel_reference)
|
|
466
|
+
|
|
467
|
+
child_datas = child_datas.map do |child_data|
|
|
468
|
+
ref = child_data.viewmodel_reference
|
|
469
|
+
new_datas.delete(ref) { child_data }
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# Assertion that all values in update_op.values are present in the collection
|
|
473
|
+
unless new_datas.empty?
|
|
474
|
+
raise ViewModel::DeserializationError::AssociatedNotFound.new(
|
|
475
|
+
association_data.association_name.to_s, new_datas.keys, blame_reference)
|
|
476
|
+
end
|
|
477
|
+
else
|
|
478
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new(
|
|
479
|
+
"Unknown functional update type: '#{fupdate.type}'",
|
|
480
|
+
blame_reference)
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
child_datas
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
child_viewmodels = resolve_child_viewmodels(association_data, child_datas, previous_child_viewmodels, update_context)
|
|
488
|
+
|
|
489
|
+
# if the new children differ, including in order, mark that one of our
|
|
490
|
+
# associations has changed and release any no-longer-attached children
|
|
491
|
+
if child_viewmodels != previous_child_viewmodels
|
|
492
|
+
viewmodel.association_changed!(association_data.association_name)
|
|
493
|
+
released_child_viewmodels = previous_child_viewmodels - child_viewmodels
|
|
494
|
+
released_child_viewmodels.each do |vm|
|
|
495
|
+
release_viewmodel(vm, association_data, update_context)
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Calculate new positions for children if in a list. Ignore previous
|
|
500
|
+
# positions for unresolved references: they'll always need to be updated
|
|
501
|
+
# anyway since their parent pointer will change.
|
|
502
|
+
new_positions = Array.new(child_viewmodels.length)
|
|
503
|
+
|
|
504
|
+
if association_data.viewmodel_class._list_member?
|
|
505
|
+
set_position = ->(index, pos) { new_positions[index] = pos }
|
|
506
|
+
get_previous_position = ->(index) do
|
|
507
|
+
vm = child_viewmodels[index]
|
|
508
|
+
vm._list_attribute unless vm.is_a?(ViewModel::Reference)
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
ActsAsManualList.update_positions(
|
|
512
|
+
(0...child_viewmodels.size).to_a, # indexes
|
|
513
|
+
position_getter: get_previous_position,
|
|
514
|
+
position_setter: set_position)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# Recursively build update operations for children
|
|
518
|
+
child_updates = child_viewmodels.zip(child_datas, new_positions).map do |child_viewmodel, association_update_data, position|
|
|
519
|
+
case child_viewmodel
|
|
520
|
+
when ViewModel::Reference # deferred
|
|
521
|
+
reference = child_viewmodel
|
|
522
|
+
update_context.new_deferred_update(reference, association_update_data, reparent_to: parent_data, reposition_to: position)
|
|
523
|
+
else
|
|
524
|
+
update_context.new_update(child_viewmodel, association_update_data, reparent_to: parent_data, reposition_to: position).build!(update_context)
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
child_updates
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
class ReferencedCollectionMember
|
|
533
|
+
attr_reader :indirect_viewmodel_reference, :direct_viewmodel
|
|
534
|
+
attr_accessor :ref_string, :position
|
|
535
|
+
|
|
536
|
+
def initialize(indirect_viewmodel_reference, direct_viewmodel)
|
|
537
|
+
@indirect_viewmodel_reference = indirect_viewmodel_reference
|
|
538
|
+
@direct_viewmodel = direct_viewmodel
|
|
539
|
+
if direct_viewmodel.class._list_member?
|
|
540
|
+
@position = direct_viewmodel._list_attribute
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def ==(other)
|
|
545
|
+
other.class == self.class &&
|
|
546
|
+
other.indirect_viewmodel_reference == self.indirect_viewmodel_reference
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
alias :eql? :==
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# Helper class to wrap the previous members of a referenced collection and
|
|
553
|
+
# provide update operations. No one member may be affected by more than one
|
|
554
|
+
# update operation. Elements removed from the collection are collected as
|
|
555
|
+
# `orphaned_members`."
|
|
556
|
+
class MutableReferencedCollection
|
|
557
|
+
attr_reader :members, :orphaned_members, :blame_reference
|
|
558
|
+
|
|
559
|
+
def initialize(association_data, update_context, members, blame_reference)
|
|
560
|
+
@association_data = association_data
|
|
561
|
+
@update_context = update_context
|
|
562
|
+
@members = members.dup
|
|
563
|
+
@blame_reference = blame_reference
|
|
564
|
+
|
|
565
|
+
@orphaned_members = []
|
|
566
|
+
|
|
567
|
+
@free_members_by_indirect_ref = @members.index_by(&:indirect_viewmodel_reference)
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def replace(references)
|
|
571
|
+
members.replace(claim_or_create_references(references))
|
|
572
|
+
|
|
573
|
+
# Any unclaimed free members after building the update target are now
|
|
574
|
+
# orphaned and their direct viewmodels can be released.
|
|
575
|
+
orphaned_members.concat(free_members_by_indirect_ref.values)
|
|
576
|
+
free_members_by_indirect_ref.clear
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def insert_before(relative_to, references)
|
|
580
|
+
insert_relative(relative_to, 0, references)
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def insert_after(relative_to, references)
|
|
584
|
+
insert_relative(relative_to, 1, references)
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def concat(references)
|
|
588
|
+
new_members = claim_or_create_references(references)
|
|
589
|
+
remove_from_members(new_members)
|
|
590
|
+
members.concat(new_members)
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def remove(vm_references)
|
|
594
|
+
removed_members = vm_references.map do |vm_ref|
|
|
595
|
+
claim_existing_member(vm_ref)
|
|
596
|
+
end
|
|
597
|
+
remove_from_members(removed_members)
|
|
598
|
+
orphaned_members.concat(removed_members)
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def update(references)
|
|
602
|
+
claim_existing_references(references)
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
private
|
|
606
|
+
|
|
607
|
+
attr_reader :free_members_by_indirect_ref
|
|
608
|
+
attr_reader :association_data, :update_context
|
|
609
|
+
|
|
610
|
+
def insert_relative(relative_vm_ref, offset, references)
|
|
611
|
+
new_members = claim_or_create_references(references)
|
|
612
|
+
remove_from_members(new_members)
|
|
613
|
+
|
|
614
|
+
index = members.find_index { |m| m.indirect_viewmodel_reference == relative_vm_ref }
|
|
615
|
+
|
|
616
|
+
unless index
|
|
617
|
+
raise ViewModel::DeserializationError::AssociatedNotFound.new(
|
|
618
|
+
association_data.association_name.to_s, relative_vm_ref, blame_reference)
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
members.insert(index + offset, *new_members)
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
# Reclaim existing members corresponding to the specified references, or create new ones if not found.
|
|
625
|
+
def claim_or_create_references(references)
|
|
626
|
+
references.map do |ref_string|
|
|
627
|
+
indirect_vm_ref = update_context.resolve_reference(ref_string, blame_reference).viewmodel_reference
|
|
628
|
+
claim_or_create_member(indirect_vm_ref, ref_string)
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# Reclaim an existing member for an update and set its ref, or create a new one if not found.
|
|
633
|
+
def claim_or_create_member(indirect_vm_ref, ref_string)
|
|
634
|
+
member = free_members_by_indirect_ref.delete(indirect_vm_ref) do
|
|
635
|
+
ReferencedCollectionMember.new(indirect_vm_ref, association_data.direct_viewmodel.for_new_model)
|
|
636
|
+
end
|
|
637
|
+
member.ref_string = ref_string
|
|
638
|
+
member
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
# Reclaim existing members corresponding to the specified references or raise if not found.
|
|
642
|
+
def claim_existing_references(references)
|
|
643
|
+
references.each do |ref_string|
|
|
644
|
+
indirect_vm_ref = update_context.resolve_reference(ref_string, blame_reference).viewmodel_reference
|
|
645
|
+
claim_existing_member(indirect_vm_ref, ref_string)
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
# Claim an existing collection member for the update and optionally set its ref.
|
|
650
|
+
def claim_existing_member(indirect_vm_ref, ref_string = nil)
|
|
651
|
+
member = free_members_by_indirect_ref.delete(indirect_vm_ref) do
|
|
652
|
+
raise ViewModel::DeserializationError::AssociatedNotFound.new(
|
|
653
|
+
association_data.association_name.to_s, indirect_vm_ref, blame_reference)
|
|
654
|
+
end
|
|
655
|
+
member.ref_string = ref_string if ref_string
|
|
656
|
+
member
|
|
657
|
+
end
|
|
658
|
+
def remove_from_members(removed_members)
|
|
659
|
+
s = removed_members.to_set
|
|
660
|
+
members.reject! { |m| s.include?(m) }
|
|
661
|
+
end
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def build_updates_for_collection_referenced_association(association_data, association_update, update_context)
|
|
665
|
+
model = self.viewmodel.model
|
|
666
|
+
|
|
667
|
+
# We have two relationships here.
|
|
668
|
+
# - the relationship from us to the join table models: direct
|
|
669
|
+
# - the relationship from the join table to the children: indirect
|
|
670
|
+
|
|
671
|
+
direct_reflection = association_data.direct_reflection
|
|
672
|
+
indirect_reflection = association_data.indirect_reflection
|
|
673
|
+
direct_viewmodel_class = association_data.direct_viewmodel
|
|
674
|
+
indirect_association_data = association_data.indirect_association_data
|
|
675
|
+
|
|
676
|
+
indirect_ref_for_direct_viewmodel = ->(direct_viewmodel) do
|
|
677
|
+
direct_model = direct_viewmodel.model
|
|
678
|
+
model_class = direct_model.association(indirect_reflection.name).klass
|
|
679
|
+
model_id = direct_model.public_send(indirect_reflection.foreign_key)
|
|
680
|
+
viewmodel_class = indirect_association_data.viewmodel_class_for_model!(model_class)
|
|
681
|
+
ViewModel::Reference.new(viewmodel_class, model_id)
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
previous_members = model.public_send(direct_reflection.name).map do |m|
|
|
685
|
+
direct_vm = direct_viewmodel_class.new(m)
|
|
686
|
+
indirect_viewmodel_ref = indirect_ref_for_direct_viewmodel.(direct_vm)
|
|
687
|
+
ReferencedCollectionMember.new(indirect_viewmodel_ref, direct_vm)
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
if direct_viewmodel_class._list_member?
|
|
691
|
+
previous_members.sort_by!(&:position)
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
target_collection = MutableReferencedCollection.new(
|
|
695
|
+
association_data, update_context, previous_members, blame_reference)
|
|
696
|
+
|
|
697
|
+
# All updates to shared collections produce a complete target list of
|
|
698
|
+
# ReferencedCollectionMembers including a ViewModel::Reference to the
|
|
699
|
+
# indirect child, and an existing (from previous) or new ViewModel for the
|
|
700
|
+
# direct child.
|
|
701
|
+
#
|
|
702
|
+
# Members participating in the update (all members in the case of Replace,
|
|
703
|
+
# specified append or update members in the case of Functional) will also
|
|
704
|
+
# include a reference string for the update operation for the indirect
|
|
705
|
+
# child, which will be subsequently added to the new UpdateOperation for
|
|
706
|
+
# the direct child.
|
|
707
|
+
case association_update
|
|
708
|
+
when ReferencedCollectionUpdate::Replace
|
|
709
|
+
target_collection.replace(association_update.references)
|
|
710
|
+
|
|
711
|
+
when ReferencedCollectionUpdate::Functional
|
|
712
|
+
# Collection updates are a list of actions modifying the list
|
|
713
|
+
# of indirect children.
|
|
714
|
+
#
|
|
715
|
+
# The target collection starts out as a copy of the previous collection
|
|
716
|
+
# members, and is then mutated based on the actions specified. All
|
|
717
|
+
# members added or modified by actions will have their `ref` set.
|
|
718
|
+
|
|
719
|
+
association_update.check_for_duplicates!(update_context, self.viewmodel.blame_reference)
|
|
720
|
+
|
|
721
|
+
association_update.actions.each do |fupdate|
|
|
722
|
+
case fupdate
|
|
723
|
+
when FunctionalUpdate::Append # Append new members, possibly relative to another member
|
|
724
|
+
case
|
|
725
|
+
when fupdate.before
|
|
726
|
+
target_collection.insert_before(fupdate.before, fupdate.contents)
|
|
727
|
+
when fupdate.after
|
|
728
|
+
target_collection.insert_after(fupdate.after, fupdate.contents)
|
|
729
|
+
else
|
|
730
|
+
target_collection.concat(fupdate.contents)
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
when FunctionalUpdate::Remove
|
|
734
|
+
target_collection.remove(fupdate.removed_vm_refs)
|
|
735
|
+
|
|
736
|
+
when FunctionalUpdate::Update # Update contents of members already in the collection
|
|
737
|
+
target_collection.update(fupdate.contents)
|
|
738
|
+
|
|
739
|
+
else
|
|
740
|
+
raise ArgumentError.new("Unknown functional update: '#{fupdate.class}'")
|
|
741
|
+
end
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
else
|
|
745
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new("Unknown association_update type '#{association_update.class.name}'", blame_reference)
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
# We should now have an updated list `target_collection.members`,
|
|
749
|
+
# containing members for the desired new collection in the order that we
|
|
750
|
+
# want them, each of which has a `direct_viewmodel` set, and additionally
|
|
751
|
+
# a `ref_string` set for those that participated in the update.
|
|
752
|
+
if target_collection.members != previous_members
|
|
753
|
+
viewmodel.association_changed!(association_data.association_name)
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
if direct_viewmodel_class._list_member?
|
|
757
|
+
ActsAsManualList.update_positions(target_collection.members)
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
parent_data = ParentData.new(direct_reflection.inverse_of, self.viewmodel)
|
|
761
|
+
|
|
762
|
+
new_direct_updates = target_collection.members.map do |member|
|
|
763
|
+
update_data = UpdateData.empty_update_for(member.direct_viewmodel)
|
|
764
|
+
|
|
765
|
+
if (ref = member.ref_string)
|
|
766
|
+
update_data.referenced_associations[indirect_reflection.name] = ref
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
update_context.new_update(member.direct_viewmodel, update_data,
|
|
770
|
+
reparent_to: parent_data,
|
|
771
|
+
reposition_to: member.position)
|
|
772
|
+
.build!(update_context)
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
# Members removed from the collection, either by `Remove` or by
|
|
776
|
+
# not being included in the new Replace list can now be
|
|
777
|
+
# released.
|
|
778
|
+
target_collection.orphaned_members.each do |member|
|
|
779
|
+
release_viewmodel(member.direct_viewmodel, association_data, update_context)
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
new_direct_updates
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
def release_viewmodel(viewmodel, association_data, update_context)
|
|
786
|
+
self.released_children << update_context.release_viewmodel(viewmodel, association_data)
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
def clear_association_cache(model, reflection)
|
|
790
|
+
association = model.association(reflection.name)
|
|
791
|
+
if reflection.collection?
|
|
792
|
+
association.target = []
|
|
793
|
+
else
|
|
794
|
+
association.target = nil
|
|
795
|
+
end
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
def blame_reference
|
|
799
|
+
self.viewmodel.blame_reference
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
def debug(msg)
|
|
803
|
+
return unless ViewModel::Config.debug_deserialization
|
|
804
|
+
|
|
805
|
+
::ActiveRecord::Base.logger.try do |logger|
|
|
806
|
+
logger.debug(msg)
|
|
807
|
+
end
|
|
808
|
+
end
|
|
809
|
+
end
|
|
810
|
+
end
|