iknow_view_models 2.9.0 → 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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/iknow_view_models.gemspec +1 -1
  3. data/lib/iknow_view_models/version.rb +1 -1
  4. data/lib/view_model/active_record/association_data.rb +206 -92
  5. data/lib/view_model/active_record/association_manipulation.rb +22 -12
  6. data/lib/view_model/active_record/cache/cacheable_view.rb +3 -13
  7. data/lib/view_model/active_record/cache.rb +2 -2
  8. data/lib/view_model/active_record/cloner.rb +11 -11
  9. data/lib/view_model/active_record/controller.rb +0 -2
  10. data/lib/view_model/active_record/update_context.rb +21 -3
  11. data/lib/view_model/active_record/update_data.rb +43 -45
  12. data/lib/view_model/active_record/update_operation.rb +265 -153
  13. data/lib/view_model/active_record/visitor.rb +9 -6
  14. data/lib/view_model/active_record.rb +94 -74
  15. data/lib/view_model/after_transaction_runner.rb +3 -18
  16. data/lib/view_model/changes.rb +24 -16
  17. data/lib/view_model/config.rb +6 -2
  18. data/lib/view_model/deserialization_error.rb +31 -0
  19. data/lib/view_model/deserialize_context.rb +2 -6
  20. data/lib/view_model/error_view.rb +6 -5
  21. data/lib/view_model/record/attribute_data.rb +11 -6
  22. data/lib/view_model/record.rb +44 -24
  23. data/lib/view_model/serialize_context.rb +2 -63
  24. data/lib/view_model.rb +17 -8
  25. data/shell.nix +1 -1
  26. data/test/helpers/arvm_test_utilities.rb +6 -0
  27. data/test/helpers/controller_test_helpers.rb +5 -3
  28. data/test/helpers/viewmodel_spec_helpers.rb +63 -52
  29. data/test/unit/view_model/access_control_test.rb +88 -37
  30. data/test/unit/view_model/active_record/belongs_to_test.rb +110 -178
  31. data/test/unit/view_model/active_record/cache_test.rb +3 -2
  32. data/test/unit/view_model/active_record/cloner_test.rb +1 -1
  33. data/test/unit/view_model/active_record/controller_test.rb +12 -20
  34. data/test/unit/view_model/active_record/has_many_test.rb +540 -316
  35. data/test/unit/view_model/active_record/has_many_through_poly_test.rb +12 -15
  36. data/test/unit/view_model/active_record/has_many_through_test.rb +15 -58
  37. data/test/unit/view_model/active_record/has_one_test.rb +288 -135
  38. data/test/unit/view_model/active_record/poly_test.rb +0 -1
  39. data/test/unit/view_model/active_record/shared_test.rb +21 -39
  40. data/test/unit/view_model/active_record/version_test.rb +3 -2
  41. data/test/unit/view_model/active_record_test.rb +5 -63
  42. data/test/unit/view_model/callbacks_test.rb +1 -0
  43. data/test/unit/view_model/record_test.rb +0 -32
  44. data/test/unit/view_model/traversal_context_test.rb +13 -12
  45. metadata +5 -8
  46. data/test/unit/view_model/active_record/optional_attribute_view_test.rb +0 -58
@@ -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
data/lib/view_model.rb CHANGED
@@ -20,6 +20,7 @@ class ViewModel
20
20
  attr_accessor :_attributes
21
21
  attr_accessor :schema_version
22
22
  attr_reader :view_aliases
23
+ attr_writer :view_name
23
24
 
24
25
  def inherited(subclass)
25
26
  subclass.initialize_as_viewmodel
@@ -41,15 +42,23 @@ class ViewModel
41
42
  end
42
43
  end
43
44
 
44
- def view_name=(name)
45
- @view_name = name
46
- end
47
-
48
45
  def add_view_alias(as)
49
46
  view_aliases << as
50
47
  ViewModel::Registry.register(self, as: as)
51
48
  end
52
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
+
53
62
  # ViewModels are typically going to be pretty simple structures. Make it a
54
63
  # bit easier to define them: attributes specified this way are given
55
64
  # accessors and assigned in order by the default constructor.
@@ -59,7 +68,7 @@ class ViewModel
59
68
 
60
69
  def attribute(attr, **_args)
61
70
  unless attr.is_a?(Symbol)
62
- raise ArgumentError.new("ViewModel attributes must be symbols")
71
+ raise ArgumentError.new('ViewModel attributes must be symbols')
63
72
  end
64
73
 
65
74
  attr_accessor attr
@@ -116,7 +125,7 @@ class ViewModel
116
125
  # If this viewmodel represents an AR model, what associations does it make
117
126
  # use of? Returns a includes spec appropriate for DeepPreloader, either as
118
127
  # AR-style nested hashes or DeepPreloader::Spec.
119
- def eager_includes(serialize_context: new_serialize_context, include_shared: true)
128
+ def eager_includes(serialize_context: new_serialize_context, include_referenced: true)
120
129
  {}
121
130
  end
122
131
 
@@ -225,10 +234,10 @@ class ViewModel
225
234
  schema_version == self.schema_version
226
235
  end
227
236
 
228
- 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)
229
238
  Array.wrap(viewmodels).group_by(&:class).each do |type, views|
230
239
  DeepPreloader.preload(views.map(&:model),
231
- type.eager_includes(serialize_context: serialize_context, include_shared: include_shared),
240
+ type.eager_includes(serialize_context: serialize_context, include_referenced: include_referenced),
232
241
  lock: lock)
233
242
  end
234
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; });
@@ -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
@@ -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