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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +152 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/Rakefile +6 -0
- data/bin/console +7 -0
- data/bin/setup +8 -0
- data/lib/nested_record.rb +15 -0
- data/lib/nested_record/base.rb +190 -0
- data/lib/nested_record/collection.rb +99 -0
- data/lib/nested_record/errors.rb +5 -0
- data/lib/nested_record/lookup_const.rb +22 -0
- data/lib/nested_record/macro.rb +15 -0
- data/lib/nested_record/setup.rb +225 -0
- data/lib/nested_record/type.rb +64 -0
- data/lib/nested_record/version.rb +5 -0
- data/nested_record.gemspec +32 -0
- data/spec/nested_record/base_spec.rb +181 -0
- data/spec/nested_record/collection_spec.rb +27 -0
- data/spec/nested_record_spec.rb +286 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/model.rb +66 -0
- metadata +153 -0
@@ -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
|