iknow_view_models 3.7.7 → 3.9.0

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