iknow_view_models 3.1.5 → 3.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +6 -6
- data/.rubocop.yml +18 -0
- data/Appraisals +6 -6
- data/Gemfile +6 -2
- data/Rakefile +5 -5
- data/gemfiles/rails_5_2.gemfile +5 -5
- data/gemfiles/rails_6_0.gemfile +9 -0
- data/iknow_view_models.gemspec +40 -38
- data/lib/iknow_view_models.rb +9 -7
- data/lib/iknow_view_models/version.rb +1 -1
- data/lib/view_model.rb +31 -17
- data/lib/view_model/access_control.rb +5 -2
- data/lib/view_model/access_control/composed.rb +10 -9
- data/lib/view_model/access_control/open.rb +2 -0
- data/lib/view_model/access_control/read_only.rb +2 -0
- data/lib/view_model/access_control/tree.rb +11 -6
- data/lib/view_model/access_control_error.rb +4 -1
- data/lib/view_model/active_record.rb +13 -12
- data/lib/view_model/active_record/association_data.rb +3 -2
- data/lib/view_model/active_record/association_manipulation.rb +6 -4
- data/lib/view_model/active_record/cache.rb +114 -34
- data/lib/view_model/active_record/cache/cacheable_view.rb +2 -2
- data/lib/view_model/active_record/collection_nested_controller.rb +3 -3
- data/lib/view_model/active_record/controller.rb +53 -1
- data/lib/view_model/active_record/controller_base.rb +4 -1
- data/lib/view_model/active_record/nested_controller_base.rb +1 -0
- data/lib/view_model/active_record/update_context.rb +8 -6
- data/lib/view_model/active_record/update_data.rb +32 -30
- data/lib/view_model/active_record/update_operation.rb +17 -13
- data/lib/view_model/active_record/visitor.rb +0 -1
- data/lib/view_model/after_transaction_runner.rb +2 -2
- data/lib/view_model/callbacks.rb +3 -1
- data/lib/view_model/controller.rb +13 -3
- data/lib/view_model/deserialization_error.rb +15 -12
- data/lib/view_model/error.rb +12 -10
- data/lib/view_model/error_view.rb +3 -1
- data/lib/view_model/migratable_view.rb +78 -0
- data/lib/view_model/migration.rb +48 -0
- data/lib/view_model/migration/no_path_error.rb +26 -0
- data/lib/view_model/migration/one_way_error.rb +24 -0
- data/lib/view_model/migration/unspecified_version_error.rb +24 -0
- data/lib/view_model/migrator.rb +108 -0
- data/lib/view_model/record.rb +15 -14
- data/lib/view_model/reference.rb +3 -1
- data/lib/view_model/references.rb +8 -5
- data/lib/view_model/registry.rb +1 -1
- data/lib/view_model/schemas.rb +9 -4
- data/lib/view_model/serialization_error.rb +4 -1
- data/lib/view_model/serialize_context.rb +4 -4
- data/lib/view_model/test_helpers.rb +8 -3
- data/lib/view_model/test_helpers/arvm_builder.rb +21 -15
- data/lib/view_model/traversal_context.rb +8 -5
- data/nix/dependencies.nix +5 -0
- data/nix/gem/generate.rb +2 -1
- data/shell.nix +8 -3
- data/test/.rubocop.yml +14 -0
- data/test/helpers/arvm_test_models.rb +12 -9
- data/test/helpers/arvm_test_utilities.rb +5 -3
- data/test/helpers/controller_test_helpers.rb +44 -28
- data/test/helpers/match_enumerator.rb +1 -0
- data/test/helpers/query_logging.rb +2 -1
- data/test/helpers/test_access_control.rb +5 -3
- data/test/helpers/viewmodel_spec_helpers.rb +88 -22
- data/test/unit/view_model/access_control_test.rb +144 -144
- data/test/unit/view_model/active_record/alias_test.rb +15 -13
- data/test/unit/view_model/active_record/belongs_to_test.rb +40 -39
- data/test/unit/view_model/active_record/cache_test.rb +68 -31
- data/test/unit/view_model/active_record/cloner_test.rb +67 -63
- data/test/unit/view_model/active_record/controller_test.rb +71 -38
- data/test/unit/view_model/active_record/counter_test.rb +10 -9
- data/test/unit/view_model/active_record/customization_test.rb +59 -58
- data/test/unit/view_model/active_record/has_many_test.rb +112 -111
- data/test/unit/view_model/active_record/has_many_through_poly_test.rb +15 -14
- data/test/unit/view_model/active_record/has_many_through_test.rb +33 -38
- data/test/unit/view_model/active_record/has_one_test.rb +37 -36
- data/test/unit/view_model/active_record/migration_test.rb +161 -0
- data/test/unit/view_model/active_record/namespacing_test.rb +19 -17
- data/test/unit/view_model/active_record/poly_test.rb +44 -45
- data/test/unit/view_model/active_record/shared_test.rb +30 -28
- data/test/unit/view_model/active_record/version_test.rb +9 -7
- data/test/unit/view_model/active_record_test.rb +72 -72
- data/test/unit/view_model/callbacks_test.rb +19 -15
- data/test/unit/view_model/controller_test.rb +4 -2
- data/test/unit/view_model/record_test.rb +92 -97
- data/test/unit/view_model/traversal_context_test.rb +4 -5
- data/test/unit/view_model_test.rb +18 -16
- metadata +36 -12
- data/.travis.yml +0 -31
- data/appveyor.yml +0 -22
- 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
|
data/lib/view_model/record.rb
CHANGED
@@ -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(
|
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,
|
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
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
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
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
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
|
data/lib/view_model/reference.rb
CHANGED
@@ -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
|
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
|
-
|
21
|
-
|
22
|
-
|
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:%
|
47
|
+
format('ref:i:%06<count>d', count: (@last_ref += 1))
|
45
48
|
end
|
46
49
|
end
|
47
50
|
end
|
data/lib/view_model/registry.rb
CHANGED
data/lib/view_model/schemas.rb
CHANGED
@@ -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)
|
@@ -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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
59
|
+
case viewmodel
|
60
|
+
when ViewModel::ActiveRecord
|
57
61
|
assert_model_represents_database(viewmodel.model, been_there: been_there)
|
58
|
-
|
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
|
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)
|
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
|
49
|
-
raise
|
50
|
-
raise
|
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
|
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 +
|
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
|
-
|
87
|
-
@model = Class.new(@model_base) do |
|
88
|
-
raise "Model already defined: #{model_name}" if
|
89
|
-
|
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
|
-
|
99
|
-
@viewmodel = Class.new(@viewmodel_base) do |
|
100
|
-
raise "Viewmodel alreay defined: #{vm_name}" if
|
101
|
-
|
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
|
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)
|
@@ -93,13 +94,15 @@ class ViewModel::TraversalContext
|
|
93
94
|
end
|
94
95
|
|
95
96
|
def run_callback(hook, view, **args)
|
96
|
-
|
97
|
-
|
98
|
-
end
|
99
|
-
|
97
|
+
# Run in-viewmodel callback hooks before context hooks, as they are
|
98
|
+
# permitted to alter the model.
|
100
99
|
if view.respond_to?(hook.dsl_viewmodel_callback_method)
|
101
100
|
view.public_send(hook.dsl_viewmodel_callback_method, hook.context_name => self, **args)
|
102
101
|
end
|
102
|
+
|
103
|
+
callbacks.each do |callback|
|
104
|
+
callback.run_callback(hook, view, self, **args)
|
105
|
+
end
|
103
106
|
end
|
104
107
|
|
105
108
|
def root?
|
@@ -116,7 +119,7 @@ class ViewModel::TraversalContext
|
|
116
119
|
|
117
120
|
def nearest_root_viewmodel
|
118
121
|
if root?
|
119
|
-
raise RuntimeError.new(
|
122
|
+
raise RuntimeError.new('Attempted to find nearest root from a root context. This is probably not what you wanted.')
|
120
123
|
elsif parent_context.root?
|
121
124
|
parent_viewmodel
|
122
125
|
else
|