iknow_view_models 2.8.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +115 -0
  3. data/.gitignore +36 -0
  4. data/.travis.yml +31 -0
  5. data/Appraisals +9 -0
  6. data/Gemfile +19 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +19 -0
  9. data/Rakefile +21 -0
  10. data/appveyor.yml +22 -0
  11. data/gemfiles/rails_5_2.gemfile +15 -0
  12. data/gemfiles/rails_6_0_beta.gemfile +15 -0
  13. data/iknow_view_models.gemspec +49 -0
  14. data/lib/iknow_view_models.rb +12 -0
  15. data/lib/iknow_view_models/railtie.rb +8 -0
  16. data/lib/iknow_view_models/version.rb +5 -0
  17. data/lib/view_model.rb +333 -0
  18. data/lib/view_model/access_control.rb +154 -0
  19. data/lib/view_model/access_control/composed.rb +216 -0
  20. data/lib/view_model/access_control/open.rb +13 -0
  21. data/lib/view_model/access_control/read_only.rb +13 -0
  22. data/lib/view_model/access_control/tree.rb +264 -0
  23. data/lib/view_model/access_control_error.rb +10 -0
  24. data/lib/view_model/active_record.rb +383 -0
  25. data/lib/view_model/active_record/association_data.rb +178 -0
  26. data/lib/view_model/active_record/association_manipulation.rb +389 -0
  27. data/lib/view_model/active_record/cache.rb +265 -0
  28. data/lib/view_model/active_record/cache/cacheable_view.rb +51 -0
  29. data/lib/view_model/active_record/cloner.rb +113 -0
  30. data/lib/view_model/active_record/collection_nested_controller.rb +100 -0
  31. data/lib/view_model/active_record/controller.rb +77 -0
  32. data/lib/view_model/active_record/controller_base.rb +185 -0
  33. data/lib/view_model/active_record/nested_controller_base.rb +93 -0
  34. data/lib/view_model/active_record/singular_nested_controller.rb +34 -0
  35. data/lib/view_model/active_record/update_context.rb +252 -0
  36. data/lib/view_model/active_record/update_data.rb +749 -0
  37. data/lib/view_model/active_record/update_operation.rb +810 -0
  38. data/lib/view_model/active_record/visitor.rb +77 -0
  39. data/lib/view_model/after_transaction_runner.rb +29 -0
  40. data/lib/view_model/callbacks.rb +219 -0
  41. data/lib/view_model/changes.rb +62 -0
  42. data/lib/view_model/config.rb +29 -0
  43. data/lib/view_model/controller.rb +142 -0
  44. data/lib/view_model/deserialization_error.rb +437 -0
  45. data/lib/view_model/deserialize_context.rb +16 -0
  46. data/lib/view_model/error.rb +191 -0
  47. data/lib/view_model/error_view.rb +35 -0
  48. data/lib/view_model/record.rb +367 -0
  49. data/lib/view_model/record/attribute_data.rb +48 -0
  50. data/lib/view_model/reference.rb +31 -0
  51. data/lib/view_model/references.rb +48 -0
  52. data/lib/view_model/registry.rb +73 -0
  53. data/lib/view_model/schemas.rb +45 -0
  54. data/lib/view_model/serialization_error.rb +10 -0
  55. data/lib/view_model/serialize_context.rb +118 -0
  56. data/lib/view_model/test_helpers.rb +103 -0
  57. data/lib/view_model/test_helpers/arvm_builder.rb +111 -0
  58. data/lib/view_model/traversal_context.rb +126 -0
  59. data/lib/view_model/utils.rb +24 -0
  60. data/lib/view_model/utils/collections.rb +49 -0
  61. data/test/helpers/arvm_test_models.rb +59 -0
  62. data/test/helpers/arvm_test_utilities.rb +187 -0
  63. data/test/helpers/callback_tracer.rb +27 -0
  64. data/test/helpers/controller_test_helpers.rb +270 -0
  65. data/test/helpers/match_enumerator.rb +58 -0
  66. data/test/helpers/query_logging.rb +71 -0
  67. data/test/helpers/test_access_control.rb +56 -0
  68. data/test/helpers/viewmodel_spec_helpers.rb +326 -0
  69. data/test/unit/view_model/access_control_test.rb +769 -0
  70. data/test/unit/view_model/active_record/alias_test.rb +35 -0
  71. data/test/unit/view_model/active_record/belongs_to_test.rb +376 -0
  72. data/test/unit/view_model/active_record/cache_test.rb +351 -0
  73. data/test/unit/view_model/active_record/cloner_test.rb +313 -0
  74. data/test/unit/view_model/active_record/controller_test.rb +561 -0
  75. data/test/unit/view_model/active_record/counter_test.rb +80 -0
  76. data/test/unit/view_model/active_record/customization_test.rb +388 -0
  77. data/test/unit/view_model/active_record/has_many_test.rb +957 -0
  78. data/test/unit/view_model/active_record/has_many_through_poly_test.rb +269 -0
  79. data/test/unit/view_model/active_record/has_many_through_test.rb +736 -0
  80. data/test/unit/view_model/active_record/has_one_test.rb +334 -0
  81. data/test/unit/view_model/active_record/namespacing_test.rb +75 -0
  82. data/test/unit/view_model/active_record/optional_attribute_view_test.rb +58 -0
  83. data/test/unit/view_model/active_record/poly_test.rb +320 -0
  84. data/test/unit/view_model/active_record/shared_test.rb +285 -0
  85. data/test/unit/view_model/active_record/version_test.rb +121 -0
  86. data/test/unit/view_model/active_record_test.rb +542 -0
  87. data/test/unit/view_model/callbacks_test.rb +582 -0
  88. data/test/unit/view_model/deserialization_error/unique_violation_test.rb +73 -0
  89. data/test/unit/view_model/record_test.rb +524 -0
  90. data/test/unit/view_model/traversal_context_test.rb +371 -0
  91. data/test/unit/view_model_test.rb +62 -0
  92. metadata +490 -0
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require 'minitest/unit'
5
+ require 'rspec/expectations/minitest_integration'
6
+
7
+ require 'view_model'
8
+ require 'view_model/deserialization_error'
9
+
10
+ class ViewModel::DeserializationError::UniqueViolationTest < ActiveSupport::TestCase
11
+ extend Minitest::Spec::DSL
12
+
13
+ # Test error parser
14
+ describe 'message parser' do
15
+ let(:parse) { ViewModel::DeserializationError::UniqueViolation.parse_message_detail(detail_message) }
16
+
17
+ describe 'with a bad message prefix' do
18
+ let(:detail_message) { 'Unexpected (x)=(y) already exists.' }
19
+
20
+ it 'refuses to parse' do
21
+ expect(parse).to be_nil
22
+ end
23
+ end
24
+
25
+ describe 'with a bad message suffix' do
26
+ let(:detail_message) { 'Key (x)=(y) is already present.' }
27
+
28
+ it 'refuses to parse' do
29
+ expect(parse).to be_nil
30
+ end
31
+ end
32
+
33
+ describe 'with a single key and value' do
34
+ let(:detail_message) { 'Key (x)=(a) already exists.' }
35
+
36
+ it 'parses the key and value' do
37
+ expect(parse).to eq([['x'], 'a'])
38
+ end
39
+ end
40
+
41
+ describe 'with multiple keys and values' do
42
+ let(:detail_message) { 'Key (x, y)=(a, b) already exists.' }
43
+
44
+ it 'parses the keys and value' do
45
+ expect(parse).to eq([['x', 'y'], 'a, b'])
46
+ end
47
+ end
48
+
49
+ describe 'with quoted keys and values' do
50
+ let(:detail_message) { 'Key ("x, y", z)=(a, b, c) already exists.' }
51
+
52
+ it 'parses the keys and value' do
53
+ expect(parse).to eq([['x, y', 'z'], 'a, b, c'])
54
+ end
55
+ end
56
+
57
+ describe 'with nested quoted keys and values' do
58
+ let(:detail_message) { 'Key ("""x"", ""y""", z)=(a, b, c) already exists.' }
59
+
60
+ it 'parses the keys and value' do
61
+ expect(parse).to eq([['"x", "y"', 'z'], 'a, b, c'])
62
+ end
63
+ end
64
+
65
+ describe 'with unescaped values' do
66
+ let(:detail_message) { 'Key (a, b)=(a, b)=(c, d) already exists.' }
67
+
68
+ it 'parses the keys and value' do
69
+ expect(parse).to eq([['a', 'b'], 'a, b)=(c, d'])
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,524 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../helpers/test_access_control.rb"
4
+
5
+ require "minitest/autorun"
6
+ require 'minitest/unit'
7
+
8
+ require "view_model"
9
+ require "view_model/record"
10
+
11
+ class ViewModel::RecordTest < ActiveSupport::TestCase
12
+ using ViewModel::Utils::Collections
13
+ extend Minitest::Spec::DSL
14
+
15
+ class TestDeserializeContext < ViewModel::DeserializeContext
16
+ class SharedContext < ViewModel::DeserializeContext::SharedContext
17
+ attr_reader :targets
18
+ def initialize(targets: [], **rest)
19
+ super(**rest)
20
+ @targets = targets
21
+ end
22
+ end
23
+
24
+ def self.shared_context_class
25
+ SharedContext
26
+ end
27
+
28
+ delegate :targets, to: :shared_context
29
+
30
+ def initialize(**rest)
31
+ super(**rest)
32
+ end
33
+ end
34
+
35
+ class TestSerializeContext < ViewModel::SerializeContext
36
+ def initialize(**rest)
37
+ super(**rest)
38
+ end
39
+ end
40
+
41
+ class TestViewModel < ViewModel::Record
42
+ self.unregistered = true
43
+
44
+ def self.deserialize_context_class
45
+ TestDeserializeContext
46
+ end
47
+
48
+ def self.serialize_context_class
49
+ TestSerializeContext
50
+ end
51
+
52
+ def self.resolve_viewmodel(_metadata, _view_hash, deserialize_context:)
53
+ if (target_model = deserialize_context.targets.shift)
54
+ self.new(target_model)
55
+ else
56
+ self.for_new_model
57
+ end
58
+ end
59
+ end
60
+
61
+ describe 'VM::Record' do
62
+ let(:attributes) { {} }
63
+ let(:model_body) { nil }
64
+ let(:viewmodel_body) { nil }
65
+
66
+ let(:model_class) do
67
+ mb = model_body
68
+ Struct.new(*attributes.keys) do
69
+ class_eval(&mb) if mb
70
+ end
71
+ end
72
+
73
+ let(:viewmodel_class) do
74
+ mc = model_class
75
+ attrs = attributes
76
+ vmb = viewmodel_body
77
+ Class.new(TestViewModel) do
78
+ # Avoid the need for teardown. Registration is only necessary for
79
+ # associations.
80
+ self.unregistered = true
81
+
82
+ self.view_name = "Model"
83
+ self.model_class = mc
84
+
85
+ attrs.each { |a, opts| attribute(a, **opts) }
86
+
87
+ class_eval(&vmb) if vmb
88
+ end
89
+ end
90
+
91
+ let(:view_base) do
92
+ {
93
+ "_type" => "Model",
94
+ "_version" => 1,
95
+ }
96
+ end
97
+
98
+ let(:attribute_names) do
99
+ attributes.map do |model_attr_name, opts|
100
+ vm_attr_name = (opts[:as] || model_attr_name).to_s
101
+ [model_attr_name.to_s, vm_attr_name]
102
+ end
103
+ end
104
+
105
+ let(:default_values) { {} }
106
+ let(:default_view_values) { default_values }
107
+ let(:default_model_values) { default_values }
108
+
109
+ let(:default_view) do
110
+ attribute_names.each_with_object(view_base.dup) do |(model_attr_name, vm_attr_name), view|
111
+ view[vm_attr_name] = default_view_values.fetch(vm_attr_name.to_sym, model_attr_name)
112
+ end
113
+ end
114
+
115
+ let(:default_model) do
116
+ attr_values = attribute_names.map do |model_attr_name, _vm_attr_name|
117
+ default_model_values.fetch(model_attr_name.to_sym, model_attr_name)
118
+ end
119
+ model_class.new(*attr_values)
120
+ end
121
+
122
+ let(:access_control) { TestAccessControl.new(true, true, true) }
123
+
124
+ let(:create_context) { TestDeserializeContext.new(access_control: access_control) }
125
+
126
+ # Prime our simplistic `resolve_viewmodel` with the desired models to update
127
+ let(:update_context) { TestDeserializeContext.new(targets: [default_model], access_control: access_control) }
128
+
129
+ def assert_edited(vm, **changes)
130
+ ref = vm.to_reference
131
+ assert(access_control.visible_checks.include?(ref))
132
+ assert(access_control.editable_checks.include?(ref))
133
+ assert_equal([ViewModel::Changes.new(**changes)],
134
+ access_control.all_valid_edit_changes(ref))
135
+ end
136
+
137
+ def assert_unchanged(vm)
138
+ ref = vm.to_reference
139
+ assert(access_control.visible_checks.include?(ref))
140
+ assert(access_control.editable_checks.include?(ref))
141
+ assert_equal([], access_control.all_valid_edit_changes(ref))
142
+ end
143
+
144
+ module CanDeserializeToNew
145
+ def self.included(base)
146
+ base.instance_eval do
147
+ it "can deserialize to a new model" do
148
+ vm = viewmodel_class.deserialize_from_view(default_view, deserialize_context: create_context)
149
+ assert_equal(default_model, vm.model)
150
+ refute(default_model.equal?(vm.model))
151
+
152
+ all_view_attrs = attribute_names.map { |_mname, vname| vname }
153
+ assert_edited(vm, new: true, changed_attributes: all_view_attrs)
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ module CanDeserializeToExisting
160
+ def self.included(base)
161
+ base.instance_eval do
162
+ it "can deserialize to existing model with no changes" do
163
+ vm = viewmodel_class.deserialize_from_view(default_view, deserialize_context: update_context)
164
+ assert(default_model.equal?(vm.model))
165
+
166
+ assert_unchanged(vm)
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ module CanSerialize
173
+ def self.included(base)
174
+ base.instance_eval do
175
+ it "can serialize to the expected view" do
176
+ h = viewmodel_class.new(default_model).to_hash
177
+ assert_equal(default_view, h)
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ describe "with simple attribute" do
184
+ let(:attributes) { { simple: {} } }
185
+ include CanSerialize
186
+ include CanDeserializeToNew
187
+ include CanDeserializeToExisting
188
+
189
+ it "can be updated" do
190
+ new_view = default_view.merge("simple" => "changed")
191
+
192
+ vm = viewmodel_class.deserialize_from_view(new_view, deserialize_context: update_context)
193
+
194
+ assert(default_model.equal?(vm.model), "returned model was not the same")
195
+ assert_equal("changed", default_model.simple)
196
+ assert_edited(vm, changed_attributes: [:simple])
197
+ end
198
+
199
+ it "rejects unknown attributes" do
200
+ view = default_view.merge("unknown" => "illegal")
201
+ ex = assert_raises(ViewModel::DeserializationError::UnknownAttribute) do
202
+ viewmodel_class.deserialize_from_view(view, deserialize_context: create_context)
203
+ end
204
+ assert_equal("unknown", ex.attribute)
205
+ end
206
+
207
+ it "can prune an attribute" do
208
+ h = viewmodel_class.new(default_model).to_hash(serialize_context: TestSerializeContext.new(prune: [:simple]))
209
+ pruned_view = default_view.tap { |v| v.delete("simple") }
210
+ assert_equal(pruned_view, h)
211
+ end
212
+
213
+ it "edit checks when creating empty" do
214
+ vm = viewmodel_class.deserialize_from_view(view_base, deserialize_context: create_context)
215
+ refute(default_model.equal?(vm.model), "returned model was the same")
216
+ assert_edited(vm, new: true)
217
+ end
218
+ end
219
+
220
+ describe "with validated simple attribute" do
221
+ let(:attributes) { { validated: {} } }
222
+ let(:viewmodel_body) do
223
+ ->(_x) do
224
+ def validate!
225
+ if validated == "naughty"
226
+ raise ViewModel::DeserializationError::Validation.new("validated", "was naughty", nil, self.blame_reference)
227
+ end
228
+ end
229
+ end
230
+ end
231
+
232
+ include CanSerialize
233
+ include CanDeserializeToNew
234
+ include CanDeserializeToExisting
235
+
236
+ it "rejects update when validation fails" do
237
+ new_view = default_view.merge("validated" => "naughty")
238
+
239
+ ex = assert_raises(ViewModel::DeserializationError::Validation) do
240
+ viewmodel_class.deserialize_from_view(new_view, deserialize_context: update_context)
241
+ end
242
+ assert_equal("validated", ex.attribute)
243
+ assert_equal("was naughty", ex.reason)
244
+ end
245
+ end
246
+
247
+ describe "with renamed attribute" do
248
+ let(:attributes) { { modelname: { as: :viewname } } }
249
+ let(:default_model_values) { { modelname: "value" } }
250
+ let(:default_view_values) { { viewname: "value" } }
251
+
252
+ include CanSerialize
253
+ include CanDeserializeToNew
254
+ include CanDeserializeToExisting
255
+
256
+ it "makes attributes available on their new names" do
257
+ value(default_model.modelname).must_equal("value")
258
+ vm = viewmodel_class.new(default_model)
259
+ value(vm.viewname).must_equal("value")
260
+ end
261
+ end
262
+
263
+ describe "with formatted attribute" do
264
+ let(:attributes) { { moment: { format: IknowParams::Serializer::Time } } }
265
+ let(:moment) { 1.week.ago.change(usec: 0) }
266
+ let(:default_model_values) { { moment: moment } }
267
+ let(:default_view_values) { { moment: moment.iso8601 } }
268
+
269
+ include CanSerialize
270
+ include CanDeserializeToNew
271
+ include CanDeserializeToExisting
272
+
273
+ it "raises correctly on an unparseable value" do
274
+ bad_view = default_view.tap { |v| v["moment"] = "not a timestamp" }
275
+ ex = assert_raises(ViewModel::DeserializationError::Validation) do
276
+ viewmodel_class.deserialize_from_view(bad_view, deserialize_context: create_context)
277
+ end
278
+ assert_equal('moment', ex.attribute)
279
+ assert_match(/could not be deserialized because.*Time/, ex.detail)
280
+ end
281
+
282
+ it "raises correctly on an undeserializable value" do
283
+ bad_model = default_model.tap { |m| m.moment = 2.7 }
284
+ ex = assert_raises(ViewModel::SerializationError) do
285
+ viewmodel_class.new(bad_model).to_hash
286
+ end
287
+ assert_match(/Could not serialize invalid value.*'moment'.*Incorrect type/, ex.detail)
288
+ end
289
+ end
290
+
291
+ describe "with read-only attribute" do
292
+ let(:attributes) { { read_only: { read_only: true } } }
293
+
294
+ include CanSerialize
295
+ include CanDeserializeToExisting
296
+
297
+ it "deserializes to new without the attribute" do
298
+ new_view = default_view.tap { |v| v.delete("read_only") }
299
+ vm = viewmodel_class.deserialize_from_view(new_view, deserialize_context: create_context)
300
+ refute(default_model.equal?(vm.model))
301
+ assert_nil(vm.model.read_only)
302
+ assert_edited(vm, new: true)
303
+ end
304
+
305
+ it "rejects deserialize from new" do
306
+ ex = assert_raises(ViewModel::DeserializationError::ReadOnlyAttribute) do
307
+ viewmodel_class.deserialize_from_view(default_view, deserialize_context: create_context)
308
+ end
309
+ assert_equal("read_only", ex.attribute)
310
+ end
311
+
312
+ it "rejects update if changed" do
313
+ new_view = default_view.merge("read_only" => "written")
314
+ ex = assert_raises(ViewModel::DeserializationError::ReadOnlyAttribute) do
315
+ viewmodel_class.deserialize_from_view(new_view, deserialize_context: update_context)
316
+ end
317
+ assert_equal("read_only", ex.attribute)
318
+ end
319
+ end
320
+
321
+ describe "with read-only write-once attribute" do
322
+ let(:attributes) { { write_once: { read_only: true, write_once: true } } }
323
+ let(:model_body) do
324
+ ->(x) do
325
+ # For the purposes of testing, we assume a record is new and can be
326
+ # written once to if write_once is nil. We will never write a nil.
327
+ def new_record?
328
+ write_once.nil?
329
+ end
330
+ end
331
+ end
332
+
333
+ include CanSerialize
334
+ include CanDeserializeToNew
335
+ include CanDeserializeToExisting
336
+
337
+ it "rejects change to attribute" do
338
+ new_view = default_view.merge("write_once" => "written")
339
+ ex = assert_raises(ViewModel::DeserializationError::ReadOnlyAttribute) do
340
+ viewmodel_class.deserialize_from_view(new_view, deserialize_context: update_context)
341
+ end
342
+ assert_equal("write_once", ex.attribute)
343
+ end
344
+ end
345
+
346
+ describe "with custom serialization" do
347
+ let(:attributes) { { overridden: {} } }
348
+ let(:default_view_values) { { overridden: 10 } }
349
+ let(:default_model_values) { { overridden: 5 } }
350
+ let(:viewmodel_body) do
351
+ ->(x) do
352
+ def serialize_overridden(json, serialize_context:)
353
+ json.overridden model.overridden.try { |o| o * 2 }
354
+ end
355
+
356
+ def deserialize_overridden(value, references:, deserialize_context:)
357
+ before_value = model.overridden
358
+ model.overridden = value.try { |v| Integer(v) / 2 }
359
+ attribute_changed!(:overridden) unless before_value == model.overridden
360
+ end
361
+ end
362
+ end
363
+
364
+ include CanSerialize
365
+ include CanDeserializeToNew
366
+ include CanDeserializeToExisting
367
+
368
+ it "can be updated" do
369
+ new_view = default_view.merge("overridden" => "20")
370
+
371
+ vm = viewmodel_class.deserialize_from_view(new_view, deserialize_context: update_context)
372
+
373
+ assert(default_model.equal?(vm.model), "returned model was not the same")
374
+ assert_equal(10, default_model.overridden)
375
+
376
+ assert_edited(vm, changed_attributes: [:overridden])
377
+ end
378
+ end
379
+
380
+ describe "with optional attributes" do
381
+ let(:attributes) { { optional: { optional: true } } }
382
+
383
+ include CanDeserializeToNew
384
+ include CanDeserializeToExisting
385
+
386
+ it "can serialize with the optional attribute" do
387
+ h = viewmodel_class.new(default_model).to_hash(serialize_context: TestSerializeContext.new(include: [:optional]))
388
+ assert_equal(default_view, h)
389
+ end
390
+
391
+ it "can serialize without the optional attribute" do
392
+ h = viewmodel_class.new(default_model).to_hash
393
+ pruned_view = default_view.tap { |v| v.delete("optional") }
394
+ assert_equal(pruned_view, h)
395
+ end
396
+ end
397
+
398
+ Nested = Struct.new(:member)
399
+
400
+ class NestedView < TestViewModel
401
+ self.view_name = "Nested"
402
+ self.model_class = Nested
403
+ attribute :member
404
+ end
405
+
406
+ describe "with nested viewmodel" do
407
+ let(:default_nested_model) { Nested.new("member") }
408
+ let(:default_nested_view) { view_base.merge("_type" => "Nested", "member" => "member") }
409
+
410
+ let(:attributes) {{ simple: {}, nested: { using: NestedView } }}
411
+
412
+ let(:default_view_values) { { nested: default_nested_view } }
413
+ let(:default_model_values) { { nested: default_nested_model } }
414
+
415
+ let(:update_context) { TestDeserializeContext.new(targets: [default_model, default_nested_model],
416
+ access_control: access_control) }
417
+
418
+ include CanSerialize
419
+ include CanDeserializeToNew
420
+ include CanDeserializeToExisting
421
+
422
+ it "can update the nested value" do
423
+ new_view = default_view.merge("nested" => default_nested_view.merge("member" => "changed"))
424
+
425
+ vm = viewmodel_class.deserialize_from_view(new_view, deserialize_context: update_context)
426
+
427
+ assert(default_model.equal?(vm.model), "returned model was not the same")
428
+ assert(default_nested_model.equal?(vm.model.nested), "returned nested model was not the same")
429
+
430
+ assert_equal("changed", default_model.nested.member)
431
+
432
+ assert_unchanged(vm)
433
+ assert_edited(vm.nested, changed_attributes: [:member])
434
+ end
435
+
436
+ it "can replace the nested value" do
437
+ # The value will be unified if it is different after deserialization
438
+ new_view = default_view.merge("nested" => default_nested_view.merge("member" => "changed"))
439
+
440
+ partial_update_context = TestDeserializeContext.new(targets: [default_model],
441
+ access_control: access_control)
442
+
443
+ vm = viewmodel_class.deserialize_from_view(new_view, deserialize_context: partial_update_context)
444
+
445
+ assert(default_model.equal?(vm.model), "returned model was not the same")
446
+ refute(default_nested_model.equal?(vm.model.nested), "returned nested model was the same")
447
+
448
+ assert_edited(vm, new: false, changed_attributes: [:nested])
449
+ assert_edited(vm.nested, new: true, changed_attributes: [:member])
450
+ end
451
+
452
+ it "can prune attributes in the nested value" do
453
+ h = viewmodel_class.new(default_model).to_hash(
454
+ serialize_context: TestSerializeContext.new(prune: { nested: [:member] }))
455
+
456
+ pruned_view = default_view.tap { |v| v["nested"].delete("member") }
457
+ assert_equal(pruned_view, h)
458
+ end
459
+ end
460
+
461
+ describe "with array of nested viewmodel" do
462
+ let(:default_nested_model_1) { Nested.new("member1") }
463
+ let(:default_nested_view_1) { view_base.merge("_type" => "Nested", "member" => "member1") }
464
+
465
+ let(:default_nested_model_2) { Nested.new("member2") }
466
+ let(:default_nested_view_2) { view_base.merge("_type" => "Nested", "member" => "member2") }
467
+
468
+ let(:attributes) {{ simple: {}, nested: { using: NestedView, array: true } }}
469
+
470
+ let(:default_view_values) { { nested: [default_nested_view_1, default_nested_view_2] } }
471
+ let(:default_model_values) { { nested: [default_nested_model_1, default_nested_model_2] } }
472
+
473
+ let(:update_context) {
474
+ TestDeserializeContext.new(targets: [default_model, default_nested_model_1, default_nested_model_2],
475
+ access_control: access_control)
476
+ }
477
+
478
+ include CanSerialize
479
+ include CanDeserializeToNew
480
+ include CanDeserializeToExisting
481
+
482
+ it "rejects change to attribute" do
483
+ new_view = default_view.merge("nested" => "terrible")
484
+ ex = assert_raises(ViewModel::DeserializationError::InvalidAttributeType) do
485
+ viewmodel_class.deserialize_from_view(new_view, deserialize_context: update_context)
486
+ end
487
+ assert_equal("nested", ex.attribute)
488
+ assert_equal("Array", ex.expected_type)
489
+ assert_equal("String", ex.provided_type)
490
+ end
491
+
492
+ it "can edit a nested value" do
493
+ default_view["nested"][0]["member"] = "changed"
494
+ vm = viewmodel_class.deserialize_from_view(default_view, deserialize_context: update_context)
495
+ assert(default_model.equal?(vm.model), "returned model was not the same")
496
+ assert_equal(2, vm.model.nested.size)
497
+ assert(default_nested_model_1.equal?(vm.model.nested[0]))
498
+ assert(default_nested_model_2.equal?(vm.model.nested[1]))
499
+
500
+ assert_unchanged(vm)
501
+ assert_edited(vm.nested[0], changed_attributes: [:member])
502
+ end
503
+
504
+ it "can append a nested value" do
505
+ default_view["nested"] << view_base.merge("_type" => "Nested", "member" => "member3")
506
+
507
+ vm = viewmodel_class.deserialize_from_view(default_view, deserialize_context: update_context)
508
+
509
+ assert(default_model.equal?(vm.model), "returned model was not the same")
510
+ assert_equal(3, vm.model.nested.size)
511
+ assert(default_nested_model_1.equal?(vm.model.nested[0]))
512
+ assert(default_nested_model_2.equal?(vm.model.nested[1]))
513
+
514
+ vm.model.nested.each_with_index do |nvm, i|
515
+ assert_equal("member#{i+1}", nvm.member)
516
+ end
517
+
518
+ assert_edited(vm, changed_attributes: [:nested])
519
+ assert_edited(vm.nested[2], new: true, changed_attributes: [:member])
520
+ end
521
+ end
522
+ end
523
+
524
+ end