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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +4 -0
  5. data/CHANGELOG.md +52 -0
  6. data/Gemfile +3 -0
  7. data/Guardfile +12 -0
  8. data/LICENSE +22 -0
  9. data/README.md +62 -0
  10. data/Rakefile +28 -0
  11. data/attributor.gemspec +40 -0
  12. data/lib/attributor.rb +89 -0
  13. data/lib/attributor/attribute.rb +271 -0
  14. data/lib/attributor/attribute_resolver.rb +116 -0
  15. data/lib/attributor/dsl_compiler.rb +106 -0
  16. data/lib/attributor/exceptions.rb +38 -0
  17. data/lib/attributor/extensions/randexp.rb +10 -0
  18. data/lib/attributor/type.rb +117 -0
  19. data/lib/attributor/types/boolean.rb +26 -0
  20. data/lib/attributor/types/collection.rb +135 -0
  21. data/lib/attributor/types/container.rb +42 -0
  22. data/lib/attributor/types/csv.rb +10 -0
  23. data/lib/attributor/types/date_time.rb +36 -0
  24. data/lib/attributor/types/file_upload.rb +11 -0
  25. data/lib/attributor/types/float.rb +27 -0
  26. data/lib/attributor/types/hash.rb +337 -0
  27. data/lib/attributor/types/ids.rb +26 -0
  28. data/lib/attributor/types/integer.rb +63 -0
  29. data/lib/attributor/types/model.rb +316 -0
  30. data/lib/attributor/types/object.rb +19 -0
  31. data/lib/attributor/types/string.rb +25 -0
  32. data/lib/attributor/types/struct.rb +50 -0
  33. data/lib/attributor/types/tempfile.rb +36 -0
  34. data/lib/attributor/version.rb +3 -0
  35. data/spec/attribute_resolver_spec.rb +227 -0
  36. data/spec/attribute_spec.rb +597 -0
  37. data/spec/attributor_spec.rb +25 -0
  38. data/spec/dsl_compiler_spec.rb +130 -0
  39. data/spec/spec_helper.rb +30 -0
  40. data/spec/support/models.rb +81 -0
  41. data/spec/support/types.rb +21 -0
  42. data/spec/type_spec.rb +134 -0
  43. data/spec/types/boolean_spec.rb +85 -0
  44. data/spec/types/collection_spec.rb +286 -0
  45. data/spec/types/container_spec.rb +49 -0
  46. data/spec/types/csv_spec.rb +17 -0
  47. data/spec/types/date_time_spec.rb +90 -0
  48. data/spec/types/file_upload_spec.rb +6 -0
  49. data/spec/types/float_spec.rb +78 -0
  50. data/spec/types/hash_spec.rb +372 -0
  51. data/spec/types/ids_spec.rb +32 -0
  52. data/spec/types/integer_spec.rb +151 -0
  53. data/spec/types/model_spec.rb +401 -0
  54. data/spec/types/string_spec.rb +55 -0
  55. data/spec/types/struct_spec.rb +189 -0
  56. data/spec/types/tempfile_spec.rb +6 -0
  57. 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