iknow_view_models 3.7.7 → 3.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 849a8c0a38a21b8d8d8c706aae9800c2d5b009a81e59432d501cc0cef8329db3
4
- data.tar.gz: b9cfa60ace9c9f6af4c711f7ae11fd8aa76934bcef7d6e7b360b749fe667d4b4
3
+ metadata.gz: c6e6ce3aa96f51d7453e79412026e0d3bc840fbac2bcf8c19603564b158ea8a9
4
+ data.tar.gz: 7ded42908be86bf155b95325af28577dad6e9e4b3f030a14db6753a7b05146bc
5
5
  SHA512:
6
- metadata.gz: 6e510f3a65791a27df5be2eabb889bd5a5addb84ee17f4e1b2f1ff7b76dfb1630978e3fc1b90bc1c26b36a83d9bb6892e9c3eb7e235b7f762946ab0c3b9b4ab4
7
- data.tar.gz: 67f12f88ce24394ddcaa639008a66ea0120fa5e062463fed5c4d30e0f20a75e7dda7823f025304b3cf4d1cda9e5640cbe281f459aab22f84ed5a4d920eb7f9b6
6
+ metadata.gz: fb370a1c5d85fe6f3ea9037ef79e006548d691eb45a6db275e4b97040a76b27f65e6af6f29c1de7e2759792343e37c000dac7cd2accf420bfe199fef11b531ab
7
+ data.tar.gz: e16ef99251d41057f0a897e7e1ba3215715c27cb3d0b6ab5aa769df9656fa617fae410944fa45daac8e243929086bbf10c7a8472c890eb1320b9e2c719b246a4
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IknowViewModels
4
- VERSION = '3.7.7'
4
+ VERSION = '3.9.0'
5
5
  end
@@ -134,7 +134,15 @@ class ViewModel::ActiveRecord
134
134
 
135
135
  updates.each do |ref, update_data|
136
136
  viewmodel =
137
- if update_data.new?
137
+ if update_data.auto_child_update?
138
+ raise ViewModel::DeserializationError::InvalidStructure.new(
139
+ 'Cannot make an automatic child update to a root node',
140
+ ViewModel::Reference.new(update_data.viewmodel_class, nil))
141
+ elsif update_data.child_update?
142
+ raise ViewModel::DeserializationError::InvalidStructure.new(
143
+ 'Cannot update an existing root node without a specified id',
144
+ ViewModel::Reference.new(update_data.viewmodel_class, nil))
145
+ elsif update_data.new?
138
146
  viewmodel_class.for_new_model(id: update_data.id)
139
147
  else
140
148
  viewmodel_class.new(existing_models[update_data.id])
@@ -520,9 +520,7 @@ class ViewModel::ActiveRecord
520
520
  end
521
521
  end
522
522
 
523
- def new?
524
- id.nil? || metadata.new?
525
- end
523
+ delegate :new?, :child_update?, :auto_child_update?, to: :metadata
526
524
 
527
525
  def self.parse_hashes(root_subtree_hashes, referenced_subtree_hashes = {})
528
526
  valid_reference_keys = referenced_subtree_hashes.keys.to_set
@@ -14,8 +14,7 @@ class ViewModel::ActiveRecord
14
14
 
15
15
  attr_accessor :viewmodel,
16
16
  :update_data,
17
- :points_to, # AssociationData => UpdateOperation (returns single new viewmodel to update fkey)
18
- :pointed_to, # AssociationData => UpdateOperation(s) (returns viewmodel(s) with which to update assoc cache)
17
+ :association_updates, # AssociationData => UpdateOperation(s)
19
18
  :reparent_to, # If node needs to update its pointer to a new parent, ParentData for the parent
20
19
  :reposition_to, # if this node participates in a list under its parent, what should its position be?
21
20
  :released_children # Set of children that have been released
@@ -23,13 +22,12 @@ class ViewModel::ActiveRecord
23
22
  delegate :attributes, to: :update_data
24
23
 
25
24
  def initialize(viewmodel, update_data, reparent_to: nil, reposition_to: nil)
26
- self.viewmodel = viewmodel
27
- self.update_data = update_data
28
- self.points_to = {}
29
- self.pointed_to = {}
30
- self.reparent_to = reparent_to
31
- self.reposition_to = reposition_to
32
- self.released_children = []
25
+ self.viewmodel = viewmodel
26
+ self.update_data = update_data
27
+ self.association_updates = {}
28
+ self.reparent_to = reparent_to
29
+ self.reposition_to = reposition_to
30
+ self.released_children = []
33
31
 
34
32
  @run_state = RunState::Pending
35
33
  @changed_associations = []
@@ -82,46 +80,47 @@ class ViewModel::ActiveRecord
82
80
  end
83
81
 
84
82
  # Visit attributes and associations as much as possible in the order
85
- # that they're declared in the view.
86
- member_ordering = viewmodel.class._members.keys.each_with_index.to_h
87
-
88
- # update user-specified attributes
89
- attribute_keys = attributes.keys.sort_by { |k| member_ordering[k] }
90
- attribute_keys.each do |attr_name|
91
- serialized_value = attributes[attr_name]
92
-
93
- # Note that the VM::AR deserialization tree asserts ownership over any
94
- # references it's provided, and so they're intentionally not passed on
95
- # to attribute deserialization for use by their `using:` viewmodels. A
96
- # (better?) alternative would be to provide them as reference-only
97
- # hashes, to indicate that no modification can be permitted.
98
- viewmodel.public_send("deserialize_#{attr_name}", serialized_value,
99
- references: {},
100
- deserialize_context: deserialize_context)
83
+ # that they're declared in the view. We can visit attributes and
84
+ # points-to associations before save, but points-from associations
85
+ # must be visited after save.
86
+ pre_save_members, post_save_members = viewmodel.class._members.values.partition do |member_data|
87
+ !member_data.association? || member_data.pointer_location == :local
101
88
  end
102
89
 
103
- # Update points-to associations before save
104
- points_to_keys = points_to.keys.sort_by do |association_data|
105
- member_ordering[association_data.association_name]
106
- end
90
+ pre_save_members.each do |member_data|
91
+ if member_data.association?
92
+ next unless association_updates.include?(member_data)
107
93
 
108
- points_to_keys.each do |association_data|
109
- child_operation = points_to[association_data]
94
+ child_operation = association_updates[member_data]
110
95
 
111
- reflection = association_data.direct_reflection
112
- debug "-> #{debug_name}: Updating points-to association '#{reflection.name}'"
96
+ reflection = member_data.direct_reflection
97
+ debug "-> #{debug_name}: Updating points-to association '#{reflection.name}'"
113
98
 
114
- association = model.association(reflection.name)
115
- new_target =
116
- if child_operation
117
- child_ctx = viewmodel.context_for_child(association_data.association_name, context: deserialize_context)
118
- child_viewmodel = child_operation.run!(deserialize_context: child_ctx)
119
- propagate_tree_changes(association_data, child_viewmodel.previous_changes)
99
+ association = model.association(reflection.name)
100
+ new_target =
101
+ if child_operation
102
+ child_ctx = viewmodel.context_for_child(member_data.association_name, context: deserialize_context)
103
+ child_viewmodel = child_operation.run!(deserialize_context: child_ctx)
104
+ propagate_tree_changes(member_data, child_viewmodel.previous_changes)
120
105
 
121
- child_viewmodel.model
122
- end
123
- association.writer(new_target)
124
- debug "<- #{debug_name}: Updated points-to association '#{reflection.name}'"
106
+ child_viewmodel.model
107
+ end
108
+ association.writer(new_target)
109
+ debug "<- #{debug_name}: Updated points-to association '#{reflection.name}'"
110
+ else
111
+ attr_name = member_data.name
112
+ next unless attributes.include?(attr_name)
113
+
114
+ serialized_value = attributes[attr_name]
115
+ # Note that the VM::AR deserialization tree asserts ownership over any
116
+ # references it's provided, and so they're intentionally not passed on
117
+ # to attribute deserialization for use by their `using:` viewmodels. A
118
+ # (better?) alternative would be to provide them as reference-only
119
+ # hashes, to indicate that no modification can be permitted.
120
+ viewmodel.public_send("deserialize_#{attr_name}", serialized_value,
121
+ references: {},
122
+ deserialize_context: deserialize_context)
123
+ end
125
124
  end
126
125
 
127
126
  # validate
@@ -144,12 +143,10 @@ class ViewModel::ActiveRecord
144
143
 
145
144
  # Update association cache of pointed-from associations after save: the
146
145
  # child update will have saved the pointer.
147
- pointed_to_keys = pointed_to.keys.sort_by do |association_data|
148
- member_ordering[association_data.association_name]
149
- end
146
+ post_save_members.each do |association_data|
147
+ next unless association_updates.include?(association_data)
150
148
 
151
- pointed_to_keys.each do |association_data|
152
- child_operation = pointed_to[association_data]
149
+ child_operation = association_updates[association_data]
153
150
  reflection = association_data.direct_reflection
154
151
 
155
152
  debug "-> #{debug_name}: Updating pointed-to association '#{reflection.name}'"
@@ -265,13 +262,7 @@ class ViewModel::ActiveRecord
265
262
  end
266
263
 
267
264
  def add_update(association_data, update)
268
- target =
269
- case association_data.pointer_location
270
- when :remote then pointed_to
271
- when :local then points_to
272
- end
273
-
274
- target[association_data] = update
265
+ self.association_updates[association_data] = update
275
266
  end
276
267
 
277
268
  private
@@ -300,6 +291,26 @@ class ViewModel::ActiveRecord
300
291
  case
301
292
  when update_data.new?
302
293
  child_viewmodel_class.for_new_model(id: update_data.id)
294
+ when update_data.child_update?
295
+ if association_data.collection?
296
+ raise ViewModel::DeserializationError::InvalidStructure.new(
297
+ 'Cannot update existing children of a collection association without specified ids',
298
+ ViewModel::Reference.new(update_data.viewmodel_class, nil))
299
+ end
300
+
301
+ child = previous_child_viewmodels[0]
302
+
303
+ if child.nil?
304
+ unless update_data.auto_child_update?
305
+ raise ViewModel::DeserializationError::PreviousChildNotFound.new(
306
+ association_data.association_name.to_s,
307
+ self.blame_reference)
308
+ end
309
+
310
+ child = child_viewmodel_class.for_new_model
311
+ end
312
+
313
+ child
303
314
  when existing_child = previous_by_key[key]
304
315
  existing_child
305
316
  when taken_child = update_context.try_take_released_viewmodel(key)
@@ -211,6 +211,23 @@ class ViewModel
211
211
  end
212
212
  end
213
213
 
214
+ class PreviousChildNotFound < NotFound
215
+ attr_reader :association
216
+
217
+ def initialize(association, blame_nodes)
218
+ @association = association
219
+ super(blame_nodes)
220
+ end
221
+
222
+ def detail
223
+ "No child currently exists to update for association '#{association}'"
224
+ end
225
+
226
+ def meta
227
+ super.merge(association: association)
228
+ end
229
+ end
230
+
214
231
  class DuplicateNodes < InvalidRequest
215
232
  attr_reader :type
216
233
 
@@ -21,7 +21,7 @@ class ViewModel::Schemas
21
21
  'description' => 'viewmodel update',
22
22
  'properties' => { ViewModel::TYPE_ATTRIBUTE => { 'type' => 'string' },
23
23
  ViewModel::ID_ATTRIBUTE => ID_SCHEMA,
24
- ViewModel::NEW_ATTRIBUTE => { 'type' => 'boolean' },
24
+ ViewModel::NEW_ATTRIBUTE => { 'oneOf' => [{ 'type' => 'boolean' }, { 'type' => 'string', 'enum' => ['auto'] }] },
25
25
  ViewModel::VERSION_ATTRIBUTE => { 'type' => 'integer' } },
26
26
  'required' => [ViewModel::TYPE_ATTRIBUTE],
27
27
  }.freeze
data/lib/view_model.rb CHANGED
@@ -11,6 +11,22 @@ class ViewModel
11
11
  ID_ATTRIBUTE = 'id'
12
12
  TYPE_ATTRIBUTE = '_type'
13
13
  VERSION_ATTRIBUTE = '_version'
14
+
15
+ # The optional _new attribute specifies explicitly whether a deserialization
16
+ # is to an new or existing model. The behaviour of _new is as follows:
17
+ # * true - Always create a new model, using the id if provided, error if
18
+ # a model with that id already exists
19
+ # * false - Never create a new model. If id isn’t provided, it's an error
20
+ # unless the viewmodel is being deserialized in the context of a
21
+ # singular association, in which case it's implicitly referring
22
+ # to the current child of that association, and is an error if
23
+ # that child doesn't exist.
24
+ # * nil - Create a new model if `id` is not specified, otherwise update the
25
+ # existing record with `id`, and error if it doesn't exist.
26
+ # * 'auto' - Can only be used when the viewmodel is being deserialized in the
27
+ # context of a singular association. `id` must not be specified. If
28
+ # the association currently has a child, update it, otherwise
29
+ # create a new one.
14
30
  NEW_ATTRIBUTE = '_new'
15
31
 
16
32
  BULK_UPDATE_TYPE = '_bulk_update'
@@ -20,10 +36,36 @@ class ViewModel
20
36
  # Migrations leave a metadata attribute _migrated on any views that they
21
37
  # alter. This attribute is accessible as metadata when deserializing migrated
22
38
  # input, and is included in the output serialization sent to clients.
23
- MIGRATED_ATTRIBUTE = '_migrated'
39
+ MIGRATED_ATTRIBUTE = '_migrated'
24
40
 
25
41
  Metadata = Struct.new(:id, :view_name, :schema_version, :new, :migrated) do
26
- alias_method :new?, :new
42
+ # Does this metadata describe deserialization to a new model, either by
43
+ # {new: true} or {new: nil, id: nil}
44
+ def new?
45
+ if new.nil?
46
+ id.nil?
47
+ else
48
+ new == true
49
+ end
50
+ end
51
+
52
+ # Does this metadata describe an change to the anonymous child of a singular
53
+ # association
54
+ def child_update?
55
+ explicit_child_update? || auto_child_update?
56
+ end
57
+
58
+ # Does this metadata describe an update to the already-existing anonymous
59
+ # child of a singular association
60
+ def explicit_child_update?
61
+ new == false && id.nil?
62
+ end
63
+
64
+ # Does this child metadata describe an create-or-update to the anonymous
65
+ # child of a singular association
66
+ def auto_child_update?
67
+ new == 'auto'
68
+ end
27
69
  end
28
70
 
29
71
  class << self
@@ -119,9 +161,14 @@ class ViewModel
119
161
  id = hash.delete(ViewModel::ID_ATTRIBUTE)
120
162
  type_name = hash.delete(ViewModel::TYPE_ATTRIBUTE)
121
163
  schema_version = hash.delete(ViewModel::VERSION_ATTRIBUTE)
122
- new = hash.delete(ViewModel::NEW_ATTRIBUTE) { false }
164
+ new = hash.delete(ViewModel::NEW_ATTRIBUTE)
123
165
  migrated = hash.delete(ViewModel::MIGRATED_ATTRIBUTE) { false }
124
166
 
167
+ if id && new == 'auto'
168
+ raise ViewModel::DeserializationError::InvalidSyntax.new(
169
+ "An explicit id must not be specified with an 'auto' child update")
170
+ end
171
+
125
172
  Metadata.new(id, type_name, schema_version, new, migrated)
126
173
  end
127
174
 
@@ -85,7 +85,7 @@ class ViewModel::ActiveRecord::BelongsToTest < ActiveSupport::TestCase
85
85
  end
86
86
  end
87
87
 
88
- def test_belongs_to_create
88
+ def test_belongs_to_create_implicit_new
89
89
  @model1.update(child: nil)
90
90
 
91
91
  alter_by_view!(ModelView, @model1) do |view, _refs|
@@ -95,6 +95,108 @@ class ViewModel::ActiveRecord::BelongsToTest < ActiveSupport::TestCase
95
95
  assert_equal('cheese', @model1.child.name)
96
96
  end
97
97
 
98
+ def test_belongs_to_create
99
+ @model1.update(child: nil)
100
+
101
+ alter_by_view!(ModelView, @model1) do |view, _refs|
102
+ view['child'] = { '_type' => 'Child', '_new' => true, 'name' => 'cheese' }
103
+ end
104
+
105
+ assert_equal('cheese', @model1.child.name)
106
+ end
107
+
108
+ def test_belongs_to_create_with_id
109
+ @model1.update(child: nil)
110
+
111
+ alter_by_view!(ModelView, @model1) do |view, _refs|
112
+ view['child'] = { '_type' => 'Child', 'id' => 9999, '_new' => true, 'name' => 'cheese' }
113
+ end
114
+
115
+ assert_equal('cheese', @model1.child.name)
116
+ assert_equal(9999, @model1.child.id)
117
+ end
118
+
119
+ def test_belongs_to_create_with_colliding_id
120
+ child_id = @model1.child_id
121
+ @model1.update(child: nil)
122
+
123
+ ex = assert_raises(ViewModel::DeserializationError::UniqueViolation) do
124
+ alter_by_view!(ModelView, @model1) do |view, _refs|
125
+ view['child'] = { '_type' => 'Child', 'id' => child_id, '_new' => true, 'name' => 'cheese' }
126
+ end
127
+ end
128
+
129
+ assert_equal(['id'], ex.columns)
130
+ assert_equal([ViewModel::Reference.new(ChildView, child_id)], ex.nodes)
131
+ end
132
+
133
+ def test_belongs_to_create_child_with_auto
134
+ @model1.update(child: nil)
135
+
136
+ alter_by_view!(ModelView, @model1) do |view, _refs|
137
+ view['child'] = { '_type' => 'Child', '_new' => 'auto', 'name' => 'cheese' }
138
+ end
139
+
140
+ assert_equal('cheese', @model1.child.name)
141
+ end
142
+
143
+ def test_belongs_to_edit_implicit_new
144
+ child_id = @model1.child_id
145
+
146
+ alter_by_view!(ModelView, @model1) do |view, _refs|
147
+ view['child'] = { '_type' => 'Child', 'id' => child_id, 'name' => 'cheese' }
148
+ end
149
+
150
+ assert_equal('cheese', @model1.child.name)
151
+ assert_equal(child_id, @model1.child.id)
152
+ end
153
+
154
+ def test_belongs_to_edit
155
+ child_id = @model1.child_id
156
+
157
+ alter_by_view!(ModelView, @model1) do |view, _refs|
158
+ view['child'] = { '_type' => 'Child', 'id' => child_id, '_new' => false, 'name' => 'cheese' }
159
+ end
160
+
161
+ assert_equal('cheese', @model1.child.name)
162
+ assert_equal(child_id, @model1.child.id)
163
+ end
164
+
165
+ def test_belongs_to_edit_child_without_id
166
+ child_id = @model1.child_id
167
+
168
+ alter_by_view!(ModelView, @model1) do |view, _refs|
169
+ view['child'] = { '_type' => 'Child', '_new' => false, 'name' => 'cheese' }
170
+ end
171
+
172
+ assert_equal('cheese', @model1.child.name)
173
+ assert_equal(child_id, @model1.child.id)
174
+ end
175
+
176
+ def test_belongs_to_one_edit_without_id_no_child
177
+ @model1.update(child: nil)
178
+
179
+ ex = assert_raises(ViewModel::DeserializationError::PreviousChildNotFound) do
180
+ alter_by_view!(ModelView, @model1) do |view, _refs|
181
+ view['child'] = { '_type' => 'Child', '_new' => false, 'name' => 'cheese' }
182
+ end
183
+ end
184
+
185
+ assert_equal('child', ex.association)
186
+ assert_equal([ViewModel::Reference.new(ModelView, @model1.id)], ex.nodes)
187
+ end
188
+
189
+ def test_belongs_to_edit_child_with_auto
190
+ child_id = @model1.child_id
191
+
192
+ alter_by_view!(ModelView, @model1) do |view, _refs|
193
+ view['child'] = { '_type' => 'Child', '_new' => 'auto', 'name' => 'cheese' }
194
+ end
195
+
196
+ assert_equal('cheese', @model1.child.name)
197
+ assert_equal(child_id, @model1.child.id)
198
+ end
199
+
98
200
  def test_belongs_to_replace
99
201
  old_child = @model1.child
100
202
 
@@ -107,7 +107,7 @@ class ViewModel::ActiveRecord::HasManyTest < ActiveSupport::TestCase
107
107
  assert(pv.model.children.blank?)
108
108
  end
109
109
 
110
- def test_create_has_many
110
+ def test_create_has_many_implicit_new
111
111
  view = { '_type' => 'Model',
112
112
  'name' => 'p',
113
113
  'children' => [{ '_type' => 'Child', 'name' => 'c1' },
@@ -123,6 +123,64 @@ class ViewModel::ActiveRecord::HasManyTest < ActiveSupport::TestCase
123
123
  assert_equal(%w[c1 c2], pv.model.children.map(&:name))
124
124
  end
125
125
 
126
+ def test_create_has_many
127
+ view = { '_type' => 'Model',
128
+ 'name' => 'p',
129
+ 'children' => [{ '_type' => 'Child', '_new' => true, 'name' => 'c1' },
130
+ { '_type' => 'Child', '_new' => true, 'name' => 'c2' },] }
131
+
132
+ context = viewmodel_class.new_deserialize_context
133
+ pv = viewmodel_class.deserialize_from_view(view, deserialize_context: context)
134
+
135
+ assert_contains_exactly(
136
+ [pv.to_reference, pv.children[0].to_reference, pv.children[1].to_reference],
137
+ context.valid_edit_refs)
138
+
139
+ assert_equal(%w[c1 c2], pv.model.children.map(&:name))
140
+ end
141
+
142
+ def test_create_has_many_with_id
143
+ view = { '_type' => 'Model',
144
+ 'name' => 'p',
145
+ 'children' => [{ '_type' => 'Child', 'id' => 9998, '_new' => true, 'name' => 'c1' },
146
+ { '_type' => 'Child', 'id' => 9999, '_new' => true, 'name' => 'c2' },] }
147
+
148
+ context = viewmodel_class.new_deserialize_context
149
+ pv = viewmodel_class.deserialize_from_view(view, deserialize_context: context)
150
+
151
+ assert_contains_exactly(
152
+ [pv.to_reference, pv.children[0].to_reference, pv.children[1].to_reference],
153
+ context.valid_edit_refs)
154
+
155
+ assert_equal(%w[c1 c2], pv.model.children.map(&:name))
156
+ end
157
+
158
+ def test_create_has_many_auto
159
+ view = { '_type' => 'Model',
160
+ 'name' => 'p',
161
+ 'children' => [{ '_type' => 'Child', '_new' => 'auto', 'name' => 'c1' }] }
162
+
163
+ context = viewmodel_class.new_deserialize_context
164
+ ex = assert_raises(ViewModel::DeserializationError::InvalidStructure) do
165
+ viewmodel_class.deserialize_from_view(view, deserialize_context: context)
166
+ end
167
+ assert_match(/existing children of a collection association without specified ids/, ex.message)
168
+ assert_equal([ViewModel::Reference.new(ChildView, nil)], ex.nodes)
169
+ end
170
+
171
+ def test_update_has_many_without_id
172
+ view = { '_type' => 'Model',
173
+ 'name' => 'p',
174
+ 'children' => [{ '_type' => 'Child', '_new' => false, 'name' => 'c1' }] }
175
+
176
+ context = viewmodel_class.new_deserialize_context
177
+ ex = assert_raises(ViewModel::DeserializationError::InvalidStructure) do
178
+ viewmodel_class.deserialize_from_view(view, deserialize_context: context)
179
+ end
180
+ assert_match(/existing children of a collection association without specified ids/, ex.message)
181
+ assert_equal([ViewModel::Reference.new(ChildView, nil)], ex.nodes)
182
+ end
183
+
126
184
  def test_nil_multiple_association
127
185
  view = {
128
186
  '_type' => 'Model',
@@ -98,7 +98,7 @@ class ViewModel::ActiveRecord::HasOneTest < ActiveSupport::TestCase
98
98
  assert_nil(pv.model.child)
99
99
  end
100
100
 
101
- def test_has_one_create
101
+ def test_has_one_create_implicit_new
102
102
  @model1.update(child: nil)
103
103
 
104
104
  alter_by_view!(ModelView, @model1) do |view, _refs|
@@ -108,12 +108,106 @@ class ViewModel::ActiveRecord::HasOneTest < ActiveSupport::TestCase
108
108
  assert_equal('t', @model1.child.name)
109
109
  end
110
110
 
111
- def test_has_one_update
111
+ def test_has_one_create
112
+ @model1.update(child: nil)
113
+
114
+ alter_by_view!(ModelView, @model1) do |view, _refs|
115
+ view['child'] = { '_type' => 'Child', '_new' => true, 'name' => 't' }
116
+ end
117
+
118
+ assert_equal('t', @model1.child.name)
119
+ end
120
+
121
+ def test_has_one_create_with_id
122
+ @model1.update(child: nil)
123
+
124
+ alter_by_view!(ModelView, @model1) do |view, _refs|
125
+ view['child'] = { '_type' => 'Child', 'id' => 9999, '_new' => true, 'name' => 't' }
126
+ end
127
+
128
+ assert_equal('t', @model1.child.name)
129
+ assert_equal(9999, @model1.child.id)
130
+ end
131
+
132
+ def test_has_one_create_with_colliding_id
133
+ child_id = @model2.child.id
134
+ @model1.update(child: nil)
135
+
136
+ ex = assert_raises(ViewModel::DeserializationError::UniqueViolation) do
137
+ alter_by_view!(ModelView, @model1) do |view, _refs|
138
+ view['child'] = { '_type' => 'Child', 'id' => child_id, '_new' => true, 'name' => 'cheese' }
139
+ end
140
+ end
141
+
142
+ assert_equal(['id'], ex.columns)
143
+ assert_equal([ViewModel::Reference.new(ChildView, child_id)], ex.nodes)
144
+ end
145
+
146
+ def test_has_one_create_child_with_auto
147
+ @model1.update(child: nil)
148
+
149
+ alter_by_view!(ModelView, @model1) do |view, _refs|
150
+ view['child'] = { '_type' => 'Child', '_new' => 'auto', 'name' => 'cheese' }
151
+ end
152
+
153
+ assert_equal('cheese', @model1.child.name)
154
+ end
155
+
156
+ def test_has_one_edit_implicit_new
157
+ child_id = @model1.child.id
158
+
159
+ alter_by_view!(ModelView, @model1) do |view, _refs|
160
+ view['child'] = { '_type' => 'Child', 'id' => child_id, 'name' => 'cheese' }
161
+ end
162
+
163
+ assert_equal('cheese', @model1.child.name)
164
+ assert_equal(child_id, @model1.child.id)
165
+ end
166
+
167
+ def test_has_one_edit
168
+ child_id = @model1.child.id
169
+
170
+ alter_by_view!(ModelView, @model1) do |view, _refs|
171
+ view['child'] = { '_type' => 'Child', 'id' => child_id, '_new' => false, 'name' => 'cheese' }
172
+ end
173
+
174
+ assert_equal('cheese', @model1.child.name)
175
+ assert_equal(child_id, @model1.child.id)
176
+ end
177
+
178
+ def test_has_one_edit_without_id
179
+ child_id = @model1.child.id
180
+
181
+ alter_by_view!(ModelView, @model1) do |view, _refs|
182
+ view['child'] = { '_type' => 'Child', '_new' => false, 'name' => 'cheese' }
183
+ end
184
+
185
+ assert_equal('cheese', @model1.child.name)
186
+ assert_equal(child_id, @model1.child.id)
187
+ end
188
+
189
+ def test_has_one_edit_without_id_no_child
190
+ @model1.update(child: nil)
191
+
192
+ ex = assert_raises(ViewModel::DeserializationError::PreviousChildNotFound) do
193
+ alter_by_view!(ModelView, @model1) do |view, _refs|
194
+ view['child'] = { '_type' => 'Child', '_new' => false, 'name' => 'cheese' }
195
+ end
196
+ end
197
+
198
+ assert_equal('child', ex.association)
199
+ assert_equal([ViewModel::Reference.new(ModelView, @model1.id)], ex.nodes)
200
+ end
201
+
202
+ def test_belongs_to_edit_child_with_auto
203
+ child_id = @model1.child.id
204
+
112
205
  alter_by_view!(ModelView, @model1) do |view, _refs|
113
- view['child']['name'] = 'hello'
206
+ view['child'] = { '_type' => 'Child', '_new' => 'auto', 'name' => 'cheese' }
114
207
  end
115
208
 
116
- assert_equal('hello', @model1.child.name)
209
+ assert_equal('cheese', @model1.child.name)
210
+ assert_equal(child_id, @model1.child.id)
117
211
  end
118
212
 
119
213
  def test_has_one_destroy
@@ -127,6 +127,30 @@ class ViewModel::ActiveRecordTest < ActiveSupport::TestCase
127
127
  assert_equal([ViewModel::Reference.new(ParentView, 9999)], ex.nodes)
128
128
  end
129
129
 
130
+ def test_create_implicit_id_raises
131
+ view = {
132
+ '_type' => 'Parent',
133
+ '_new' => false,
134
+ }
135
+ ex = assert_raises(ViewModel::DeserializationError::InvalidStructure) do
136
+ ParentView.deserialize_from_view(view)
137
+ end
138
+ assert_match(/existing root node without a specified id/, ex.message)
139
+ assert_equal([ViewModel::Reference.new(ParentView, nil)], ex.nodes)
140
+ end
141
+
142
+ def test_create_auto_raises
143
+ view = {
144
+ '_type' => 'Parent',
145
+ '_new' => 'auto',
146
+ }
147
+ ex = assert_raises(ViewModel::DeserializationError::InvalidStructure) do
148
+ ParentView.deserialize_from_view(view)
149
+ end
150
+ assert_match(/automatic child update to a root node/, ex.message)
151
+ assert_equal([ViewModel::Reference.new(ParentView, nil)], ex.nodes)
152
+ end
153
+
130
154
  def test_read_only_raises_with_id
131
155
  view = {
132
156
  '_type' => 'Parent',
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iknow_view_models
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.7.7
4
+ version: 3.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - iKnow Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-15 00:00:00.000000000 Z
11
+ date: 2024-02-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack