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 +4 -4
- data/lib/iknow_view_models/version.rb +1 -1
- data/lib/view_model/record.rb +13 -5
- data/test/helpers/test_access_control.rb +18 -0
- data/test/unit/view_model/record_test.rb +209 -100
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 573266179096880a34febada583570484e420b91a242920198cfa119192c3fbc
|
4
|
+
data.tar.gz: c024eb9398a4b0d49103d1806379d0f20312f7c6965e9fe2751b09bb720f2fc3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 353f42c901c4d52cba6bbe459cc6266c2e59f3a56ca432bac616cd4534c95116d0a0e27b8329799717259640e098c704c4f6b63c83713cd42cb3096c6ca6c5b3
|
7
|
+
data.tar.gz: 4e74aa987e3ff26dd9b14bb3fbebc291ce86087dca8f8bcc93ef9b320d7d7ed22f150ce4f3153e27c51f6a2f8d6561fe53cdcb7ac45734caf156f50b10a543d1
|
data/lib/view_model/record.rb
CHANGED
@@ -359,12 +359,20 @@ class ViewModel::Record < ViewModel
|
|
359
359
|
|
360
360
|
attribute_changed!(vm_attr_name)
|
361
361
|
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
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
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
100
|
-
let(:
|
101
|
-
let(:default_model_values) { default_values }
|
118
|
+
# Default values for each model attribute, nil if absent
|
119
|
+
let(:model_defaults) { {} }
|
102
120
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
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
|
-
|
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: [
|
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(
|
143
|
-
assert_equal(
|
144
|
-
refute(
|
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(
|
158
|
-
assert(
|
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(
|
171
|
-
assert_equal(
|
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
|
-
|
223
|
+
update_view = subject_view.merge('simple' => 'changed')
|
185
224
|
|
186
|
-
vm = viewmodel_class.deserialize_from_view(
|
225
|
+
vm = viewmodel_class.deserialize_from_view(update_view, deserialize_context: update_context)
|
187
226
|
|
188
|
-
assert(
|
189
|
-
assert_equal('changed',
|
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 =
|
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 =
|
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(
|
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
|
-
|
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(
|
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(:
|
245
|
-
let(:
|
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(
|
253
|
-
vm = viewmodel_class.new(
|
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(:
|
262
|
-
let(:
|
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 =
|
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 =
|
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
|
-
|
290
|
-
|
332
|
+
describe 'asserting the default' do
|
333
|
+
include CanSerialize
|
334
|
+
include CanDeserializeToExisting
|
291
335
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
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
|
-
|
301
|
-
|
302
|
-
viewmodel_class.deserialize_from_view(
|
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
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
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 =
|
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(:
|
344
|
-
let(:
|
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 =
|
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(
|
369
|
-
assert_equal(10,
|
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(:
|
402
|
-
let(:
|
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(:
|
407
|
-
let(:
|
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(
|
411
|
-
|
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
|
-
|
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 =
|
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(
|
424
|
-
assert(
|
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',
|
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 =
|
530
|
+
new_view = subject_view.merge('nested' => subject_nested_view.merge('member' => 'changed'))
|
435
531
|
|
436
|
-
partial_update_context = TestDeserializeContext.new(targets: [
|
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(
|
442
|
-
refute(
|
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(:
|
451
|
-
let(:
|
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(:
|
454
|
-
let(:
|
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(:
|
459
|
-
let(:
|
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: [
|
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
|
-
|
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 =
|
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
|
-
|
482
|
-
vm = viewmodel_class.deserialize_from_view(
|
483
|
-
assert(
|
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(
|
486
|
-
assert(
|
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
|
-
|
602
|
+
subject_view['nested'] << view_base.merge('_type' => 'Nested', 'member' => 'member3')
|
494
603
|
|
495
|
-
vm = viewmodel_class.deserialize_from_view(
|
604
|
+
vm = viewmodel_class.deserialize_from_view(subject_view, deserialize_context: update_context)
|
496
605
|
|
497
|
-
assert(
|
606
|
+
assert(subject_model.equal?(vm.model), 'returned model was not the same')
|
498
607
|
assert_equal(3, vm.model.nested.size)
|
499
|
-
assert(
|
500
|
-
assert(
|
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.
|
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
|
+
date: 2021-12-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|