iknow_view_models 3.4.2 → 3.5.1
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/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 +125 -13
- data/lib/view_model/active_record/singular_nested_controller.rb +5 -2
- data/lib/view_model/active_record.rb +46 -5
- data/lib/view_model/callbacks.rb +1 -1
- data/lib/view_model/controller.rb +24 -2
- data/lib/view_model/migrator.rb +26 -0
- data/lib/view_model/record.rb +1 -1
- data/lib/view_model/schemas.rb +44 -0
- data/lib/view_model.rb +12 -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/active_record/migration_test.rb +29 -0
- data/test/unit/view_model/traversal_context_test.rb +15 -1
- metadata +7 -5
@@ -7,9 +7,45 @@ 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)
|
@@ -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
|
@@ -35,14 +35,9 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
35
35
|
|
36
36
|
class << self
|
37
37
|
attr_reader :_list_attribute_name
|
38
|
-
attr_accessor :synthetic
|
39
38
|
|
40
39
|
delegate :transaction, to: :model_class
|
41
40
|
|
42
|
-
def should_register?
|
43
|
-
super && !synthetic
|
44
|
-
end
|
45
|
-
|
46
41
|
# Specifies that the model backing this viewmodel is a member of an
|
47
42
|
# `acts_as_manual_list` collection.
|
48
43
|
def acts_as_list(attr = :position)
|
@@ -368,6 +363,52 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
368
363
|
end
|
369
364
|
end
|
370
365
|
|
366
|
+
# Rails 6.1 introduced "previously_new_record?", but this library still
|
367
|
+
# supports activerecord >= 5.0. This is an approximation.
|
368
|
+
def self.model_previously_new?(model)
|
369
|
+
if (id_changes = model.saved_change_to_id)
|
370
|
+
old_id, _new_id = id_changes
|
371
|
+
return true if old_id.nil?
|
372
|
+
end
|
373
|
+
false
|
374
|
+
end
|
375
|
+
|
376
|
+
# Helper to return entities that were part of the last deserialization. The
|
377
|
+
# interface is complex due to the data requirements, and the implementation is
|
378
|
+
# inefficient.
|
379
|
+
#
|
380
|
+
# Intended to be used by replace_associated style methods which may touch very
|
381
|
+
# large collections that must not be returned fully. Since the collection is
|
382
|
+
# not being returned, order is also ignored.
|
383
|
+
def _read_association_touched(association_name, touched_ids:)
|
384
|
+
association_data = self.class._association_data(association_name)
|
385
|
+
|
386
|
+
associated = model.public_send(association_data.direct_reflection.name)
|
387
|
+
return nil if associated.nil?
|
388
|
+
|
389
|
+
case
|
390
|
+
when association_data.through?
|
391
|
+
# associated here are join-table models; we need to get the far side out
|
392
|
+
associated.map do |through_model|
|
393
|
+
model = through_model.public_send(association_data.indirect_reflection.name)
|
394
|
+
|
395
|
+
next unless self.class.model_previously_new?(through_model) || touched_ids.include?(model.id)
|
396
|
+
|
397
|
+
association_data.viewmodel_class_for_model!(model.class).new(model)
|
398
|
+
end.reject(&:nil?)
|
399
|
+
when association_data.collection?
|
400
|
+
associated.map do |model|
|
401
|
+
next unless self.class.model_previously_new?(model) || touched_ids.include?(model.id)
|
402
|
+
|
403
|
+
association_data.viewmodel_class_for_model!(model.class).new(model)
|
404
|
+
end.reject(&:nil?)
|
405
|
+
else
|
406
|
+
# singleton always touched by definition
|
407
|
+
model = associated
|
408
|
+
association_data.viewmodel_class_for_model!(model.class).new(model)
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
371
412
|
def _serialize_association(association_name, json, serialize_context:)
|
372
413
|
associated = self.public_send(association_name)
|
373
414
|
association_data = self.class._association_data(association_name)
|
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/migrator.rb
CHANGED
@@ -75,6 +75,32 @@ class ViewModel
|
|
75
75
|
class UpMigrator < Migrator
|
76
76
|
private
|
77
77
|
|
78
|
+
def migrate_tree!(node, references:)
|
79
|
+
if node.is_a?(Hash) && node[ViewModel::TYPE_ATTRIBUTE] == ViewModel::ActiveRecord::FUNCTIONAL_UPDATE_TYPE
|
80
|
+
migrate_functional_update!(node, references: references)
|
81
|
+
else
|
82
|
+
super
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
NESTED_FUPDATE_TYPES = ['append', 'update'].freeze
|
87
|
+
|
88
|
+
# The functional update structure uses `_type` internally with a
|
89
|
+
# context-dependent meaning. Retrospectively this was a poor choice, but we
|
90
|
+
# need to account for it here.
|
91
|
+
def migrate_functional_update!(node, references:)
|
92
|
+
actions = node[ViewModel::ActiveRecord::ACTIONS_ATTRIBUTE]
|
93
|
+
actions&.each do |action|
|
94
|
+
action_type = action[ViewModel::TYPE_ATTRIBUTE]
|
95
|
+
next unless NESTED_FUPDATE_TYPES.include?(action_type)
|
96
|
+
|
97
|
+
values = action[ViewModel::ActiveRecord::VALUES_ATTRIBUTE]
|
98
|
+
values&.each do |value|
|
99
|
+
migrate_tree!(value, references: references)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
78
104
|
def migrate_viewmodel!(view_name, source_version, view_hash, references)
|
79
105
|
path = @paths[view_name]
|
80
106
|
return false unless path
|
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
|
data/lib/view_model.rb
CHANGED
@@ -12,6 +12,10 @@ class ViewModel
|
|
12
12
|
VERSION_ATTRIBUTE = '_version'
|
13
13
|
NEW_ATTRIBUTE = '_new'
|
14
14
|
|
15
|
+
BULK_UPDATE_TYPE = '_bulk_update'
|
16
|
+
BULK_UPDATES_ATTRIBUTE = 'updates'
|
17
|
+
BULK_UPDATE_ATTRIBUTE = 'update'
|
18
|
+
|
15
19
|
# Migrations leave a metadata attribute _migrated on any views that they
|
16
20
|
# alter. This attribute is accessible as metadata when deserializing migrated
|
17
21
|
# input, and is included in the output serialization sent to clients.
|
@@ -27,6 +31,14 @@ class ViewModel
|
|
27
31
|
attr_reader :view_aliases
|
28
32
|
attr_writer :view_name
|
29
33
|
|
34
|
+
# Boolean to indicate if the viewmodel is synthetic. Synthetic
|
35
|
+
# viewmodels are nearly-invisible glue. They're full viewmodels,
|
36
|
+
# but do not participate in hooks or registration. For example, a
|
37
|
+
# join table connecting A and B through T has a synthetic
|
38
|
+
# viewmodel T to represent the join model, but the external
|
39
|
+
# interface is a relationship of A to a list of Bs.
|
40
|
+
attr_accessor :synthetic
|
41
|
+
|
30
42
|
def inherited(subclass)
|
31
43
|
super
|
32
44
|
subclass.initialize_as_viewmodel
|
@@ -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
|
@@ -13,8 +13,11 @@ require 'acts_as_manual_list'
|
|
13
13
|
|
14
14
|
# models for ARVM controller test
|
15
15
|
module ControllerTestModels
|
16
|
-
def
|
17
|
-
|
16
|
+
def build_controller_test_models(externalize: [])
|
17
|
+
unsupported_externals = externalize - [:label, :target, :child]
|
18
|
+
unless unsupported_externals.empty?
|
19
|
+
raise ArgumentError.new("build_controller_test_models cannot externalize: #{unsupported_externals.join(", ")}")
|
20
|
+
end
|
18
21
|
|
19
22
|
build_viewmodel(:Label) do
|
20
23
|
define_schema do |t|
|
@@ -25,6 +28,7 @@ module ControllerTestModels
|
|
25
28
|
has_one :target
|
26
29
|
end
|
27
30
|
define_viewmodel do
|
31
|
+
root! if externalize.include?(:label)
|
28
32
|
attributes :text
|
29
33
|
end
|
30
34
|
end
|
@@ -80,16 +84,19 @@ module ControllerTestModels
|
|
80
84
|
has_one :target, dependent: :destroy, inverse_of: :parent
|
81
85
|
belongs_to :poly, polymorphic: true, dependent: :destroy, inverse_of: :parent
|
82
86
|
belongs_to :category
|
87
|
+
has_many :parent_tags
|
83
88
|
end
|
84
89
|
define_viewmodel do
|
85
90
|
root!
|
86
91
|
self.schema_version = 2
|
87
92
|
|
88
93
|
attributes :name
|
89
|
-
|
90
|
-
association :
|
94
|
+
association :target, external: externalize.include?(:target)
|
95
|
+
association :label, external: externalize.include?(:label)
|
96
|
+
association :children, external: externalize.include?(:child)
|
91
97
|
association :poly, viewmodels: [:PolyOne, :PolyTwo]
|
92
98
|
association :category, external: true
|
99
|
+
association :tags, through: :parent_tags, external: true
|
93
100
|
|
94
101
|
migrates from: 1, to: 2 do
|
95
102
|
up do |view, _refs|
|
@@ -105,6 +112,31 @@ module ControllerTestModels
|
|
105
112
|
end
|
106
113
|
end
|
107
114
|
|
115
|
+
build_viewmodel(:Tag) do
|
116
|
+
define_schema do |t|
|
117
|
+
t.string :name, null: false
|
118
|
+
end
|
119
|
+
define_model do
|
120
|
+
has_many :parent_tags
|
121
|
+
end
|
122
|
+
define_viewmodel do
|
123
|
+
root!
|
124
|
+
attributes :name
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
build_viewmodel(:ParentTag) do
|
129
|
+
define_schema do |t|
|
130
|
+
t.references :parent, foreign_key: true
|
131
|
+
t.references :tag, foreign_key: true
|
132
|
+
end
|
133
|
+
define_model do
|
134
|
+
belongs_to :parent
|
135
|
+
belongs_to :tag
|
136
|
+
end
|
137
|
+
no_viewmodel
|
138
|
+
end
|
139
|
+
|
108
140
|
build_viewmodel(:Child) do
|
109
141
|
define_schema do |t|
|
110
142
|
t.references :parent, null: false, foreign_key: true
|
@@ -122,6 +154,7 @@ module ControllerTestModels
|
|
122
154
|
validates :age, numericality: { less_than: 42 }, allow_nil: true
|
123
155
|
end
|
124
156
|
define_viewmodel do
|
157
|
+
root! if externalize.include?(:child)
|
125
158
|
attributes :name, :age
|
126
159
|
acts_as_list :position
|
127
160
|
end
|
@@ -138,11 +171,22 @@ module ControllerTestModels
|
|
138
171
|
belongs_to :label, dependent: :destroy
|
139
172
|
end
|
140
173
|
define_viewmodel do
|
174
|
+
root! if externalize.include?(:target)
|
141
175
|
attributes :text
|
142
176
|
association :label
|
143
177
|
end
|
144
178
|
end
|
145
179
|
end
|
180
|
+
|
181
|
+
def make_parent(name: 'p', child_names: ['c1', 'c2'])
|
182
|
+
Parent.create(
|
183
|
+
name: name,
|
184
|
+
children: child_names.each_with_index.map { |c, pos|
|
185
|
+
Child.new(name: "c#{pos + 1}", position: (pos + 1).to_f)
|
186
|
+
},
|
187
|
+
label: Label.new,
|
188
|
+
target: Target.new)
|
189
|
+
end
|
146
190
|
end
|
147
191
|
|
148
192
|
## Dummy Rails Controllers
|
@@ -253,43 +297,30 @@ module CallbackTracing
|
|
253
297
|
end
|
254
298
|
|
255
299
|
module ControllerTestControllers
|
300
|
+
CONTROLLER_NAMES = [
|
301
|
+
:ParentController,
|
302
|
+
:ChildController,
|
303
|
+
:LabelController,
|
304
|
+
:TargetController,
|
305
|
+
:CategoryController,
|
306
|
+
:TagController,
|
307
|
+
]
|
308
|
+
|
256
309
|
def before_all
|
257
310
|
super
|
258
311
|
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
Class.new(DummyController) do |_c|
|
267
|
-
Object.const_set(:ChildController, self)
|
268
|
-
include ViewModel::ActiveRecord::Controller
|
269
|
-
include CallbackTracing
|
270
|
-
self.access_control = ViewModel::AccessControl::Open
|
271
|
-
nested_in :parent, as: :children
|
272
|
-
end
|
273
|
-
|
274
|
-
Class.new(DummyController) do |_c|
|
275
|
-
Object.const_set(:LabelController, self)
|
276
|
-
include ViewModel::ActiveRecord::Controller
|
277
|
-
include CallbackTracing
|
278
|
-
self.access_control = ViewModel::AccessControl::Open
|
279
|
-
nested_in :parent, as: :label
|
280
|
-
end
|
281
|
-
|
282
|
-
Class.new(DummyController) do |_c|
|
283
|
-
Object.const_set(:TargetController, self)
|
284
|
-
include ViewModel::ActiveRecord::Controller
|
285
|
-
include CallbackTracing
|
286
|
-
self.access_control = ViewModel::AccessControl::Open
|
287
|
-
nested_in :parent, as: :target
|
312
|
+
CONTROLLER_NAMES.each do |name|
|
313
|
+
Class.new(DummyController) do |_c|
|
314
|
+
Object.const_set(name, self)
|
315
|
+
include ViewModel::ActiveRecord::Controller
|
316
|
+
include CallbackTracing
|
317
|
+
self.access_control = ViewModel::AccessControl::Open
|
318
|
+
end
|
288
319
|
end
|
289
320
|
end
|
290
321
|
|
291
322
|
def after_all
|
292
|
-
|
323
|
+
CONTROLLER_NAMES.each do |name|
|
293
324
|
Object.send(:remove_const, name)
|
294
325
|
end
|
295
326
|
ActiveSupport::Dependencies::Reference.clear!
|