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.
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