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 +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
|