iknow_view_models 2.10.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +119 -0
  3. data/.travis.yml +31 -0
  4. data/Appraisals +6 -16
  5. data/gemfiles/{rails_7_0.gemfile → rails_6_0_beta.gemfile} +2 -2
  6. data/iknow_view_models.gemspec +3 -5
  7. data/lib/iknow_view_models/version.rb +1 -1
  8. data/lib/view_model/active_record/association_data.rb +206 -92
  9. data/lib/view_model/active_record/association_manipulation.rb +22 -12
  10. data/lib/view_model/active_record/cache/cacheable_view.rb +3 -13
  11. data/lib/view_model/active_record/cache.rb +2 -2
  12. data/lib/view_model/active_record/cloner.rb +11 -11
  13. data/lib/view_model/active_record/controller.rb +0 -2
  14. data/lib/view_model/active_record/update_context.rb +21 -3
  15. data/lib/view_model/active_record/update_data.rb +43 -45
  16. data/lib/view_model/active_record/update_operation.rb +265 -153
  17. data/lib/view_model/active_record/visitor.rb +9 -6
  18. data/lib/view_model/active_record.rb +94 -74
  19. data/lib/view_model/after_transaction_runner.rb +3 -18
  20. data/lib/view_model/callbacks.rb +2 -2
  21. data/lib/view_model/changes.rb +24 -16
  22. data/lib/view_model/config.rb +6 -2
  23. data/lib/view_model/deserialization_error.rb +31 -0
  24. data/lib/view_model/deserialize_context.rb +2 -6
  25. data/lib/view_model/error_view.rb +6 -5
  26. data/lib/view_model/record/attribute_data.rb +11 -6
  27. data/lib/view_model/record.rb +44 -24
  28. data/lib/view_model/serialize_context.rb +2 -63
  29. data/lib/view_model/test_helpers/arvm_builder.rb +2 -4
  30. data/lib/view_model/traversal_context.rb +2 -2
  31. data/lib/view_model.rb +21 -13
  32. data/shell.nix +1 -1
  33. data/test/helpers/arvm_test_models.rb +4 -12
  34. data/test/helpers/arvm_test_utilities.rb +6 -0
  35. data/test/helpers/controller_test_helpers.rb +6 -6
  36. data/test/helpers/viewmodel_spec_helpers.rb +63 -52
  37. data/test/unit/view_model/access_control_test.rb +88 -37
  38. data/test/unit/view_model/active_record/belongs_to_test.rb +110 -178
  39. data/test/unit/view_model/active_record/cache_test.rb +11 -5
  40. data/test/unit/view_model/active_record/cloner_test.rb +1 -1
  41. data/test/unit/view_model/active_record/controller_test.rb +12 -20
  42. data/test/unit/view_model/active_record/has_many_test.rb +540 -316
  43. data/test/unit/view_model/active_record/has_many_through_poly_test.rb +12 -15
  44. data/test/unit/view_model/active_record/has_many_through_test.rb +15 -58
  45. data/test/unit/view_model/active_record/has_one_test.rb +288 -135
  46. data/test/unit/view_model/active_record/poly_test.rb +0 -1
  47. data/test/unit/view_model/active_record/shared_test.rb +21 -39
  48. data/test/unit/view_model/active_record/version_test.rb +3 -2
  49. data/test/unit/view_model/active_record_test.rb +5 -63
  50. data/test/unit/view_model/callbacks_test.rb +1 -0
  51. data/test/unit/view_model/record_test.rb +0 -32
  52. data/test/unit/view_model/traversal_context_test.rb +13 -12
  53. metadata +15 -25
  54. data/.github/workflows/gem-push.yml +0 -31
  55. data/.github/workflows/test.yml +0 -65
  56. data/gemfiles/rails_6_0.gemfile +0 -9
  57. data/gemfiles/rails_6_1.gemfile +0 -9
  58. data/test/unit/view_model/active_record/optional_attribute_view_test.rb +0 -58
@@ -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, optional: 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, model_attribute_name, using, format,
54
- array, optional, read_only, write_once)
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 = false
170
- @changed_attributes = []
171
- @changed_children = false
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 changed_children?
193
- @changed_children
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, member_data|
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 children_changed!
231
- @changed_children = true
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: new_model?,
237
- changed_attributes: changed_attributes,
238
- changed_children: changed_children?)
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 = changes
243
- @new_model = false
244
- @changed_attributes = []
245
- @changed_children = false
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
- Array.wrap(value).any? { |v| v.respond_to?(:previous_changes) && v.previous_changes.changed_tree? }
353
- self.children_changed!
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
- if ActiveSupport::VERSION::MAJOR < 7
66
- # prevent cached old class from being used to resolve associations
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("ViewModel attributes must be symbols")
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, include_shared: true)
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, include_shared: true, lock: nil)
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, include_shared: include_shared),
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
@@ -1,7 +1,7 @@
1
1
  with import <nixpkgs> {};
2
2
 
3
3
  (bundlerEnv {
4
- name = "dev";
4
+ name = "iknow-view-models-shell";
5
5
  gemdir = ./nix/gem;
6
6
 
7
7
  gemConfig = (defaultGemConfig.override { postgresql = postgresql_11; });
@@ -6,18 +6,10 @@ require "view_model/active_record/controller"
6
6
 
7
7
  require "acts_as_manual_list"
8
8
 
9
- db_config =
10
- if (url = ENV['DATABASE_URL'])
11
- url
12
- else
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, optional: true
84
+ association :children
83
85
  association :poly, viewmodels: [:PolyOne, :PolyTwo]
84
- association :category, shared: true
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
- if ActiveSupport::VERSION::MAJOR < 7
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
- super.merge(schema: ->(t) { t.integer :next_id },
143
- model: ->(m) {
144
- belongs_to :next, class_name: self.name, inverse_of: :previous, dependent: :destroy
145
- has_one :previous, class_name: self.name, foreign_key: :next_id, inverse_of: :next
146
- },
147
- viewmodel: ->(v) { association :next })
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 ParentAndOrderedChildren
244
+ module ParentAndSharedHasManyChildren
214
245
  extend ActiveSupport::Concern
215
- include ViewModelSpecHelpers::Base
216
-
217
- def model_attributes
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.references :model, foreign_key: true; t.float :position, null: false },
227
- model: ->(m) { belongs_to :model, inverse_of: :children },
228
- viewmodel: ->(v) { acts_as_list :position },
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 constraiont
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
- def model_attributes
256
- super.merge(
257
- schema: ->(t) { t.references :child, foreign_key: true },
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 subject_association
276
- viewmodel_class._association_data('child')
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, shared: true, through: :model_children, through_order_attr: :position }
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