iknow_view_models 3.1.7 → 3.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +6 -6
  3. data/.rubocop.yml +18 -0
  4. data/Appraisals +6 -6
  5. data/Gemfile +6 -2
  6. data/Rakefile +5 -5
  7. data/gemfiles/rails_5_2.gemfile +5 -5
  8. data/gemfiles/rails_6_0.gemfile +9 -0
  9. data/iknow_view_models.gemspec +40 -38
  10. data/lib/iknow_view_models.rb +9 -7
  11. data/lib/iknow_view_models/version.rb +1 -1
  12. data/lib/view_model.rb +31 -17
  13. data/lib/view_model/access_control.rb +5 -2
  14. data/lib/view_model/access_control/composed.rb +10 -9
  15. data/lib/view_model/access_control/open.rb +2 -0
  16. data/lib/view_model/access_control/read_only.rb +2 -0
  17. data/lib/view_model/access_control/tree.rb +11 -6
  18. data/lib/view_model/access_control_error.rb +4 -1
  19. data/lib/view_model/active_record.rb +17 -15
  20. data/lib/view_model/active_record/association_data.rb +2 -1
  21. data/lib/view_model/active_record/association_manipulation.rb +6 -4
  22. data/lib/view_model/active_record/cache.rb +114 -34
  23. data/lib/view_model/active_record/cache/cacheable_view.rb +2 -2
  24. data/lib/view_model/active_record/collection_nested_controller.rb +3 -3
  25. data/lib/view_model/active_record/controller.rb +68 -1
  26. data/lib/view_model/active_record/controller_base.rb +4 -1
  27. data/lib/view_model/active_record/nested_controller_base.rb +1 -0
  28. data/lib/view_model/active_record/update_context.rb +8 -6
  29. data/lib/view_model/active_record/update_data.rb +32 -30
  30. data/lib/view_model/active_record/update_operation.rb +17 -13
  31. data/lib/view_model/active_record/visitor.rb +0 -1
  32. data/lib/view_model/after_transaction_runner.rb +2 -2
  33. data/lib/view_model/callbacks.rb +3 -1
  34. data/lib/view_model/controller.rb +13 -3
  35. data/lib/view_model/deserialization_error.rb +15 -12
  36. data/lib/view_model/error.rb +12 -10
  37. data/lib/view_model/error_view.rb +3 -1
  38. data/lib/view_model/migratable_view.rb +78 -0
  39. data/lib/view_model/migration.rb +48 -0
  40. data/lib/view_model/migration/no_path_error.rb +26 -0
  41. data/lib/view_model/migration/one_way_error.rb +24 -0
  42. data/lib/view_model/migration/unspecified_version_error.rb +24 -0
  43. data/lib/view_model/migrator.rb +108 -0
  44. data/lib/view_model/record.rb +15 -14
  45. data/lib/view_model/reference.rb +3 -1
  46. data/lib/view_model/references.rb +8 -5
  47. data/lib/view_model/registry.rb +1 -1
  48. data/lib/view_model/schemas.rb +9 -4
  49. data/lib/view_model/serialization_error.rb +4 -1
  50. data/lib/view_model/serialize_context.rb +4 -4
  51. data/lib/view_model/test_helpers.rb +8 -3
  52. data/lib/view_model/test_helpers/arvm_builder.rb +21 -15
  53. data/lib/view_model/traversal_context.rb +2 -1
  54. data/nix/dependencies.nix +5 -0
  55. data/nix/gem/generate.rb +2 -1
  56. data/shell.nix +8 -3
  57. data/test/.rubocop.yml +14 -0
  58. data/test/helpers/arvm_test_models.rb +12 -9
  59. data/test/helpers/arvm_test_utilities.rb +5 -3
  60. data/test/helpers/controller_test_helpers.rb +55 -32
  61. data/test/helpers/match_enumerator.rb +1 -0
  62. data/test/helpers/query_logging.rb +2 -1
  63. data/test/helpers/test_access_control.rb +5 -3
  64. data/test/helpers/viewmodel_spec_helpers.rb +88 -22
  65. data/test/unit/view_model/access_control_test.rb +144 -144
  66. data/test/unit/view_model/active_record/alias_test.rb +15 -13
  67. data/test/unit/view_model/active_record/belongs_to_test.rb +40 -39
  68. data/test/unit/view_model/active_record/cache_test.rb +68 -31
  69. data/test/unit/view_model/active_record/cloner_test.rb +67 -63
  70. data/test/unit/view_model/active_record/controller_test.rb +113 -65
  71. data/test/unit/view_model/active_record/counter_test.rb +10 -9
  72. data/test/unit/view_model/active_record/customization_test.rb +59 -58
  73. data/test/unit/view_model/active_record/has_many_test.rb +112 -111
  74. data/test/unit/view_model/active_record/has_many_through_poly_test.rb +15 -14
  75. data/test/unit/view_model/active_record/has_many_through_test.rb +33 -38
  76. data/test/unit/view_model/active_record/has_one_test.rb +37 -36
  77. data/test/unit/view_model/active_record/migration_test.rb +161 -0
  78. data/test/unit/view_model/active_record/namespacing_test.rb +19 -17
  79. data/test/unit/view_model/active_record/poly_test.rb +44 -45
  80. data/test/unit/view_model/active_record/shared_test.rb +30 -28
  81. data/test/unit/view_model/active_record/version_test.rb +9 -7
  82. data/test/unit/view_model/active_record_test.rb +72 -72
  83. data/test/unit/view_model/callbacks_test.rb +19 -15
  84. data/test/unit/view_model/controller_test.rb +4 -2
  85. data/test/unit/view_model/record_test.rb +92 -97
  86. data/test/unit/view_model/traversal_context_test.rb +4 -5
  87. data/test/unit/view_model_test.rb +18 -16
  88. metadata +36 -12
  89. data/.travis.yml +0 -31
  90. data/appveyor.yml +0 -22
  91. data/gemfiles/rails_6_0_beta.gemfile +0 -9
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ViewModel::Migration::UnspecifiedVersionError < ViewModel::AbstractError
4
+ attr_reader :vm_name, :version
5
+
6
+ status 400
7
+
8
+ def initialize(vm_name, version)
9
+ @vm_name = vm_name
10
+ @version = version
11
+ super()
12
+ end
13
+
14
+ def detail
15
+ "Provided view for #{vm_name} at version #{version} does not match request"
16
+ end
17
+
18
+ def meta
19
+ {
20
+ viewmodel: vm_name,
21
+ version: version,
22
+ }
23
+ end
24
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ViewModel
4
+ class Migrator
5
+ class << self
6
+ def migrated_deep_schema_version(viewmodel_class, required_versions, include_referenced: true)
7
+ deep_schema_version = viewmodel_class.deep_schema_version(include_referenced: include_referenced)
8
+
9
+ if required_versions.present?
10
+ deep_schema_version = deep_schema_version.dup
11
+
12
+ required_versions.each do |required_vm_class, required_version|
13
+ name = required_vm_class.view_name
14
+ if deep_schema_version.has_key?(name)
15
+ deep_schema_version[name] = required_version
16
+ end
17
+ end
18
+ end
19
+
20
+ deep_schema_version
21
+ end
22
+ end
23
+
24
+ def initialize(required_versions)
25
+ @paths = required_versions.each_with_object({}) do |(viewmodel_class, required_version), h|
26
+ if required_version != viewmodel_class.schema_version
27
+ path = viewmodel_class.migration_path(from: required_version, to: viewmodel_class.schema_version)
28
+ h[viewmodel_class.view_name] = path
29
+ end
30
+ end
31
+
32
+ @versions = required_versions.each_with_object({}) do |(viewmodel_class, required_version), h|
33
+ h[viewmodel_class.view_name] = [required_version, viewmodel_class.schema_version]
34
+ end
35
+ end
36
+
37
+ def migrate!(node, references:)
38
+ case node
39
+ when Hash
40
+ if (type = node[ViewModel::TYPE_ATTRIBUTE])
41
+ version = node[ViewModel::VERSION_ATTRIBUTE]
42
+
43
+ if migrate_viewmodel!(type, version, node, references)
44
+ node[ViewModel::MIGRATED_ATTRIBUTE] = true
45
+ end
46
+ end
47
+
48
+ node.each_value do |child|
49
+ migrate!(child, references: references)
50
+ end
51
+ when Array
52
+ node.each { |child| migrate!(child, references: references) }
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def migrate_viewmodel!(_view_name, _version, _view_hash, _references)
59
+ raise RuntimeError.new('abstract method')
60
+ end
61
+ end
62
+
63
+ class UpMigrator < Migrator
64
+ private
65
+
66
+ def migrate_viewmodel!(view_name, source_version, view_hash, references)
67
+ path = @paths[view_name]
68
+ return false unless path
69
+
70
+ # We assume that an unspecified source version is the same as the required
71
+ # version.
72
+ required_version, current_version = @versions[view_name]
73
+
74
+ unless source_version.nil? || source_version == required_version
75
+ raise ViewModel::Migration::UnspecifiedVersionError.new(view_name, source_version)
76
+ end
77
+
78
+ path.each do |migration|
79
+ migration.up(view_hash, references)
80
+ end
81
+
82
+ view_hash[ViewModel::VERSION_ATTRIBUTE] = current_version
83
+
84
+ true
85
+ end
86
+ end
87
+
88
+ # down migrations find a reverse path from the current schema version to the
89
+ # specific version requested by the client.
90
+ class DownMigrator < Migrator
91
+ private
92
+
93
+ def migrate_viewmodel!(view_name, _, view_hash, references)
94
+ path = @paths[view_name]
95
+ return false unless path
96
+
97
+ required_version, _current_version = @versions[view_name]
98
+
99
+ path.reverse_each do |migration|
100
+ migration.down(view_hash, references)
101
+ end
102
+
103
+ view_hash[ViewModel::VERSION_ATTRIBUTE] = required_version
104
+
105
+ true
106
+ end
107
+ end
108
+ end
@@ -10,6 +10,9 @@ class ViewModel::Record < ViewModel
10
10
  attr_accessor :model
11
11
 
12
12
  require 'view_model/record/attribute_data'
13
+ require 'view_model/migratable_view'
14
+
15
+ include ViewModel::MigratableView
13
16
 
14
17
  class << self
15
18
  attr_reader :_members
@@ -113,7 +116,7 @@ class ViewModel::Record < ViewModel
113
116
  end
114
117
  end
115
118
 
116
- def resolve_viewmodel(metadata, view_hash, deserialize_context:)
119
+ def resolve_viewmodel(_metadata, _view_hash, deserialize_context:)
117
120
  self.for_new_model
118
121
  end
119
122
 
@@ -178,6 +181,8 @@ class ViewModel::Record < ViewModel
178
181
  @changed_attributes = []
179
182
  @changed_nested_children = false
180
183
  @changed_referenced_children = false
184
+
185
+ super()
181
186
  end
182
187
 
183
188
  # VM::Record identity matches the identity of its model. If the model has a
@@ -207,7 +212,7 @@ class ViewModel::Record < ViewModel
207
212
  end
208
213
 
209
214
  def serialize_view(json, serialize_context: self.class.new_serialize_context)
210
- json.set!(ViewModel::ID_ATTRIBUTE, model.id) if model.respond_to?(:id)
215
+ json.set!(ViewModel::ID_ATTRIBUTE, self.id) if stable_id?
211
216
  json.set!(ViewModel::TYPE_ATTRIBUTE, self.view_name)
212
217
  json.set!(ViewModel::VERSION_ATTRIBUTE, self.class.schema_version)
213
218
 
@@ -306,12 +311,10 @@ class ViewModel::Record < ViewModel
306
311
  # viewmodel), it's only desired for converting the value to and from wire
307
312
  # format, so conversion is deferred to serialization time.
308
313
  value = attr_data.map_value(value) do |v|
309
- begin
310
- attr_data.attribute_serializer.dump(v, json: true)
311
- rescue IknowParams::Serializer::DumpError => ex
312
- raise ViewModel::SerializationError.new(
313
- "Could not serialize invalid value '#{vm_attr_name}': #{ex.message}")
314
- end
314
+ attr_data.attribute_serializer.dump(v, json: true)
315
+ rescue IknowParams::Serializer::DumpError => ex
316
+ raise ViewModel::SerializationError.new(
317
+ "Could not serialize invalid value '#{vm_attr_name}': #{ex.message}")
315
318
  end
316
319
  end
317
320
 
@@ -339,12 +342,10 @@ class ViewModel::Record < ViewModel
339
342
  end
340
343
  when attr_data.using_serializer?
341
344
  attr_data.map_value(serialized_value) do |sv|
342
- begin
343
- attr_data.attribute_serializer.load(sv)
344
- rescue IknowParams::Serializer::LoadError => ex
345
- reason = "could not be deserialized because #{ex.message}"
346
- raise ViewModel::DeserializationError::Validation.new(vm_attr_name, reason, {}, blame_reference)
347
- end
345
+ attr_data.attribute_serializer.load(sv)
346
+ rescue IknowParams::Serializer::LoadError => ex
347
+ reason = "could not be deserialized because #{ex.message}"
348
+ raise ViewModel::DeserializationError::Validation.new(vm_attr_name, reason, {}, blame_reference)
348
349
  end
349
350
  else
350
351
  serialized_value
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ViewModel
2
4
  # Key to identify a viewmodel with some kind of inherent ID (e.g. an ViewModel::ActiveRecord)
3
5
  class Reference
@@ -22,7 +24,7 @@ class ViewModel
22
24
  other.model_id == model_id
23
25
  end
24
26
 
25
- alias :eql? :==
27
+ alias eql? ==
26
28
 
27
29
  def hash
28
30
  [viewmodel_class, model_id].hash
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ViewModel
2
4
  # A bucket for configuration, used for serializing and deserializing.
3
5
  class References
@@ -17,14 +19,15 @@ class ViewModel
17
19
  # under which the data is stored. If the data is not present, will compute
18
20
  # it by calling the given block.
19
21
  def add_reference(value)
20
- if (ref = @ref_by_value[value]).present?
21
- ref
22
- else
22
+ ref = @ref_by_value[value]
23
+
24
+ unless ref.present?
23
25
  ref = new_ref!(value)
24
26
  @ref_by_value[value] = ref
25
27
  @value_by_ref[ref] = value
26
- ref
27
28
  end
29
+
30
+ ref
28
31
  end
29
32
 
30
33
  def clear!
@@ -41,7 +44,7 @@ class ViewModel
41
44
  hash = Digest::SHA256.base64digest("#{vm_ref.viewmodel_class.name}.#{vm_ref.model_id}")
42
45
  "ref:h:#{hash}"
43
46
  else
44
- 'ref:i:%06d' % (@last_ref += 1)
47
+ format('ref:i:%06<count>d', count: (@last_ref += 1))
45
48
  end
46
49
  end
47
50
  end
@@ -12,7 +12,7 @@ class ViewModel::Registry
12
12
 
13
13
  def initialize
14
14
  @lock = Monitor.new
15
- @viewmodel_classes_by_name = {}
15
+ @viewmodel_classes_by_name = {}
16
16
  @deferred_viewmodel_classes = []
17
17
  end
18
18
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'json_schema'
3
5
 
@@ -9,7 +11,8 @@ class ViewModel::Schemas
9
11
 
10
12
  ID_SCHEMA =
11
13
  { 'oneOf' => [{ 'type' => 'integer' },
12
- { 'type' => 'string', 'format' => 'uuid' }] }
14
+ { 'type' => 'string', 'format' => 'uuid' },] }.freeze
15
+
13
16
  ID = JsonSchema.parse!(ID_SCHEMA)
14
17
 
15
18
  VIEWMODEL_UPDATE_SCHEMA =
@@ -20,8 +23,9 @@ class ViewModel::Schemas
20
23
  ViewModel::ID_ATTRIBUTE => ID_SCHEMA,
21
24
  ViewModel::NEW_ATTRIBUTE => { 'type' => 'boolean' },
22
25
  ViewModel::VERSION_ATTRIBUTE => { 'type' => 'integer' } },
23
- 'required' => [ViewModel::TYPE_ATTRIBUTE]
24
- }
26
+ 'required' => [ViewModel::TYPE_ATTRIBUTE],
27
+ }.freeze
28
+
25
29
  VIEWMODEL_UPDATE = JsonSchema.parse!(VIEWMODEL_UPDATE_SCHEMA)
26
30
 
27
31
  VIEWMODEL_REFERENCE_SCHEMA =
@@ -31,7 +35,8 @@ class ViewModel::Schemas
31
35
  'properties' => { ViewModel::REFERENCE_ATTRIBUTE => { 'type' => 'string' } },
32
36
  'additionalProperties' => false,
33
37
  'required' => [ViewModel::REFERENCE_ATTRIBUTE],
34
- }
38
+ }.freeze
39
+
35
40
  VIEWMODEL_REFERENCE = JsonSchema.parse!(VIEWMODEL_REFERENCE_SCHEMA)
36
41
 
37
42
  def self.verify_schema!(schema, value)
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ViewModel::SerializationError < ViewModel::AbstractError
2
4
  attr_reader :detail
5
+
3
6
  status 400
4
- code "SerializationError"
7
+ code 'SerializationError'
5
8
 
6
9
  def initialize(detail)
7
10
  @detail = detail
@@ -38,10 +38,10 @@ class ViewModel::SerializeContext < ViewModel::TraversalContext
38
38
 
39
39
  while references.present?
40
40
  extract_referenced_views!.each do |ref, value|
41
- unless serialized_refs.has_key?(ref)
42
- serialized_refs[ref] = Jbuilder.new do |j|
43
- ViewModel.serialize(value, j, serialize_context: reference_context)
44
- end
41
+ next if serialized_refs.has_key?(ref)
42
+
43
+ serialized_refs[ref] = Jbuilder.new do |j|
44
+ ViewModel.serialize(value, j, serialize_context: reference_context)
45
45
  end
46
46
  end
47
47
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ##
2
4
  # Helpers useful for writing tests for viewmodel implementations
3
5
  module ViewModel::TestHelpers
@@ -51,11 +53,13 @@ module ViewModel::TestHelpers
51
53
 
52
54
  def assert_consistent_record(viewmodel, been_there: Set.new)
53
55
  return if been_there.include?(viewmodel.model)
56
+
54
57
  been_there << viewmodel.model
55
58
 
56
- if viewmodel.is_a?(ViewModel::ActiveRecord)
59
+ case viewmodel
60
+ when ViewModel::ActiveRecord
57
61
  assert_model_represents_database(viewmodel.model, been_there: been_there)
58
- elsif viewmodel.is_a?(ViewModel::Record)
62
+ when ViewModel::Record
59
63
  viewmodel.class._members.each do |name, attribute_data|
60
64
  if attribute_data.attribute_viewmodel
61
65
  assert_consistent_record(viewmodel.send(name), been_there: been_there)
@@ -66,6 +70,7 @@ module ViewModel::TestHelpers
66
70
 
67
71
  def assert_model_represents_database(model, been_there: Set.new)
68
72
  return if been_there.include?(model)
73
+
69
74
  been_there << model
70
75
 
71
76
  refute(model.new_record?, 'model represents database entity')
@@ -83,7 +88,7 @@ module ViewModel::TestHelpers
83
88
  next unless association.loaded?
84
89
 
85
90
  case
86
- when association.target == nil
91
+ when association.target.nil?
87
92
  assert_nil(database_model.association(reflection.name).target,
88
93
  'in memory nil association matches database')
89
94
  when reflection.collection?
@@ -1,10 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ViewModel::TestHelpers::ARVMBuilder
2
4
  attr_reader :name, :model, :viewmodel, :namespace
3
5
 
4
6
  # Building an ARVM requires three blocks, to define schema, model and
5
7
  # viewmodel. Support providing these either in an spec argument or as a
6
8
  # dsl-style builder.
7
- Spec = Struct.new(:schema, :model, :viewmodel) do
9
+ Spec = Struct.new(:schema, :model, :viewmodel)
10
+ class Spec
8
11
  def initialize(schema:, model:, viewmodel:)
9
12
  super(schema, model, viewmodel)
10
13
  end
@@ -45,16 +48,16 @@ class ViewModel::TestHelpers::ARVMBuilder
45
48
  instance_eval(&block)
46
49
  end
47
50
 
48
- raise "Model not created in ARVMBuilder" unless model
49
- raise "Schema not created in ARVMBuilder" unless model.table_exists?
50
- raise "ViewModel not created in ARVMBuilder" unless (viewmodel || @no_viewmodel)
51
+ raise 'Model not created in ARVMBuilder' unless model
52
+ raise 'Schema not created in ARVMBuilder' unless model.table_exists?
53
+ raise 'ViewModel not created in ARVMBuilder' unless viewmodel || @no_viewmodel
51
54
 
52
55
  # Force the realization of the view model into the library's lookup
53
56
  # table. If this doesn't happen the library may have conflicting entries in
54
57
  # the deferred table, and will allow viewmodels to leak between tests.
55
58
  unless @no_viewmodel || !(@viewmodel < ViewModel::Record)
56
59
  resolved = ViewModel::Registry.for_view_name(viewmodel.view_name)
57
- raise "Failed to register expected new class!" unless resolved == @viewmodel
60
+ raise 'Failed to register expected new class!' unless resolved == @viewmodel
58
61
  end
59
62
  end
60
63
 
@@ -69,7 +72,7 @@ class ViewModel::TestHelpers::ARVMBuilder
69
72
  private
70
73
 
71
74
  def viewmodel_name
72
- self.name + "View"
75
+ self.name + 'View'
73
76
  end
74
77
 
75
78
  def define_schema(&block)
@@ -83,10 +86,11 @@ class ViewModel::TestHelpers::ARVMBuilder
83
86
 
84
87
  def define_model(&block)
85
88
  model_name = name
86
- _namespace = namespace
87
- @model = Class.new(@model_base) do |c|
88
- raise "Model already defined: #{model_name}" if _namespace.const_defined?(model_name, false)
89
- _namespace.const_set(model_name, self)
89
+ model_namespace = namespace
90
+ @model = Class.new(@model_base) do |_c|
91
+ raise "Model already defined: #{model_name}" if model_namespace.const_defined?(model_name, false)
92
+
93
+ model_namespace.const_set(model_name, self)
90
94
  class_eval(&block)
91
95
  reset_column_information
92
96
  end
@@ -95,13 +99,15 @@ class ViewModel::TestHelpers::ARVMBuilder
95
99
 
96
100
  def define_viewmodel(&block)
97
101
  vm_name = viewmodel_name
98
- _namespace = namespace
99
- @viewmodel = Class.new(@viewmodel_base) do |c|
100
- raise "Viewmodel alreay defined: #{vm_name}" if _namespace.const_defined?(vm_name, false)
101
- _namespace.const_set(vm_name, self)
102
+ vm_namespace = namespace
103
+ @viewmodel = Class.new(@viewmodel_base) do |_c|
104
+ raise "Viewmodel alreay defined: #{vm_name}" if vm_namespace.const_defined?(vm_name, false)
105
+
106
+ vm_namespace.const_set(vm_name, self)
102
107
  class_eval(&block)
103
108
  end
104
- raise "help help" if @viewmodel.name.nil?
109
+ raise 'help help' if @viewmodel.name.nil?
110
+
105
111
  @viewmodel
106
112
  end
107
113
 
@@ -21,6 +21,7 @@ class ViewModel::TraversalContext
21
21
  end
22
22
 
23
23
  attr_reader :shared_context
24
+
24
25
  delegate :access_control, :callbacks, to: :shared_context
25
26
 
26
27
  def self.new_child(*args)
@@ -118,7 +119,7 @@ class ViewModel::TraversalContext
118
119
 
119
120
  def nearest_root_viewmodel
120
121
  if root?
121
- raise RuntimeError.new("Attempted to find nearest root from a root context. This is probably not what you wanted.")
122
+ raise RuntimeError.new('Attempted to find nearest root from a root context. This is probably not what you wanted.')
122
123
  elsif parent_context.root?
123
124
  parent_viewmodel
124
125
  else