iknow_view_models 3.5.3 → 3.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 27ea471c380869f02c7f103025a151b53d99a1c8f19181566852f12f78ba7cd3
4
- data.tar.gz: dc18591e8c0b39e986cd0895e691e6d4d3a89df6cf04c4c486b3ecfc8699da4f
3
+ metadata.gz: 573266179096880a34febada583570484e420b91a242920198cfa119192c3fbc
4
+ data.tar.gz: c024eb9398a4b0d49103d1806379d0f20312f7c6965e9fe2751b09bb720f2fc3
5
5
  SHA512:
6
- metadata.gz: 156474c34631689f937fda7e3f4a2b2421a8205b3d172a2e72476aa8e440c5bb45b760aaf72a9e70ae9ff9830a209f8be727eb27d233a875aabf963ad88ce85b
7
- data.tar.gz: 8e39b0542bef145e8bbc6e6c30ff17d4ff4de2e08c93f7cc7b228cccdac6443159de74e9e3419fada058c09c38b1eba571d4f16633ce83abe6972331a8a4017c
6
+ metadata.gz: 353f42c901c4d52cba6bbe459cc6266c2e59f3a56ca432bac616cd4534c95116d0a0e27b8329799717259640e098c704c4f6b63c83713cd42cb3096c6ca6c5b3
7
+ data.tar.gz: 4e74aa987e3ff26dd9b14bb3fbebc291ce86087dca8f8bcc93ef9b320d7d7ed22f150ce4f3153e27c51f6a2f8d6561fe53cdcb7ac45734caf156f50b10a543d1
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IknowViewModels
4
- VERSION = '3.5.3'
4
+ VERSION = '3.6.0'
5
5
  end
@@ -359,12 +359,20 @@ class ViewModel::Record < ViewModel
359
359
 
360
360
  attribute_changed!(vm_attr_name)
361
361
 
362
- if attr_data.using_viewmodel? && !value.nil?
363
- # Extract model from target viewmodel(s) to attach to our model
364
- value = attr_data.map_value(value) { |vm| vm.model }
365
- end
362
+ model_value =
363
+ if attr_data.using_viewmodel? && !value.nil?
364
+ # Extract model from target viewmodel(s) to attach to our model
365
+ attr_data.map_value(value) { |vm| vm.model }
366
+ else
367
+ value
368
+ end
369
+
370
+ model.public_send("#{attr_data.model_attr_name}=", model_value)
366
371
 
367
- model.public_send("#{attr_data.model_attr_name}=", value)
372
+ elsif new_model?
373
+ # Record attribute_changed for mutable values asserted on a new model, even where
374
+ # they match the ActiveRecord default.
375
+ attribute_changed!(vm_attr_name) unless attr_data.read_only? && !attr_data.write_once?
368
376
  end
369
377
 
370
378
  if attr_data.using_viewmodel?
@@ -13,6 +13,7 @@ class TestAccessControl < ViewModel::AccessControl
13
13
  @editable_checks = []
14
14
  @visible_checks = []
15
15
  @valid_edit_checks = []
16
+ @changes = []
16
17
  end
17
18
 
18
19
  # Collect
@@ -33,6 +34,17 @@ class TestAccessControl < ViewModel::AccessControl
33
34
  ViewModel::AccessControl::Result.new(@can_view)
34
35
  end
35
36
 
37
+ def record_deserialize_changes(ref, changes)
38
+ @changes << [ref, changes]
39
+ end
40
+
41
+ # Collect all changes on after_deserialize, to allow inspecting changes that
42
+ # didn't result in `changed?`
43
+ after_deserialize do
44
+ ref = view.to_reference
45
+ record_deserialize_changes(ref, changes)
46
+ end
47
+
36
48
  # Query (also see attr_accessors)
37
49
 
38
50
  def valid_edit_refs
@@ -55,4 +67,10 @@ class TestAccessControl < ViewModel::AccessControl
55
67
  def was_edited?(ref)
56
68
  all_valid_edit_changes(ref).present?
57
69
  end
70
+
71
+ def all_changes(ref)
72
+ @changes
73
+ .select { |cref, _changes| cref == ref }
74
+ .map { |_cref, changes| changes }
75
+ end
58
76
  end
@@ -57,11 +57,30 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
57
57
  let(:model_body) { nil }
58
58
  let(:viewmodel_body) { nil }
59
59
 
60
+ # Generate an ActiveModel-like keyword argument constructor.
61
+ def generate_model_constructor(model_class, model_defaults)
62
+ args = model_class.members
63
+ params = args.map do |arg_name|
64
+ "#{arg_name}: self.class.__constructor_default(:#{arg_name})"
65
+ end
66
+
67
+ <<-SRC
68
+ def initialize(#{params.join(", ")})
69
+ super(#{args.join(", ")})
70
+ end
71
+ SRC
72
+ end
73
+
60
74
  let(:model_class) do
61
75
  mb = model_body
62
- Struct.new(*attributes.keys) do
63
- class_eval(&mb) if mb
64
- end
76
+ mds = model_defaults
77
+
78
+ model = Struct.new(*attributes.keys)
79
+ constructor = generate_model_constructor(model, mds)
80
+ model.class_eval(constructor)
81
+ model.define_singleton_method(:__constructor_default) { |name| mds[name] }
82
+ model.class_eval(&mb) if mb
83
+ model
65
84
  end
66
85
 
67
86
  let(:viewmodel_class) do
@@ -96,21 +115,39 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
96
115
  end
97
116
  end
98
117
 
99
- let(:default_values) { {} }
100
- let(:default_view_values) { default_values }
101
- let(:default_model_values) { default_values }
118
+ # Default values for each model attribute, nil if absent
119
+ let(:model_defaults) { {} }
102
120
 
103
- let(:default_view) do
104
- attribute_names.each_with_object(view_base.dup) do |(model_attr_name, vm_attr_name), view|
105
- view[vm_attr_name] = default_view_values.fetch(vm_attr_name.to_sym, model_attr_name)
106
- end
121
+ # attribute values used to instantiate the subject model and subject view (if not overridden)
122
+ let(:subject_attributes) { {} }
123
+
124
+ # attribute values used to instantiate the subject model
125
+ let(:subject_model_attributes) { subject_attributes }
126
+
127
+ # attribute values used to deserialize the subject view: these are expected to
128
+ # deserialize to create a model equal to subject_model
129
+ let(:subject_view_attributes) { subject_attributes }
130
+
131
+ # Subject model to compare with or deserialize into
132
+ let(:subject_model) do
133
+ model_class.new(**subject_model_attributes)
107
134
  end
108
135
 
109
- let(:default_model) do
110
- attr_values = attribute_names.map do |model_attr_name, _vm_attr_name|
111
- default_model_values.fetch(model_attr_name.to_sym, model_attr_name)
136
+ # View that when deserialized into a new model will be equal to subject_model
137
+ let(:subject_view) do
138
+ view_base.merge(subject_view_attributes.stringify_keys)
139
+ end
140
+
141
+ # The expected result of serializing subject_model (depends on subject_view corresponding to subject_model)
142
+ let(:expected_view) do
143
+ view = subject_view.dup
144
+ attribute_names.each do |model_attr_name, vm_attr_name|
145
+ unless view.has_key?(vm_attr_name)
146
+ expected_value = subject_model_attributes.fetch(model_attr_name) { model_defaults[model_attr_name] }
147
+ view[vm_attr_name] = expected_value
148
+ end
112
149
  end
113
- model_class.new(*attr_values)
150
+ view
114
151
  end
115
152
 
116
153
  let(:access_control) { TestAccessControl.new(true, true, true) }
@@ -118,7 +155,7 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
118
155
  let(:create_context) { TestDeserializeContext.new(access_control: access_control) }
119
156
 
120
157
  # Prime our simplistic `resolve_viewmodel` with the desired models to update
121
- let(:update_context) { TestDeserializeContext.new(targets: [default_model], access_control: access_control) }
158
+ let(:update_context) { TestDeserializeContext.new(targets: [subject_model], access_control: access_control) }
122
159
 
123
160
  def assert_edited(vm, **changes)
124
161
  ref = vm.to_reference
@@ -139,9 +176,9 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
139
176
  def self.included(base)
140
177
  base.instance_eval do
141
178
  it 'can deserialize to a new model' do
142
- vm = viewmodel_class.deserialize_from_view(default_view, deserialize_context: create_context)
143
- assert_equal(default_model, vm.model)
144
- refute(default_model.equal?(vm.model))
179
+ vm = viewmodel_class.deserialize_from_view(subject_view, deserialize_context: create_context)
180
+ assert_equal(subject_model, vm.model)
181
+ refute(subject_model.equal?(vm.model))
145
182
 
146
183
  all_view_attrs = attribute_names.map { |_mname, vname| vname }
147
184
  assert_edited(vm, new: true, changed_attributes: all_view_attrs)
@@ -154,8 +191,8 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
154
191
  def self.included(base)
155
192
  base.instance_eval do
156
193
  it 'can deserialize to existing model with no changes' do
157
- vm = viewmodel_class.deserialize_from_view(default_view, deserialize_context: update_context)
158
- assert(default_model.equal?(vm.model))
194
+ vm = viewmodel_class.deserialize_from_view(subject_view, deserialize_context: update_context)
195
+ assert(subject_model.equal?(vm.model))
159
196
 
160
197
  assert_unchanged(vm)
161
198
  end
@@ -167,8 +204,8 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
167
204
  def self.included(base)
168
205
  base.instance_eval do
169
206
  it 'can serialize to the expected view' do
170
- h = viewmodel_class.new(default_model).to_hash
171
- assert_equal(default_view, h)
207
+ h = viewmodel_class.new(subject_model).to_hash
208
+ assert_equal(expected_view, h)
172
209
  end
173
210
  end
174
211
  end
@@ -176,22 +213,24 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
176
213
 
177
214
  describe 'with simple attribute' do
178
215
  let(:attributes) { { simple: {} } }
216
+ let(:subject_attributes) { { simple: "simple" } }
217
+
179
218
  include CanSerialize
180
219
  include CanDeserializeToNew
181
220
  include CanDeserializeToExisting
182
221
 
183
222
  it 'can be updated' do
184
- new_view = default_view.merge('simple' => 'changed')
223
+ update_view = subject_view.merge('simple' => 'changed')
185
224
 
186
- vm = viewmodel_class.deserialize_from_view(new_view, deserialize_context: update_context)
225
+ vm = viewmodel_class.deserialize_from_view(update_view, deserialize_context: update_context)
187
226
 
188
- assert(default_model.equal?(vm.model), 'returned model was not the same')
189
- assert_equal('changed', default_model.simple)
227
+ assert(subject_model.equal?(vm.model), 'returned model was not the same')
228
+ assert_equal('changed', subject_model.simple)
190
229
  assert_edited(vm, changed_attributes: [:simple])
191
230
  end
192
231
 
193
232
  it 'rejects unknown attributes' do
194
- view = default_view.merge('unknown' => 'illegal')
233
+ view = subject_view.merge('unknown' => 'illegal')
195
234
  ex = assert_raises(ViewModel::DeserializationError::UnknownAttribute) do
196
235
  viewmodel_class.deserialize_from_view(view, deserialize_context: create_context)
197
236
  end
@@ -199,7 +238,7 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
199
238
  end
200
239
 
201
240
  it 'rejects unknown versions' do
202
- view = default_view.merge(ViewModel::VERSION_ATTRIBUTE => 100)
241
+ view = subject_view.merge(ViewModel::VERSION_ATTRIBUTE => 100)
203
242
  ex = assert_raises(ViewModel::DeserializationError::SchemaVersionMismatch) do
204
243
  viewmodel_class.deserialize_from_view(view, deserialize_context: create_context)
205
244
  end
@@ -207,13 +246,15 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
207
246
 
208
247
  it 'edit checks when creating empty' do
209
248
  vm = viewmodel_class.deserialize_from_view(view_base, deserialize_context: create_context)
210
- refute(default_model.equal?(vm.model), 'returned model was the same')
249
+ refute(subject_model.equal?(vm.model), 'returned model was the same')
211
250
  assert_edited(vm, new: true)
212
251
  end
213
252
  end
214
253
 
215
254
  describe 'with validated simple attribute' do
216
255
  let(:attributes) { { validated: {} } }
256
+ let(:subject_attributes) { { validated: "validated" } }
257
+
217
258
  let(:viewmodel_body) do
218
259
  ->(_x) do
219
260
  def validate!
@@ -229,10 +270,10 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
229
270
  include CanDeserializeToExisting
230
271
 
231
272
  it 'rejects update when validation fails' do
232
- new_view = default_view.merge('validated' => 'naughty')
273
+ update_view = subject_view.merge('validated' => 'naughty')
233
274
 
234
275
  ex = assert_raises(ViewModel::DeserializationError::Validation) do
235
- viewmodel_class.deserialize_from_view(new_view, deserialize_context: update_context)
276
+ viewmodel_class.deserialize_from_view(update_view, deserialize_context: update_context)
236
277
  end
237
278
  assert_equal('validated', ex.attribute)
238
279
  assert_equal('was naughty', ex.reason)
@@ -241,16 +282,16 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
241
282
 
242
283
  describe 'with renamed attribute' do
243
284
  let(:attributes) { { modelname: { as: :viewname } } }
244
- let(:default_model_values) { { modelname: 'value' } }
245
- let(:default_view_values) { { viewname: 'value' } }
285
+ let(:subject_model_attributes) { { modelname: 'value' } }
286
+ let(:subject_view_attributes) { { viewname: 'value' } }
246
287
 
247
288
  include CanSerialize
248
289
  include CanDeserializeToNew
249
290
  include CanDeserializeToExisting
250
291
 
251
292
  it 'makes attributes available on their new names' do
252
- value(default_model.modelname).must_equal('value')
253
- vm = viewmodel_class.new(default_model)
293
+ value(subject_model.modelname).must_equal('value')
294
+ vm = viewmodel_class.new(subject_model)
254
295
  value(vm.viewname).must_equal('value')
255
296
  end
256
297
  end
@@ -258,15 +299,15 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
258
299
  describe 'with formatted attribute' do
259
300
  let(:attributes) { { moment: { format: IknowParams::Serializer::Time } } }
260
301
  let(:moment) { 1.week.ago.change(usec: 0) }
261
- let(:default_model_values) { { moment: moment } }
262
- let(:default_view_values) { { moment: moment.iso8601 } }
302
+ let(:subject_model_attributes) { { moment: moment } }
303
+ let(:subject_view_attributes) { { moment: moment.iso8601 } }
263
304
 
264
305
  include CanSerialize
265
306
  include CanDeserializeToNew
266
307
  include CanDeserializeToExisting
267
308
 
268
309
  it 'raises correctly on an unparseable value' do
269
- bad_view = default_view.tap { |v| v['moment'] = 'not a timestamp' }
310
+ bad_view = subject_view.merge('moment' => 'not a timestamp')
270
311
  ex = assert_raises(ViewModel::DeserializationError::Validation) do
271
312
  viewmodel_class.deserialize_from_view(bad_view, deserialize_context: create_context)
272
313
  end
@@ -275,7 +316,7 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
275
316
  end
276
317
 
277
318
  it 'raises correctly on an undeserializable value' do
278
- bad_model = default_model.tap { |m| m.moment = 2.7 }
319
+ bad_model = subject_model.tap { |m| m.moment = 2.7 }
279
320
  ex = assert_raises(ViewModel::SerializationError) do
280
321
  viewmodel_class.new(bad_model).to_hash
281
322
  end
@@ -285,36 +326,51 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
285
326
 
286
327
  describe 'with read-only attribute' do
287
328
  let(:attributes) { { read_only: { read_only: true } } }
329
+ let(:model_defaults) { { read_only: 'immutable' } }
330
+ let(:subject_attributes) { { read_only: 'immutable' } }
288
331
 
289
- include CanSerialize
290
- include CanDeserializeToExisting
332
+ describe 'asserting the default' do
333
+ include CanSerialize
334
+ include CanDeserializeToExisting
291
335
 
292
- it 'deserializes to new without the attribute' do
293
- new_view = default_view.tap { |v| v.delete('read_only') }
294
- vm = viewmodel_class.deserialize_from_view(new_view, deserialize_context: create_context)
295
- refute(default_model.equal?(vm.model))
296
- assert_nil(vm.model.read_only)
297
- assert_edited(vm, new: true)
298
- end
336
+ it 'deserializes to new with the attribute' do
337
+ vm = viewmodel_class.deserialize_from_view(subject_view, deserialize_context: create_context)
338
+ assert_equal(subject_model, vm.model)
339
+ refute(subject_model.equal?(vm.model))
340
+ assert_edited(vm, new: true)
341
+ end
299
342
 
300
- it 'rejects deserialize from new' do
301
- ex = assert_raises(ViewModel::DeserializationError::ReadOnlyAttribute) do
302
- viewmodel_class.deserialize_from_view(default_view, deserialize_context: create_context)
343
+ it 'deserializes to new without the attribute' do
344
+ new_view = subject_view.tap { |v| v.delete('read_only') }
345
+ vm = viewmodel_class.deserialize_from_view(new_view, deserialize_context: create_context)
346
+ assert_equal(subject_model, vm.model)
347
+ refute(subject_model.equal?(vm.model))
348
+ assert_edited(vm, new: true)
303
349
  end
304
- assert_equal('read_only', ex.attribute)
305
350
  end
306
351
 
307
- it 'rejects update if changed' do
308
- new_view = default_view.merge('read_only' => 'written')
309
- ex = assert_raises(ViewModel::DeserializationError::ReadOnlyAttribute) do
310
- viewmodel_class.deserialize_from_view(new_view, deserialize_context: update_context)
352
+ describe 'attempting a change' do
353
+ let(:update_view) { subject_view.merge('read_only' => 'attempted change') }
354
+
355
+ it 'rejects deserialize from new' do
356
+ ex = assert_raises(ViewModel::DeserializationError::ReadOnlyAttribute) do
357
+ viewmodel_class.deserialize_from_view(update_view, deserialize_context: create_context)
358
+ end
359
+ assert_equal('read_only', ex.attribute)
360
+ end
361
+
362
+ it 'rejects update' do
363
+ ex = assert_raises(ViewModel::DeserializationError::ReadOnlyAttribute) do
364
+ viewmodel_class.deserialize_from_view(update_view, deserialize_context: update_context)
365
+ end
366
+ assert_equal('read_only', ex.attribute)
311
367
  end
312
- assert_equal('read_only', ex.attribute)
313
368
  end
314
369
  end
315
370
 
316
371
  describe 'with read-only write-once attribute' do
317
372
  let(:attributes) { { write_once: { read_only: true, write_once: true } } }
373
+ let(:subject_attributes) { { write_once: 'frozen' } }
318
374
  let(:model_body) do
319
375
  ->(_x) do
320
376
  # For the purposes of testing, we assume a record is new and can be
@@ -330,7 +386,7 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
330
386
  include CanDeserializeToExisting
331
387
 
332
388
  it 'rejects change to attribute' do
333
- new_view = default_view.merge('write_once' => 'written')
389
+ new_view = subject_view.merge('write_once' => 'written')
334
390
  ex = assert_raises(ViewModel::DeserializationError::ReadOnlyAttribute) do
335
391
  viewmodel_class.deserialize_from_view(new_view, deserialize_context: update_context)
336
392
  end
@@ -338,10 +394,33 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
338
394
  end
339
395
  end
340
396
 
397
+ describe 'with unspecified attributes falling back to the model default' do
398
+ let(:attributes) { { value: {} } }
399
+ let(:model_defaults) { { value: 5 } }
400
+ let(:subject_view_attributes) { { } }
401
+ let(:subject_model_attributes) { { value: 5 } }
402
+
403
+ it 'can deserialize to a new model' do
404
+ vm = viewmodel_class.deserialize_from_view(subject_view, deserialize_context: create_context)
405
+ assert_equal(subject_model, vm.model)
406
+ refute(subject_model.equal?(vm.model))
407
+ assert_edited(vm, new: true, changed_attributes: [])
408
+ end
409
+ end
410
+
411
+ describe 'with model defaults being asserted' do
412
+ let(:attributes) { { value: {} } }
413
+ let(:model_defaults) { { value: 5 } }
414
+ let(:subject_attributes) { { value: 5 } }
415
+
416
+ include CanDeserializeToNew
417
+ end
418
+
341
419
  describe 'with custom serialization' do
342
420
  let(:attributes) { { overridden: {} } }
343
- let(:default_view_values) { { overridden: 10 } }
344
- let(:default_model_values) { { overridden: 5 } }
421
+ let(:subject_model_attributes) { { overridden: 5 } }
422
+ let(:subject_view_attributes) { { overridden: 10 } }
423
+
345
424
  let(:viewmodel_body) do
346
425
  ->(_x) do
347
426
  def serialize_overridden(json, serialize_context:)
@@ -351,7 +430,7 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
351
430
  def deserialize_overridden(value, references:, deserialize_context:)
352
431
  before_value = model.overridden
353
432
  model.overridden = value.try { |v| Integer(v) / 2 }
354
- attribute_changed!(:overridden) unless before_value == model.overridden
433
+ attribute_changed!(:overridden) unless !new_model? && before_value == model.overridden
355
434
  end
356
435
  end
357
436
  end
@@ -361,12 +440,12 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
361
440
  include CanDeserializeToExisting
362
441
 
363
442
  it 'can be updated' do
364
- new_view = default_view.merge('overridden' => '20')
443
+ new_view = subject_view.merge('overridden' => '20')
365
444
 
366
445
  vm = viewmodel_class.deserialize_from_view(new_view, deserialize_context: update_context)
367
446
 
368
- assert(default_model.equal?(vm.model), 'returned model was not the same')
369
- assert_equal(10, default_model.overridden)
447
+ assert(subject_model.equal?(vm.model), 'returned model was not the same')
448
+ assert_equal(10, subject_model.overridden)
370
449
 
371
450
  assert_edited(vm, changed_attributes: [:overridden])
372
451
  end
@@ -398,77 +477,102 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
398
477
  end
399
478
 
400
479
  describe 'with nested viewmodel' do
401
- let(:default_nested_model) { nested_model_class.new('member') }
402
- let(:default_nested_view) { view_base.merge('_type' => 'Nested', 'member' => 'member') }
480
+ let(:subject_nested_model) { nested_model_class.new('member') }
481
+ let(:subject_nested_view) { view_base.merge('_type' => 'Nested', 'member' => 'member') }
403
482
 
404
483
  let(:attributes) { { simple: {}, nested: { using: nested_viewmodel_class } } }
405
484
 
406
- let(:default_view_values) { { nested: default_nested_view } }
407
- let(:default_model_values) { { nested: default_nested_model } }
485
+ let(:subject_view_attributes) { { nested: subject_nested_view } }
486
+ let(:subject_model_attributes) { { nested: subject_nested_model } }
408
487
 
409
488
  let(:update_context) do
410
- TestDeserializeContext.new(targets: [default_model, default_nested_model],
411
- access_control: access_control)
489
+ TestDeserializeContext.new(
490
+ targets: [subject_model, subject_nested_model],
491
+ access_control: access_control)
412
492
  end
413
493
 
414
494
  include CanSerialize
415
- include CanDeserializeToNew
495
+
496
+ it 'can deserialize to a new model' do
497
+ vm = viewmodel_class.deserialize_from_view(subject_view, deserialize_context: create_context)
498
+ assert_equal(subject_model, vm.model)
499
+ refute(subject_model.equal?(vm.model))
500
+
501
+ assert_equal(subject_nested_model, vm.model.nested)
502
+ refute(subject_nested_model.equal?(vm.model.nested))
503
+
504
+ assert_edited(vm, new: true, changed_attributes: ['nested'], changed_nested_children: true)
505
+ end
506
+
416
507
  include CanDeserializeToExisting
417
508
 
418
509
  it 'can update the nested value' do
419
- new_view = default_view.merge('nested' => default_nested_view.merge('member' => 'changed'))
510
+ new_view = subject_view.merge('nested' => subject_nested_view.merge('member' => 'changed'))
420
511
 
421
512
  vm = viewmodel_class.deserialize_from_view(new_view, deserialize_context: update_context)
422
513
 
423
- assert(default_model.equal?(vm.model), 'returned model was not the same')
424
- assert(default_nested_model.equal?(vm.model.nested), 'returned nested model was not the same')
514
+ assert(subject_model.equal?(vm.model), 'returned model was not the same')
515
+ assert(subject_nested_model.equal?(vm.model.nested), 'returned nested model was not the same')
425
516
 
426
- assert_equal('changed', default_model.nested.member)
517
+ assert_equal('changed', subject_model.nested.member)
427
518
 
428
519
  assert_unchanged(vm)
520
+
521
+ # The parent is itself not `changed?`, but it must record that its children are
522
+ change = access_control.all_changes(vm.to_reference)[0]
523
+ assert_equal(ViewModel::Changes.new(changed_nested_children: true), change)
524
+
429
525
  assert_edited(vm.nested, changed_attributes: [:member])
430
526
  end
431
527
 
432
528
  it 'can replace the nested value' do
433
529
  # The value will be unified if it is different after deserialization
434
- new_view = default_view.merge('nested' => default_nested_view.merge('member' => 'changed'))
530
+ new_view = subject_view.merge('nested' => subject_nested_view.merge('member' => 'changed'))
435
531
 
436
- partial_update_context = TestDeserializeContext.new(targets: [default_model],
532
+ partial_update_context = TestDeserializeContext.new(targets: [subject_model],
437
533
  access_control: access_control)
438
534
 
439
535
  vm = viewmodel_class.deserialize_from_view(new_view, deserialize_context: partial_update_context)
440
536
 
441
- assert(default_model.equal?(vm.model), 'returned model was not the same')
442
- refute(default_nested_model.equal?(vm.model.nested), 'returned nested model was the same')
537
+ assert(subject_model.equal?(vm.model), 'returned model was not the same')
538
+ refute(subject_nested_model.equal?(vm.model.nested), 'returned nested model was the same')
443
539
 
444
- assert_edited(vm, new: false, changed_attributes: [:nested])
540
+ assert_edited(vm, new: false, changed_attributes: [:nested], changed_nested_children: true)
445
541
  assert_edited(vm.nested, new: true, changed_attributes: [:member])
446
542
  end
447
543
  end
448
544
 
449
545
  describe 'with array of nested viewmodel' do
450
- let(:default_nested_model_1) { nested_model_class.new('member1') }
451
- let(:default_nested_view_1) { view_base.merge('_type' => 'Nested', 'member' => 'member1') }
546
+ let(:subject_nested_model_1) { nested_model_class.new('member1') }
547
+ let(:subject_nested_view_1) { view_base.merge('_type' => 'Nested', 'member' => 'member1') }
452
548
 
453
- let(:default_nested_model_2) { nested_model_class.new('member2') }
454
- let(:default_nested_view_2) { view_base.merge('_type' => 'Nested', 'member' => 'member2') }
549
+ let(:subject_nested_model_2) { nested_model_class.new('member2') }
550
+ let(:subject_nested_view_2) { view_base.merge('_type' => 'Nested', 'member' => 'member2') }
455
551
 
456
552
  let(:attributes) { { simple: {}, nested: { using: nested_viewmodel_class, array: true } } }
457
553
 
458
- let(:default_view_values) { { nested: [default_nested_view_1, default_nested_view_2] } }
459
- let(:default_model_values) { { nested: [default_nested_model_1, default_nested_model_2] } }
554
+ let(:subject_view_attributes) { { nested: [subject_nested_view_1, subject_nested_view_2] } }
555
+ let(:subject_model_attributes) { { nested: [subject_nested_model_1, subject_nested_model_2] } }
460
556
 
461
557
  let(:update_context) {
462
- TestDeserializeContext.new(targets: [default_model, default_nested_model_1, default_nested_model_2],
558
+ TestDeserializeContext.new(targets: [subject_model, subject_nested_model_1, subject_nested_model_2],
463
559
  access_control: access_control)
464
560
  }
465
561
 
466
562
  include CanSerialize
467
- include CanDeserializeToNew
563
+
564
+ it 'can deserialize to a new model' do
565
+ vm = viewmodel_class.deserialize_from_view(subject_view, deserialize_context: create_context)
566
+ assert_equal(subject_model, vm.model)
567
+ refute(subject_model.equal?(vm.model))
568
+
569
+ assert_edited(vm, new: true, changed_attributes: ['nested'], changed_nested_children: true)
570
+ end
571
+
468
572
  include CanDeserializeToExisting
469
573
 
470
574
  it 'rejects change to attribute' do
471
- new_view = default_view.merge('nested' => 'terrible')
575
+ new_view = subject_view.merge('nested' => 'terrible')
472
576
  ex = assert_raises(ViewModel::DeserializationError::InvalidAttributeType) do
473
577
  viewmodel_class.deserialize_from_view(new_view, deserialize_context: update_context)
474
578
  end
@@ -478,32 +582,37 @@ class ViewModel::RecordTest < ActiveSupport::TestCase
478
582
  end
479
583
 
480
584
  it 'can edit a nested value' do
481
- default_view['nested'][0]['member'] = 'changed'
482
- vm = viewmodel_class.deserialize_from_view(default_view, deserialize_context: update_context)
483
- assert(default_model.equal?(vm.model), 'returned model was not the same')
585
+ subject_view['nested'][0]['member'] = 'changed'
586
+ vm = viewmodel_class.deserialize_from_view(subject_view, deserialize_context: update_context)
587
+ assert(subject_model.equal?(vm.model), 'returned model was not the same')
484
588
  assert_equal(2, vm.model.nested.size)
485
- assert(default_nested_model_1.equal?(vm.model.nested[0]))
486
- assert(default_nested_model_2.equal?(vm.model.nested[1]))
589
+ assert(subject_nested_model_1.equal?(vm.model.nested[0]))
590
+ assert(subject_nested_model_2.equal?(vm.model.nested[1]))
487
591
 
488
592
  assert_unchanged(vm)
593
+
594
+ # The parent is itself not `changed?`, but it must record that its children are
595
+ change = access_control.all_changes(vm.to_reference)[0]
596
+ assert_equal(ViewModel::Changes.new(changed_nested_children: true), change)
597
+
489
598
  assert_edited(vm.nested[0], changed_attributes: [:member])
490
599
  end
491
600
 
492
601
  it 'can append a nested value' do
493
- default_view['nested'] << view_base.merge('_type' => 'Nested', 'member' => 'member3')
602
+ subject_view['nested'] << view_base.merge('_type' => 'Nested', 'member' => 'member3')
494
603
 
495
- vm = viewmodel_class.deserialize_from_view(default_view, deserialize_context: update_context)
604
+ vm = viewmodel_class.deserialize_from_view(subject_view, deserialize_context: update_context)
496
605
 
497
- assert(default_model.equal?(vm.model), 'returned model was not the same')
606
+ assert(subject_model.equal?(vm.model), 'returned model was not the same')
498
607
  assert_equal(3, vm.model.nested.size)
499
- assert(default_nested_model_1.equal?(vm.model.nested[0]))
500
- assert(default_nested_model_2.equal?(vm.model.nested[1]))
608
+ assert(subject_nested_model_1.equal?(vm.model.nested[0]))
609
+ assert(subject_nested_model_2.equal?(vm.model.nested[1]))
501
610
 
502
611
  vm.model.nested.each_with_index do |nvm, i|
503
612
  assert_equal("member#{i + 1}", nvm.member)
504
613
  end
505
614
 
506
- assert_edited(vm, changed_attributes: [:nested])
615
+ assert_edited(vm, changed_attributes: [:nested], changed_nested_children: true)
507
616
  assert_edited(vm.nested[2], new: true, changed_attributes: [:member])
508
617
  end
509
618
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iknow_view_models
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.5.3
4
+ version: 3.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - iKnow Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-11-12 00:00:00.000000000 Z
11
+ date: 2021-12-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord