iknow_view_models 3.1.6 → 3.2.2

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 (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 +13 -12
  20. data/lib/view_model/active_record/association_data.rb +3 -2
  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