iknow_view_models 2.9.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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