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
@@ -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] =