iknow_view_models 2.10.1 → 3.0.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/.circleci/config.yml +119 -0
- data/.travis.yml +31 -0
- data/Appraisals +6 -16
- data/gemfiles/{rails_7_0.gemfile → rails_6_0_beta.gemfile} +2 -2
- data/iknow_view_models.gemspec +3 -5
- data/lib/iknow_view_models/version.rb +1 -1
- data/lib/view_model/active_record/association_data.rb +206 -92
- data/lib/view_model/active_record/association_manipulation.rb +22 -12
- data/lib/view_model/active_record/cache/cacheable_view.rb +3 -13
- data/lib/view_model/active_record/cache.rb +2 -2
- data/lib/view_model/active_record/cloner.rb +11 -11
- data/lib/view_model/active_record/controller.rb +0 -2
- data/lib/view_model/active_record/update_context.rb +21 -3
- data/lib/view_model/active_record/update_data.rb +43 -45
- data/lib/view_model/active_record/update_operation.rb +265 -153
- data/lib/view_model/active_record/visitor.rb +9 -6
- data/lib/view_model/active_record.rb +94 -74
- data/lib/view_model/after_transaction_runner.rb +3 -18
- data/lib/view_model/callbacks.rb +2 -2
- data/lib/view_model/changes.rb +24 -16
- data/lib/view_model/config.rb +6 -2
- data/lib/view_model/deserialization_error.rb +31 -0
- data/lib/view_model/deserialize_context.rb +2 -6
- data/lib/view_model/error_view.rb +6 -5
- data/lib/view_model/record/attribute_data.rb +11 -6
- data/lib/view_model/record.rb +44 -24
- data/lib/view_model/serialize_context.rb +2 -63
- data/lib/view_model/test_helpers/arvm_builder.rb +2 -4
- data/lib/view_model/traversal_context.rb +2 -2
- data/lib/view_model.rb +21 -13
- data/shell.nix +1 -1
- data/test/helpers/arvm_test_models.rb +4 -12
- data/test/helpers/arvm_test_utilities.rb +6 -0
- data/test/helpers/controller_test_helpers.rb +6 -6
- data/test/helpers/viewmodel_spec_helpers.rb +63 -52
- data/test/unit/view_model/access_control_test.rb +88 -37
- data/test/unit/view_model/active_record/belongs_to_test.rb +110 -178
- data/test/unit/view_model/active_record/cache_test.rb +11 -5
- data/test/unit/view_model/active_record/cloner_test.rb +1 -1
- data/test/unit/view_model/active_record/controller_test.rb +12 -20
- data/test/unit/view_model/active_record/has_many_test.rb +540 -316
- data/test/unit/view_model/active_record/has_many_through_poly_test.rb +12 -15
- data/test/unit/view_model/active_record/has_many_through_test.rb +15 -58
- data/test/unit/view_model/active_record/has_one_test.rb +288 -135
- data/test/unit/view_model/active_record/poly_test.rb +0 -1
- data/test/unit/view_model/active_record/shared_test.rb +21 -39
- data/test/unit/view_model/active_record/version_test.rb +3 -2
- data/test/unit/view_model/active_record_test.rb +5 -63
- data/test/unit/view_model/callbacks_test.rb +1 -0
- data/test/unit/view_model/record_test.rb +0 -32
- data/test/unit/view_model/traversal_context_test.rb +13 -12
- metadata +15 -25
- data/.github/workflows/gem-push.yml +0 -31
- data/.github/workflows/test.yml +0 -65
- data/gemfiles/rails_6_0.gemfile +0 -9
- data/gemfiles/rails_6_1.gemfile +0 -9
- data/test/unit/view_model/active_record/optional_attribute_view_test.rb +0 -58
data/lib/view_model/record.rb
CHANGED
@@ -36,22 +36,30 @@ class ViewModel::Record < ViewModel
|
|
36
36
|
end
|
37
37
|
|
38
38
|
# Specifies an attribute from the model to be serialized in this view
|
39
|
-
def attribute(attr, as: nil, read_only: false, write_once: false, using: nil, format: nil, array: false
|
39
|
+
def attribute(attr, as: nil, read_only: false, write_once: false, using: nil, format: nil, array: false)
|
40
40
|
model_attribute_name = attr.to_s
|
41
41
|
vm_attribute_name = (as || attr).to_s
|
42
42
|
|
43
43
|
if using && format
|
44
|
-
raise ArgumentError.new("Only one of :using and :format may be specified")
|
44
|
+
raise ArgumentError.new("Only one of ':using' and ':format' may be specified")
|
45
45
|
end
|
46
46
|
if using && !(using.is_a?(Class) && using < ViewModel)
|
47
47
|
raise ArgumentError.new("Invalid 'using:' viewmodel: not a viewmodel class")
|
48
48
|
end
|
49
|
+
if using && using.root?
|
50
|
+
raise ArgumentError.new("Invalid 'using:' viewmodel: is a root")
|
51
|
+
end
|
49
52
|
if format && !format.respond_to?(:dump) && !format.respond_to?(:load)
|
50
53
|
raise ArgumentError.new("Invalid 'format:' serializer: must respond to :dump and :load")
|
51
54
|
end
|
52
55
|
|
53
|
-
attr_data = AttributeData.new(vm_attribute_name,
|
54
|
-
|
56
|
+
attr_data = AttributeData.new(name: vm_attribute_name,
|
57
|
+
model_attr_name: model_attribute_name,
|
58
|
+
attribute_viewmodel: using,
|
59
|
+
attribute_serializer: format,
|
60
|
+
array: array,
|
61
|
+
read_only: read_only,
|
62
|
+
write_once: write_once)
|
55
63
|
_members[vm_attribute_name] = attr_data
|
56
64
|
|
57
65
|
@generated_accessor_module.module_eval do
|
@@ -166,9 +174,10 @@ class ViewModel::Record < ViewModel
|
|
166
174
|
|
167
175
|
self.model = model
|
168
176
|
|
169
|
-
@new_model
|
170
|
-
@changed_attributes
|
171
|
-
@
|
177
|
+
@new_model = false
|
178
|
+
@changed_attributes = []
|
179
|
+
@changed_nested_children = false
|
180
|
+
@changed_referenced_children = false
|
172
181
|
end
|
173
182
|
|
174
183
|
# VM::Record identity matches the identity of its model. If the model has a
|
@@ -189,8 +198,12 @@ class ViewModel::Record < ViewModel
|
|
189
198
|
@new_model
|
190
199
|
end
|
191
200
|
|
192
|
-
def
|
193
|
-
@
|
201
|
+
def changed_nested_children?
|
202
|
+
@changed_nested_children
|
203
|
+
end
|
204
|
+
|
205
|
+
def changed_referenced_children?
|
206
|
+
@changed_referenced_children
|
194
207
|
end
|
195
208
|
|
196
209
|
def serialize_view(json, serialize_context: self.class.new_serialize_context)
|
@@ -202,9 +215,7 @@ class ViewModel::Record < ViewModel
|
|
202
215
|
end
|
203
216
|
|
204
217
|
def serialize_members(json, serialize_context:)
|
205
|
-
self.class._members.each do |member_name,
|
206
|
-
next unless serialize_context.includes_member?(member_name, !member_data.optional?)
|
207
|
-
|
218
|
+
self.class._members.each do |member_name, _member_data|
|
208
219
|
self.public_send("serialize_#{member_name}", json, serialize_context: serialize_context)
|
209
220
|
end
|
210
221
|
end
|
@@ -227,22 +238,29 @@ class ViewModel::Record < ViewModel
|
|
227
238
|
@changed_attributes << attr_name.to_s
|
228
239
|
end
|
229
240
|
|
230
|
-
def
|
231
|
-
@
|
241
|
+
def nested_children_changed!
|
242
|
+
@changed_nested_children = true
|
243
|
+
end
|
244
|
+
|
245
|
+
def referenced_children_changed!
|
246
|
+
@changed_referenced_children = true
|
232
247
|
end
|
233
248
|
|
234
249
|
def changes
|
235
250
|
ViewModel::Changes.new(
|
236
|
-
new:
|
237
|
-
changed_attributes:
|
238
|
-
|
251
|
+
new: new_model?,
|
252
|
+
changed_attributes: changed_attributes,
|
253
|
+
changed_nested_children: changed_nested_children?,
|
254
|
+
changed_referenced_children: changed_referenced_children?,
|
255
|
+
)
|
239
256
|
end
|
240
257
|
|
241
258
|
def clear_changes!
|
242
|
-
@previous_changes
|
243
|
-
@new_model
|
244
|
-
@changed_attributes
|
245
|
-
@
|
259
|
+
@previous_changes = changes
|
260
|
+
@new_model = false
|
261
|
+
@changed_attributes = []
|
262
|
+
@changed_nested_children = false
|
263
|
+
@changed_referenced_children = false
|
246
264
|
previous_changes
|
247
265
|
end
|
248
266
|
|
@@ -348,9 +366,11 @@ class ViewModel::Record < ViewModel
|
|
348
366
|
model.public_send("#{attr_data.model_attr_name}=", value)
|
349
367
|
end
|
350
368
|
|
351
|
-
if attr_data.using_viewmodel?
|
352
|
-
|
353
|
-
|
369
|
+
if attr_data.using_viewmodel?
|
370
|
+
previous_changes = Array.wrap(value).select { |v| v.respond_to?(:previous_changes) }.map!(&:previous_changes)
|
371
|
+
|
372
|
+
self.nested_children_changed! if previous_changes.any? { |pc| pc.changed_nested_tree? }
|
373
|
+
self.referenced_children_changed! if previous_changes.any? { |pc| pc.changed_referenced_children? }
|
354
374
|
end
|
355
375
|
end
|
356
376
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support/core_ext'
|
2
4
|
require 'view_model/traversal_context'
|
3
5
|
|
@@ -17,52 +19,6 @@ class ViewModel::SerializeContext < ViewModel::TraversalContext
|
|
17
19
|
end
|
18
20
|
|
19
21
|
delegate :references, :flatten_references, to: :shared_context
|
20
|
-
|
21
|
-
attr_reader :include, :prune
|
22
|
-
|
23
|
-
def initialize(include: nil, prune: nil, **rest)
|
24
|
-
super(**rest)
|
25
|
-
@include = self.class.normalize_includes(include)
|
26
|
-
@prune = self.class.normalize_includes(prune)
|
27
|
-
end
|
28
|
-
|
29
|
-
def initialize_as_child(include:, prune:, **rest)
|
30
|
-
super(**rest)
|
31
|
-
@include = include
|
32
|
-
@prune = prune
|
33
|
-
end
|
34
|
-
|
35
|
-
def for_child(parent_viewmodel, association_name:, **rest)
|
36
|
-
super(parent_viewmodel,
|
37
|
-
association_name: association_name,
|
38
|
-
include: @include.try { |i| i[association_name] },
|
39
|
-
prune: @prune.try { |p| p[association_name] },
|
40
|
-
**rest)
|
41
|
-
end
|
42
|
-
|
43
|
-
def includes_member?(member_name, default)
|
44
|
-
member_name = member_name.to_s
|
45
|
-
|
46
|
-
# Every node in the include tree is to be included
|
47
|
-
included = @include.try { |is| is.has_key?(member_name) }
|
48
|
-
# whereas only the leaves of the prune tree are to be removed
|
49
|
-
pruned = @prune.try { |ps| ps.fetch(member_name, :sentinel).nil? }
|
50
|
-
|
51
|
-
(default || included) && !pruned
|
52
|
-
end
|
53
|
-
|
54
|
-
def add_includes(includes)
|
55
|
-
return if includes.blank?
|
56
|
-
@include ||= {}
|
57
|
-
@include.deep_merge!(self.class.normalize_includes(includes))
|
58
|
-
end
|
59
|
-
|
60
|
-
def add_prunes(prunes)
|
61
|
-
return if prunes.blank?
|
62
|
-
@prune ||= {}
|
63
|
-
@prune.deep_merge!(self.class.normalize_includes(prunes))
|
64
|
-
end
|
65
|
-
|
66
22
|
delegate :add_reference, :has_references?, to: :references
|
67
23
|
|
68
24
|
# Return viewmodels referenced during serialization and clear @references.
|
@@ -98,21 +54,4 @@ class ViewModel::SerializeContext < ViewModel::TraversalContext
|
|
98
54
|
def serialize_references_to_hash
|
99
55
|
Jbuilder.new { |json| serialize_references(json) }.attributes!
|
100
56
|
end
|
101
|
-
|
102
|
-
def self.normalize_includes(includes)
|
103
|
-
case includes
|
104
|
-
when Array
|
105
|
-
includes.each_with_object({}) do |v, new_includes|
|
106
|
-
new_includes.merge!(normalize_includes(v))
|
107
|
-
end
|
108
|
-
when Hash
|
109
|
-
includes.each_with_object({}) do |(k,v), new_includes|
|
110
|
-
new_includes[k.to_s] = normalize_includes(v)
|
111
|
-
end
|
112
|
-
when nil
|
113
|
-
nil
|
114
|
-
else
|
115
|
-
{ includes.to_s => nil }
|
116
|
-
end
|
117
|
-
end
|
118
57
|
end
|
@@ -62,10 +62,8 @@ class ViewModel::TestHelpers::ARVMBuilder
|
|
62
62
|
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS #{name.underscore.pluralize} CASCADE")
|
63
63
|
namespace.send(:remove_const, name)
|
64
64
|
namespace.send(:remove_const, viewmodel_name) if viewmodel
|
65
|
-
|
66
|
-
|
67
|
-
ActiveSupport::Dependencies::Reference.clear!
|
68
|
-
end
|
65
|
+
# prevent cached old class from being used to resolve associations
|
66
|
+
ActiveSupport::Dependencies::Reference.clear!
|
69
67
|
end
|
70
68
|
|
71
69
|
private
|
@@ -23,8 +23,8 @@ class ViewModel::TraversalContext
|
|
23
23
|
attr_reader :shared_context
|
24
24
|
delegate :access_control, :callbacks, to: :shared_context
|
25
25
|
|
26
|
-
def self.new_child(
|
27
|
-
self.allocate.tap { |c| c.initialize_as_child(
|
26
|
+
def self.new_child(*args)
|
27
|
+
self.allocate.tap { |c| c.initialize_as_child(*args) }
|
28
28
|
end
|
29
29
|
|
30
30
|
def initialize(shared_context: nil, **shared_context_params)
|
data/lib/view_model.rb
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
# A ViewModel encapsulates a particular aggregation of data calculated via the
|
4
4
|
# underlying models and provides a means of serializing it into views.
|
5
5
|
require 'jbuilder'
|
6
|
-
require 'base64'
|
7
6
|
require 'deep_preloader'
|
8
7
|
|
9
8
|
class ViewModel
|
@@ -21,6 +20,7 @@ class ViewModel
|
|
21
20
|
attr_accessor :_attributes
|
22
21
|
attr_accessor :schema_version
|
23
22
|
attr_reader :view_aliases
|
23
|
+
attr_writer :view_name
|
24
24
|
|
25
25
|
def inherited(subclass)
|
26
26
|
subclass.initialize_as_viewmodel
|
@@ -42,15 +42,23 @@ class ViewModel
|
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
45
|
-
def view_name=(name)
|
46
|
-
@view_name = name
|
47
|
-
end
|
48
|
-
|
49
45
|
def add_view_alias(as)
|
50
46
|
view_aliases << as
|
51
47
|
ViewModel::Registry.register(self, as: as)
|
52
48
|
end
|
53
49
|
|
50
|
+
# ViewModels are either roots or children. Root viewmodels may be
|
51
|
+
# (de)serialized directly, whereas child viewmodels are always nested within
|
52
|
+
# their parent. Associations to root viewmodel types always use indirect
|
53
|
+
# references.
|
54
|
+
def root?
|
55
|
+
false
|
56
|
+
end
|
57
|
+
|
58
|
+
def root!
|
59
|
+
define_singleton_method(:root?) { true }
|
60
|
+
end
|
61
|
+
|
54
62
|
# ViewModels are typically going to be pretty simple structures. Make it a
|
55
63
|
# bit easier to define them: attributes specified this way are given
|
56
64
|
# accessors and assigned in order by the default constructor.
|
@@ -60,7 +68,7 @@ class ViewModel
|
|
60
68
|
|
61
69
|
def attribute(attr, **_args)
|
62
70
|
unless attr.is_a?(Symbol)
|
63
|
-
raise ArgumentError.new(
|
71
|
+
raise ArgumentError.new('ViewModel attributes must be symbols')
|
64
72
|
end
|
65
73
|
|
66
74
|
attr_accessor attr
|
@@ -117,7 +125,7 @@ class ViewModel
|
|
117
125
|
# If this viewmodel represents an AR model, what associations does it make
|
118
126
|
# use of? Returns a includes spec appropriate for DeepPreloader, either as
|
119
127
|
# AR-style nested hashes or DeepPreloader::Spec.
|
120
|
-
def eager_includes(serialize_context: new_serialize_context,
|
128
|
+
def eager_includes(serialize_context: new_serialize_context, include_referenced: true)
|
121
129
|
{}
|
122
130
|
end
|
123
131
|
|
@@ -210,26 +218,26 @@ class ViewModel
|
|
210
218
|
ViewModel::SerializeContext
|
211
219
|
end
|
212
220
|
|
213
|
-
def new_serialize_context(
|
214
|
-
serialize_context_class.new(
|
221
|
+
def new_serialize_context(*args)
|
222
|
+
serialize_context_class.new(*args)
|
215
223
|
end
|
216
224
|
|
217
225
|
def deserialize_context_class
|
218
226
|
ViewModel::DeserializeContext
|
219
227
|
end
|
220
228
|
|
221
|
-
def new_deserialize_context(
|
222
|
-
deserialize_context_class.new(
|
229
|
+
def new_deserialize_context(*args)
|
230
|
+
deserialize_context_class.new(*args)
|
223
231
|
end
|
224
232
|
|
225
233
|
def accepts_schema_version?(schema_version)
|
226
234
|
schema_version == self.schema_version
|
227
235
|
end
|
228
236
|
|
229
|
-
def preload_for_serialization(viewmodels, serialize_context: new_serialize_context,
|
237
|
+
def preload_for_serialization(viewmodels, serialize_context: new_serialize_context, include_referenced: true, lock: nil)
|
230
238
|
Array.wrap(viewmodels).group_by(&:class).each do |type, views|
|
231
239
|
DeepPreloader.preload(views.map(&:model),
|
232
|
-
type.eager_includes(serialize_context: serialize_context,
|
240
|
+
type.eager_includes(serialize_context: serialize_context, include_referenced: include_referenced),
|
233
241
|
lock: lock)
|
234
242
|
end
|
235
243
|
end
|
data/shell.nix
CHANGED
@@ -6,18 +6,10 @@ require "view_model/active_record/controller"
|
|
6
6
|
|
7
7
|
require "acts_as_manual_list"
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
db_config_path = File.join(File.dirname(__FILE__), '../config/database.yml')
|
14
|
-
yaml = YAML.safe_load(File.open(db_config_path))
|
15
|
-
raise 'Test database configuration missing' unless yaml['test']
|
16
|
-
|
17
|
-
yaml['test']
|
18
|
-
end
|
19
|
-
|
20
|
-
ActiveRecord::Base.establish_connection(db_config)
|
9
|
+
db_config_path = File.join(File.dirname(__FILE__), '../config/database.yml')
|
10
|
+
db_config = YAML.load(File.open(db_config_path))
|
11
|
+
raise "Test database configuration missing" unless db_config["test"]
|
12
|
+
ActiveRecord::Base.establish_connection(db_config["test"])
|
21
13
|
|
22
14
|
# Remove test tables if any exist
|
23
15
|
%w[labels parents children targets poly_ones poly_twos owners
|
@@ -4,6 +4,12 @@ require 'minitest/hooks'
|
|
4
4
|
require 'view_model'
|
5
5
|
require 'view_model/test_helpers'
|
6
6
|
|
7
|
+
unless ViewModel::Config.configured?
|
8
|
+
ViewModel::Config.configure! do
|
9
|
+
debug_deserialization true
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
7
13
|
require_relative 'query_logging.rb'
|
8
14
|
|
9
15
|
ActiveSupport::TestCase.include(Minitest::Hooks)
|
@@ -33,6 +33,7 @@ module ControllerTestModels
|
|
33
33
|
has_many :parents
|
34
34
|
end
|
35
35
|
define_viewmodel do
|
36
|
+
root!
|
36
37
|
attributes :name
|
37
38
|
end
|
38
39
|
end
|
@@ -77,11 +78,12 @@ module ControllerTestModels
|
|
77
78
|
belongs_to :category
|
78
79
|
end
|
79
80
|
define_viewmodel do
|
81
|
+
root!
|
80
82
|
attributes :name
|
81
83
|
associations :label, :target
|
82
|
-
association :children
|
84
|
+
association :children
|
83
85
|
association :poly, viewmodels: [:PolyOne, :PolyTwo]
|
84
|
-
association :category,
|
86
|
+
association :category, external: true
|
85
87
|
end
|
86
88
|
end
|
87
89
|
|
@@ -99,7 +101,7 @@ module ControllerTestModels
|
|
99
101
|
define_model do
|
100
102
|
belongs_to :parent, inverse_of: :children
|
101
103
|
acts_as_manual_list scope: :parent
|
102
|
-
validates :age, numericality: {less_than: 42}, allow_nil: true
|
104
|
+
validates :age, numericality: { less_than: 42 }, allow_nil: true
|
103
105
|
end
|
104
106
|
define_viewmodel do
|
105
107
|
attributes :name, :age
|
@@ -267,9 +269,7 @@ module ControllerTestControllers
|
|
267
269
|
[:ParentController, :ChildController, :LabelController, :TargetController].each do |name|
|
268
270
|
Object.send(:remove_const, name)
|
269
271
|
end
|
270
|
-
|
271
|
-
ActiveSupport::Dependencies::Reference.clear!
|
272
|
-
end
|
272
|
+
ActiveSupport::Dependencies::Reference.clear!
|
273
273
|
super
|
274
274
|
end
|
275
275
|
end
|
@@ -77,7 +77,7 @@ module ViewModelSpecHelpers
|
|
77
77
|
ViewModel::TestHelpers::ARVMBuilder::Spec.new(
|
78
78
|
schema: ->(t) { t.string :name },
|
79
79
|
model: ->(m) {},
|
80
|
-
viewmodel: ->(v) { attribute :name }
|
80
|
+
viewmodel: ->(v) { root!; attribute :name }
|
81
81
|
)
|
82
82
|
end
|
83
83
|
|
@@ -99,6 +99,10 @@ module ViewModelSpecHelpers
|
|
99
99
|
raise RuntimeError.new('Test model does not have a child association')
|
100
100
|
end
|
101
101
|
|
102
|
+
def subject_association_features
|
103
|
+
{}
|
104
|
+
end
|
105
|
+
|
102
106
|
def subject_association_name
|
103
107
|
subject_association.association_name
|
104
108
|
end
|
@@ -114,9 +118,10 @@ module ViewModelSpecHelpers
|
|
114
118
|
include ViewModelSpecHelpers::Base
|
115
119
|
|
116
120
|
def model_attributes
|
121
|
+
f = subject_association_features
|
117
122
|
super.merge(schema: ->(t) { t.references :child, foreign_key: true },
|
118
123
|
model: ->(m) { belongs_to :child, inverse_of: :model, dependent: :destroy },
|
119
|
-
viewmodel: ->(v) { association :child })
|
124
|
+
viewmodel: ->(v) { association :child, **f })
|
120
125
|
end
|
121
126
|
|
122
127
|
def child_attributes
|
@@ -134,17 +139,33 @@ module ViewModelSpecHelpers
|
|
134
139
|
end
|
135
140
|
end
|
136
141
|
|
142
|
+
module ParentAndSharedBelongsToChild
|
143
|
+
extend ActiveSupport::Concern
|
144
|
+
include ViewModelSpecHelpers::ParentAndBelongsToChild
|
145
|
+
def child_attributes
|
146
|
+
super.merge(viewmodel: ->(v) { root! })
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
137
150
|
module List
|
138
151
|
extend ActiveSupport::Concern
|
139
152
|
include ViewModelSpecHelpers::Base
|
140
153
|
|
141
154
|
def model_attributes
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
155
|
+
ViewModel::TestHelpers::ARVMBuilder::Spec.new(
|
156
|
+
schema: ->(t) {
|
157
|
+
t.string :name
|
158
|
+
t.integer :next_id
|
159
|
+
},
|
160
|
+
model: ->(m) {
|
161
|
+
belongs_to :next, class_name: self.name, inverse_of: :previous, dependent: :destroy
|
162
|
+
has_one :previous, class_name: self.name, foreign_key: :next_id, inverse_of: :next
|
163
|
+
},
|
164
|
+
viewmodel: ->(v) {
|
165
|
+
# Not a root
|
166
|
+
association :next
|
167
|
+
attribute :name
|
168
|
+
})
|
148
169
|
end
|
149
170
|
|
150
171
|
def subject_association
|
@@ -157,9 +178,10 @@ module ViewModelSpecHelpers
|
|
157
178
|
include ViewModelSpecHelpers::Base
|
158
179
|
|
159
180
|
def model_attributes
|
181
|
+
f = subject_association_features
|
160
182
|
super.merge(
|
161
183
|
model: ->(m) { has_one :child, inverse_of: :model, dependent: :destroy },
|
162
|
-
viewmodel: ->(v) { association :child }
|
184
|
+
viewmodel: ->(v) { association :child, **f }
|
163
185
|
)
|
164
186
|
end
|
165
187
|
|
@@ -181,14 +203,23 @@ module ViewModelSpecHelpers
|
|
181
203
|
end
|
182
204
|
end
|
183
205
|
|
206
|
+
module ParentAndReferencedHasOneChild
|
207
|
+
extend ActiveSupport::Concern
|
208
|
+
include ViewModelSpecHelpers::ParentAndHasOneChild
|
209
|
+
def child_attributes
|
210
|
+
super.merge(viewmodel: ->(v) { root! })
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
184
214
|
module ParentAndHasManyChildren
|
185
215
|
extend ActiveSupport::Concern
|
186
216
|
include ViewModelSpecHelpers::Base
|
187
217
|
|
188
218
|
def model_attributes
|
219
|
+
f = subject_association_features
|
189
220
|
super.merge(
|
190
221
|
model: ->(m) { has_many :children, inverse_of: :model, dependent: :destroy },
|
191
|
-
viewmodel: ->(v) { association :children }
|
222
|
+
viewmodel: ->(v) { association :children, **f }
|
192
223
|
)
|
193
224
|
end
|
194
225
|
|
@@ -210,22 +241,23 @@ module ViewModelSpecHelpers
|
|
210
241
|
end
|
211
242
|
end
|
212
243
|
|
213
|
-
module
|
244
|
+
module ParentAndSharedHasManyChildren
|
214
245
|
extend ActiveSupport::Concern
|
215
|
-
include ViewModelSpecHelpers::
|
216
|
-
|
217
|
-
|
218
|
-
super.merge(
|
219
|
-
model: ->(m) { has_many :children, inverse_of: :model, dependent: :destroy },
|
220
|
-
viewmodel: ->(v) { association :children },
|
221
|
-
)
|
246
|
+
include ViewModelSpecHelpers::ParentAndHasManyChildren
|
247
|
+
def child_attributes
|
248
|
+
super.merge(viewmodel: ->(v) { root! })
|
222
249
|
end
|
250
|
+
end
|
251
|
+
|
252
|
+
module ParentAndOrderedChildren
|
253
|
+
extend ActiveSupport::Concern
|
254
|
+
include ViewModelSpecHelpers::ParentAndHasManyChildren
|
223
255
|
|
224
256
|
def child_attributes
|
225
257
|
super.merge(
|
226
|
-
schema: ->(t) { t.
|
227
|
-
model: ->(
|
228
|
-
viewmodel: ->(
|
258
|
+
schema: ->(t) { t.float :position, null: false },
|
259
|
+
model: ->(_m) { acts_as_manual_list scope: :model },
|
260
|
+
viewmodel: ->(_v) { acts_as_list :position },
|
229
261
|
)
|
230
262
|
end
|
231
263
|
|
@@ -233,7 +265,7 @@ module ViewModelSpecHelpers
|
|
233
265
|
# child depends on parent, ensure it's touched first
|
234
266
|
viewmodel_class
|
235
267
|
|
236
|
-
# Add a deferrable unique position
|
268
|
+
# Add a deferrable unique position constraint
|
237
269
|
super do |klass|
|
238
270
|
model = klass.model_class
|
239
271
|
table = model.table_name
|
@@ -242,38 +274,15 @@ module ViewModelSpecHelpers
|
|
242
274
|
SQL
|
243
275
|
end
|
244
276
|
end
|
245
|
-
|
246
|
-
def subject_association
|
247
|
-
viewmodel_class._association_data('children')
|
248
|
-
end
|
249
277
|
end
|
250
278
|
|
251
|
-
module ParentAndSharedChild
|
252
|
-
extend ActiveSupport::Concern
|
253
|
-
include ViewModelSpecHelpers::Base
|
254
279
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
model: ->(m) { belongs_to :child, inverse_of: :model, dependent: :destroy },
|
259
|
-
viewmodel: ->(v) { association :child, shared: true }
|
260
|
-
)
|
261
|
-
end
|
262
|
-
|
263
|
-
def child_attributes
|
264
|
-
super.merge(
|
265
|
-
model: ->(m) { has_one :model, inverse_of: :child }
|
266
|
-
)
|
267
|
-
end
|
268
|
-
|
269
|
-
# parent depends on child, ensure it's touched first
|
270
|
-
def viewmodel_class
|
271
|
-
child_viewmodel_class
|
272
|
-
super
|
273
|
-
end
|
280
|
+
module ParentAndExternalSharedChild
|
281
|
+
extend ActiveSupport::Concern
|
282
|
+
include ViewModelSpecHelpers::ParentAndSharedBelongsToChild
|
274
283
|
|
275
|
-
def
|
276
|
-
|
284
|
+
def subject_association_features
|
285
|
+
{ external: true }
|
277
286
|
end
|
278
287
|
end
|
279
288
|
|
@@ -282,15 +291,17 @@ module ViewModelSpecHelpers
|
|
282
291
|
include ViewModelSpecHelpers::Base
|
283
292
|
|
284
293
|
def model_attributes
|
294
|
+
f = subject_association_features
|
285
295
|
super.merge(
|
286
296
|
model: ->(m) { has_many :model_children, inverse_of: :model, dependent: :destroy },
|
287
|
-
viewmodel: ->(v) { association :children,
|
297
|
+
viewmodel: ->(v) { association :children, through: :model_children, through_order_attr: :position, **f }
|
288
298
|
)
|
289
299
|
end
|
290
300
|
|
291
301
|
def child_attributes
|
292
302
|
super.merge(
|
293
|
-
model: ->(m) { has_many :model_children, inverse_of: :child, dependent: :destroy }
|
303
|
+
model: ->(m) { has_many :model_children, inverse_of: :child, dependent: :destroy },
|
304
|
+
viewmodel: ->(v) { root! }
|
294
305
|
)
|
295
306
|
end
|
296
307
|
|