iknow_view_models 3.4.1 → 3.5.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.
- checksums.yaml +4 -4
- data/iknow_view_models.gemspec +2 -2
- data/lib/iknow_view_models/version.rb +1 -1
- data/lib/view_model.rb +12 -0
- data/lib/view_model/active_record.rb +46 -5
- data/lib/view_model/active_record/association_data.rb +3 -1
- data/lib/view_model/active_record/association_manipulation.rb +186 -49
- data/lib/view_model/active_record/cloner.rb +13 -12
- data/lib/view_model/active_record/collection_nested_controller.rb +5 -2
- data/lib/view_model/active_record/controller_base.rb +45 -6
- data/lib/view_model/active_record/nested_controller_base.rb +126 -14
- data/lib/view_model/active_record/singular_nested_controller.rb +5 -2
- data/lib/view_model/callbacks.rb +1 -1
- data/lib/view_model/controller.rb +24 -2
- data/lib/view_model/record.rb +1 -1
- data/lib/view_model/schemas.rb +44 -0
- data/test/helpers/arvm_test_utilities.rb +65 -0
- data/test/helpers/controller_test_helpers.rb +65 -34
- data/test/unit/view_model/active_record/controller_nested_test.rb +599 -0
- data/test/unit/view_model/active_record/controller_test.rb +6 -362
- data/test/unit/view_model/active_record/has_many_test.rb +24 -7
- data/test/unit/view_model/active_record/has_many_through_test.rb +28 -12
- data/test/unit/view_model/traversal_context_test.rb +15 -1
- metadata +7 -5
@@ -3,8 +3,7 @@
|
|
3
3
|
require 'view_model/active_record/nested_controller_base'
|
4
4
|
|
5
5
|
# Controller mixin for accessing a root ViewModel which can be accessed in a
|
6
|
-
# collection by a parent model.
|
7
|
-
# :children` on the viewmodel controller
|
6
|
+
# collection by a parent model.
|
8
7
|
|
9
8
|
# Contributes the following routes:
|
10
9
|
# PUT /parents/:parent_id/children #append -- deserialize (possibly existing) children and append to collection
|
@@ -32,6 +31,10 @@ module ViewModel::ActiveRecord::CollectionNestedController
|
|
32
31
|
write_association(serialize_context: serialize_context, deserialize_context: deserialize_context, lock_owner: lock_owner, &block)
|
33
32
|
end
|
34
33
|
|
34
|
+
def replace_bulk(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, &block)
|
35
|
+
write_association_bulk(serialize_context: serialize_context, deserialize_context: deserialize_context, &block)
|
36
|
+
end
|
37
|
+
|
35
38
|
def disassociate_all(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
|
36
39
|
destroy_association(true, serialize_context: serialize_context, deserialize_context: deserialize_context, lock_owner: lock_owner)
|
37
40
|
end
|
@@ -102,15 +102,35 @@ module ActionDispatch
|
|
102
102
|
def arvm_resources(resource_name, options = {}, &block)
|
103
103
|
except = options.delete(:except) { [] }
|
104
104
|
add_shallow_routes = options.delete(:add_shallow_routes) { true }
|
105
|
+
defaults = options.delete(:defaults) { {} }
|
105
106
|
|
106
107
|
nested = shallow_nesting_depth > 0
|
107
108
|
|
109
|
+
association_name = options.delete(:association_name) { resource_name.to_s }
|
110
|
+
owner_viewmodel = options.delete(:owner_viewmodel) do
|
111
|
+
if nested
|
112
|
+
parent_resource.name.to_s.singularize.camelize
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
defaults = {
|
117
|
+
association_name: association_name,
|
118
|
+
owner_viewmodel: owner_viewmodel,
|
119
|
+
}.merge(defaults)
|
120
|
+
|
108
121
|
only_routes = []
|
109
122
|
only_routes += [:create] unless nested
|
110
123
|
only_routes += [:show, :destroy] if add_shallow_routes
|
111
124
|
only_routes -= except
|
112
125
|
|
113
|
-
|
126
|
+
# Bulk replace
|
127
|
+
if nested && !except.include?(:replace_bulk)
|
128
|
+
collection do
|
129
|
+
post resource_name, controller: resource_name, action: :replace_bulk, as: nil, defaults: defaults
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
resources resource_name, shallow: true, only: only_routes, defaults: defaults, **options do
|
114
134
|
instance_eval(&block) if block_given?
|
115
135
|
|
116
136
|
if nested
|
@@ -149,16 +169,35 @@ module ActionDispatch
|
|
149
169
|
def arvm_resource(resource_name, options = {}, &block)
|
150
170
|
except = options.delete(:except) { [] }
|
151
171
|
add_shallow_routes = options.delete(:add_shallow_routes) { true }
|
172
|
+
defaults = options.delete(:defaults) { {} }
|
152
173
|
|
153
174
|
only_routes = []
|
154
|
-
|
155
|
-
|
156
|
-
|
175
|
+
nested = shallow_nesting_depth > 0
|
176
|
+
|
177
|
+
association_name = options.delete(:association_name) { resource_name.to_s }
|
178
|
+
owner_viewmodel = options.delete(:owner_viewmodel) do
|
179
|
+
if nested
|
180
|
+
parent_resource.name.to_s.singularize.camelize
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
defaults = {
|
185
|
+
association_name: association_name,
|
186
|
+
owner_viewmodel: owner_viewmodel,
|
187
|
+
}.merge(defaults)
|
188
|
+
|
189
|
+
if nested && !except.include?(:create_associated_bulk)
|
190
|
+
collection do
|
191
|
+
post resource_name, controller: resource_name, action: :create_associated_bulk, as: nil, defaults: defaults
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
resource resource_name, shallow: true, only: only_routes, defaults: defaults, **options do
|
157
196
|
instance_eval(&block) if block_given?
|
158
197
|
|
159
198
|
name_route = { as: '' } # Only one route may take the name
|
160
199
|
|
161
|
-
if
|
200
|
+
if nested
|
162
201
|
post('', action: :create_associated, **name_route.extract!(:as)) unless except.include?(:create)
|
163
202
|
get('', action: :show_associated, **name_route.extract!(:as)) unless except.include?(:show)
|
164
203
|
delete('', action: :destroy_associated, **name_route.extract!(:as)) unless except.include?(:destroy)
|
@@ -170,7 +209,7 @@ module ActionDispatch
|
|
170
209
|
end
|
171
210
|
|
172
211
|
# singularly nested resources provide collection accessors at the top level
|
173
|
-
if
|
212
|
+
if nested && add_shallow_routes
|
174
213
|
resources resource_name.to_s.pluralize, shallow: true, only: [:show, :destroy] - except do
|
175
214
|
shallow_scope do
|
176
215
|
collection do
|
@@ -7,15 +7,51 @@ require 'view_model/active_record/controller_base'
|
|
7
7
|
module ViewModel::ActiveRecord::NestedControllerBase
|
8
8
|
extend ActiveSupport::Concern
|
9
9
|
|
10
|
+
class ParentProxyModel < ViewModel
|
11
|
+
# Prevent this from appearing in hooks
|
12
|
+
self.synthetic = true
|
13
|
+
|
14
|
+
attr_reader :parent, :association_data, :changed_children
|
15
|
+
|
16
|
+
def initialize(parent, association_data, changed_children)
|
17
|
+
@parent = parent
|
18
|
+
@association_data = association_data
|
19
|
+
@changed_children = changed_children
|
20
|
+
end
|
21
|
+
|
22
|
+
def serialize(json, serialize_context:)
|
23
|
+
ViewModel::Callbacks.wrap_serialize(parent, context: serialize_context) do
|
24
|
+
child_context = parent.context_for_child(association_data.association_name, context: serialize_context)
|
25
|
+
|
26
|
+
json.set!(ViewModel::ID_ATTRIBUTE, parent.id)
|
27
|
+
json.set!(ViewModel::BULK_UPDATE_ATTRIBUTE) do
|
28
|
+
if association_data.referenced? && !association_data.owned?
|
29
|
+
if association_data.collection?
|
30
|
+
json.array!(changed_children) do |child|
|
31
|
+
ViewModel.serialize_as_reference(child, json, serialize_context: child_context)
|
32
|
+
end
|
33
|
+
else
|
34
|
+
ViewModel.serialize_as_reference(changed_children, json, serialize_context: child_context)
|
35
|
+
end
|
36
|
+
else
|
37
|
+
ViewModel.serialize(changed_children, json, serialize_context: child_context)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
10
44
|
protected
|
11
45
|
|
12
46
|
def show_association(scope: nil, serialize_context: new_serialize_context, lock_owner: nil)
|
47
|
+
require_external_referenced_association!
|
48
|
+
|
13
49
|
associated_views = nil
|
14
50
|
pre_rendered = owner_viewmodel.transaction do
|
15
51
|
owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false, lock: lock_owner)
|
16
52
|
ViewModel::Callbacks.wrap_serialize(owner_view, context: serialize_context) do
|
17
53
|
# Association manipulation methods construct child contexts internally
|
18
|
-
associated_views = owner_view.load_associated(association_name, scope: scope)
|
54
|
+
associated_views = owner_view.load_associated(association_name, scope: scope, serialize_context: serialize_context)
|
19
55
|
|
20
56
|
associated_views = yield(associated_views) if block_given?
|
21
57
|
|
@@ -27,11 +63,27 @@ module ViewModel::ActiveRecord::NestedControllerBase
|
|
27
63
|
associated_views
|
28
64
|
end
|
29
65
|
|
66
|
+
# This method always takes direct update hashes, and returns
|
67
|
+
# viewmodels directly.
|
68
|
+
#
|
69
|
+
# There's no multi membership, so when viewing the children of a
|
70
|
+
# single parent each child can only appear once. This means it's
|
71
|
+
# safe to use update hashes directly.
|
30
72
|
def write_association(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
|
73
|
+
require_external_referenced_association!
|
74
|
+
|
31
75
|
association_view = nil
|
32
76
|
pre_rendered = owner_viewmodel.transaction do
|
33
77
|
update_hash, refs = parse_viewmodel_updates
|
34
78
|
|
79
|
+
update_hash =
|
80
|
+
ViewModel::ActiveRecord.add_reference_indirection(
|
81
|
+
update_hash,
|
82
|
+
association_data: association_data,
|
83
|
+
references: refs,
|
84
|
+
key: 'write-association',
|
85
|
+
)
|
86
|
+
|
35
87
|
owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false, lock: lock_owner)
|
36
88
|
|
37
89
|
association_view = owner_view.replace_associated(association_name, update_hash,
|
@@ -49,7 +101,67 @@ module ViewModel::ActiveRecord::NestedControllerBase
|
|
49
101
|
association_view
|
50
102
|
end
|
51
103
|
|
104
|
+
# This method takes direct update hashes for owned associations, and
|
105
|
+
# reference hashes for shared associations. The return value matches
|
106
|
+
# the input structure.
|
107
|
+
#
|
108
|
+
# If an association is referenced and owned, each child may only
|
109
|
+
# appear once so each is guaranteed to have a unique update
|
110
|
+
# hash. This means it's only safe to use update hashes directly in
|
111
|
+
# this case.
|
112
|
+
def write_association_bulk(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
|
113
|
+
require_external_referenced_association!
|
114
|
+
|
115
|
+
updated_by_parent_viewmodel = nil
|
116
|
+
|
117
|
+
pre_rendered = owner_viewmodel.transaction do
|
118
|
+
updates_by_parent_id, references = parse_bulk_update
|
119
|
+
|
120
|
+
if association_data.owned?
|
121
|
+
updates_by_parent_id.transform_values!.with_index do |update_hash, index|
|
122
|
+
ViewModel::ActiveRecord.add_reference_indirection(
|
123
|
+
update_hash,
|
124
|
+
association_data: association_data,
|
125
|
+
references: references,
|
126
|
+
key: "write-association-bulk-#{index}",
|
127
|
+
)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
updated_by_parent_viewmodel =
|
132
|
+
owner_viewmodel.replace_associated_bulk(
|
133
|
+
association_name,
|
134
|
+
updates_by_parent_id,
|
135
|
+
references: references,
|
136
|
+
deserialize_context: deserialize_context,
|
137
|
+
)
|
138
|
+
|
139
|
+
views = updated_by_parent_viewmodel.flat_map { |_parent_viewmodel, updated_views| Array.wrap(updated_views) }
|
140
|
+
|
141
|
+
ViewModel.preload_for_serialization(views)
|
142
|
+
|
143
|
+
updated_by_parent_viewmodel = yield(updated_by_parent_viewmodel) if block_given?
|
144
|
+
|
145
|
+
return_updates = updated_by_parent_viewmodel.map do |owner_view, updated_views|
|
146
|
+
ParentProxyModel.new(owner_view, association_data, updated_views)
|
147
|
+
end
|
148
|
+
|
149
|
+
return_structure = {
|
150
|
+
ViewModel::TYPE_ATTRIBUTE => ViewModel::BULK_UPDATE_TYPE,
|
151
|
+
ViewModel::BULK_UPDATES_ATTRIBUTE => return_updates,
|
152
|
+
}
|
153
|
+
|
154
|
+
prerender_viewmodel(return_structure, serialize_context: serialize_context)
|
155
|
+
end
|
156
|
+
|
157
|
+
render_json_string(pre_rendered)
|
158
|
+
updated_by_parent_viewmodel
|
159
|
+
end
|
160
|
+
|
161
|
+
|
52
162
|
def destroy_association(collection, serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
|
163
|
+
require_external_referenced_association!
|
164
|
+
|
53
165
|
if lock_owner
|
54
166
|
owner_viewmodel.find(owner_viewmodel_id, eager_include: false, lock: lock_owner)
|
55
167
|
end
|
@@ -61,7 +173,7 @@ module ViewModel::ActiveRecord::NestedControllerBase
|
|
61
173
|
end
|
62
174
|
|
63
175
|
def association_data
|
64
|
-
owner_viewmodel._association_data(association_name)
|
176
|
+
@association_data ||= owner_viewmodel._association_data(association_name)
|
65
177
|
end
|
66
178
|
|
67
179
|
def owner_update_hash(update)
|
@@ -78,22 +190,22 @@ module ViewModel::ActiveRecord::NestedControllerBase
|
|
78
190
|
parse_param(id_param_name, **default)
|
79
191
|
end
|
80
192
|
|
81
|
-
|
82
|
-
|
193
|
+
def owner_viewmodel_class_for_name(name)
|
194
|
+
ViewModel::Registry.for_view_name(name)
|
83
195
|
end
|
84
196
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
unless owner.is_a?(Class) && owner < ViewModel::Record
|
90
|
-
owner = ViewModel::Registry.for_view_name(owner.to_s.camelize)
|
91
|
-
end
|
197
|
+
def owner_viewmodel
|
198
|
+
name = params.fetch(:owner_viewmodel) { raise ArgumentError.new("No owner viewmodel present") }
|
199
|
+
owner_viewmodel_class_for_name(name.to_s.camelize)
|
200
|
+
end
|
92
201
|
|
93
|
-
|
94
|
-
|
202
|
+
def association_name
|
203
|
+
params.fetch(:association_name) { raise ArgumentError.new('No association name from routes') }
|
204
|
+
end
|
95
205
|
|
96
|
-
|
206
|
+
def require_external_referenced_association!
|
207
|
+
unless association_data.referenced? && association_data.external?
|
208
|
+
raise ArgumentError.new("Expected referenced external association: '#{association_name}'")
|
97
209
|
end
|
98
210
|
end
|
99
211
|
end
|
@@ -1,8 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Controller mixin for accessing a root ViewModel which can be accessed
|
4
|
-
# individually by a parent model.
|
5
|
-
# :child` on the viewmodel controller
|
4
|
+
# individually by a parent model.
|
6
5
|
|
7
6
|
# Contributes the following routes:
|
8
7
|
# POST /parents/:parent_id/child #create_associated -- deserialize (possibly existing) child, replacing existing child
|
@@ -28,6 +27,10 @@ module ViewModel::ActiveRecord::SingularNestedController
|
|
28
27
|
write_association(serialize_context: serialize_context, deserialize_context: deserialize_context, lock_owner: lock_owner, &block)
|
29
28
|
end
|
30
29
|
|
30
|
+
def create_associated_bulk(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, &block)
|
31
|
+
write_association_bulk(serialize_context: serialize_context, deserialize_context: deserialize_context, &block)
|
32
|
+
end
|
33
|
+
|
31
34
|
def destroy_associated(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
|
32
35
|
destroy_association(false, serialize_context: serialize_context, deserialize_context: deserialize_context, lock_owner: lock_owner)
|
33
36
|
end
|
data/lib/view_model/callbacks.rb
CHANGED
@@ -181,7 +181,7 @@ module ViewModel::Callbacks
|
|
181
181
|
# ARVM synthetic views are considered part of their association and as such
|
182
182
|
# are not visited by callbacks. Eligibility exclusion is intended to be
|
183
183
|
# library-internal: subclasses should not attempt to extend this.
|
184
|
-
view.is_a?(ViewModel
|
184
|
+
view.is_a?(ViewModel) && view.class.synthetic
|
185
185
|
end
|
186
186
|
|
187
187
|
def self.wrap_serialize(viewmodel, context:)
|
@@ -75,12 +75,34 @@ module ViewModel::Controller
|
|
75
75
|
protected
|
76
76
|
|
77
77
|
def parse_viewmodel_updates
|
78
|
-
|
79
|
-
|
78
|
+
data_param = params.fetch(:data) do
|
79
|
+
raise ViewModel::Error.new(status: 400, detail: "Missing 'data' parameter")
|
80
|
+
end
|
81
|
+
refs_param = params.fetch(:references, {})
|
82
|
+
|
83
|
+
update_hash = _extract_update_data(data_param)
|
84
|
+
refs = _extract_param_hash(refs_param)
|
80
85
|
|
81
86
|
return update_hash, refs
|
82
87
|
end
|
83
88
|
|
89
|
+
def parse_bulk_update
|
90
|
+
data, references = parse_viewmodel_updates
|
91
|
+
|
92
|
+
ViewModel::Schemas.verify_schema!(ViewModel::Schemas::BULK_UPDATE, data)
|
93
|
+
|
94
|
+
updates_by_parent =
|
95
|
+
data.fetch(ViewModel::BULK_UPDATES_ATTRIBUTE).each_with_object({}) do |parent_update, acc|
|
96
|
+
parent_id = parent_update.fetch(ViewModel::ID_ATTRIBUTE)
|
97
|
+
update = parent_update.fetch(ViewModel::BULK_UPDATE_ATTRIBUTE)
|
98
|
+
|
99
|
+
acc[parent_id] = update
|
100
|
+
end
|
101
|
+
|
102
|
+
return updates_by_parent, references
|
103
|
+
end
|
104
|
+
|
105
|
+
|
84
106
|
private
|
85
107
|
|
86
108
|
def _extract_update_data(data)
|
data/lib/view_model/record.rb
CHANGED
@@ -35,7 +35,7 @@ class ViewModel::Record < ViewModel
|
|
35
35
|
|
36
36
|
# Should this class be registered in the viewmodel registry
|
37
37
|
def should_register?
|
38
|
-
!abstract_class && !unregistered
|
38
|
+
!abstract_class && !unregistered && !synthetic
|
39
39
|
end
|
40
40
|
|
41
41
|
# Specifies an attribute from the model to be serialized in this view
|
data/lib/view_model/schemas.rb
CHANGED
@@ -39,6 +39,50 @@ class ViewModel::Schemas
|
|
39
39
|
|
40
40
|
VIEWMODEL_REFERENCE = JsonSchema.parse!(VIEWMODEL_REFERENCE_SCHEMA)
|
41
41
|
|
42
|
+
BULK_UPDATE_SCHEMA =
|
43
|
+
{
|
44
|
+
'type' => 'object',
|
45
|
+
'description' => 'bulk update collection',
|
46
|
+
'properties' => {
|
47
|
+
ViewModel::TYPE_ATTRIBUTE => {
|
48
|
+
'type' => 'string',
|
49
|
+
'enum' => [ViewModel::BULK_UPDATE_TYPE],
|
50
|
+
},
|
51
|
+
|
52
|
+
ViewModel::BULK_UPDATES_ATTRIBUTE => {
|
53
|
+
'type' => 'array',
|
54
|
+
'items' => {
|
55
|
+
'type' => 'object',
|
56
|
+
'properties' => {
|
57
|
+
ViewModel::ID_ATTRIBUTE => ID_SCHEMA,
|
58
|
+
|
59
|
+
# These will be checked by the main deserialize operation. Any operations on the data
|
60
|
+
# before the main serialization must do its own checking of the presented update data.
|
61
|
+
|
62
|
+
ViewModel::BULK_UPDATE_ATTRIBUTE => {
|
63
|
+
'oneOf' => [
|
64
|
+
{ 'type' => 'array' },
|
65
|
+
{ 'type' => 'object' },
|
66
|
+
]
|
67
|
+
},
|
68
|
+
},
|
69
|
+
'additionalProperties' => false,
|
70
|
+
'required' => [
|
71
|
+
ViewModel::ID_ATTRIBUTE,
|
72
|
+
ViewModel::BULK_UPDATE_ATTRIBUTE,
|
73
|
+
],
|
74
|
+
},
|
75
|
+
}
|
76
|
+
},
|
77
|
+
'additionalProperties' => false,
|
78
|
+
'required' => [
|
79
|
+
ViewModel::TYPE_ATTRIBUTE,
|
80
|
+
ViewModel::BULK_UPDATES_ATTRIBUTE,
|
81
|
+
],
|
82
|
+
}.freeze
|
83
|
+
|
84
|
+
BULK_UPDATE = JsonSchema.parse!(BULK_UPDATE_SCHEMA)
|
85
|
+
|
42
86
|
def self.verify_schema!(schema, value)
|
43
87
|
valid, errors = schema.validate(value)
|
44
88
|
unless valid
|
@@ -192,4 +192,69 @@ module ARVMTestUtilities
|
|
192
192
|
def build_fupdate(attrs = {}, &block)
|
193
193
|
FupdateBuilder.new.build!(&block).merge(attrs)
|
194
194
|
end
|
195
|
+
|
196
|
+
def each_hook_span(trace)
|
197
|
+
return enum_for(:each_hook_span, trace) unless block_given?
|
198
|
+
|
199
|
+
hook_nesting = []
|
200
|
+
|
201
|
+
trace.each_with_index do |t, i|
|
202
|
+
case t.hook
|
203
|
+
when ViewModel::Callbacks::Hook::OnChange,
|
204
|
+
ViewModel::Callbacks::Hook::BeforeValidate
|
205
|
+
# ignore
|
206
|
+
when ViewModel::Callbacks::Hook::BeforeVisit,
|
207
|
+
ViewModel::Callbacks::Hook::BeforeDeserialize
|
208
|
+
hook_nesting.push([t, i])
|
209
|
+
|
210
|
+
when ViewModel::Callbacks::Hook::AfterVisit,
|
211
|
+
ViewModel::Callbacks::Hook::AfterDeserialize
|
212
|
+
(nested_top, nested_index) = hook_nesting.pop
|
213
|
+
|
214
|
+
unless nested_top.hook.name == t.hook.name.sub(/^After/, 'Before')
|
215
|
+
raise "Invalid nesting, processing '#{t.hook.name}', expected matching '#{nested_top.hook.name}'"
|
216
|
+
end
|
217
|
+
|
218
|
+
unless nested_top.view == t.view
|
219
|
+
raise "Invalid nesting, processing '#{t.hook.name}', " \
|
220
|
+
"expected viewmodel '#{t.view}' to match '#{nested_top.view}'"
|
221
|
+
end
|
222
|
+
|
223
|
+
yield t.view, (nested_index..i), t.hook.name.sub(/^After/, '')
|
224
|
+
|
225
|
+
else
|
226
|
+
raise 'Unexpected hook type'
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def show_span(view, range, hook)
|
232
|
+
"#{view.class.name}(#{view.id}) #{range} #{hook}"
|
233
|
+
end
|
234
|
+
|
235
|
+
def enclosing_hooks(spans, inner_range)
|
236
|
+
spans.select do |_view, range, _hook|
|
237
|
+
inner_range != range && range.cover?(inner_range.min) && range.cover?(inner_range.max)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def assert_all_hooks_nested_inside_parent_hook(trace)
|
242
|
+
spans = each_hook_span(trace).to_a
|
243
|
+
|
244
|
+
spans.reject { |view, _range, _hook| view.class == ParentView }.each do |view, range, hook|
|
245
|
+
enclosing_spans = enclosing_hooks(spans, range)
|
246
|
+
|
247
|
+
enclosing_parent_hook = enclosing_spans.detect do |other_view, _other_range, other_hook|
|
248
|
+
other_hook == hook && other_view.class == ParentView
|
249
|
+
end
|
250
|
+
|
251
|
+
next if enclosing_parent_hook
|
252
|
+
|
253
|
+
self_str = show_span(view, range, hook)
|
254
|
+
enclosing_str = enclosing_spans.map { |ov, ora, oh| show_span(ov, ora, oh) }.join("\n")
|
255
|
+
assert_not_nil(
|
256
|
+
enclosing_parent_hook,
|
257
|
+
"Invalid nesting of hook: #{self_str}\nEnclosing hooks:\n#{enclosing_str}")
|
258
|
+
end
|
259
|
+
end
|
195
260
|
end
|