iknow_view_models 3.5.3 → 3.6.0

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