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,121 @@
1
+ require_relative "../../../helpers/arvm_test_utilities.rb"
2
+ require_relative "../../../helpers/arvm_test_models.rb"
3
+
4
+ require "minitest/autorun"
5
+
6
+ require "view_model/active_record"
7
+
8
+ class ViewModel::ActiveRecord::VersionTest < ActiveSupport::TestCase
9
+ include ARVMTestUtilities
10
+
11
+ def before_all
12
+ super
13
+
14
+ build_viewmodel(:ChildA) do
15
+ define_schema {}
16
+ define_model do
17
+ has_one :parent, inverse_of: :child
18
+ end
19
+ define_viewmodel do
20
+ self.schema_version = 10
21
+ end
22
+ end
23
+
24
+
25
+ build_viewmodel(:Target) do
26
+ define_schema {}
27
+ define_model do
28
+ has_one :parent, inverse_of: :target
29
+ end
30
+ define_viewmodel do
31
+ self.schema_version = 20
32
+ end
33
+ end
34
+
35
+ build_viewmodel(:Parent) do
36
+ define_schema do |t|
37
+ t.string :child_type
38
+ t.integer :child_id
39
+ t.integer :target_id
40
+ end
41
+ define_model do
42
+ belongs_to :child, polymorphic: true
43
+ belongs_to :target
44
+ end
45
+ define_viewmodel do
46
+ self.schema_version = 5
47
+ association :child, viewmodels: [:ChildA]
48
+ association :target, shared: true, optional: false
49
+ end
50
+ end
51
+ end
52
+
53
+ def setup
54
+ super
55
+ @parent_with_a = Parent.create(child: ChildA.new, target: Target.new)
56
+ end
57
+
58
+ def test_schema_versions_reflected_in_output
59
+ data, refs = serialize_with_references(ParentView.new(@parent_with_a))
60
+
61
+ target_ref = refs.keys.first
62
+
63
+ assert_equal({ '_type' => 'Parent',
64
+ 'id' => @parent_with_a.id,
65
+ '_version' => 5,
66
+ 'child' => {
67
+ '_type' => 'ChildA',
68
+ 'id' => @parent_with_a.child.id,
69
+ '_version' => 10,
70
+ },
71
+ 'target' => { '_ref' => target_ref } },
72
+ data)
73
+
74
+ assert_equal({ target_ref =>
75
+ {
76
+ '_type' => 'Target',
77
+ 'id' => @parent_with_a.target.id,
78
+ '_version' => 20
79
+ } },
80
+ refs)
81
+ end
82
+
83
+ def test_regular_version_verification
84
+ ex = assert_raise(ViewModel::DeserializationError::SchemaVersionMismatch) do
85
+ ParentView.deserialize_from_view(
86
+ { '_type' => 'Parent',
87
+ '_new' => true,
88
+ '_version' => 99 },)
89
+ end
90
+ assert_match(/schema version/, ex.message)
91
+ end
92
+
93
+ def test_polymorphic_version_verification
94
+ ex = assert_raise(ViewModel::DeserializationError::SchemaVersionMismatch) do
95
+ ParentView.deserialize_from_view(
96
+ { '_type' => 'Parent',
97
+ '_new' => true,
98
+ 'child' => {
99
+ '_type' => 'ChildA',
100
+ '_version' => 99,
101
+ } })
102
+ end
103
+ assert_match(/schema version/, ex.message)
104
+ end
105
+
106
+ def test_shared_parse_version_verification
107
+ ex = assert_raise(ViewModel::DeserializationError::SchemaVersionMismatch) do
108
+ ParentView.deserialize_from_view(
109
+ { '_type' => 'Parent',
110
+ '_new' => true,
111
+ 'target' => { '_ref' => 't1' },
112
+ },
113
+ references: { 't1' => {
114
+ '_type' => 'Target',
115
+ '_new' => true,
116
+ '_version' => 99,
117
+ } })
118
+ end
119
+ assert_match(/schema version/, ex.message)
120
+ end
121
+ end
@@ -0,0 +1,542 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require_relative "../../helpers/arvm_test_utilities.rb"
4
+ require_relative "../../helpers/arvm_test_models.rb"
5
+
6
+ require "minitest/autorun"
7
+ require 'minitest/unit'
8
+
9
+ require "view_model/active_record"
10
+
11
+ class ViewModel::ActiveRecordTest < ActiveSupport::TestCase
12
+ include ARVMTestUtilities
13
+
14
+ def before_all
15
+ super
16
+
17
+ build_viewmodel(:Trivial) do
18
+ define_schema
19
+ define_model {}
20
+ define_viewmodel {}
21
+ end
22
+
23
+ build_viewmodel(:Parent) do
24
+ define_schema do |t|
25
+ t.string :name, null: false
26
+ t.integer :one, null: false, default: 1
27
+ t.integer :lock_version, null: false
28
+ end
29
+
30
+ define_model do
31
+ validates :name, exclusion: {
32
+ in: %w[invalid],
33
+ message: 'invalid due to matching test sentinel',
34
+ }
35
+ end
36
+
37
+ define_viewmodel do
38
+ attributes :name, :lock_version
39
+ attribute :one, read_only: true
40
+ end
41
+ end
42
+ end
43
+
44
+ def setup
45
+ @parent1 = Parent.create(name: "p1")
46
+ @parent2 = Parent.create(name: "p2")
47
+
48
+ super
49
+ end
50
+
51
+ ## Tests
52
+
53
+ def test_find
54
+ parentview = ParentView.find(@parent1.id)
55
+ assert_equal(@parent1, parentview.model)
56
+ end
57
+
58
+ def test_find_multiple
59
+ pv1, pv2 = ParentView.find([@parent1.id, @parent2.id])
60
+ assert_equal(@parent1, pv1.model)
61
+ assert_equal(@parent2, pv2.model)
62
+ end
63
+
64
+ def test_find_errors
65
+ ex = assert_raises(ViewModel::DeserializationError::NotFound) do
66
+ ParentView.find([@parent1.id, 9999])
67
+ end
68
+ assert_equal([ViewModel::Reference.new(ParentView, 9999)], ex.nodes)
69
+ end
70
+
71
+ def test_load
72
+ parentviews = ParentView.load
73
+ assert_equal(2, parentviews.size)
74
+
75
+ h = parentviews.index_by(&:id)
76
+ assert_equal(@parent1, h[@parent1.id].model)
77
+ assert_equal(@parent2, h[@parent2.id].model)
78
+ end
79
+
80
+ def test_create_from_view
81
+ view = {
82
+ "_type" => "Parent",
83
+ "name" => "p",
84
+ }
85
+
86
+ pv = ParentView.deserialize_from_view(view)
87
+ p = pv.model
88
+
89
+ assert(!p.changed?)
90
+ assert(!p.new_record?)
91
+
92
+ assert_equal("p", p.name)
93
+ end
94
+
95
+ def test_create_from_view_with_explicit_id
96
+ view = {
97
+ "_type" => "Parent",
98
+ "id" => 9999,
99
+ "name" => "p",
100
+ "_new" => true
101
+ }
102
+ pv = ParentView.deserialize_from_view(view)
103
+ p = pv.model
104
+
105
+ assert(!p.changed?)
106
+ assert(!p.new_record?)
107
+ assert_equal(9999, p.id)
108
+ end
109
+
110
+ def test_create_explicit_id_raises_with_id
111
+ view = {
112
+ "_type" => "Parent",
113
+ "id" => 9999,
114
+ "_new" => true
115
+ }
116
+ ex = assert_raises(ViewModel::DeserializationError::DatabaseConstraint) do
117
+ ParentView.deserialize_from_view(view)
118
+ end
119
+ assert_match(/not-null constraint/, ex.message)
120
+ assert_equal([ViewModel::Reference.new(ParentView, 9999)], ex.nodes)
121
+ end
122
+
123
+ def test_read_only_raises_with_id
124
+ view = {
125
+ "_type" => "Parent",
126
+ "one" => 2,
127
+ "id" => 9999,
128
+ "_new" => true
129
+ }
130
+ ex = assert_raises(ViewModel::DeserializationError::ReadOnlyAttribute) do
131
+ ParentView.deserialize_from_view(view)
132
+ end
133
+ assert_match("one", ex.attribute)
134
+ assert_equal([ViewModel::Reference.new(ParentView, 9999)], ex.nodes)
135
+ end
136
+
137
+ def test_visibility_raises
138
+ parentview = ParentView.new(@parent1)
139
+
140
+ assert_raises(ViewModel::AccessControlError) do
141
+ no_view_context = ViewModelBase.new_serialize_context(can_view: false)
142
+ parentview.to_hash(serialize_context: no_view_context)
143
+ end
144
+
145
+ assert_raises(ViewModel::AccessControlError) do
146
+ no_view_context = ViewModelBase.new_deserialize_context(can_view: false)
147
+ ParentView.deserialize_from_view({'_type' => 'Parent', 'name' => 'p'},
148
+ deserialize_context: no_view_context)
149
+ end
150
+ end
151
+
152
+ def test_editability_checks_create
153
+ context = ViewModelBase.new_deserialize_context
154
+ pv = ParentView.deserialize_from_view({ '_type' => 'Parent', 'name' => 'p' },
155
+ deserialize_context: context)
156
+
157
+ assert_equal([pv.to_reference],
158
+ context.valid_edit_refs)
159
+ end
160
+
161
+ def test_editability_checks_create_on_empty_record
162
+ context = ViewModelBase.new_deserialize_context
163
+ TrivialView.deserialize_from_view({'_type' => 'Trivial' },
164
+ deserialize_context: context)
165
+
166
+ ref = ViewModel::Reference.new(TrivialView, nil)
167
+ assert_equal([ref], context.valid_edit_refs)
168
+
169
+ changes = context.valid_edit_changes(ref)
170
+ assert_equal(true, changes.new?)
171
+ assert_empty(changes.changed_attributes)
172
+ assert_empty(changes.changed_associations)
173
+ assert_equal(false, changes.deleted?)
174
+ end
175
+
176
+ def test_editability_raises
177
+ no_edit_context = ViewModelBase.new_deserialize_context(can_edit: false)
178
+
179
+ ex = assert_raises(ViewModel::AccessControlError) do
180
+ # create
181
+ ParentView.deserialize_from_view({ "_type" => "Parent", "name" => "p" }, deserialize_context: no_edit_context)
182
+ end
183
+ assert_match(/Illegal edit/, ex.message)
184
+
185
+ ex = assert_raises(ViewModel::AccessControlError) do
186
+ # edit
187
+ v = ParentView.new(@parent1).to_hash.merge("name" => "p2")
188
+ ParentView.deserialize_from_view(v, deserialize_context: no_edit_context)
189
+ end
190
+ assert_match(/Illegal edit/, ex.message)
191
+
192
+ ex = assert_raises(ViewModel::AccessControlError) do
193
+ # destroy
194
+ ParentView.new(@parent1).destroy!(deserialize_context: no_edit_context)
195
+ end
196
+ assert_match(/Illegal edit/, ex.message)
197
+ end
198
+
199
+ def test_valid_edit_raises
200
+ no_edit_context = ViewModelBase.new_deserialize_context(can_change: false)
201
+
202
+ ex = assert_raises(ViewModel::AccessControlError) do
203
+ # create
204
+ ParentView.deserialize_from_view({ "_type" => "Parent", "name" => "p" }, deserialize_context: no_edit_context)
205
+ end
206
+ assert_match(/Illegal edit/, ex.message)
207
+
208
+ ex = assert_raises(ViewModel::AccessControlError) do
209
+ # edit
210
+ v = ParentView.new(@parent1).to_hash.merge("name" => "p2")
211
+ ParentView.deserialize_from_view(v, deserialize_context: no_edit_context)
212
+ end
213
+ assert_match(/Illegal edit/, ex.message)
214
+
215
+ ex = assert_raises(ViewModel::AccessControlError) do
216
+ # destroy
217
+ ParentView.new(@parent1).destroy!(deserialize_context: no_edit_context)
218
+ end
219
+ assert_match(/Illegal edit/, ex.message)
220
+ end
221
+
222
+ def test_create_multiple
223
+ view = [{'_type' => 'Parent', 'name' => 'newp1'},
224
+ {'_type' => 'Parent', 'name' => 'newp2'}]
225
+
226
+ result = ParentView.deserialize_from_view(view)
227
+
228
+ new_parents = Parent.where(id: result.map{|x| x.model.id})
229
+
230
+ assert_equal(%w{newp1 newp2}, new_parents.pluck(:name).sort)
231
+ end
232
+
233
+ def test_update_duplicate_specification
234
+ view = [
235
+ {'_type' => 'Parent', 'id' => @parent1.id},
236
+ {'_type' => 'Parent', 'id' => @parent1.id},
237
+ ]
238
+ assert_raises(ViewModel::DeserializationError::DuplicateNodes) do
239
+ ParentView.deserialize_from_view(view)
240
+ end
241
+ end
242
+
243
+ def test_create_invalid_type
244
+ build_viewmodel(:Invalid) do
245
+ define_schema { |t| }
246
+ define_model {}
247
+ define_viewmodel {}
248
+ end
249
+
250
+ ex = assert_raises(ViewModel::DeserializationError::InvalidSyntax) do
251
+ ParentView.deserialize_from_view({ "target" => [] })
252
+ end
253
+ assert_match(/"_type" wasn't supplied/, ex.message)
254
+
255
+ ex = assert_raises(ViewModel::DeserializationError::InvalidViewType) do
256
+ ParentView.deserialize_from_view({ "_type" => "Invalid" })
257
+ end
258
+
259
+ ex = assert_raises(ViewModel::DeserializationError::UnknownView) do
260
+ ParentView.deserialize_from_view({ "_type" => "NotAViewmodelType" })
261
+ end
262
+ end
263
+
264
+ def test_edit_attribute_from_view
265
+ alter_by_view!(ParentView, @parent1) do |view, refs|
266
+ view['name'] = 'renamed'
267
+ end
268
+ assert_equal('renamed', @parent1.name)
269
+ end
270
+
271
+ def test_edit_attribute_validation_failure
272
+ old_name = @parent1.name
273
+ ex = assert_raises(ViewModel::DeserializationError::Validation) do
274
+ alter_by_view!(ParentView, @parent1) do |view, refs|
275
+ view['name'] = 'invalid'
276
+ end
277
+ end
278
+ assert_equal(old_name, @parent1.name, 'validation failure causes rollback')
279
+ assert_equal(ex.attribute, "name")
280
+ assert_equal(ex.reason, "invalid due to matching test sentinel")
281
+ end
282
+
283
+ def test_edit_readonly_attribute
284
+ assert_raises(ViewModel::DeserializationError::ReadOnlyAttribute) do
285
+ ex = alter_by_view!(ParentView, @parent1) do |view, refs|
286
+ view['one'] = 2
287
+ end
288
+ assert_equal("one", ex.attribute)
289
+ end
290
+ end
291
+
292
+ def test_edit_missing_root
293
+ view = {
294
+ "_type" => "Parent",
295
+ "id" => 9999
296
+ }
297
+
298
+ ex = assert_raises(ViewModel::DeserializationError::NotFound) do
299
+ ParentView.deserialize_from_view(view)
300
+ end
301
+
302
+ assert_equal(ex.nodes, [ViewModel::Reference.new(ParentView, 9999)])
303
+ end
304
+
305
+ def test_optimistic_locking
306
+ @parent1.name = "changed"
307
+ @parent1.save!
308
+
309
+ assert_raises(ViewModel::DeserializationError::LockFailure) do
310
+ alter_by_view!(ParentView, @parent1) do |view, _refs|
311
+ view['lock_version'] = 0
312
+ end
313
+ end
314
+ end
315
+
316
+
317
+ # Tests for overriding the serialization of attributes using custom viewmodels
318
+ class CustomAttributeViewsTests < ActiveSupport::TestCase
319
+ include ARVMTestUtilities
320
+
321
+ class ComplexAttributeView < ViewModel
322
+ attribute :array
323
+
324
+ def serialize_view(json, serialize_context:)
325
+ json.a array[0]
326
+ json.b array[1]
327
+ end
328
+
329
+ def self.deserialize_from_view(hash_data, references: {}, deserialize_context:)
330
+ array = [hash_data["a"], hash_data["b"]]
331
+ self.new(array)
332
+ end
333
+ end
334
+
335
+ def before_all
336
+ super
337
+ build_viewmodel(:Pair) do
338
+ define_schema do |t|
339
+ t.column :pair, "integer[]"
340
+ end
341
+
342
+ define_model do
343
+ end
344
+
345
+ define_viewmodel do
346
+ attribute :pair, using: ComplexAttributeView
347
+ end
348
+ end
349
+ end
350
+
351
+ def setup
352
+ super
353
+ @pair = Pair.create!(pair: [1,2])
354
+ end
355
+
356
+ def test_serialize_view
357
+ view, _refs = serialize_with_references(PairView.new(@pair))
358
+
359
+ assert_equal({ "_type" => "Pair",
360
+ "_version" => 1,
361
+ "id" => @pair.id,
362
+ "pair" => { "a" => 1, "b" => 2 } },
363
+ view)
364
+ end
365
+
366
+ def test_create
367
+ view = { "_type" => "Pair", "pair" => { "a" => 3, "b" => 4 } }
368
+ pv = PairView.deserialize_from_view(view)
369
+ assert_equal([3,4], pv.model.pair)
370
+ end
371
+ end
372
+
373
+ # Tests for functionality common to all ARVM instances, but require some kind
374
+ # of relationship.
375
+ class RelationshipTests < ActiveSupport::TestCase
376
+ include ARVMTestUtilities
377
+
378
+ def before_all
379
+ super
380
+
381
+ build_viewmodel(:Parent) do
382
+ define_schema do |t|
383
+ t.string :name
384
+ end
385
+
386
+ define_model do
387
+ has_many :children, dependent: :destroy, inverse_of: :parent
388
+ end
389
+
390
+ define_viewmodel do
391
+ attributes :name
392
+ associations :children
393
+ end
394
+ end
395
+
396
+ build_viewmodel(:Child) do
397
+ define_schema do |t|
398
+ t.references :parent, null: false, foreign_key: true
399
+ t.string :name
400
+ end
401
+
402
+ define_model do
403
+ belongs_to :parent, inverse_of: :children
404
+ end
405
+
406
+ define_viewmodel do
407
+ attributes :name
408
+ end
409
+ end
410
+ end
411
+
412
+ def test_updated_associations_returned
413
+ # This test ensures the data is passed back through the context. The tests
414
+ # for the values are in the relationship-specific tests.
415
+
416
+ updated_by_view = ->(view) do
417
+ context = ViewModelBase.new_deserialize_context
418
+ ParentView.deserialize_from_view(view, deserialize_context: context)
419
+ context.updated_associations
420
+ end
421
+
422
+ assert_equal({},
423
+ updated_by_view.({ '_type' => 'Parent',
424
+ 'name' => 'p' }))
425
+
426
+ assert_equal({ 'children' => {} },
427
+ updated_by_view.({ '_type' => 'Parent',
428
+ 'name' => 'p',
429
+ 'children' => [] }))
430
+ end
431
+ end
432
+
433
+ # Parent view should be correctly passed down the tree when deserializing
434
+ class DeserializationParentContextTest < ActiveSupport::TestCase
435
+ include ARVMTestUtilities
436
+
437
+ class RefError < RuntimeError
438
+ attr_reader :ref
439
+ def initialize(ref)
440
+ super("Boom")
441
+ @ref = ref
442
+ end
443
+ end
444
+
445
+ def before_all
446
+ super
447
+
448
+ build_viewmodel(:List) do
449
+ define_schema do |t|
450
+ t.integer :child_id
451
+ end
452
+
453
+ define_model do
454
+ belongs_to :child, class_name: :List
455
+ end
456
+
457
+ define_viewmodel do
458
+ association :child
459
+ attribute :explode
460
+ # Escape deserialization with the parent context
461
+ define_method(:deserialize_explode) do |val, references:, deserialize_context: |
462
+ raise RefError.new(deserialize_context.parent_ref) if val
463
+ end
464
+ end
465
+ end
466
+ end
467
+
468
+ def setup
469
+ @list = List.new(child: List.new(child: nil))
470
+ end
471
+
472
+ def test_deserialize_context
473
+ view = {
474
+ "_type" => "List",
475
+ "id" => 1000,
476
+ "_new" => true,
477
+ "child" => {
478
+ "_type" => "List",
479
+ }}
480
+
481
+ ref_error = assert_raises(RefError) do
482
+ ListView.deserialize_from_view(view.deep_merge("child" => { "explode" => true }))
483
+ end
484
+
485
+ assert_equal(ListView, ref_error.ref.viewmodel_class)
486
+ assert_equal(1000, ref_error.ref.model_id)
487
+
488
+ ref_error = assert_raises(RefError) do
489
+ ListView.deserialize_from_view(view.deep_merge("explode" => true))
490
+ end
491
+
492
+ assert_nil(ref_error.ref)
493
+ end
494
+ end
495
+
496
+ # Parent view should be correctly passed down the tree when deserializing
497
+ class DeferredConstraintTest < ActiveSupport::TestCase
498
+ include ARVMTestUtilities
499
+
500
+ def before_all
501
+ super
502
+
503
+ build_viewmodel(:List) do
504
+ define_schema do |t|
505
+ t.integer :child_id
506
+ end
507
+
508
+ define_model do
509
+ belongs_to :child, class_name: :List
510
+ end
511
+
512
+ define_viewmodel do
513
+ association :child, shared: true
514
+ end
515
+ end
516
+ List.connection.execute("ALTER TABLE lists ADD CONSTRAINT unique_child UNIQUE (child_id) DEFERRABLE INITIALLY DEFERRED")
517
+ end
518
+
519
+ def test_deferred_constraint_violation
520
+ l1 = List.create!(child: List.new)
521
+ l2 = List.create!
522
+
523
+ ex = assert_raises(ViewModel::DeserializationError::UniqueViolation) do
524
+ alter_by_view!(ListView, l2) do |view, refs|
525
+ view['child'] = { "_ref" => "r1" }
526
+ refs["r1"] = { "_type" => "List", "id" => l1.child.id }
527
+ end
528
+ end
529
+
530
+ constraint = 'unique_child'
531
+ columns = ['child_id']
532
+ values = l1.child.id.to_s
533
+
534
+ assert_match(/#{constraint}/, ex.message)
535
+ assert_equal(constraint, ex.constraint)
536
+ assert_equal(columns, ex.columns)
537
+ assert_equal(values, ex.values)
538
+
539
+ assert_equal({ constraint: constraint, columns: columns, values: values, nodes: [] }, ex.meta)
540
+ end
541
+ end
542
+ end