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,78 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb')
|
2
|
+
|
3
|
+
describe Attributor::Float do
|
4
|
+
|
5
|
+
subject(:type) { Attributor::Float }
|
6
|
+
|
7
|
+
context '.native_type' do
|
8
|
+
its(:native_type) { should be(::Float) }
|
9
|
+
end
|
10
|
+
|
11
|
+
context '.example' do
|
12
|
+
its(:example) { should be_a(::Float) }
|
13
|
+
|
14
|
+
context 'with options' do
|
15
|
+
let(:min) { 1 }
|
16
|
+
let(:max) { 2 }
|
17
|
+
|
18
|
+
subject(:examples) { (0..100).collect { type.example(options:{min:min, max:max})}}
|
19
|
+
|
20
|
+
its(:min) { should be > min }
|
21
|
+
its(:max) { should be < max }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context '.load' do
|
26
|
+
let(:value) { nil }
|
27
|
+
|
28
|
+
context 'for incoming Float values' do
|
29
|
+
|
30
|
+
it 'returns the incoming value' do
|
31
|
+
[0.0, -1.0, 1.0, 1e-10].each do |value|
|
32
|
+
type.load(value).should be(value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'for incoming Integer values' do
|
38
|
+
|
39
|
+
context 'with an integer value' do
|
40
|
+
let(:value) { 1 }
|
41
|
+
it 'decodes it if the Integer represents a Float' do
|
42
|
+
type.load(value).should == 1.0
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'for incoming String values' do
|
48
|
+
|
49
|
+
context 'that are valid Floats' do
|
50
|
+
['0.0', '-1.0', '1.0', '1e-10'].each do |value|
|
51
|
+
it 'decodes it if the String represents a Float' do
|
52
|
+
type.load(value).should == Float(value)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'that are valid Integers' do
|
58
|
+
let(:value) { '1' }
|
59
|
+
it 'decodes it if the String represents an Integer' do
|
60
|
+
type.load(value).should == 1.0
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'that are not valid Floats' do
|
65
|
+
|
66
|
+
context 'with simple alphanumeric text' do
|
67
|
+
let(:value) { 'not a Float' }
|
68
|
+
|
69
|
+
it 'raises an error' do
|
70
|
+
expect { type.load(value) }.to raise_error(/invalid value/)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,372 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb')
|
2
|
+
|
3
|
+
|
4
|
+
describe Attributor::Hash do
|
5
|
+
|
6
|
+
subject(:type) { Attributor::Hash }
|
7
|
+
|
8
|
+
its(:native_type) { should be(type) }
|
9
|
+
|
10
|
+
context '.example' do
|
11
|
+
context 'for a simple hash' do
|
12
|
+
subject(:example) { Attributor::Hash.example }
|
13
|
+
|
14
|
+
it { should be_kind_of(Attributor::Hash) }
|
15
|
+
it { should be_empty }
|
16
|
+
it { should eq(::Hash.new) }
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'for a typed hash' do
|
20
|
+
subject(:example){ Attributor::Hash.of(value: Integer).example}
|
21
|
+
|
22
|
+
it 'returns a hash with keys and/or values of the right type' do
|
23
|
+
example.should be_kind_of(Attributor::Hash)
|
24
|
+
example.keys.size.should > 0
|
25
|
+
example.values.all? {|v| v.kind_of? Integer}.should be(true)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context '.load' do
|
31
|
+
let(:value) { {one: 'two', three: 4} }
|
32
|
+
subject(:hash) { type.load(value) }
|
33
|
+
|
34
|
+
context 'for a simple hash' do
|
35
|
+
it { should eq(value) }
|
36
|
+
it 'equals the hash' do
|
37
|
+
hash.should eq value
|
38
|
+
hash[:one].should eq('two')
|
39
|
+
hash[:three].should eq(4)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'for a JSON encoded hash' do
|
44
|
+
let(:value_as_hash) { {'one' => 'two', 'three' => 4} }
|
45
|
+
let(:value) { JSON.dump( value_as_hash ) }
|
46
|
+
it 'deserializes and converts it to a real hash' do
|
47
|
+
hash.should eq(value_as_hash)
|
48
|
+
hash['one'].should eq 'two'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'for a typed hash' do
|
53
|
+
subject(:type){ Attributor::Hash.of(key: String, value: Integer)}
|
54
|
+
context 'with good values' do
|
55
|
+
let(:value) { {one: '1', 'three' => 3} }
|
56
|
+
it 'coerces good values into the correct types' do
|
57
|
+
hash.should eq({'one' => 1, 'three' => 3})
|
58
|
+
hash['one'].should eq(1)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'with incompatible values' do
|
63
|
+
let(:value) { {one: 'two', three: 4} }
|
64
|
+
it 'fails' do
|
65
|
+
expect{
|
66
|
+
type.load(value)
|
67
|
+
}.to raise_error(/invalid value for Integer/)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'for a partially typed hash' do
|
74
|
+
subject(:type){ Attributor::Hash.of(value: Integer) }
|
75
|
+
context 'with good values' do
|
76
|
+
let(:value) { {one: '1', [1,2,3] => 3} }
|
77
|
+
it 'coerces only values into the correct types (and leave keys alone)' do
|
78
|
+
hash.should eq({:one => 1, [1,2,3] => 3})
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
context '.of' do
|
86
|
+
context 'specific key and value types' do
|
87
|
+
let(:key_type){ String }
|
88
|
+
let(:value_type){ Integer }
|
89
|
+
|
90
|
+
subject(:type) { Attributor::Hash.of(key: key_type, value: value_type) }
|
91
|
+
|
92
|
+
it { should be_a(::Class) }
|
93
|
+
its(:ancestors) { should include(Attributor::Hash) }
|
94
|
+
its(:key_type) { should == Attributor::String }
|
95
|
+
its(:value_type) { should == Attributor::Integer }
|
96
|
+
|
97
|
+
context '.load' do
|
98
|
+
let(:value) { {one: '2', 3 => 4} }
|
99
|
+
|
100
|
+
subject(:hash) { type.load(value) }
|
101
|
+
|
102
|
+
it 'coerces the types properly' do
|
103
|
+
hash['one'].should eq(2)
|
104
|
+
hash['3'].should eq(4)
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
context '.construct' do
|
114
|
+
let(:block) do
|
115
|
+
proc do
|
116
|
+
key 'a string', String
|
117
|
+
key '1', Integer
|
118
|
+
key :some_date, DateTime
|
119
|
+
key 'defaulted', String, default: 'default value'
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
subject(:type) { Attributor::Hash.construct(block) }
|
125
|
+
|
126
|
+
it { should_not be(Attributor::Hash)
|
127
|
+
}
|
128
|
+
|
129
|
+
context 'loading' do
|
130
|
+
let(:date) { DateTime.parse("2014-07-15") }
|
131
|
+
let(:value) do
|
132
|
+
{'a string' => 12, '1' => '2', :some_date => date.to_s}
|
133
|
+
end
|
134
|
+
|
135
|
+
subject(:hash) { type.load(value)}
|
136
|
+
|
137
|
+
it 'loads' do
|
138
|
+
hash['a string'].should eq('12')
|
139
|
+
hash['1'].should eq(2)
|
140
|
+
hash[:some_date].should eq(date)
|
141
|
+
hash['defaulted'].should eq('default value')
|
142
|
+
end
|
143
|
+
|
144
|
+
context 'with unknown keys in input' do
|
145
|
+
it 'raises an error' do
|
146
|
+
expect {
|
147
|
+
type.load({'other_key' => :value})
|
148
|
+
}.to raise_error(Attributor::AttributorException)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
context 'with a key_type' do
|
154
|
+
let(:block) do
|
155
|
+
proc do
|
156
|
+
key 'a string', String
|
157
|
+
key '1', Integer
|
158
|
+
key 'some_date', DateTime
|
159
|
+
key 'defaulted', String, default: 'default value'
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
subject(:type) { Attributor::Hash.of(key: String).construct(block) }
|
164
|
+
let(:value) do
|
165
|
+
{'a string' => 12, 1 => '2', :some_date => date.to_s}
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'loads' do
|
169
|
+
hash['a string'].should eq('12')
|
170
|
+
hash['1'].should eq(2)
|
171
|
+
hash['some_date'].should eq(date)
|
172
|
+
hash['defaulted'].should eq('default value')
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
context 'with key names of the wrong type' do
|
179
|
+
let(:block) do
|
180
|
+
proc do
|
181
|
+
key :some_date, DateTime
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
it 'raises an error' do
|
186
|
+
expect {
|
187
|
+
Attributor::Hash.of(key:String).construct(block).keys
|
188
|
+
}.to raise_error(/Invalid key: :some_date, must be instance of String/)
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
context '.check_option!' do
|
195
|
+
it 'accepts key_type:' do
|
196
|
+
subject.check_option!(:key_type, String).should == :ok
|
197
|
+
end
|
198
|
+
it 'accepts value_type' do
|
199
|
+
subject.check_option!(:value_type, Object).should == :ok
|
200
|
+
end
|
201
|
+
it 'rejects unknown options' do
|
202
|
+
subject.check_option!(:bad_option, Object).should == :unknown
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
|
207
|
+
context '.dump' do
|
208
|
+
|
209
|
+
let(:value) { {one: 1, two: 2} }
|
210
|
+
let(:opts) { {} }
|
211
|
+
|
212
|
+
context 'for a simple (untyped) hash' do
|
213
|
+
it 'returns the untouched hash value' do
|
214
|
+
type.dump(value, opts).should eq(value)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
context 'for a typed hash' do
|
219
|
+
let(:value1) { {first: "Joe", last: "Moe"} }
|
220
|
+
let(:value2) { {first: "Mary", last: "Foe"} }
|
221
|
+
let(:value) { { id1: subtype.new(value1), id2: subtype.new(value2) } }
|
222
|
+
let(:subtype) do
|
223
|
+
Class.new(Attributor::Model) do
|
224
|
+
attributes do
|
225
|
+
attribute :first, String
|
226
|
+
attribute :last, String
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
let(:type) { Attributor::Hash.of(key: String, value: subtype) }
|
231
|
+
|
232
|
+
it 'returns a hash with the dumped values and keys' do
|
233
|
+
subtype.should_receive(:dump).exactly(2).times.and_call_original
|
234
|
+
dumped_value = type.dump(value, opts)
|
235
|
+
dumped_value.should be_kind_of(::Hash)
|
236
|
+
dumped_value.keys.should =~ [:id1,:id2]
|
237
|
+
dumped_value.values.should have(2).items
|
238
|
+
value[:id1].should be_kind_of subtype
|
239
|
+
value[:id2].should be_kind_of subtype
|
240
|
+
dumped_value[:id1].should == value1
|
241
|
+
dumped_value[:id2].should == value2
|
242
|
+
end
|
243
|
+
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
context '#validate' do
|
248
|
+
context 'for a key and value typed hash' do
|
249
|
+
let(:key_type){ Integer }
|
250
|
+
let(:value_type){ DateTime }
|
251
|
+
|
252
|
+
let(:type) { Attributor::Hash.of(key: key_type, value: value_type) }
|
253
|
+
subject(:hash) { type.new('one' => :two) }
|
254
|
+
|
255
|
+
it 'returns errors for key and value' do
|
256
|
+
errors = hash.validate
|
257
|
+
errors.should have(2).items
|
258
|
+
|
259
|
+
errors.should include("Attribute $.key(\"one\") received value: \"one\" is of the wrong type (got: String, expected: Attributor::Integer)")
|
260
|
+
errors.should include("Attribute $.value(:two) received value: :two is of the wrong type (got: Symbol, expected: Attributor::DateTime)")
|
261
|
+
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
context 'for a hash with defined keys' do
|
266
|
+
let(:block) do
|
267
|
+
proc do
|
268
|
+
key 'integer', Integer
|
269
|
+
key 'datetime', DateTime
|
270
|
+
key 'not-optional', String, required: true
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
let(:type) { Attributor::Hash.construct(block) }
|
275
|
+
|
276
|
+
let(:values) { {'integer' => 'one', 'datetime' => 'now' } }
|
277
|
+
subject(:hash) { type.new(values) }
|
278
|
+
|
279
|
+
it 'validates the keys' do
|
280
|
+
errors = hash.validate
|
281
|
+
errors.should have(3).items
|
282
|
+
errors.should include("Attribute $.get(\"not-optional\") is required")
|
283
|
+
end
|
284
|
+
|
285
|
+
end
|
286
|
+
|
287
|
+
end
|
288
|
+
|
289
|
+
context 'in an Attribute' do
|
290
|
+
let(:options) { {} }
|
291
|
+
subject(:attribute) { Attributor::Attribute.new(Attributor::Hash, options)}
|
292
|
+
|
293
|
+
context 'with an example option that is a proc' do
|
294
|
+
let(:example_hash) { {:key => "value"} }
|
295
|
+
let(:options) { { example: proc { example_hash } } }
|
296
|
+
it 'uses the hash' do
|
297
|
+
attribute.example.should be(example_hash)
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
end
|
302
|
+
|
303
|
+
context '.describe' do
|
304
|
+
subject(:description) { type.describe }
|
305
|
+
context 'for hashes with key and value types' do
|
306
|
+
it 'describes the type correctly' do
|
307
|
+
description[:name].should eq('Hash')
|
308
|
+
description[:key].should eq(type:{name: 'Object'})
|
309
|
+
description[:value].should eq(type:{name: 'Object'})
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
context 'for hashes specific keys defined' do
|
314
|
+
let(:block) do
|
315
|
+
proc do
|
316
|
+
key 'a string', String
|
317
|
+
key '1', Integer, min: 1, max: 20
|
318
|
+
key 'some_date', DateTime
|
319
|
+
key 'defaulted', String, default: 'default value'
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
let(:type) { Attributor::Hash.of(key: String).construct(block) }
|
324
|
+
|
325
|
+
it 'describes the type correctly' do
|
326
|
+
description[:name].should eq('Hash')
|
327
|
+
description[:key].should eq(type:{name: 'String'})
|
328
|
+
description.should_not have_key(:value)
|
329
|
+
|
330
|
+
keys = description[:keys]
|
331
|
+
|
332
|
+
keys['a string'].should eq(type: {name: 'String'} )
|
333
|
+
keys['1'].should eq(type: {name: 'Integer'}, options: {min: 1, max: 20} )
|
334
|
+
keys['some_date'].should eq(type: {name: 'DateTime' }) #
|
335
|
+
keys['defaulted'].should eq(type: {name: 'String'}, default: 'default value')
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
context '#dump' do
|
341
|
+
let(:key_type){ String }
|
342
|
+
let(:value_type){ Integer }
|
343
|
+
let(:hash) { {one: '2', 3 => 4} }
|
344
|
+
let(:type) { Attributor::Hash.of(key: key_type, value: value_type) }
|
345
|
+
let(:value) { type.load(hash) }
|
346
|
+
|
347
|
+
subject(:output) { value.dump }
|
348
|
+
|
349
|
+
it 'dumps the contents properly' do
|
350
|
+
output.should be_kind_of(::Hash)
|
351
|
+
output.should eq('one' => 2, '3' => 4)
|
352
|
+
end
|
353
|
+
|
354
|
+
context 'with a model as value type' do
|
355
|
+
let(:value_type) do
|
356
|
+
Class.new(Attributor::Model) do
|
357
|
+
attributes do
|
358
|
+
attribute :first, String
|
359
|
+
attribute :last, String
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
363
|
+
let(:hash) { {one: value_type.example} }
|
364
|
+
|
365
|
+
it 'works too' do
|
366
|
+
#pp output
|
367
|
+
end
|
368
|
+
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
end
|