nested_record 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,181 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe NestedRecord::Base do
4
+ nested_model(:Foo) do
5
+ attribute :x, :string
6
+ attribute :y, :integer
7
+ attribute :z, :boolean
8
+ end
9
+
10
+ describe '.deep_inherited?' do
11
+ subject { Foo.deep_inherited? }
12
+
13
+ context 'when subclassing from NestedRecord::Base' do
14
+ nested_model(:Foo, NestedRecord::Base)
15
+
16
+ it { is_expected.to be false }
17
+ end
18
+
19
+ context 'when subclassing from NestedRecord::Base' do
20
+ nested_model(:Bar, NestedRecord::Base)
21
+ nested_model(:Foo, :Bar)
22
+
23
+ it { is_expected.to be true }
24
+ end
25
+ end
26
+
27
+ describe '.collection_class' do
28
+ subject { Foo.collection_class }
29
+
30
+ it 'is a subclass of NestedRecord::Collection' do
31
+ is_expected.to be < NestedRecord::Collection
32
+ end
33
+
34
+ it 'holds a reference to record_class' do
35
+ is_expected.to have_attributes(record_class: Foo)
36
+ end
37
+
38
+ context 'with .collection_methods' do
39
+ before do
40
+ Foo.collection_methods do
41
+ def filter_by_something; end
42
+ end
43
+ end
44
+
45
+ it 'has collection methods defined' do
46
+ expect(Foo.collection_class.method_defined?(:filter_by_something)).to be true
47
+ end
48
+ end
49
+
50
+ context 'when inherited' do
51
+ nested_model(:Bar)
52
+ nested_model(:Foo, :Bar)
53
+
54
+ it 'uses ancestor collection class as a subclass' do
55
+ is_expected.to be < Bar.collection_class
56
+ end
57
+ end
58
+ end
59
+
60
+ describe '.new' do
61
+ context 'with inheritance' do
62
+ nested_model(:Bar)
63
+ nested_model(:Foo, :Bar)
64
+ nested_model(:Baz)
65
+
66
+ it 'returns an instance of subclass' do
67
+ expect(Bar.new).to be_an_instance_of(Bar)
68
+ expect(Bar.new(type: 'Foo')).to be_an_instance_of(Foo)
69
+ end
70
+
71
+ it 'sets a type attribute' do
72
+ expect(Foo.new.type).to eq 'Foo'
73
+ end
74
+
75
+ it 'raises error when type is not a subclass' do
76
+ expect { Bar.new(type: 'Baz') }.to raise_error(NestedRecord::InvalidTypeError)
77
+ end
78
+
79
+ it 'raises error when type is invalid' do
80
+ expect { Bar.new(type: 'Buzz') }.to raise_error(NestedRecord::InvalidTypeError)
81
+ end
82
+
83
+ context 'with namespaced models and inherited_types full: true' do
84
+ nested_model('A::Bar') do
85
+ inherited_types full: true
86
+ end
87
+ nested_model('A::Foo', 'A::Bar')
88
+
89
+ it 'looks up for a subclass globally' do
90
+ expect(A::Bar.new(type: 'A::Foo')).to be_an_instance_of(A::Foo)
91
+ expect { A::Bar.new(type: 'Foo') }.to raise_error(NestedRecord::InvalidTypeError)
92
+ end
93
+ end
94
+
95
+ context 'with namespaced models and inherited_types full: false' do
96
+ nested_model('A::Bar') do
97
+ inherited_types full: false
98
+ end
99
+ nested_model('A::Foo', 'A::Bar')
100
+
101
+ it 'looks up for a subclass through all namespaces' do
102
+ expect(A::Bar.new(type: 'A::Foo')).to be_an_instance_of(A::Foo)
103
+ expect(A::Bar.new(type: 'Foo')).to be_an_instance_of(A::Foo)
104
+ end
105
+ end
106
+
107
+ context 'with inherited_types :namespace option' do
108
+ nested_model('A::Bar') do
109
+ inherited_types namespace: 'A::Bars'
110
+ end
111
+ nested_model('A::Bars::Foo', 'A::Bar')
112
+ nested_model('A::Foo', 'A::Bar')
113
+
114
+ it 'looks up for a subclass through a specific namespace' do
115
+ expect(A::Bar.new(type: 'Foo')).to be_an_instance_of(A::Bars::Foo)
116
+ end
117
+
118
+ it 'sets a shortened :type attribute' do
119
+ expect(A::Bars::Foo.new.type).to eq 'Foo'
120
+ end
121
+
122
+ it 'keeps full class type for records outside namespace' do
123
+ expect(A::Bar.new(type: 'A::Foo').type).to eq 'A::Foo'
124
+ end
125
+ end
126
+
127
+ context 'with inherited_types underscored: true' do
128
+ nested_model('A::Bar') do
129
+ inherited_types underscored: true, full: false
130
+ end
131
+ nested_model('A::Bar::Foo', 'A::Bar')
132
+
133
+ it 'looks up for a subclass through a specific namespace' do
134
+ expect(A::Bar.new(type: 'bar/foo')).to be_an_instance_of(A::Bar::Foo)
135
+ end
136
+
137
+ it 'sets a shortened :type attribute' do
138
+ expect(A::Bar::Foo.new.type).to eq 'a/bar/foo'
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ describe '#as_json' do
145
+ it 'serializes as a hash of attributes' do
146
+ foo = Foo.new(x: 'aa', y: 123, z: true)
147
+ expect(foo.as_json).to eq('x' => 'aa', 'y' => 123, 'z' => true)
148
+ end
149
+ end
150
+
151
+ describe '#read_attribute' do
152
+ it 'reads attribute value' do
153
+ foo = Foo.new(x: 'aa')
154
+ expect(foo.read_attribute(:x)).to eq 'aa'
155
+ end
156
+
157
+ it 'returns nil for unknown attributes' do
158
+ foo = Foo.new(x: 'aa')
159
+ expect(foo.read_attribute(:lol)).to be nil
160
+ end
161
+ end
162
+
163
+ describe '#match?' do
164
+ let(:record) { Foo.new(x: 'aa', y: 123, z: true) }
165
+
166
+ it 'matches by a single value' do
167
+ expect(record.match?(x: 'aa')).to be true
168
+ expect(record.match?(x: 'ab')).to be false
169
+ end
170
+
171
+ it 'matches by a set of values' do
172
+ expect(record.match?(x: 'aa', y: 123)).to be true
173
+ expect(record.match?(x: 'aa', y: 111)).to be false
174
+ end
175
+
176
+ it 'matches by a range of values' do
177
+ expect(record.match?(x: ['aa', 'bb'])).to be true
178
+ expect(record.match?(x: ['bb', 'cc'])).to be false
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe NestedRecord::Collection do
4
+ nested_model(:Foo) do
5
+ attribute :id, :integer
6
+ end
7
+ nested_model(:Bar, :Foo)
8
+
9
+ let(:collection_class) { Foo.collection_class }
10
+ let(:collection) { collection_class.new }
11
+
12
+ describe '#<<' do
13
+ it 'adds records to the collection' do
14
+ expect { collection << Foo.new }.to change(collection, :empty?).from(true).to(false)
15
+ expect(collection.first).to be_an_instance_of(Foo)
16
+ end
17
+
18
+ it 'refuses to push arbitrary objects' do
19
+ expect { collection << :foo }.to raise_error(NestedRecord::TypeMismatchError)
20
+ expect { collection << nil }.to raise_error(NestedRecord::TypeMismatchError)
21
+ end
22
+
23
+ it 'allows to add a record of subtypes' do
24
+ expect { collection << Bar.new }.not_to raise_error
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,286 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe NestedRecord do
4
+ describe 'has_one_nested' do
5
+ nested_model(:Bar) do
6
+ attribute :x, :string
7
+ attribute :y, :integer
8
+ attribute :z, :boolean
9
+ end
10
+
11
+ active_model(:Foo) do
12
+ has_one_nested :bar
13
+ end
14
+
15
+ it 'defines a reader method' do
16
+ expect(Foo.new).to respond_to :bar
17
+ end
18
+
19
+ describe 'initialization' do
20
+ it 'allows to initialize with instance of a nested record class' do
21
+ foo = Foo.new(bar: Bar.new)
22
+ expect(foo.bar).to be_an_instance_of Bar
23
+ end
24
+
25
+ it 'preserves nested attributes' do
26
+ foo = Foo.new(bar: Bar.new(x: 'xx'))
27
+ expect(foo.bar.x).to eq 'xx'
28
+ end
29
+
30
+ it 'allows to initialize with a nil value' do
31
+ foo = Foo.new(bar: nil)
32
+ expect(foo.bar).to be nil
33
+ end
34
+
35
+ it 'does not allow to initialize with anything else' do
36
+ expect { Foo.new(bar: :baz) }.to raise_error NestedRecord::TypeMismatchError
37
+ end
38
+ end
39
+
40
+ describe 'writer' do
41
+ it 'is defined' do
42
+ foo = Foo.new
43
+ expect(foo).to respond_to(:bar=)
44
+ end
45
+
46
+ it 'allows to assign instance of a nested record class' do
47
+ foo = Foo.new
48
+ foo.bar = Bar.new
49
+ expect(foo.bar).to be_an_instance_of Bar
50
+ end
51
+
52
+ it 'preserves nested attributes' do
53
+ foo = Foo.new
54
+ foo.bar = Bar.new(x: 'xx')
55
+ expect(foo.bar.x).to eq 'xx'
56
+ end
57
+
58
+ it 'allows to assign a nil value' do
59
+ foo = Foo.new
60
+ foo.bar = nil
61
+ expect(foo.bar).to be nil
62
+ end
63
+
64
+ it 'does not allow to assign anything else' do
65
+ foo = Foo.new
66
+ expect { foo.bar = :baz }.to raise_error NestedRecord::TypeMismatchError
67
+ end
68
+ end
69
+
70
+ describe 'attributes writer' do
71
+ it 'is defined' do
72
+ foo = Foo.new
73
+ expect(foo).to respond_to(:bar_attributes=)
74
+ end
75
+
76
+ context 'when turned off' do
77
+ active_model(:Foo) do
78
+ has_one_nested :bar, attributes_writer: false
79
+ end
80
+
81
+ it 'is not defined' do
82
+ foo = Foo.new
83
+ expect(foo).not_to respond_to(:bar_attributes=)
84
+ end
85
+ end
86
+
87
+ it 'allows initialize with nested attributes' do
88
+ foo = Foo.new(bar_attributes: { x: 'xx', y: '123', z: '0' })
89
+ expect(foo.bar).to match an_object_having_attributes(x: 'xx', y: 123, z: false)
90
+ end
91
+
92
+ it 'allows to assign nested attributes' do
93
+ foo = Foo.new
94
+ foo.bar_attributes = { x: 'xx', y: '123', z: '0' }
95
+ expect(foo.bar).to match an_object_having_attributes(x: 'xx', y: 123, z: false)
96
+ end
97
+ end
98
+ end
99
+
100
+ describe 'has_many_nested' do
101
+ nested_model(:Bar) do
102
+ attribute :x, :string
103
+ attribute :y, :integer
104
+ attribute :z, :boolean
105
+ end
106
+
107
+ active_model(:Foo) do
108
+ has_many_nested :bars
109
+ end
110
+
111
+ it 'defines a reader method' do
112
+ expect(Foo.new).to respond_to :bars
113
+ end
114
+
115
+ describe 'when used with block' do
116
+ active_model(:Foo) do
117
+ has_many_nested :bars do
118
+ def filter_by_something; end
119
+ end
120
+ end
121
+
122
+ it 'adds extension method to the collection' do
123
+ expect(Foo.new.bars).to respond_to(:filter_by_something)
124
+ end
125
+ end
126
+
127
+ describe 'initialization' do
128
+ it 'allows to initialize with an array of instances of a nested record class' do
129
+ foo = Foo.new(bars: [Bar.new, Bar.new])
130
+ expect(foo.bars).to match [an_instance_of(Bar), an_instance_of(Bar)]
131
+ end
132
+
133
+ it 'preserves nested attributes' do
134
+ foo = Foo.new(bars: [Bar.new(x: 'xx'), Bar.new(x: 'yy')])
135
+ expect(foo.bars).to match [
136
+ an_object_having_attributes(x: 'xx'),
137
+ an_object_having_attributes(x: 'yy')
138
+ ]
139
+ end
140
+
141
+ it 'allows to initialize with an empty array' do
142
+ foo = Foo.new(bars: [])
143
+ expect(foo.bars).to be_empty
144
+ end
145
+ end
146
+
147
+ describe 'writer' do
148
+ it 'is defined' do
149
+ foo = Foo.new
150
+ expect(foo).to respond_to(:bars=)
151
+ end
152
+
153
+ it 'allows to assign instance of a nested record class' do
154
+ foo = Foo.new
155
+ foo.bars = [Bar.new, Bar.new]
156
+ expect(foo.bars).to match [
157
+ an_instance_of(Bar),
158
+ an_instance_of(Bar)
159
+ ]
160
+ end
161
+
162
+ it 'preserves nested attributes' do
163
+ foo = Foo.new
164
+ foo.bars = [Bar.new(x: 'xx'), Bar.new(x: 'yy')]
165
+ expect(foo.bars).to match [
166
+ an_object_having_attributes(x: 'xx'),
167
+ an_object_having_attributes(x: 'yy')
168
+ ]
169
+ end
170
+
171
+ it 'allows to assign an empty array' do
172
+ foo = Foo.new
173
+ foo.bars = []
174
+ expect(foo.bars).to be_empty
175
+ end
176
+ end
177
+
178
+ describe 'attributes writer' do
179
+ it 'is defined' do
180
+ foo = Foo.new
181
+ expect(foo).to respond_to(:bars_attributes=)
182
+ end
183
+
184
+ context 'when turned off' do
185
+ active_model(:Foo) do
186
+ has_many_nested :bars, attributes_writer: false
187
+ end
188
+
189
+ it 'is not defined' do
190
+ foo = Foo.new
191
+ expect(foo).not_to respond_to(:bars_attributes=)
192
+ end
193
+ end
194
+
195
+ it 'allows initialize with a collection of attributes' do
196
+ foo = Foo.new(bars_attributes: [{ x: 'xx' }, { x: 'yy' }])
197
+ expect(foo.bars).to match [
198
+ an_object_having_attributes(x: 'xx'),
199
+ an_object_having_attributes(x: 'yy')
200
+ ]
201
+ end
202
+
203
+ it 'allows to assign attributes' do
204
+ foo = Foo.new
205
+ foo.bars_attributes = [{ x: 'xx' }, { x: 'yy' }]
206
+ expect(foo.bars).to match [
207
+ an_object_having_attributes(x: 'xx'),
208
+ an_object_having_attributes(x: 'yy')
209
+ ]
210
+ end
211
+
212
+ it 'allows to use a rails form hash' do
213
+ foo = Foo.new(
214
+ bars_attributes: {
215
+ '0' => { 'x' => 'xx' },
216
+ '1' => { 'x' => 'yy' }
217
+ }
218
+ )
219
+ expect(foo.bars).to match [
220
+ an_object_having_attributes(x: 'xx'),
221
+ an_object_having_attributes(x: 'yy')
222
+ ]
223
+ end
224
+
225
+ context 'with :reject_if option' do
226
+ active_model(:Foo) do
227
+ has_many_nested :bars,
228
+ attributes_writer: {
229
+ reject_if: ->(attributes) { attributes['_destroy'].present? }
230
+ }
231
+ end
232
+ nested_model(:Bar) do
233
+ attribute :id, :integer
234
+ attr_accessor :_destroy
235
+ end
236
+
237
+ it 'filters records' do
238
+ foo = Foo.new(
239
+ bars_attributes: [
240
+ { id: '1', _destroy: '' },
241
+ { id: '2', _destroy: '1' },
242
+ { id: '3', _destroy: '' },
243
+ ]
244
+ )
245
+ expect(foo.bars).to match [
246
+ an_object_having_attributes(id: 1),
247
+ an_object_having_attributes(id: 3)
248
+ ]
249
+ end
250
+ end
251
+ end
252
+ end
253
+
254
+ describe 'class_name resolution with namespaced models' do
255
+ active_model('A::B::Foo') do
256
+ has_one_nested :bar
257
+ end
258
+
259
+ context 'when nested model is defined in the model namespace' do
260
+ nested_model('A::B::Foo::Bar')
261
+
262
+ it 'locates the nested model' do
263
+ foo = A::B::Foo.new(bar_attributes: {})
264
+ expect(foo.bar).to be_an_instance_of(A::B::Foo::Bar)
265
+ end
266
+ end
267
+
268
+ context 'when nested model is defined in the common namespace' do
269
+ nested_model('A::B::Bar')
270
+
271
+ it 'locates the nested model' do
272
+ foo = A::B::Foo.new(bar_attributes: {})
273
+ expect(foo.bar).to be_an_instance_of(A::B::Bar)
274
+ end
275
+ end
276
+
277
+ context 'when nested model is defined in the upper namespace' do
278
+ nested_model('A::Bar')
279
+
280
+ it 'locates the nested model' do
281
+ foo = A::B::Foo.new(bar_attributes: {})
282
+ expect(foo.bar).to be_an_instance_of(A::Bar)
283
+ end
284
+ end
285
+ end
286
+ end