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