nested_record 0.1.1 → 1.0.0.beta

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