iknow_view_models 2.8.4

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