nested_record 0.1.1 → 1.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class NestedRecord::Type < ActiveRecord::Type::Json
4
+ require 'nested_record/type/many'
5
+ require 'nested_record/type/one'
6
+
4
7
  def initialize(setup)
5
8
  @setup = setup
6
9
  end
@@ -22,43 +25,4 @@ class NestedRecord::Type < ActiveRecord::Type::Json
22
25
  def record_class
23
26
  @setup.record_class
24
27
  end
25
-
26
- class HasMany < self
27
- private
28
-
29
- def collection_class
30
- @setup.collection_class
31
- end
32
-
33
- def cast_value(data)
34
- return unless data
35
- collection = collection_class.new
36
- data.each do |obj|
37
- if obj.is_a? Hash
38
- collection << record_class.instantiate(obj)
39
- elsif obj.kind_of?(record_class)
40
- collection << obj
41
- else
42
- raise "Cannot cast #{obj.inspect}"
43
- end
44
- end
45
- collection
46
- end
47
- end
48
-
49
- class HasOne < self
50
- private
51
-
52
- def cast_value(obj)
53
- return unless obj
54
-
55
- if obj.is_a? Hash
56
- record_class.instantiate(obj)
57
- elsif obj.kind_of?(record_class)
58
- obj
59
- else
60
- raise "Cannot cast #{obj.inspect}"
61
- end
62
- end
63
- end
64
28
  end
@@ -0,0 +1,24 @@
1
+ class NestedRecord::Type
2
+ class Many < self
3
+ private
4
+
5
+ def collection_class
6
+ @setup.collection_class
7
+ end
8
+
9
+ def cast_value(data)
10
+ return unless data
11
+ collection = collection_class.new
12
+ data.each do |obj|
13
+ if obj.is_a? Hash
14
+ collection << record_class.instantiate(obj)
15
+ elsif obj.kind_of?(record_class)
16
+ collection << obj
17
+ else
18
+ raise "Cannot cast #{obj.inspect}"
19
+ end
20
+ end
21
+ collection
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ class NestedRecord::Type
2
+ class One < self
3
+ private
4
+
5
+ def cast_value(obj)
6
+ return unless obj
7
+
8
+ if obj.is_a? Hash
9
+ record_class.instantiate(obj)
10
+ elsif obj.kind_of?(record_class)
11
+ obj
12
+ else
13
+ raise "Cannot cast #{obj.inspect}"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NestedRecord
4
- VERSION = '0.1.1'
4
+ VERSION = '1.0.0.beta'
5
5
  end
@@ -58,6 +58,20 @@ RSpec.describe NestedRecord::Base do
58
58
  end
59
59
 
60
60
  describe '.new' do
61
+ context 'with after_initialize callbacks' do
62
+ nested_model(:Foo) do
63
+ attribute :x, :integer
64
+ attribute :y, :integer
65
+ attribute :z, :integer
66
+
67
+ after_initialize { self.z = 1234 }
68
+ end
69
+
70
+ it 'calls them' do
71
+ expect(Foo.new(x: 1, y: 2)).to match an_object_having_attributes(x: 1, y: 2, z: 1234)
72
+ end
73
+ end
74
+
61
75
  context 'with inheritance' do
62
76
  nested_model(:Bar)
63
77
  nested_model(:Foo, :Bar)
@@ -80,21 +94,21 @@ RSpec.describe NestedRecord::Base do
80
94
  expect { Bar.new(type: 'Buzz') }.to raise_error(NestedRecord::InvalidTypeError)
81
95
  end
82
96
 
83
- context 'with namespaced models and inherited_types full: true' do
97
+ context 'with namespaced models and subtypes full: true' do
84
98
  nested_model('A::Bar') do
85
- inherited_types full: true
99
+ subtypes full: true
86
100
  end
87
101
  nested_model('A::Foo', 'A::Bar')
88
102
 
89
103
  it 'looks up for a subclass globally' do
90
- expect(A::Bar.new(type: 'A::Foo')).to be_an_instance_of(A::Foo)
104
+ expect(A::Bar.new(type: 'A::Foo')).to be_an_instance_of(A::Foo)
91
105
  expect { A::Bar.new(type: 'Foo') }.to raise_error(NestedRecord::InvalidTypeError)
92
106
  end
93
107
  end
94
108
 
95
- context 'with namespaced models and inherited_types full: false' do
109
+ context 'with namespaced models and subtypes full: false' do
96
110
  nested_model('A::Bar') do
97
- inherited_types full: false
111
+ subtypes full: false
98
112
  end
99
113
  nested_model('A::Foo', 'A::Bar')
100
114
 
@@ -104,9 +118,9 @@ RSpec.describe NestedRecord::Base do
104
118
  end
105
119
  end
106
120
 
107
- context 'with inherited_types :namespace option' do
121
+ context 'with subtypes :namespace option' do
108
122
  nested_model('A::Bar') do
109
- inherited_types namespace: 'A::Bars'
123
+ subtypes namespace: 'A::Bars'
110
124
  end
111
125
  nested_model('A::Bars::Foo', 'A::Bar')
112
126
  nested_model('A::Foo', 'A::Bar')
@@ -124,9 +138,9 @@ RSpec.describe NestedRecord::Base do
124
138
  end
125
139
  end
126
140
 
127
- context 'with inherited_types underscored: true' do
141
+ context 'with subtypes underscored: true' do
128
142
  nested_model('A::Bar') do
129
- inherited_types underscored: true, full: false
143
+ subtypes underscored: true, full: false
130
144
  end
131
145
  nested_model('A::Bar::Foo', 'A::Bar')
132
146
 
@@ -138,6 +152,89 @@ RSpec.describe NestedRecord::Base do
138
152
  expect(A::Bar::Foo.new.type).to eq 'a/bar/foo'
139
153
  end
140
154
  end
155
+
156
+ context 'with local subtypes' do
157
+ nested_model('Baz') do
158
+ subtypes underscored: true
159
+ attribute :x, :integer
160
+
161
+ subtype :foo do
162
+ attribute :y, :integer
163
+ end
164
+
165
+ subtype :bar do
166
+ attribute :z, :integer
167
+ end
168
+ end
169
+
170
+ it 'looks up local subtypes' do
171
+ foo = Baz.new(type: 'foo', x: 1, y: 2)
172
+ bar = Baz.new(type: 'bar', x: 1, z: 3)
173
+ expect(foo).to be_an_instance_of Baz::LocalTypes::Foo
174
+ expect(bar).to be_an_instance_of Baz::LocalTypes::Bar
175
+ expect(foo).to match an_object_having_attributes(x: 1, y: 2)
176
+ expect(bar).to match an_object_having_attributes(x: 1, z: 3)
177
+ end
178
+
179
+ it 'sets a local type when called on subtypes' do
180
+ expect(Baz::LocalTypes::Foo.new).to match an_object_having_attributes(type: 'foo')
181
+ expect(Baz::LocalTypes::Bar.new).to match an_object_having_attributes(type: 'bar')
182
+ end
183
+ end
184
+
185
+ context 'with local subtypes in anonymous records' do
186
+ nested_model('Baz') do
187
+ has_one_nested :aux do
188
+ subtypes underscored: true
189
+ attribute :x, :integer
190
+
191
+ subtype :foo do
192
+ attribute :y, :integer
193
+ end
194
+
195
+ subtype :bar do
196
+ attribute :z, :integer
197
+ end
198
+ end
199
+ end
200
+
201
+ it 'looks up local subtypes' do
202
+ baz = Baz.new
203
+ foo = baz.build_aux(type: 'foo', x: 1, y: 2)
204
+ expect(foo).to match an_object_having_attributes(x: 1, y: 2)
205
+ bar = baz.build_aux(type: 'bar', x: 1, z: 3)
206
+ expect(bar).to match an_object_having_attributes(x: 1, z: 3)
207
+ end
208
+ end
209
+ end
210
+ end
211
+
212
+ describe '.instantiate' do
213
+ context 'with after_initialize callbacks' do
214
+ nested_model(:Foo) do
215
+ attribute :x, :integer
216
+ attribute :y, :integer
217
+ attribute :z, :integer
218
+
219
+ after_initialize { self.z = 1234 }
220
+ end
221
+
222
+ it 'calls them' do
223
+ expect(Foo.instantiate('x' => 1, 'y' => 2)).to match an_object_having_attributes(x: 1, y: 2, z: 1234)
224
+ end
225
+ end
226
+ end
227
+
228
+ describe '.subtype' do
229
+ nested_model('Baz') do
230
+ subtype :foo
231
+ subtype :bar
232
+ end
233
+
234
+ it 'defines subclasses in Subtypes namespace' do
235
+ expect(Baz::LocalTypes).to be_an_instance_of Module
236
+ expect(Baz::LocalTypes::Foo).to be < Baz
237
+ expect(Baz::LocalTypes::Bar).to be < Baz
141
238
  end
142
239
  end
143
240
 
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe NestedRecord::Type::Many do
4
+ nested_model(:Foo) do
5
+ attribute :id, :integer
6
+ attribute :x, :integer
7
+ attribute :s, :string
8
+
9
+ after_initialize { self.id = 1000 + x }
10
+ end
11
+ let(:collection_class) { Foo.collection_class }
12
+ let(:setup) { double(record_class: Foo, collection_class: collection_class) }
13
+ let(:type) { described_class.new(setup) }
14
+
15
+ describe '#deserialize' do
16
+ subject { type.deserialize('[{"x": 1, "s": "yeah"}, {"x": 2, "s": "hoorah"}]') }
17
+
18
+ it 'deserializes a json string to record object' do
19
+ is_expected.to be_an_instance_of(collection_class)
20
+ is_expected.to match [an_object_having_attributes(id: 1001, x: 1, s: 'yeah'), an_object_having_attributes(id: 1002, x: 2, s: 'hoorah')]
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe NestedRecord::Type::One do
4
+ nested_model(:Foo) do
5
+ attribute :id, :integer
6
+ attribute :x, :integer
7
+ attribute :s, :string
8
+
9
+ after_initialize { self.id = 1000 }
10
+ end
11
+ let(:setup) { double(record_class: Foo) }
12
+ let(:type) { described_class.new(setup) }
13
+
14
+ describe '#deserialize' do
15
+ subject { type.deserialize('{"x": 123, "s": "yeah"}') }
16
+
17
+ it 'deserializes a json string to record object' do
18
+ is_expected.to be_an_instance_of(Foo)
19
+ is_expected.to match an_object_having_attributes(id: 1000, x: 123, s: 'yeah')
20
+ end
21
+ end
22
+ end
@@ -37,19 +37,51 @@ RSpec.describe NestedRecord do
37
37
  end
38
38
  end
39
39
 
40
- context 'with plural model name' do
41
- nested_model(:Points) do
42
- attribute :x, :string
43
- attribute :y, :integer
44
- attribute :z, :boolean
40
+ describe 'build_ method' do
41
+ it 'builds an object' do
42
+ foo = Foo.new
43
+ foo.build_bar(x: 'xx')
44
+ expect(foo.bar).to be_an_instance_of(Bar)
45
+ expect(foo.bar.x).to eq 'xx'
45
46
  end
46
47
 
47
- active_model(:Foo) do
48
- has_one_nested :points
48
+ it 'rewrites the existing object' do
49
+ foo = Foo.new(bar: Bar.new(x: 'x'))
50
+ foo.build_bar(x: 'xx')
51
+ expect(foo.bar).to be_an_instance_of(Bar)
52
+ expect(foo.bar.x).to eq 'xx'
49
53
  end
50
54
 
51
- it 'properly locates the model class' do
52
- expect(Foo.new.build_points).to be_an_instance_of(Points)
55
+ context 'with plural model name' do
56
+ nested_model(:Points) do
57
+ attribute :x, :string
58
+ attribute :y, :integer
59
+ attribute :z, :boolean
60
+ end
61
+
62
+ active_model(:Foo) do
63
+ has_one_nested :points
64
+ end
65
+
66
+ it 'properly locates the model class' do
67
+ expect(Foo.new.build_points).to be_an_instance_of(Points)
68
+ end
69
+ end
70
+ end
71
+
72
+ describe 'bang ! method' do
73
+ it 'initializes an object if it is missing' do
74
+ foo = Foo.new(bar: nil)
75
+ foo.bar!
76
+ expect(foo.bar).to be_an_instance_of(Bar)
77
+ expect(foo.bar.x).to be nil
78
+ end
79
+
80
+ it 'uses existing object' do
81
+ foo = Foo.new(bar: Bar.new(x: 'xx'))
82
+ foo.bar!
83
+ expect(foo.bar).to be_an_instance_of(Bar)
84
+ expect(foo.bar.x).to eq 'xx'
53
85
  end
54
86
  end
55
87
 
@@ -110,11 +142,41 @@ RSpec.describe NestedRecord do
110
142
  foo.bar_attributes = { x: 'xx', y: '123', z: '0' }
111
143
  expect(foo.bar).to match an_object_having_attributes(x: 'xx', y: 123, z: false)
112
144
  end
145
+
146
+ context 'with :upsert strategy' do
147
+ active_model(:Foo) do
148
+ has_one_nested :bar, attributes_writer: { strategy: :upsert }
149
+ end
150
+
151
+ it 'updates the existing data' do
152
+ foo = Foo.new(bar_attributes: { x: 'x', y: 1 })
153
+ expect(foo.bar).to match an_object_having_attributes(x: 'x', y: 1, z: nil)
154
+ foo.bar_attributes = { x: 'xx', z: '1' }
155
+ expect(foo.bar).to match an_object_having_attributes(x: 'xx', y: 1, z: true)
156
+ end
157
+ end
158
+ end
159
+
160
+ describe 'validations' do
161
+ nested_model(:Bar) do
162
+ attribute :x, :string
163
+ validates :x, presence: true
164
+ end
165
+
166
+ it 'validates the record and adds an error entry' do
167
+ foo = Foo.new(bar: Bar.new(x: 'x'))
168
+ expect(foo).to be_valid
169
+ expect(foo.errors).to be_empty
170
+ foo = Foo.new(bar: Bar.new(x: ''))
171
+ expect(foo).not_to be_valid
172
+ expect(foo.errors['bar.x']).to eq ["can't be blank"]
173
+ end
113
174
  end
114
175
  end
115
176
 
116
177
  describe 'has_many_nested' do
117
178
  nested_model(:Bar) do
179
+ def_primary_uuid :id
118
180
  attribute :x, :string
119
181
  attribute :y, :integer
120
182
  attribute :z, :boolean
@@ -128,15 +190,89 @@ RSpec.describe NestedRecord do
128
190
  expect(Foo.new).to respond_to :bars
129
191
  end
130
192
 
193
+ describe 'when used with custom :class_name' do
194
+ active_model(:Foo) do
195
+ has_many_nested :bars, class_name: 'Barr'
196
+ end
197
+ nested_model(:Barr) do
198
+ def_primary_uuid :id
199
+ attribute :a, :integer
200
+ attribute :b, :integer
201
+ end
202
+
203
+ it 'uses a custom class' do
204
+ foo = Foo.new(bars_attributes: [{ a: 1, b: 2 }])
205
+ expect(foo.bars.first).to_not be_an_instance_of(Bar)
206
+ expect(foo.bars).to match [an_object_having_attributes(a: 1, b: 2)]
207
+ end
208
+ end
209
+
210
+ describe 'when used with class_name: <unknown>' do
211
+ it 'raises an error' do
212
+ expect do
213
+ new_active_model(:Fooo) do
214
+ has_many_nested :bars, class_name: []
215
+ end
216
+ end.to raise_error(NestedRecord::ConfigurationError, 'Bad :class_name option []')
217
+ end
218
+ end
219
+
131
220
  describe 'when used with block' do
132
221
  active_model(:Foo) do
133
222
  has_many_nested :bars do
134
- def filter_by_something; end
223
+ def_primary_uuid :id
224
+ attribute :a, :integer
225
+ attribute :b, :integer
226
+ collection_methods do
227
+ def filter_by_something; end
228
+ end
229
+ end
230
+ end
231
+
232
+ it 'defines an anonymous record class' do
233
+ foo = Foo.new(bars_attributes: [{ a: 1, b: 2 }])
234
+ expect(foo.bars.first).to_not be_an_instance_of(Bar)
235
+ expect(foo.bars).to match [an_object_having_attributes(a: 1, b: 2)]
236
+ expect(foo.bars).to respond_to(:filter_by_something)
237
+ end
238
+ end
239
+
240
+ describe 'when used with block and class_name: true' do
241
+ active_model(:Foo) do
242
+ has_many_nested :bars, class_name: true do
243
+ def_primary_uuid :id
244
+ end
245
+ end
246
+
247
+ it 'names a class' do
248
+ expect(Foo::Bar).to be < NestedRecord::Base
249
+ foo = Foo.new(bars_attributes: [{}])
250
+ expect(foo.bars.first).to be_an_instance_of(Foo::Bar)
251
+ end
252
+ end
253
+
254
+ describe 'when used with block and class_name: "String"' do
255
+ active_model(:Foo) do
256
+ has_many_nested :bars, class_name: 'Barr' do
257
+ def_primary_uuid :id
135
258
  end
136
259
  end
137
260
 
138
- it 'adds extension method to the collection' do
139
- expect(Foo.new.bars).to respond_to(:filter_by_something)
261
+ it 'names a class' do
262
+ expect(Foo::Barr).to be < NestedRecord::Base
263
+ foo = Foo.new(bars_attributes: [{}])
264
+ expect(foo.bars.first).to be_an_instance_of(Foo::Barr)
265
+ end
266
+ end
267
+
268
+ describe 'when used with block and class_name: <unknown>' do
269
+ it 'raises an error' do
270
+ expect do
271
+ new_active_model(:Fooo) do
272
+ has_many_nested :bars, class_name: [] do
273
+ end
274
+ end
275
+ end.to raise_error(NestedRecord::ConfigurationError, 'Bad :class_name option []')
140
276
  end
141
277
  end
142
278
 
@@ -208,6 +344,16 @@ RSpec.describe NestedRecord do
208
344
  end
209
345
  end
210
346
 
347
+ context 'when used with invalid :attributes_writer option' do
348
+ it 'raises an error' do
349
+ expect do
350
+ new_active_model(:Fooo) do
351
+ has_many_nested :bars, attributes_writer: 'foo'
352
+ end
353
+ end.to raise_error(NestedRecord::ConfigurationError, 'Bad :attributes_writer option "foo"')
354
+ end
355
+ end
356
+
211
357
  it 'allows initialize with a collection of attributes' do
212
358
  foo = Foo.new(bars_attributes: [{ x: 'xx' }, { x: 'yy' }])
213
359
  expect(foo.bars).to match [
@@ -246,7 +392,7 @@ RSpec.describe NestedRecord do
246
392
  }
247
393
  end
248
394
  nested_model(:Bar) do
249
- attribute :id, :integer
395
+ attribute :id, :integer, primary: true
250
396
  attr_accessor :_destroy
251
397
  end
252
398
 
@@ -264,6 +410,193 @@ RSpec.describe NestedRecord do
264
410
  ]
265
411
  end
266
412
  end
413
+
414
+ context 'with :unknown strategy' do
415
+ it 'raises an error' do
416
+ expect do
417
+ new_active_model(:Fooo) do
418
+ has_many_nested :bars, attributes_writer: :unknown do
419
+ attribute :val, :string
420
+ end
421
+ end
422
+ end.to raise_error(NestedRecord::ConfigurationError, 'Unknown strategy :unknown')
423
+ end
424
+ end
425
+
426
+ context 'with :rewrite strategy' do
427
+ active_model(:Foo) do
428
+ has_many_nested :bars, attributes_writer: :rewrite do
429
+ attribute :val, :string
430
+ end
431
+ end
432
+
433
+ it 'replaces the whole contents of the collection' do
434
+ foo = Foo.new(bars_attributes: [{ val: 'foo' }, { val: 'bar' }])
435
+ foo.bars_attributes = [{ val: 'ping' }, { val: 'pong' }]
436
+ expect(foo.bars).to match [an_object_having_attributes(val: 'ping'), an_object_having_attributes(val: 'pong')]
437
+ end
438
+ end
439
+
440
+ context 'with :upsert strategy when primary key specified on association level' do
441
+ active_model(:Foo) do
442
+ has_many_nested :bars, attributes_writer: { strategy: :upsert }, primary_key: :id do
443
+ attribute :id, :integer
444
+ attribute :val, :string
445
+ end
446
+ end
447
+
448
+ it 'upserts the data' do
449
+ foo = Foo.new(bars_attributes: [{ id: 1, val: 'x' }, { id: 2, val: 'y' }])
450
+ expect(foo.bars).to match [
451
+ an_object_having_attributes(id: 1, val: 'x'),
452
+ an_object_having_attributes(id: 2, val: 'y')
453
+ ]
454
+
455
+ foo.bars_attributes = [{ id: 3, val: 'z' }, { id: 2, val: 'yy' }]
456
+ expect(foo.bars).to match [
457
+ an_object_having_attributes(id: 1, val: 'x'),
458
+ an_object_having_attributes(id: 2, val: 'yy'),
459
+ an_object_having_attributes(id: 3, val: 'z')
460
+ ]
461
+ end
462
+ end
463
+
464
+ context 'with :upsert strategy when primary key specified on model level' do
465
+ active_model(:Foo) do
466
+ has_many_nested :bars, attributes_writer: { strategy: :upsert } do
467
+ primary_key :id
468
+ attribute :id, :integer
469
+ attribute :val, :string
470
+ end
471
+ end
472
+
473
+ it 'upserts the data' do
474
+ foo = Foo.new(bars_attributes: [{ id: 1, val: 'x' }, { id: 2, val: 'y' }])
475
+ expect(foo.bars).to match [
476
+ an_object_having_attributes(id: 1, val: 'x'),
477
+ an_object_having_attributes(id: 2, val: 'y')
478
+ ]
479
+
480
+ foo.bars_attributes = [{ id: 3, val: 'z' }, { id: 2, val: 'yy' }]
481
+ expect(foo.bars).to match [
482
+ an_object_having_attributes(id: 1, val: 'x'),
483
+ an_object_having_attributes(id: 2, val: 'yy'),
484
+ an_object_having_attributes(id: 3, val: 'z')
485
+ ]
486
+ end
487
+ end
488
+
489
+ context 'with :upsert strategy when primary key specified on model attribute level' do
490
+ active_model(:Foo) do
491
+ has_many_nested :bars, attributes_writer: { strategy: :upsert } do
492
+ attribute :id, :integer, primary: true
493
+ attribute :val, :string
494
+ end
495
+ end
496
+
497
+ it 'upserts the data' do
498
+ foo = Foo.new(bars_attributes: [{ id: 1, val: 'x' }, { id: 2, val: 'y' }])
499
+ expect(foo.bars).to match [
500
+ an_object_having_attributes(id: 1, val: 'x'),
501
+ an_object_having_attributes(id: 2, val: 'y')
502
+ ]
503
+
504
+ foo.bars_attributes = [{ id: 3, val: 'z' }, { id: 2, val: 'yy' }]
505
+ expect(foo.bars).to match [
506
+ an_object_having_attributes(id: 1, val: 'x'),
507
+ an_object_having_attributes(id: 2, val: 'yy'),
508
+ an_object_having_attributes(id: 3, val: 'z')
509
+ ]
510
+ end
511
+ end
512
+
513
+ context 'with :upsert strategy when primary key specified on model attribute level and subtyping is used' do
514
+ active_model(:Foo) do
515
+ has_many_nested :bars, attributes_writer: { strategy: :upsert } do
516
+ attribute :id, :integer, primary: true
517
+ attribute :value, :string
518
+ subtype :x
519
+ subtype :y
520
+ end
521
+ end
522
+
523
+ it 'upserts the data according to its type' do
524
+ foo = Foo.new(bars_attributes: [{ type: 'x', id: '1', value: 'x' }, { type: 'y', id: '2', value: 'y2' }])
525
+ expect(foo.bars).to match [
526
+ an_object_having_attributes(id: 1, value: 'x'),
527
+ an_object_having_attributes(id: 2, value: 'y2')
528
+ ]
529
+
530
+ foo.bars_attributes = [{ type: 'y', id: '3', value: 'y3' }, { type: 'x', id: '1', value: 'xx' }, { type: 'y', id: '2', value: 'yy' }]
531
+ expect(foo.bars).to match [
532
+ an_object_having_attributes(id: 1, value: 'xx'),
533
+ an_object_having_attributes(id: 2, value: 'yy'),
534
+ an_object_having_attributes(id: 3, value: 'y3')
535
+ ]
536
+ end
537
+ end
538
+
539
+ context 'with :upsert strategy when primary key is specific for each subtype' do
540
+ active_model(:Foo) do
541
+ has_many_nested :bars, attributes_writer: { strategy: :upsert } do
542
+ attribute :value, :string
543
+ subtype :x do
544
+ attribute :xid, :integer, primary: true
545
+ end
546
+ subtype :y do
547
+ attribute :yid, :integer, primary: true
548
+ end
549
+ end
550
+ end
551
+
552
+ it 'upserts the data according to its type' do
553
+ foo = Foo.new(bars_attributes: [{ type: 'x', xid: '1', value: 'x' }, { type: 'y', yid: '2', value: 'y2' }])
554
+ expect(foo.bars).to match [
555
+ an_object_having_attributes(xid: 1, value: 'x'),
556
+ an_object_having_attributes(yid: 2, value: 'y2')
557
+ ]
558
+
559
+ foo.bars_attributes = [{ type: 'x', xid: '2', value: 'x2' }, { type: 'y', yid: '1', value: 'y' }, { type: 'x', xid: '1', value: 'xx' }]
560
+ expect(foo.bars).to match [
561
+ an_object_having_attributes(xid: 1, value: 'xx'),
562
+ an_object_having_attributes(yid: 2, value: 'y2'),
563
+ an_object_having_attributes(xid: 2, value: 'x2'),
564
+ an_object_having_attributes(yid: 1, value: 'y')
565
+ ]
566
+ end
567
+ end
568
+
569
+ context 'with :upsert strategy when primary key is not specified at all' do
570
+ active_model(:Foo) do
571
+ has_many_nested :bars, attributes_writer: { strategy: :upsert } do
572
+ attribute :id, :integer
573
+ attribute :val, :string
574
+ end
575
+ end
576
+
577
+ it 'upserts the data' do
578
+ foo = Foo.new
579
+ expect { foo.bars_attributes = [{ id: 0 }] }
580
+ .to raise_error(NestedRecord::ConfigurationError, /You should specify a primary_key/)
581
+ end
582
+ end
583
+ end
584
+
585
+ describe 'validations' do
586
+ nested_model(:Bar) do
587
+ def_primary_uuid :id
588
+ attribute :x, :string
589
+ validates :x, presence: true
590
+ end
591
+
592
+ it 'validates the record and adds an error entry' do
593
+ foo = Foo.new(bars: [Bar.new(x: 'x')])
594
+ expect(foo).to be_valid
595
+ expect(foo.errors).to be_empty
596
+ foo = Foo.new(bars: [Bar.new(x: 'y'), Bar.new(x: '')])
597
+ expect(foo).not_to be_valid
598
+ expect(foo.errors['bars[1].x']).to eq ["can't be blank"]
599
+ end
267
600
  end
268
601
  end
269
602