iknow_view_models 2.10.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +119 -0
  3. data/.travis.yml +31 -0
  4. data/Appraisals +6 -16
  5. data/gemfiles/{rails_7_0.gemfile → rails_6_0_beta.gemfile} +2 -2
  6. data/iknow_view_models.gemspec +3 -5
  7. data/lib/iknow_view_models/version.rb +1 -1
  8. data/lib/view_model/active_record/association_data.rb +206 -92
  9. data/lib/view_model/active_record/association_manipulation.rb +22 -12
  10. data/lib/view_model/active_record/cache/cacheable_view.rb +3 -13
  11. data/lib/view_model/active_record/cache.rb +2 -2
  12. data/lib/view_model/active_record/cloner.rb +11 -11
  13. data/lib/view_model/active_record/controller.rb +0 -2
  14. data/lib/view_model/active_record/update_context.rb +21 -3
  15. data/lib/view_model/active_record/update_data.rb +43 -45
  16. data/lib/view_model/active_record/update_operation.rb +265 -153
  17. data/lib/view_model/active_record/visitor.rb +9 -6
  18. data/lib/view_model/active_record.rb +94 -74
  19. data/lib/view_model/after_transaction_runner.rb +3 -18
  20. data/lib/view_model/callbacks.rb +2 -2
  21. data/lib/view_model/changes.rb +24 -16
  22. data/lib/view_model/config.rb +6 -2
  23. data/lib/view_model/deserialization_error.rb +31 -0
  24. data/lib/view_model/deserialize_context.rb +2 -6
  25. data/lib/view_model/error_view.rb +6 -5
  26. data/lib/view_model/record/attribute_data.rb +11 -6
  27. data/lib/view_model/record.rb +44 -24
  28. data/lib/view_model/serialize_context.rb +2 -63
  29. data/lib/view_model/test_helpers/arvm_builder.rb +2 -4
  30. data/lib/view_model/traversal_context.rb +2 -2
  31. data/lib/view_model.rb +21 -13
  32. data/shell.nix +1 -1
  33. data/test/helpers/arvm_test_models.rb +4 -12
  34. data/test/helpers/arvm_test_utilities.rb +6 -0
  35. data/test/helpers/controller_test_helpers.rb +6 -6
  36. data/test/helpers/viewmodel_spec_helpers.rb +63 -52
  37. data/test/unit/view_model/access_control_test.rb +88 -37
  38. data/test/unit/view_model/active_record/belongs_to_test.rb +110 -178
  39. data/test/unit/view_model/active_record/cache_test.rb +11 -5
  40. data/test/unit/view_model/active_record/cloner_test.rb +1 -1
  41. data/test/unit/view_model/active_record/controller_test.rb +12 -20
  42. data/test/unit/view_model/active_record/has_many_test.rb +540 -316
  43. data/test/unit/view_model/active_record/has_many_through_poly_test.rb +12 -15
  44. data/test/unit/view_model/active_record/has_many_through_test.rb +15 -58
  45. data/test/unit/view_model/active_record/has_one_test.rb +288 -135
  46. data/test/unit/view_model/active_record/poly_test.rb +0 -1
  47. data/test/unit/view_model/active_record/shared_test.rb +21 -39
  48. data/test/unit/view_model/active_record/version_test.rb +3 -2
  49. data/test/unit/view_model/active_record_test.rb +5 -63
  50. data/test/unit/view_model/callbacks_test.rb +1 -0
  51. data/test/unit/view_model/record_test.rb +0 -32
  52. data/test/unit/view_model/traversal_context_test.rb +13 -12
  53. metadata +15 -25
  54. data/.github/workflows/gem-push.yml +0 -31
  55. data/.github/workflows/test.yml +0 -65
  56. data/gemfiles/rails_6_0.gemfile +0 -9
  57. data/gemfiles/rails_6_1.gemfile +0 -9
  58. data/test/unit/view_model/active_record/optional_attribute_view_test.rb +0 -58
@@ -40,10 +40,6 @@ class ViewModel::ActiveRecord
40
40
  end
41
41
  end
42
42
 
43
- def deferred?
44
- viewmodel.nil?
45
- end
46
-
47
43
  def built?
48
44
  @built
49
45
  end
@@ -112,9 +108,8 @@ class ViewModel::ActiveRecord
112
108
  if child_operation
113
109
  child_ctx = viewmodel.context_for_child(association_data.association_name, context: deserialize_context)
114
110
  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
111
+ propagate_tree_changes(association_data, child_viewmodel.previous_changes)
112
+
118
113
  child_viewmodel.model
119
114
  end
120
115
  association.writer(new_target)
@@ -153,9 +148,8 @@ class ViewModel::ActiveRecord
153
148
  if child_operation
154
149
  ViewModel::Utils.map_one_or_many(child_operation) do |op|
155
150
  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
151
+ propagate_tree_changes(association_data, child_viewmodel.previous_changes)
152
+
159
153
  child_viewmodel.model
160
154
  end
161
155
  end
@@ -183,7 +177,11 @@ class ViewModel::ActiveRecord
183
177
  child_hook_control.record_changes(changes)
184
178
  end
185
179
 
186
- viewmodel.children_changed! unless child_association_data.shared?
180
+ if child_association_data.nested?
181
+ viewmodel.nested_children_changed!
182
+ elsif child_association_data.owned?
183
+ viewmodel.referenced_children_changed!
184
+ end
187
185
  end
188
186
  debug "<- #{debug_name}: Finished checking released children permissions"
189
187
  end
@@ -208,9 +206,18 @@ class ViewModel::ActiveRecord
208
206
  raise ViewModel::DeserializationError::DatabaseConstraint.from_exception(ex, blame_reference)
209
207
  end
210
208
 
209
+ def propagate_tree_changes(association_data, child_changes)
210
+ if association_data.nested?
211
+ viewmodel.nested_children_changed! if child_changes.changed_nested_tree?
212
+ viewmodel.referenced_children_changed! if child_changes.changed_referenced_children?
213
+ elsif association_data.owned?
214
+ viewmodel.referenced_children_changed! if child_changes.changed_owned_tree?
215
+ end
216
+ end
217
+
211
218
  # Recursively builds UpdateOperations for the associations in our UpdateData
212
219
  def build!(update_context)
213
- raise ViewModel::DeserializationError::Internal.new("Internal error: UpdateOperation cannot build a deferred update") if deferred?
220
+ raise ViewModel::DeserializationError::Internal.new("Internal error: UpdateOperation cannot build a deferred update") if viewmodel.nil?
214
221
  return self if built?
215
222
 
216
223
  update_data.associations.each do |association_name, association_update_data|
@@ -231,8 +238,10 @@ class ViewModel::ActiveRecord
231
238
  update =
232
239
  if association_data.through?
233
240
  build_updates_for_collection_referenced_association(association_data, reference_string, update_context)
241
+ elsif association_data.collection?
242
+ build_updates_for_collection_association(association_data, reference_string, update_context)
234
243
  else
235
- build_update_for_single_referenced_association(association_data, reference_string, update_context)
244
+ build_update_for_single_association(association_data, reference_string, update_context)
236
245
  end
237
246
 
238
247
  add_update(association_data, update)
@@ -254,38 +263,6 @@ class ViewModel::ActiveRecord
254
263
 
255
264
  private
256
265
 
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
266
  # Resolve or construct viewmodels for incoming update data. Where a child
290
267
  # hash references an existing model not currently attached to this parent,
291
268
  # it must be found before recursing into that child. If the model is
@@ -321,6 +298,60 @@ class ViewModel::ActiveRecord
321
298
  end
322
299
  end
323
300
 
301
+ def resolve_referenced_viewmodels(association_data, update_datas, previous_child_viewmodels, update_context)
302
+ previous_child_viewmodels = Array.wrap(previous_child_viewmodels).index_by(&:to_reference)
303
+
304
+ ViewModel::Utils.map_one_or_many(update_datas) do |update_data|
305
+ if update_data.is_a?(UpdateData)
306
+ # Dummy child update data for an unmodified previous child of a
307
+ # functional update; create it an empty update operation.
308
+ viewmodel = previous_child_viewmodels.fetch(update_data.viewmodel_reference)
309
+ update = update_context.new_update(viewmodel, update_data)
310
+ next [update, viewmodel]
311
+ end
312
+
313
+ reference_string = update_data
314
+ child_update = update_context.resolve_reference(reference_string, blame_reference)
315
+ child_viewmodel = child_update.viewmodel
316
+
317
+ unless association_data.accepts?(child_viewmodel.class)
318
+ raise ViewModel::DeserializationError::InvalidAssociationType.new(
319
+ association_data.association_name.to_s,
320
+ child_viewmodel.class.view_name,
321
+ blame_reference)
322
+ end
323
+
324
+ child_ref = child_viewmodel.to_reference
325
+
326
+ # The case of two potential owners trying to claim a new referenced
327
+ # child is covered by set_reference_update_parent.
328
+ claimed = !association_data.owned? ||
329
+ child_update.update_data.new? ||
330
+ previous_child_viewmodels.has_key?(child_ref) ||
331
+ update_context.try_take_released_viewmodel(child_ref).present?
332
+
333
+ if claimed
334
+ [child_update, child_viewmodel]
335
+ else
336
+ # Return the reference to indicate a deferred update
337
+ [child_update, child_ref]
338
+ end
339
+ end
340
+ end
341
+
342
+ def set_reference_update_parent(association_data, update, parent_data)
343
+ if update.reparent_to
344
+ # Another parent has already tried to take this (probably new)
345
+ # owned referenced view. It can only be claimed by one of them.
346
+ other_parent = update.reparent_to.viewmodel.to_reference
347
+ raise ViewModel::DeserializationError::DuplicateOwner.new(
348
+ association_data.association_name,
349
+ [blame_reference, other_parent])
350
+ end
351
+
352
+ update.reparent_to = parent_data
353
+ end
354
+
324
355
  def build_update_for_single_association(association_data, association_update_data, update_context)
325
356
  model = self.viewmodel.model
326
357
 
@@ -329,25 +360,63 @@ class ViewModel::ActiveRecord
329
360
  vm_class.new(previous_child_model)
330
361
  end
331
362
 
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)
363
+ if association_data.pointer_location == :remote
364
+ if previous_child_viewmodel.present?
365
+ # Clear the cached association so that AR's save behaviour doesn't
366
+ # conflict with our explicit parent updates. If we still have a child
367
+ # after the update, we'll either call `Association#writer` or manually
368
+ # fix the target cache after recursing in run!(). If we don't, we promise
369
+ # that the child will no longer be attached in the database, so the new
370
+ # cached data of nil will be correct.
371
+ clear_association_cache(model, association_data.direct_reflection)
372
+ end
373
+
374
+ reparent_data =
375
+ ParentData.new(association_data.direct_reflection.inverse_of, viewmodel)
340
376
  end
341
377
 
342
- child_viewmodel =
343
- if association_update_data.present?
344
- resolve_child_viewmodels(association_data, association_update_data, previous_child_viewmodel, update_context)
378
+ if association_update_data.present?
379
+ if association_data.referenced?
380
+ # resolve reference string
381
+ reference_string = association_update_data
382
+ child_update, child_viewmodel = resolve_referenced_viewmodels(association_data, reference_string,
383
+ previous_child_viewmodel, update_context)
384
+
385
+ if reparent_data
386
+ set_reference_update_parent(association_data, child_update, reparent_data)
387
+ end
388
+
389
+ if child_viewmodel.is_a?(ViewModel::Reference)
390
+ update_context.defer_update(child_viewmodel, child_update)
391
+ end
392
+ else
393
+ # Resolve direct children
394
+ child_viewmodel =
395
+ resolve_child_viewmodels(association_data, association_update_data, previous_child_viewmodel, update_context)
396
+
397
+ child_update =
398
+ if child_viewmodel.is_a?(ViewModel::Reference)
399
+ update_context.new_deferred_update(child_viewmodel, association_update_data, reparent_to: reparent_data)
400
+ else
401
+ update_context.new_update(child_viewmodel, association_update_data, reparent_to: reparent_data)
402
+ end
345
403
  end
346
404
 
405
+ # Build the update if we've claimed it
406
+ unless child_viewmodel.is_a?(ViewModel::Reference)
407
+ child_update.build!(update_context)
408
+ end
409
+ else
410
+ child_update = nil
411
+ child_viewmodel = nil
412
+ end
413
+
414
+ # Handle changes
347
415
  if previous_child_viewmodel != child_viewmodel
348
416
  viewmodel.association_changed!(association_data.association_name)
349
- # free previous child if present
350
- if previous_child_viewmodel.present?
417
+
418
+ # free previous child if present and owned
419
+ if previous_child_viewmodel.present? && association_data.owned?
351
420
  if association_data.pointer_location == :local
352
421
  # When we free a child that's pointed to from its old parent, we need to
353
422
  # clear the cached association to that old parent. If we don't do this,
@@ -358,12 +427,7 @@ class ViewModel::ActiveRecord
358
427
  # model.association(...).inverse_reflection_for(previous_child_model), but
359
428
  # that's private.
360
429
 
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
430
+ inverse_reflection = association_data.direct_reflection_inverse(previous_child_viewmodel.model.class)
367
431
 
368
432
  if inverse_reflection.present?
369
433
  clear_association_cache(previous_child_viewmodel.model, inverse_reflection)
@@ -374,25 +438,7 @@ class ViewModel::ActiveRecord
374
438
  end
375
439
  end
376
440
 
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
441
+ child_update
396
442
  end
397
443
 
398
444
  def build_updates_for_collection_association(association_data, association_update, update_context)
@@ -402,11 +448,13 @@ class ViewModel::ActiveRecord
402
448
  parent_data = ParentData.new(association_data.direct_reflection.inverse_of, viewmodel)
403
449
 
404
450
  # load children already attached to this model
405
- child_viewmodel_class = association_data.viewmodel_class
451
+ child_viewmodel_class = association_data.viewmodel_class
452
+
406
453
  previous_child_viewmodels =
407
454
  model.public_send(association_data.direct_reflection.name).map do |child_model|
408
455
  child_viewmodel_class.new(child_model)
409
456
  end
457
+
410
458
  if child_viewmodel_class._list_member?
411
459
  previous_child_viewmodels.sort_by!(&:_list_attribute)
412
460
  end
@@ -421,114 +469,178 @@ class ViewModel::ActiveRecord
421
469
  clear_association_cache(model, association_data.direct_reflection)
422
470
  end
423
471
 
424
- child_datas =
425
- case association_update
426
- when OwnedCollectionUpdate::Replace
427
- association_update.update_datas
472
+ # Update contents are either UpdateData in the case of a nested
473
+ # association, or reference strings in the case of a reference association.
474
+ # The former are resolved with resolve_child_viewmodels, the latter with
475
+ # resolve_referenced_viewmodels.
476
+ resolve_child_data_reference = ->(child_data) do
477
+ case child_data
478
+ when UpdateData
479
+ child_data.viewmodel_reference if child_data.id
480
+ when String
481
+ update_context.resolve_reference(child_data, nil).viewmodel_reference
482
+ else
483
+ raise ViewModel::DeserializationError::Internal.new(
484
+ "Unexpected child data type in collection update: #{child_data.class.name}")
485
+ end
486
+ end
428
487
 
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
488
+ case association_update
489
+ when AbstractCollectionUpdate::Replace
490
+ child_datas = association_update.contents
434
491
 
435
- association_update.check_for_duplicates!(update_context, self.viewmodel.blame_reference)
492
+ when AbstractCollectionUpdate::Functional
493
+ # A fupdate isn't permitted to edit the same model twice.
494
+ association_update.check_for_duplicates!(update_context, blame_reference)
436
495
 
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) }
496
+ # Construct empty updates for previous children
497
+ child_datas =
498
+ previous_child_viewmodels.map do |previous_child_viewmodel|
499
+ UpdateData.empty_update_for(previous_child_viewmodel)
500
+ end
443
501
 
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
502
+ # Insert or replace with either real UpdateData or reference strings
503
+ association_update.actions.each do |fupdate|
504
+ case fupdate
505
+ when FunctionalUpdate::Append
506
+ # If we're referring to existing members, ensure that they're removed before we append/insert
507
+ existing_refs = fupdate.contents
508
+ .map(&resolve_child_data_reference)
509
+ .to_set
510
+
511
+ child_datas.reject! do |child_data|
512
+ child_ref = resolve_child_data_reference.(child_data)
513
+ child_ref && existing_refs.include?(child_ref)
514
+ end
450
515
 
451
- index += 1 if fupdate.after
452
- child_datas.insert(index, *fupdate.contents)
516
+ if fupdate.before || fupdate.after
517
+ rel_ref = fupdate.before || fupdate.after
453
518
 
454
- else
455
- child_datas.concat(fupdate.contents)
519
+ # Find the relative insert location. This might be an empty
520
+ # UpdateData from a previous child or an already-fupdated
521
+ # reference string.
522
+ index = child_datas.find_index do |child_data|
523
+ rel_ref == resolve_child_data_reference.(child_data)
524
+ end
456
525
 
526
+ unless index
527
+ raise ViewModel::DeserializationError::AssociatedNotFound.new(
528
+ association_data.association_name.to_s, rel_ref, blame_reference)
457
529
  end
458
530
 
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) }
531
+ index += 1 if fupdate.after
532
+ child_datas.insert(index, *fupdate.contents)
533
+
534
+ else
535
+ child_datas.concat(fupdate.contents)
536
+ end
462
537
 
463
- when FunctionalUpdate::Update
464
- # Already guaranteed that each ref has a single data attached
465
- new_datas = fupdate.contents.index_by(&:viewmodel_reference)
538
+ when FunctionalUpdate::Remove
539
+ removed_refs = fupdate.removed_vm_refs.to_set
540
+ child_datas.reject! do |child_data|
541
+ child_ref = resolve_child_data_reference.(child_data)
542
+ removed_refs.include?(child_ref)
543
+ end
466
544
 
467
- child_datas = child_datas.map do |child_data|
468
- ref = child_data.viewmodel_reference
469
- new_datas.delete(ref) { child_data }
470
- end
545
+ when FunctionalUpdate::Update
546
+ # Already guaranteed that each ref has a single existing child attached
547
+ new_child_datas = fupdate.contents.index_by(&resolve_child_data_reference)
471
548
 
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)
549
+ # Replace matched child_datas with the update contents.
550
+ child_datas.map! do |child_data|
551
+ child_ref = resolve_child_data_reference.(child_data)
552
+ new_child_datas.delete(child_ref) { child_data }
481
553
  end
554
+
555
+ # Assertion that all values in the update were found in child_datas
556
+ unless new_child_datas.empty?
557
+ raise ViewModel::DeserializationError::AssociatedNotFound.new(
558
+ association_data.association_name.to_s, new_child_datas.keys, blame_reference)
559
+ end
560
+ else
561
+ raise ViewModel::DeserializationError::InvalidSyntax.new(
562
+ "Unknown functional update type: '#{fupdate.type}'",
563
+ blame_reference)
482
564
  end
565
+ end
566
+ end
567
+
568
+ if association_data.referenced?
569
+ # child_datas are either pre-resolved UpdateData (for non-fupdated
570
+ # existing members) or reference strings. Resolve into pairs of
571
+ # [UpdateOperation, ViewModel] if we can create or claim the
572
+ # UpdateOperation or [UpdateOperation, ViewModelReference] otherwise.
573
+ resolved_children =
574
+ resolve_referenced_viewmodels(association_data, child_datas, previous_child_viewmodels, update_context)
483
575
 
484
- child_datas
576
+ resolved_children.each do |child_update, child_viewmodel|
577
+ set_reference_update_parent(association_data, child_update, parent_data)
578
+
579
+ if child_viewmodel.is_a?(ViewModel::Reference)
580
+ update_context.defer_update(child_viewmodel, child_update)
581
+ end
485
582
  end
486
583
 
487
- child_viewmodels = resolve_child_viewmodels(association_data, child_datas, previous_child_viewmodels, update_context)
584
+ else
585
+ # child datas are all UpdateData
586
+ child_viewmodels = resolve_child_viewmodels(association_data, child_datas, previous_child_viewmodels, update_context)
488
587
 
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)
588
+ resolved_children = child_datas.zip(child_viewmodels).map do |child_data, child_viewmodel|
589
+ child_update =
590
+ if child_viewmodel.is_a?(ViewModel::Reference)
591
+ update_context.new_deferred_update(child_viewmodel, child_data, reparent_to: parent_data)
592
+ else
593
+ update_context.new_update(child_viewmodel, child_data, reparent_to: parent_data)
594
+ end
595
+
596
+ [child_update, child_viewmodel]
496
597
  end
497
598
  end
498
599
 
499
600
  # 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)
601
+ # positions (i.e. return nil) for unresolved references: they'll always
602
+ # need to be updated anyway since their parent pointer will change.
603
+ new_positions = Array.new(resolved_children.length)
503
604
 
504
605
  if association_data.viewmodel_class._list_member?
505
606
  set_position = ->(index, pos) { new_positions[index] = pos }
607
+
506
608
  get_previous_position = ->(index) do
507
- vm = child_viewmodels[index]
609
+ vm = resolved_children[index][1]
508
610
  vm._list_attribute unless vm.is_a?(ViewModel::Reference)
509
611
  end
510
612
 
511
613
  ActsAsManualList.update_positions(
512
- (0...child_viewmodels.size).to_a, # indexes
614
+ (0...resolved_children.size).to_a, # indexes
513
615
  position_getter: get_previous_position,
514
616
  position_setter: set_position)
515
617
  end
516
618
 
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)
619
+ resolved_children.zip(new_positions).each do |(child_update, child_viewmodel), new_position|
620
+ child_update.reposition_to = new_position
621
+
622
+ # Recurse into building child updates that we've claimed
623
+ unless child_viewmodel.is_a?(ViewModel::Reference)
624
+ child_update.build!(update_context)
625
+ end
626
+ end
627
+
628
+ child_updates, child_viewmodels = resolved_children.transpose.presence || [[], []]
629
+
630
+ # if the new children differ, including in order, mark that this
631
+ # association has changed and release any no-longer-attached children
632
+ if child_viewmodels != previous_child_viewmodels
633
+ viewmodel.association_changed!(association_data.association_name)
634
+
635
+ released_child_viewmodels = previous_child_viewmodels - child_viewmodels
636
+ released_child_viewmodels.each do |vm|
637
+ release_viewmodel(vm, association_data, update_context)
525
638
  end
526
639
  end
527
640
 
528
641
  child_updates
529
642
  end
530
643
 
531
-
532
644
  class ReferencedCollectionMember
533
645
  attr_reader :indirect_viewmodel_reference, :direct_viewmodel
534
646
  attr_accessor :ref_string, :position
@@ -694,7 +806,7 @@ class ViewModel::ActiveRecord
694
806
  target_collection = MutableReferencedCollection.new(
695
807
  association_data, update_context, previous_members, blame_reference)
696
808
 
697
- # All updates to shared collections produce a complete target list of
809
+ # All updates to referenced collections produce a complete target list of
698
810
  # ReferencedCollectionMembers including a ViewModel::Reference to the
699
811
  # indirect child, and an existing (from previous) or new ViewModel for the
700
812
  # direct child.
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ViewModel::ActiveRecord::Visitor
4
- attr_reader :visit_shared, :for_edit
4
+ attr_reader :visit_referenced, :visit_shared, :for_edit
5
5
 
6
- def initialize(visit_shared: true, for_edit: false)
7
- @visit_shared = visit_shared
8
- @for_edit = for_edit
6
+ def initialize(visit_referenced: true, visit_shared: true, for_edit: false)
7
+ @visit_referenced = visit_referenced
8
+ @visit_shared = visit_shared
9
+ @for_edit = for_edit
9
10
  end
10
11
 
11
12
  def visit(view, context: nil)
@@ -29,8 +30,10 @@ class ViewModel::ActiveRecord::Visitor
29
30
  # visit the underlying viewmodel for each association, ignoring any
30
31
  # customization
31
32
  view.class._members.each do |name, member_data|
32
- next unless member_data.is_a?(ViewModel::ActiveRecord::AssociationData)
33
- next if member_data.shared? && !visit_shared
33
+ next unless member_data.association?
34
+ next if member_data.referenced? && !visit_referenced
35
+ next if !member_data.owned? && !visit_shared
36
+
34
37
  children = Array.wrap(view._read_association(name))
35
38
  children.each do |child|
36
39
  if context