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