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.
- checksums.yaml +4 -4
- data/Gemfile.lock +12 -12
- data/lib/nested_record.rb +1 -0
- data/lib/nested_record/base.rb +135 -54
- data/lib/nested_record/collection.rb +20 -1
- data/lib/nested_record/errors.rb +1 -0
- data/lib/nested_record/macro.rb +2 -2
- data/lib/nested_record/methods.rb +50 -0
- data/lib/nested_record/methods/many.rb +113 -0
- data/lib/nested_record/methods/one.rb +86 -0
- data/lib/nested_record/setup.rb +72 -177
- data/lib/nested_record/type.rb +3 -39
- data/lib/nested_record/type/many.rb +24 -0
- data/lib/nested_record/type/one.rb +17 -0
- data/lib/nested_record/version.rb +1 -1
- data/spec/nested_record/base_spec.rb +106 -9
- data/spec/nested_record/type/many_spec.rb +23 -0
- data/spec/nested_record/type/one_spec.rb +22 -0
- data/spec/nested_record_spec.rb +346 -13
- data/spec/spec_helper.rb +1 -0
- data/spec/support/model.rb +19 -15
- metadata +11 -4
data/lib/nested_record/type.rb
CHANGED
@@ -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
|
@@ -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
|
97
|
+
context 'with namespaced models and subtypes full: true' do
|
84
98
|
nested_model('A::Bar') do
|
85
|
-
|
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
|
-
|
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
|
109
|
+
context 'with namespaced models and subtypes full: false' do
|
96
110
|
nested_model('A::Bar') do
|
97
|
-
|
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
|
121
|
+
context 'with subtypes :namespace option' do
|
108
122
|
nested_model('A::Bar') do
|
109
|
-
|
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
|
141
|
+
context 'with subtypes underscored: true' do
|
128
142
|
nested_model('A::Bar') do
|
129
|
-
|
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
|
data/spec/nested_record_spec.rb
CHANGED
@@ -37,19 +37,51 @@ RSpec.describe NestedRecord do
|
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
48
|
-
|
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
|
-
|
52
|
-
|
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
|
-
|
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 '
|
139
|
-
expect(Foo
|
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
|
|