attributor 2.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 +15 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CHANGELOG.md +52 -0
- data/Gemfile +3 -0
- data/Guardfile +12 -0
- data/LICENSE +22 -0
- data/README.md +62 -0
- data/Rakefile +28 -0
- data/attributor.gemspec +40 -0
- data/lib/attributor.rb +89 -0
- data/lib/attributor/attribute.rb +271 -0
- data/lib/attributor/attribute_resolver.rb +116 -0
- data/lib/attributor/dsl_compiler.rb +106 -0
- data/lib/attributor/exceptions.rb +38 -0
- data/lib/attributor/extensions/randexp.rb +10 -0
- data/lib/attributor/type.rb +117 -0
- data/lib/attributor/types/boolean.rb +26 -0
- data/lib/attributor/types/collection.rb +135 -0
- data/lib/attributor/types/container.rb +42 -0
- data/lib/attributor/types/csv.rb +10 -0
- data/lib/attributor/types/date_time.rb +36 -0
- data/lib/attributor/types/file_upload.rb +11 -0
- data/lib/attributor/types/float.rb +27 -0
- data/lib/attributor/types/hash.rb +337 -0
- data/lib/attributor/types/ids.rb +26 -0
- data/lib/attributor/types/integer.rb +63 -0
- data/lib/attributor/types/model.rb +316 -0
- data/lib/attributor/types/object.rb +19 -0
- data/lib/attributor/types/string.rb +25 -0
- data/lib/attributor/types/struct.rb +50 -0
- data/lib/attributor/types/tempfile.rb +36 -0
- data/lib/attributor/version.rb +3 -0
- data/spec/attribute_resolver_spec.rb +227 -0
- data/spec/attribute_spec.rb +597 -0
- data/spec/attributor_spec.rb +25 -0
- data/spec/dsl_compiler_spec.rb +130 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/support/models.rb +81 -0
- data/spec/support/types.rb +21 -0
- data/spec/type_spec.rb +134 -0
- data/spec/types/boolean_spec.rb +85 -0
- data/spec/types/collection_spec.rb +286 -0
- data/spec/types/container_spec.rb +49 -0
- data/spec/types/csv_spec.rb +17 -0
- data/spec/types/date_time_spec.rb +90 -0
- data/spec/types/file_upload_spec.rb +6 -0
- data/spec/types/float_spec.rb +78 -0
- data/spec/types/hash_spec.rb +372 -0
- data/spec/types/ids_spec.rb +32 -0
- data/spec/types/integer_spec.rb +151 -0
- data/spec/types/model_spec.rb +401 -0
- data/spec/types/string_spec.rb +55 -0
- data/spec/types/struct_spec.rb +189 -0
- data/spec/types/tempfile_spec.rb +6 -0
- metadata +348 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb')
|
2
|
+
|
3
|
+
describe Attributor::Ids do
|
4
|
+
|
5
|
+
context '.for' do
|
6
|
+
let(:chickens) { 10.times.collect { Chicken.example } }
|
7
|
+
|
8
|
+
let(:emails) { chickens.collect(&:email) }
|
9
|
+
let(:value) { emails.join(',') }
|
10
|
+
|
11
|
+
subject!(:ids) { Attributor::Ids.for(Chicken) }
|
12
|
+
|
13
|
+
its(:member_attribute) { should be(Chicken.attributes[:email]) }
|
14
|
+
|
15
|
+
it 'loads' do
|
16
|
+
ids.load(value).should eq(emails)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'generates valid examples' do
|
20
|
+
ids.validate(ids.example).should be_empty
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'attempting to define it as a collection using .of(type)' do
|
26
|
+
it 'raises an error' do
|
27
|
+
expect{
|
28
|
+
Attributor::Ids.of(Chicken)
|
29
|
+
}.to raise_error(/Defining Ids.of\(type\) is not allowed/)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb')
|
2
|
+
|
3
|
+
describe Attributor::Integer do
|
4
|
+
|
5
|
+
subject(:type) { Attributor::Integer }
|
6
|
+
|
7
|
+
context '.example' do
|
8
|
+
|
9
|
+
context 'when :min and :max are unspecified' do
|
10
|
+
context 'valid cases' do
|
11
|
+
it "returns an Integer in the range [0,#{Attributor::Integer::EXAMPLE_RANGE}]" do
|
12
|
+
20.times do
|
13
|
+
value = type.example
|
14
|
+
value.should be_a(::Integer)
|
15
|
+
value.should <= Attributor::Integer::EXAMPLE_RANGE
|
16
|
+
value.should >= 0
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'when :min is unspecified' do
|
23
|
+
context 'valid cases' do
|
24
|
+
[5, 100000000000000000000, -100000000000000000000].each do |max|
|
25
|
+
it "returns an Integer in the range [,#{max.inspect}]" do
|
26
|
+
20.times do
|
27
|
+
value = type.example(nil, options: {max: max})
|
28
|
+
value.should be_a(::Integer)
|
29
|
+
value.should <= max
|
30
|
+
value.should >= max - Attributor::Integer::EXAMPLE_RANGE
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'invalid cases' do
|
37
|
+
['invalid', false].each do |max|
|
38
|
+
it "raises for the invalid range [,#{max.inspect}]" do
|
39
|
+
expect {
|
40
|
+
value = type.example(nil, options: {max: max})
|
41
|
+
value.should be_a(::Integer)
|
42
|
+
}.to raise_error(Attributor::AttributorException, "Invalid range: [, #{max.inspect}]")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'when :max is unspecified' do
|
49
|
+
context 'valid cases' do
|
50
|
+
[1, -100000000000000000000, 100000000000000000000].each do |min|
|
51
|
+
it "returns an Integer in the range [#{min.inspect},]" do
|
52
|
+
20.times do
|
53
|
+
value = type.example(nil, options: {min: min})
|
54
|
+
value.should be_a(::Integer)
|
55
|
+
value.should <= min + Attributor::Integer::EXAMPLE_RANGE
|
56
|
+
value.should >= min
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'invalid cases' do
|
63
|
+
['invalid', false].each do |min|
|
64
|
+
it "raises for the invalid range [#{min.inspect},]" do
|
65
|
+
expect {
|
66
|
+
value = type.example(nil, options: {min: min})
|
67
|
+
value.should be_a(::Integer)
|
68
|
+
}.to raise_error(Attributor::AttributorException, "Invalid range: [#{min.inspect},]")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'when :min and :max are specified' do
|
75
|
+
context 'valid cases' do
|
76
|
+
[
|
77
|
+
[1,1],
|
78
|
+
[1,5],
|
79
|
+
[-2,-2],
|
80
|
+
[-3,2],
|
81
|
+
[-1000000000000000,1000000000000000]
|
82
|
+
].each do |min, max|
|
83
|
+
it "returns an Integer in the range [#{min.inspect},#{max.inspect}]" do
|
84
|
+
20.times do
|
85
|
+
value = type.example(nil, options: {max: max, min: min})
|
86
|
+
value.should <= max
|
87
|
+
value.should >= min
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context 'invalid cases' do
|
94
|
+
[[1,-1], [1,"5"], ["-2",4], [false, false], [true, true]].each do |min, max|
|
95
|
+
it "raises for the invalid range [#{min.inspect}, #{max.inspect}]" do
|
96
|
+
opts = {options: {max: max, min: min}}
|
97
|
+
expect {
|
98
|
+
type.example(nil, opts)
|
99
|
+
}.to raise_error(Attributor::AttributorException, "Invalid range: [#{min.inspect}, #{max.inspect}]")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context '.load' do
|
108
|
+
let(:value) { nil }
|
109
|
+
|
110
|
+
|
111
|
+
context 'for incoming integer values' do
|
112
|
+
let(:value) { 1 }
|
113
|
+
|
114
|
+
it 'returns the incoming value' do
|
115
|
+
type.load(value).should be(value)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context 'for incoming string values' do
|
120
|
+
|
121
|
+
|
122
|
+
context 'that are valid integers' do
|
123
|
+
let(:value) { '1024' }
|
124
|
+
it 'decodes it if the string represents an integer' do
|
125
|
+
type.load(value).should == 1024
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'that are not valid integers' do
|
130
|
+
|
131
|
+
context 'with simple alphanumeric text' do
|
132
|
+
let(:value) { 'not an integer' }
|
133
|
+
|
134
|
+
it 'raises an error' do
|
135
|
+
expect { type.load(value) }.to raise_error(/invalid value/)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
context 'with a floating point value' do
|
140
|
+
let(:value) { '98.76' }
|
141
|
+
it 'raises an error' do
|
142
|
+
expect { type.load(value) }.to raise_error(/invalid value/)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
@@ -0,0 +1,401 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb')
|
2
|
+
|
3
|
+
describe Attributor::Model do
|
4
|
+
|
5
|
+
context 'class methods' do
|
6
|
+
subject(:chicken) { Chicken }
|
7
|
+
let(:context){ ["root","subattr"] }
|
8
|
+
|
9
|
+
its(:native_type) { should eq(Chicken) }
|
10
|
+
|
11
|
+
context '.example' do
|
12
|
+
subject(:chicken) { Chicken.example }
|
13
|
+
|
14
|
+
let(:age_opts) { {options: Chicken.attributes[:age].options } }
|
15
|
+
let(:age) { /\d{2}/.gen.to_i }
|
16
|
+
|
17
|
+
context 'for a simple model' do
|
18
|
+
it { should be_kind_of(Chicken) }
|
19
|
+
|
20
|
+
context 'and attribute without :example option' do
|
21
|
+
before do
|
22
|
+
Attributor::Integer.should_receive(:example).with(kind_of(Array), age_opts).and_return(age)
|
23
|
+
end
|
24
|
+
|
25
|
+
its(:age) { should == age }
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'and attribute with :example options' do
|
29
|
+
before do
|
30
|
+
Attributor::Integer.should_not_receive(:example) # due to lazy-evaluation of examples
|
31
|
+
Attributor::String.should_not_receive(:example) # due to the :example option on the attribute
|
32
|
+
end
|
33
|
+
its(:email) { should =~ /\w+@.*\.example\.org/ }
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'with given values' do
|
37
|
+
let(:name) { 'Sir Clucksalot' }
|
38
|
+
subject(:example) { Chicken.example(name: name)}
|
39
|
+
|
40
|
+
its(:name) {should eq(name) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'generating multiple examples' do
|
45
|
+
context 'without a context' do
|
46
|
+
subject(:other_chicken) { Chicken.example }
|
47
|
+
its(:attributes) { should_not eq(chicken.attributes) }
|
48
|
+
end
|
49
|
+
context 'with identical contexts' do
|
50
|
+
let(:example_context) { 'some context' }
|
51
|
+
let(:some_chicken) { Chicken.example(example_context) }
|
52
|
+
subject(:another_chicken) { Chicken.example(example_context) }
|
53
|
+
|
54
|
+
its(:attributes) { should eq(some_chicken.attributes) }
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'with attributes that are also models' do
|
60
|
+
subject(:turducken) { Turducken.example }
|
61
|
+
|
62
|
+
its(:attributes) { should have_key(:chicken) }
|
63
|
+
its(:chicken) { should be_kind_of(Chicken)}
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'with infinitely-expanding sub-attributes' do
|
67
|
+
let(:model_class) do
|
68
|
+
Class.new(Attributor::Model) do
|
69
|
+
this = self
|
70
|
+
attributes do
|
71
|
+
attribute :name, String
|
72
|
+
attribute :child, this
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
subject(:example) { model_class.example }
|
78
|
+
|
79
|
+
it 'terminates example generation at MAX_EXAMPLE_DEPTH' do
|
80
|
+
# call .child on example MAX_EXAMPLE_DEPTH times
|
81
|
+
terminal_child = Attributor::Model::MAX_EXAMPLE_DEPTH.times.inject(example) do |object, i|
|
82
|
+
object.child
|
83
|
+
end
|
84
|
+
# after which .child will return nil
|
85
|
+
terminal_child.child.should be(nil)
|
86
|
+
# but simple attributes will be generated
|
87
|
+
terminal_child.name.should_not be(nil)
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
context '.definition' do
|
96
|
+
subject(:definition) { Chicken.definition }
|
97
|
+
|
98
|
+
context '#attributes' do
|
99
|
+
subject(:attributes) { Chicken.attributes }
|
100
|
+
it { should have_key :age }
|
101
|
+
it { should have_key :email }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
context '.load' do
|
107
|
+
let(:age) { 1 }
|
108
|
+
let(:email) { "cluck@example.org" }
|
109
|
+
let(:hash) { {:age => age, :email => email} }
|
110
|
+
|
111
|
+
subject(:model) { Chicken.load(hash) }
|
112
|
+
|
113
|
+
context 'with an instance of the model' do
|
114
|
+
it 'returns the instance' do
|
115
|
+
Chicken.load(model).should be(model)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context 'with a nil value' do
|
120
|
+
it 'returns nil' do
|
121
|
+
Chicken.load(nil).should be_nil
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context 'with a JSON-serialized hash' do
|
126
|
+
let(:context){ ['root','subattr'] }
|
127
|
+
let(:expected_hash) { {"age" => age, "email" => email} }
|
128
|
+
let(:json) { hash.to_json }
|
129
|
+
before do
|
130
|
+
Chicken.should_receive(:from_hash).
|
131
|
+
with(expected_hash,context)
|
132
|
+
JSON.should_receive(:parse).with(json).and_call_original
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'deserializes and calls from_hash' do
|
136
|
+
Chicken.load(json,context)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
context 'with an invalid JSON string' do
|
141
|
+
let(:json) { "{'invalid'}" }
|
142
|
+
|
143
|
+
it 'catches the error and reports it correctly' do
|
144
|
+
JSON.should_receive(:parse).with(json).and_call_original
|
145
|
+
expect {
|
146
|
+
Chicken.load(json,context)
|
147
|
+
}.to raise_error(Attributor::DeserializationError, /Error deserializing a String using JSON.*#{context.join('.')}/)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
context 'with an invalid object type' do
|
153
|
+
it 'raises some sort of error' do
|
154
|
+
expect {
|
155
|
+
Chicken.load(Object.new, context)
|
156
|
+
}.to raise_error(Attributor::IncompatibleTypeError, /Type Chicken cannot load values of type Object.*#{context.join('.')}/)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
context "with a hash" do
|
161
|
+
context 'for a complete set of attributes' do
|
162
|
+
it 'loads the given attributes' do
|
163
|
+
model.age.should == age
|
164
|
+
model.email.should == email
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
context 'for a subset of attributes' do
|
169
|
+
let(:hash) { Hash.new }
|
170
|
+
|
171
|
+
it 'sets the defaults' do
|
172
|
+
model.age.should == 1
|
173
|
+
model.email.should == nil
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
context 'for a superset of attributes' do
|
178
|
+
let(:hash) { {"invalid_attribute" => "value"} }
|
179
|
+
|
180
|
+
it 'raises an error' do
|
181
|
+
expect {
|
182
|
+
Chicken.load(hash, context)
|
183
|
+
}.to raise_error(Attributor::AttributorException, /Unknown attributes.*#{context.join('.')}/)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
context 'instance methods' do
|
195
|
+
subject(:chicken) { Chicken.new }
|
196
|
+
|
197
|
+
context '#respond_to?' do
|
198
|
+
[:age, :email, :age=, :email=].each do |method|
|
199
|
+
it { should respond_to(method) }
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
context 'initialize' do
|
204
|
+
|
205
|
+
subject(:chicken) { Chicken.new( attributes_data ) }
|
206
|
+
context 'supports passing an initial hash object for attribute values' do
|
207
|
+
let(:attributes_data){ {age: '1', email:'rooster@coup.com'} }
|
208
|
+
it 'and sets them in loaded format onto the instance attributes' do
|
209
|
+
Chicken.should_receive(:load).with(attributes_data).and_call_original
|
210
|
+
attributes_data.keys.each do |attr_name|
|
211
|
+
Chicken.attributes[attr_name].should_receive(:load).with(attributes_data[attr_name],instance_of(Array)).and_call_original
|
212
|
+
end
|
213
|
+
subject.age.should be(1)
|
214
|
+
subject.email.should be(attributes_data[:email])
|
215
|
+
end
|
216
|
+
end
|
217
|
+
context 'supports passing a JSON encoded data object' do
|
218
|
+
let(:attributes_hash){ {age: 1, email:'rooster@coup.com'} }
|
219
|
+
let(:attributes_data){ JSON.dump(attributes_hash) }
|
220
|
+
it 'and sets them in loaded format onto the instance attributes' do
|
221
|
+
Chicken.should_receive(:load).with(attributes_data).and_call_original
|
222
|
+
attributes_hash.keys.each do |attr_name|
|
223
|
+
Chicken.attributes[attr_name].should_receive(:load).with(attributes_hash[attr_name],instance_of(Array)).and_call_original
|
224
|
+
end
|
225
|
+
subject.age.should be(1)
|
226
|
+
subject.email.should == attributes_hash[:email]
|
227
|
+
end
|
228
|
+
end
|
229
|
+
context 'supports passing a native model for the data object' do
|
230
|
+
let(:attributes_data){ Chicken.example }
|
231
|
+
it 'sets a new instance pointing to the exact same attributes (careful about modifications!)' do
|
232
|
+
attributes_data.attributes.each do |attr_name, attr_value|
|
233
|
+
subject.send(attr_name).should be(attr_value)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
context 'getting and setting attributes' do
|
240
|
+
context 'for valid attributes' do
|
241
|
+
let(:age) { 1 }
|
242
|
+
it 'gets and sets attributes' do
|
243
|
+
chicken.age = age
|
244
|
+
chicken.age.should == age
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
context 'setting nil' do
|
249
|
+
it 'assigns the default value if there is one' do
|
250
|
+
chicken.age = nil
|
251
|
+
chicken.age.should == 1
|
252
|
+
end
|
253
|
+
|
254
|
+
it 'sets the value to nil if there is no default' do
|
255
|
+
chicken.email = nil
|
256
|
+
chicken.email.should == nil
|
257
|
+
end
|
258
|
+
|
259
|
+
end
|
260
|
+
|
261
|
+
context 'for unknown attributes' do
|
262
|
+
it 'raises an exception' do
|
263
|
+
expect {
|
264
|
+
chicken.invalid_attribute = 'value'
|
265
|
+
}.to raise_error(NoMethodError, /undefined method/)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
context 'for false attributes' do
|
270
|
+
subject(:person) { Person.example(okay: false) }
|
271
|
+
it 'properly memoizes the value' do
|
272
|
+
person.okay.should be(false)
|
273
|
+
person.okay.should be(false) # second call to ensure we hit the memoized value
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
end
|
279
|
+
|
280
|
+
|
281
|
+
context 'validation' do
|
282
|
+
context 'for simple models' do
|
283
|
+
context 'that are valid' do
|
284
|
+
subject(:chicken) { Chicken.example }
|
285
|
+
its(:validate) { should be_empty}
|
286
|
+
end
|
287
|
+
context 'that are invalid' do
|
288
|
+
subject(:chicken) { Chicken.example(age: 150) }
|
289
|
+
its(:validate) { should_not be_empty }
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
context 'for models with circular sub-attributes' do
|
294
|
+
context 'that are valid' do
|
295
|
+
subject(:person) { Person.example }
|
296
|
+
its(:validate) { should be_empty}
|
297
|
+
end
|
298
|
+
|
299
|
+
context 'that are invalid' do
|
300
|
+
subject(:person) do
|
301
|
+
# TODO: support this? Person.example(title: 'dude', address: {name: 'ME'} )
|
302
|
+
|
303
|
+
obj = Person.example(title: 'dude')
|
304
|
+
obj.address.state = 'ME'
|
305
|
+
obj
|
306
|
+
end
|
307
|
+
|
308
|
+
its(:validate) { should have(2).items }
|
309
|
+
|
310
|
+
it 'recursively-validates sub-attributes with the right context' do
|
311
|
+
title_error, state_error = person.validate('person')
|
312
|
+
title_error.should =~ /^Attribute person\.title:/
|
313
|
+
state_error.should =~ /^Attribute person\.address\.state:/
|
314
|
+
end
|
315
|
+
|
316
|
+
end
|
317
|
+
|
318
|
+
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
|
323
|
+
context '#dump' do
|
324
|
+
|
325
|
+
|
326
|
+
context 'with circular references' do
|
327
|
+
subject(:person) { Person.example }
|
328
|
+
let(:output) { person.dump }
|
329
|
+
|
330
|
+
it 'terminates' do
|
331
|
+
expect {
|
332
|
+
Person.example.dump
|
333
|
+
}.to_not raise_error
|
334
|
+
end
|
335
|
+
|
336
|
+
it 'outputs "..." for circular references' do
|
337
|
+
person.address.person.should be(person)
|
338
|
+
output[:address][:person].should eq(Attributor::Model::CIRCULAR_REFERENCE_MARKER)
|
339
|
+
end
|
340
|
+
|
341
|
+
end
|
342
|
+
|
343
|
+
end
|
344
|
+
|
345
|
+
context 'extending' do
|
346
|
+
subject(:model) do
|
347
|
+
Class.new(Attributor::Model) do
|
348
|
+
attributes do
|
349
|
+
attribute :id, Integer
|
350
|
+
attribute :timestamps do
|
351
|
+
attribute :created_at, DateTime
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
context 'adding a top-level attribute' do
|
358
|
+
before do
|
359
|
+
model.attributes do
|
360
|
+
attribute :name, String
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
it 'adds the attribute' do
|
365
|
+
model.attributes.keys.should =~ [:id, :name, :timestamps]
|
366
|
+
end
|
367
|
+
|
368
|
+
end
|
369
|
+
|
370
|
+
context 'adding to an inner-Struct' do
|
371
|
+
before do
|
372
|
+
model.attributes do
|
373
|
+
attribute :timestamps, Struct do
|
374
|
+
attribute :updated_at, DateTime
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
it 'merges with sub-attributes' do
|
380
|
+
model.attributes[:timestamps].attributes.keys.should =~ [:created_at, :updated_at]
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
|
385
|
+
context 'redefining an attribute' do
|
386
|
+
before do
|
387
|
+
model.attributes do
|
388
|
+
attribute :id, String
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
it 'works' do
|
393
|
+
model.attributes[:id].type.should be(Attributor::String)
|
394
|
+
end
|
395
|
+
|
396
|
+
end
|
397
|
+
|
398
|
+
end
|
399
|
+
|
400
|
+
|
401
|
+
end
|