iknow_view_models 2.10.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -12,17 +12,7 @@ module ViewModel::ActiveRecord::Cache::CacheableView
12
12
  CacheClearer = Struct.new(:cache, :id) do
13
13
  include ViewModel::AfterTransactionRunner
14
14
 
15
- # It's important that we clear the cache before committing, because we rely
16
- # on database locking to prevent cache race conditions. We require
17
- # reading/refreshing the cache to obtain a FOR SHARE lock, which means that
18
- # a reader must wait for a concurrent writer to commit before continuing to
19
- # the cache. If the writer cleared the cache after commit, the reader could
20
- # obtain old data before the clear, and then save the old data after it.
21
- def before_commit
22
- cache.delete(id)
23
- end
24
-
25
- def after_rollback
15
+ def after_transaction
26
16
  cache.delete(id)
27
17
  end
28
18
 
@@ -49,12 +39,12 @@ module ViewModel::ActiveRecord::Cache::CacheableView
49
39
  end
50
40
  end
51
41
 
52
- # Clear the cache if the view or its owned children were changed during
42
+ # Clear the cache if the view or its nested children were changed during
53
43
  # deserialization
54
44
  def after_deserialize(deserialize_context:, changes:)
55
45
  super if defined?(super)
56
46
 
57
- if !changes.new? && changes.changed_tree?
47
+ if !changes.new? && changes.changed_nested_tree?
58
48
  CacheClearer.new(self.class.viewmodel_cache, id).add_to_transaction
59
49
  end
60
50
  end
@@ -198,7 +198,7 @@ class ViewModel::ActiveRecord::Cache
198
198
  end
199
199
 
200
200
  ViewModel.preload_for_serialization(viewmodels,
201
- include_shared: false,
201
+ include_referenced: false,
202
202
  lock: "FOR SHARE",
203
203
  serialize_context: serialize_context)
204
204
 
@@ -259,7 +259,7 @@ class ViewModel::ActiveRecord::Cache
259
259
  end
260
260
 
261
261
  def cache_version
262
- version_string = @viewmodel_class.deep_schema_version(include_shared: false).to_a.sort.join(',')
262
+ version_string = @viewmodel_class.deep_schema_version(include_referenced: false).to_a.sort.join(',')
263
263
  Base64.urlsafe_encode64(Digest::MD5.digest(version_string))
264
264
  end
265
265
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Simple visitor for cloning models through the tree structure defined by
2
4
  # ViewModel::ActiveRecord. Owned associations will be followed and cloned, while
3
- # shared associations will be copied directly. Attributes (including association
4
- # foreign keys not covered by ViewModel `association`s) will be copied from the
5
- # original.
5
+ # non-owned referenced associations will be copied directly as references.
6
+ # Attributes (including association foreign keys not covered by ViewModel
7
+ # `association`s) will be copied from the original.
6
8
  #
7
9
  # To customize, subclasses may define methods `visit_x_view(node, new_model)`
8
10
  # for each type they wish to affect. These callbacks may update attributes of
@@ -19,9 +21,9 @@ class ViewModel::ActiveRecord::Cloner
19
21
  return nil if ignored?
20
22
 
21
23
  if node.class.name
22
- class_name = node.class.name.underscore.gsub('/', '__')
23
- visit = :"visit_#{class_name}"
24
- end_visit = :"end_visit_#{class_name}"
24
+ class_name = node.class.name.underscore.gsub('/', '__')
25
+ visit = :"visit_#{class_name}"
26
+ end_visit = :"end_visit_#{class_name}"
25
27
  end
26
28
 
27
29
  if visit && respond_to?(visit, true)
@@ -44,7 +46,7 @@ class ViewModel::ActiveRecord::Cloner
44
46
 
45
47
  if associated.nil?
46
48
  new_associated = nil
47
- elsif association_data.shared? && !association_data.through?
49
+ elsif !association_data.owned? && !association_data.through?
48
50
  # simply attach the associated target to the new model
49
51
  new_associated = associated
50
52
  else
@@ -82,11 +84,9 @@ class ViewModel::ActiveRecord::Cloner
82
84
  new_model
83
85
  end
84
86
 
85
- def pre_visit(node, new_model)
86
- end
87
+ def pre_visit(node, new_model); end
87
88
 
88
- def post_visit(node, new_model)
89
- end
89
+ def post_visit(node, new_model); end
90
90
 
91
91
  private
92
92
 
@@ -46,8 +46,6 @@ module ViewModel::ActiveRecord::Controller
46
46
  pre_rendered = viewmodel_class.transaction do
47
47
  view = viewmodel_class.deserialize_from_view(update_hash, references: refs, deserialize_context: deserialize_context)
48
48
 
49
- serialize_context.add_includes(deserialize_context.updated_associations)
50
-
51
49
  view = yield(view) if block_given?
52
50
 
53
51
  ViewModel.preload_for_serialization(view, serialize_context: serialize_context)
@@ -74,7 +74,7 @@ class ViewModel::ActiveRecord
74
74
 
75
75
  def initialize
76
76
  @root_update_operations = [] # The subject(s) of this update
77
- @referenced_update_operations = {} # Shared data updates, referred to by a ref hash
77
+ @referenced_update_operations = {} # data updates to other root models, referred to by a ref hash
78
78
 
79
79
  # Set of ViewModel::Reference used to assert only a single update is
80
80
  # present for each viewmodel
@@ -178,8 +178,20 @@ class ViewModel::ActiveRecord
178
178
  raise ViewModel::DeserializationError::ParentNotFound.new(@worklist.keys)
179
179
  end
180
180
 
181
- deferred_update = @worklist.delete(key)
182
- deferred_update.viewmodel = @release_pool.claim_from_pool(key)
181
+ deferred_update = @worklist.delete(key)
182
+ released_viewmodel = @release_pool.claim_from_pool(key)
183
+
184
+ if deferred_update.viewmodel
185
+ # Deferred reference updates already have a viewmodel: ensure it
186
+ # matches the tree
187
+ unless deferred_update.viewmodel == released_viewmodel
188
+ raise ViewModel::DeserializationError::Internal.new(
189
+ "Released viewmodel doesn't match reference update", blame_reference)
190
+ end
191
+ else
192
+ deferred_update.viewmodel = released_viewmodel
193
+ end
194
+
183
195
  deferred_update.build!(self)
184
196
  end
185
197
 
@@ -201,6 +213,12 @@ class ViewModel::ActiveRecord
201
213
  update_operation = ViewModel::ActiveRecord::UpdateOperation.new(
202
214
  nil, update_data, reparent_to: reparent_to, reposition_to: reposition_to)
203
215
  check_unique_update!(viewmodel_reference)
216
+ defer_update(viewmodel_reference, update_operation)
217
+ end
218
+
219
+ # Defer an existing update: used if we need to ensure that an owned
220
+ # reference has been freed before we use it.
221
+ def defer_update(viewmodel_reference, update_operation)
204
222
  @worklist[viewmodel_reference] = update_operation
205
223
  end
206
224
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'renum'
2
4
  require 'view_model/schemas'
3
5
 
@@ -45,14 +47,13 @@ class ViewModel::ActiveRecord
45
47
  end
46
48
  end
47
49
 
48
- # Parser for collection updates. Collection updates have a regular
49
- # structure, but vary based on the contents. Parsing a {direct,
50
- # owned} collection recurses deeply and creates a tree of
51
- # UpdateDatas, while parsing a {through, shared} collection collects
52
- # reference strings.
50
+ # Parser for collection updates. Collection updates have a regular structure,
51
+ # but vary based on the contents. Parsing a nested collection recurses deeply
52
+ # and creates a tree of UpdateDatas, while parsing a referenced collection
53
+ # collects reference strings.
53
54
  class AbstractCollectionUpdate
54
- # Wraps a complete collection of new data: either UpdateDatas for owned
55
- # collections or reference strings for shared.
55
+ # Wraps a complete collection of new data: either UpdateDatas for non-root
56
+ # associations or reference strings for root.
56
57
  class Replace
57
58
  attr_reader :contents
58
59
  def initialize(contents)
@@ -61,7 +62,8 @@ class ViewModel::ActiveRecord
61
62
  end
62
63
 
63
64
  # Wraps an ordered list of FunctionalUpdates, each of whose `contents` are
64
- # either UpdateData for owned collections or references for shared.
65
+ # either UpdateData for nested associations or references for referenced
66
+ # associations.
65
67
  class Functional
66
68
  attr_reader :actions
67
69
  def initialize(actions)
@@ -81,8 +83,8 @@ class ViewModel::ActiveRecord
81
83
 
82
84
  # Resolve ViewModel::References used in the update's contents, whether by
83
85
  # reference or value.
84
- def used_vm_refs(update_context)
85
- raise "abstract"
86
+ def used_vm_refs(_update_context)
87
+ raise RuntimeError.new('abstract method')
86
88
  end
87
89
 
88
90
  def removed_vm_refs
@@ -153,7 +155,8 @@ class ViewModel::ActiveRecord
153
155
  # The shape of the actions are always the same
154
156
 
155
157
  # Parse an anchor for a functional_update, before/after
156
- # May only contain type and id fields, is never a reference even for shared collections.
158
+ # May only contain type and id fields, is never a reference even for
159
+ # referenced associations.
157
160
  def parse_anchor(child_hash) # final
158
161
  child_metadata = ViewModel.extract_reference_only_metadata(child_hash)
159
162
 
@@ -271,9 +274,13 @@ class ViewModel::ActiveRecord
271
274
 
272
275
  def used_vm_refs(update_context)
273
276
  update_datas
274
- .map { |upd| upd.viewmodel_reference if upd.id }
277
+ .map { |upd| resolve_vm_reference(upd, update_context) }
275
278
  .compact
276
279
  end
280
+
281
+ def resolve_vm_reference(update_data, _update_context)
282
+ update_data.viewmodel_reference if update_data.id
283
+ end
277
284
  end
278
285
 
279
286
  class Parser < AbstractCollectionUpdate::Parser
@@ -319,9 +326,13 @@ class ViewModel::ActiveRecord
319
326
 
320
327
  def used_vm_refs(update_context)
321
328
  references.map do |ref|
322
- update_context.resolve_reference(ref, nil).viewmodel_reference
329
+ resolve_vm_reference(ref, update_context)
323
330
  end
324
331
  end
332
+
333
+ def resolve_vm_reference(ref, update_context)
334
+ update_context.resolve_reference(ref, nil).viewmodel_reference
335
+ end
325
336
  end
326
337
 
327
338
  class Parser < AbstractCollectionUpdate::Parser
@@ -356,6 +367,7 @@ class ViewModel::ActiveRecord
356
367
  unless valid_reference_keys.include?(ref)
357
368
  raise ViewModel::DeserializationError::InvalidSharedReference.new(ref, blame_reference)
358
369
  end
370
+
359
371
  ref
360
372
  end
361
373
  end
@@ -421,7 +433,7 @@ class ViewModel::ActiveRecord
421
433
  fupdate_owned =
422
434
  fupdate_base.(ViewModel::Schemas::VIEWMODEL_UPDATE_SCHEMA)
423
435
 
424
- fupdate_shared =
436
+ fupdate_shared =
425
437
  fupdate_base.({ 'oneOf' => [ViewModel::Schemas::VIEWMODEL_REFERENCE_SCHEMA,
426
438
  viewmodel_reference_only] })
427
439
 
@@ -571,30 +583,15 @@ class ViewModel::ActiveRecord
571
583
  case
572
584
  when value.nil?
573
585
  []
574
- when association_data.shared?
586
+ when association_data.referenced?
575
587
  []
576
- when association_data.collection? # not shared, because of shared? check above
588
+ when association_data.collection? # nested, because of referenced? check above
577
589
  value.update_datas
578
590
  else
579
591
  [value]
580
592
  end
581
593
  end
582
594
 
583
- # Updates in terms of viewmodel associations
584
- def updated_associations
585
- deps = {}
586
-
587
- (associations.merge(referenced_associations)).each do |assoc_name, assoc_update|
588
- deps[assoc_name] =
589
- to_sequence(assoc_name, assoc_update)
590
- .each_with_object({}) do |update_data, updated_associations|
591
- updated_associations.deep_merge!(update_data.updated_associations)
592
- end
593
- end
594
-
595
- deps
596
- end
597
-
598
595
  def build_preload_specs(association_data, updates)
599
596
  if association_data.polymorphic?
600
597
  updates.map do |update_data|
@@ -682,6 +679,7 @@ class ViewModel::ActiveRecord
682
679
 
683
680
  when AssociationData
684
681
  association_data = member_data
682
+
685
683
  case
686
684
  when value.nil?
687
685
  if association_data.collection?
@@ -689,28 +687,28 @@ class ViewModel::ActiveRecord
689
687
  "Invalid collection update value 'nil' for association '#{name}'",
690
688
  blame_reference)
691
689
  end
692
- if association_data.shared?
690
+ if association_data.referenced?
693
691
  referenced_associations[name] = nil
694
692
  else
695
693
  associations[name] = nil
696
694
  end
697
695
 
698
- when association_data.through?
699
- referenced_associations[name] =
700
- ReferencedCollectionUpdate::Parser
701
- .new(association_data, blame_reference, valid_reference_keys)
702
- .parse(value)
696
+ when association_data.referenced?
697
+ if association_data.collection?
698
+ referenced_associations[name] =
699
+ ReferencedCollectionUpdate::Parser
700
+ .new(association_data, blame_reference, valid_reference_keys)
701
+ .parse(value)
702
+ else
703
+ # Extract and check reference
704
+ ref = ViewModel.extract_reference_metadata(value)
703
705
 
704
- when association_data.shared?
705
- # Extract and check reference
706
- ref = ViewModel.extract_reference_metadata(value)
706
+ unless valid_reference_keys.include?(ref)
707
+ raise ViewModel::DeserializationError::InvalidSharedReference.new(ref, blame_reference)
708
+ end
707
709
 
708
- unless valid_reference_keys.include?(ref)
709
- raise ViewModel::DeserializationError::InvalidSharedReference.new(ref, blame_reference)
710
+ referenced_associations[name] = ref
710
711
  end
711
-
712
- referenced_associations[name] = ref
713
-
714
712
  else
715
713
  if association_data.collection?
716
714
  associations[name] =