attributor 2.2.1 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +12 -4
- data/lib/attributor.rb +4 -2
- data/lib/attributor/attribute.rb +8 -9
- data/lib/attributor/exceptions.rb +6 -6
- data/lib/attributor/extensions/randexp.rb +5 -0
- data/lib/attributor/types/bigdecimal.rb +28 -0
- data/lib/attributor/types/boolean.rb +2 -0
- data/lib/attributor/types/collection.rb +6 -5
- data/lib/attributor/types/csv.rb +21 -0
- data/lib/attributor/types/date.rb +37 -0
- data/lib/attributor/types/date_time.rb +1 -1
- data/lib/attributor/types/hash.rb +64 -18
- data/lib/attributor/types/model.rb +14 -6
- data/lib/attributor/types/string.rb +1 -1
- data/lib/attributor/types/time.rb +39 -0
- data/lib/attributor/version.rb +1 -1
- data/spec/attribute_spec.rb +5 -12
- data/spec/support/hashes.rb +7 -0
- data/spec/support/models.rb +3 -1
- data/spec/types/bigdecimal_spec.rb +48 -0
- data/spec/types/boolean_spec.rb +4 -0
- data/spec/types/collection_spec.rb +12 -1
- data/spec/types/csv_spec.rb +31 -0
- data/spec/types/date_spec.rb +94 -0
- data/spec/types/date_time_spec.rb +21 -17
- data/spec/types/float_spec.rb +4 -0
- data/spec/types/hash_spec.rb +122 -11
- data/spec/types/ids_spec.rb +2 -2
- data/spec/types/integer_spec.rb +3 -0
- data/spec/types/model_spec.rb +16 -4
- data/spec/types/string_spec.rb +4 -0
- data/spec/types/time_spec.rb +91 -0
- metadata +12 -1
@@ -142,8 +142,15 @@ module Attributor
|
|
142
142
|
|
143
143
|
|
144
144
|
# Model-specific decoding and coercion of the attribute.
|
145
|
-
def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
146
|
-
|
145
|
+
def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, recurse: false, **options)
|
146
|
+
if value.nil?
|
147
|
+
if recurse
|
148
|
+
value = {}
|
149
|
+
else
|
150
|
+
return value
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
147
154
|
return value if value.kind_of?(self.native_type)
|
148
155
|
|
149
156
|
context = Array(context)
|
@@ -162,18 +169,19 @@ module Attributor
|
|
162
169
|
raise IncompatibleTypeError, context: context, value_type: value.class, type: self
|
163
170
|
end
|
164
171
|
|
165
|
-
self.from_hash(hash,context)
|
172
|
+
self.from_hash(hash,context, recurse: recurse)
|
166
173
|
end
|
167
174
|
|
168
175
|
|
169
|
-
def self.from_hash(hash,context)
|
176
|
+
def self.from_hash(hash,context, recurse: false)
|
170
177
|
model = self.new
|
171
178
|
|
172
179
|
self.attributes.each do |attribute_name, attribute|
|
180
|
+
# p [attribute_name, attribute]
|
173
181
|
# OPTIMIZE: deleting the keys as we go is mucho faster, but also very risky
|
174
182
|
# Note: use "load" vs. attribute assignment so we can propagate the right context down.
|
175
183
|
sub_context = self.generate_subcontext(context,attribute_name)
|
176
|
-
model.attributes[attribute_name] = attribute.load(hash[attribute_name] || hash[attribute_name.to_s], sub_context)
|
184
|
+
model.attributes[attribute_name] = attribute.load(hash[attribute_name] || hash[attribute_name.to_s], sub_context, recurse: recurse)
|
177
185
|
end
|
178
186
|
|
179
187
|
unknown_keys = hash.keys.collect {|k| k.to_sym} - self.attributes.keys
|
@@ -237,7 +245,7 @@ module Attributor
|
|
237
245
|
@validating = false
|
238
246
|
@dumping = false
|
239
247
|
if data
|
240
|
-
loaded = self.class.load(
|
248
|
+
loaded = self.class.load(data)
|
241
249
|
@attributes = loaded.attributes
|
242
250
|
else
|
243
251
|
@attributes = ::Hash.new
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module Attributor
|
4
|
+
|
5
|
+
class Time
|
6
|
+
include Type
|
7
|
+
|
8
|
+
def self.native_type
|
9
|
+
return ::Time
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.example(context=nil, options: {})
|
13
|
+
return self.load(/[:time:]/.gen, context)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
17
|
+
return value if value.is_a?(self.native_type)
|
18
|
+
return nil if value.nil?
|
19
|
+
|
20
|
+
return value.to_time if value.respond_to?(:to_time)
|
21
|
+
|
22
|
+
case value
|
23
|
+
when ::Integer
|
24
|
+
return ::Time.at(value)
|
25
|
+
when ::String
|
26
|
+
begin
|
27
|
+
return ::Time.parse(value)
|
28
|
+
rescue ArgumentError => e
|
29
|
+
raise Attributor::DeserializationError, context: context, from: value.class, encoding: "Time" , value: value
|
30
|
+
end
|
31
|
+
else
|
32
|
+
raise CoercionError, context: context, from: value.class, to: self, value: value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
data/lib/attributor/version.rb
CHANGED
data/spec/attribute_spec.rb
CHANGED
@@ -226,11 +226,6 @@ describe Attributor::Attribute do
|
|
226
226
|
let(:context){ ['context'] }
|
227
227
|
let(:value) { '1' }
|
228
228
|
|
229
|
-
it 'does not call type.load for nil values' do
|
230
|
-
type.should_not_receive(:load)
|
231
|
-
attribute.load(nil)
|
232
|
-
end
|
233
|
-
|
234
229
|
it 'delegates to type.load' do
|
235
230
|
type.should_receive(:load).with(value,context, {})
|
236
231
|
attribute.load(value,context)
|
@@ -252,15 +247,13 @@ describe Attributor::Attribute do
|
|
252
247
|
it { should == default_value}
|
253
248
|
end
|
254
249
|
|
255
|
-
|
256
|
-
|
257
|
-
let(:
|
258
|
-
|
259
|
-
type.should_receive(:load).and_return(nil)
|
260
|
-
end
|
250
|
+
context 'for false' do
|
251
|
+
let(:type) { Attributor::Boolean }
|
252
|
+
let(:default_value) { false }
|
253
|
+
let(:value) { nil }
|
261
254
|
it { should == default_value}
|
262
|
-
end
|
263
255
|
|
256
|
+
end
|
264
257
|
end
|
265
258
|
|
266
259
|
context 'validating a value' do
|
data/spec/support/models.rb
CHANGED
@@ -33,7 +33,7 @@ end
|
|
33
33
|
|
34
34
|
class Turducken < Attributor::Model
|
35
35
|
attributes do
|
36
|
-
attribute :name, String, :description => "Turducken name", :example => /[:name:]/
|
36
|
+
attribute :name, String, :default => "Turkey McDucken", :description => "Turducken name", :example => /[:name:]/
|
37
37
|
attribute :chicken, Chicken
|
38
38
|
attribute :duck, Duck
|
39
39
|
attribute :turkey, Turkey, :description => "The turkey"
|
@@ -79,3 +79,5 @@ class Address < Attributor::Model
|
|
79
79
|
attribute :person, Person, example: proc { |address, context| Person.example(context, address: address) }
|
80
80
|
end
|
81
81
|
end
|
82
|
+
|
83
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb')
|
2
|
+
|
3
|
+
describe Attributor::BigDecimal do
|
4
|
+
subject(:type) { Attributor::BigDecimal }
|
5
|
+
|
6
|
+
context '.native_type' do
|
7
|
+
its(:native_type) { should be(::BigDecimal) }
|
8
|
+
end
|
9
|
+
|
10
|
+
context '.example' do
|
11
|
+
its(:example) { should be_a(::BigDecimal) }
|
12
|
+
it do
|
13
|
+
ex = type.example
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context '.load' do
|
18
|
+
let(:value) { nil }
|
19
|
+
it 'returns nil for nil' do
|
20
|
+
type.load(nil).should be(nil)
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'for incoming Float values' do
|
24
|
+
it 'returns the incoming value' do
|
25
|
+
[0.0, -1.0, 1.0, 1e-10, 0.25135].each do |value|
|
26
|
+
type.load(value).should eq(value)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'for incoming Integer values' do
|
32
|
+
it 'should equal the incoming value' do
|
33
|
+
[0, -1, 1].each do |value|
|
34
|
+
type.load(value).should eq(value)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'for incoming String values' do
|
40
|
+
it 'should equal the value' do
|
41
|
+
type.load('0').should eq(0)
|
42
|
+
type.load('100').should eq(100)
|
43
|
+
type.load('0.1').should eq(0.1)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
data/spec/types/boolean_spec.rb
CHANGED
@@ -64,6 +64,10 @@ describe Attributor::Boolean do
|
|
64
64
|
|
65
65
|
end
|
66
66
|
|
67
|
+
it 'returns nil for nil' do
|
68
|
+
type.load(nil).should be(nil)
|
69
|
+
end
|
70
|
+
|
67
71
|
context 'that are not valid Booleans' do
|
68
72
|
let(:context){ ['root','subattr'] }
|
69
73
|
['string', 2, 1.0, Class, Object.new].each do |value|
|
@@ -282,6 +282,17 @@ describe Attributor::Collection do
|
|
282
282
|
value.all? { |element| member_type.valid_type?(element) }.should be_true
|
283
283
|
end
|
284
284
|
end
|
285
|
+
|
286
|
+
it "returns an Array with size specified" do
|
287
|
+
type.example(options: {size: 5}).size.should eq(5)
|
288
|
+
end
|
289
|
+
|
290
|
+
it "returns an Array with size within a range" do
|
291
|
+
5.times do
|
292
|
+
type.example(options: {size: (2..4)}).size.should be_within(1).of(3)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
285
296
|
context "passing a non array context" do
|
286
297
|
it 'still is handled correctly ' do
|
287
298
|
expect{
|
@@ -289,6 +300,6 @@ describe Attributor::Collection do
|
|
289
300
|
}.to_not raise_error
|
290
301
|
end
|
291
302
|
end
|
292
|
-
|
303
|
+
|
293
304
|
end
|
294
305
|
end
|
data/spec/types/csv_spec.rb
CHANGED
@@ -11,7 +11,38 @@ describe Attributor::CSV do
|
|
11
11
|
it 'parses the value and returns an array with the right types' do
|
12
12
|
csv.load(value).should eq(array)
|
13
13
|
end
|
14
|
+
end
|
15
|
+
|
16
|
+
context '.example' do
|
17
|
+
let!(:example) { csv.example }
|
18
|
+
let!(:loaded_example) { csv.load(example) }
|
19
|
+
|
20
|
+
it 'generates a String example' do
|
21
|
+
example.should be_a(String)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'generates a comma-separated list of Integer values' do
|
25
|
+
loaded_example.should be_a(Array)
|
26
|
+
loaded_example.size.should be > 1
|
27
|
+
loaded_example.each { |e| e.should be_a(Integer) }
|
28
|
+
end
|
29
|
+
end
|
14
30
|
|
31
|
+
context '.dump' do
|
32
|
+
let!(:int_vals) { [1, 2, 3] }
|
33
|
+
let!(:str_vals) { (0..2).collect { /\w+/.gen} }
|
34
|
+
|
35
|
+
it 'dumps a String value' do
|
36
|
+
csv.dump(int_vals).should be_a(String)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'dumps a comma-separated list of Integers' do
|
40
|
+
csv.dump(int_vals).should eq(int_vals.join(','))
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'dumps non-Integer values also' do
|
44
|
+
csv.dump(str_vals).should eq(str_vals.join(','))
|
45
|
+
end
|
15
46
|
end
|
16
47
|
|
17
48
|
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb')
|
2
|
+
|
3
|
+
describe Attributor::Date do
|
4
|
+
|
5
|
+
subject(:type) { Attributor::Date }
|
6
|
+
|
7
|
+
context '.native_type' do
|
8
|
+
its(:native_type) { should be(::Date) }
|
9
|
+
end
|
10
|
+
|
11
|
+
context '.example' do
|
12
|
+
its(:example) { should be_a(::Date) }
|
13
|
+
end
|
14
|
+
|
15
|
+
context '.load' do
|
16
|
+
|
17
|
+
it 'returns nil for nil' do
|
18
|
+
type.load(nil).should be(nil)
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'for incoming objects' do
|
22
|
+
|
23
|
+
it "returns correct Date for Time objects" do
|
24
|
+
object = Time.now
|
25
|
+
loaded = type.load(object)
|
26
|
+
loaded.should be_a(::Date)
|
27
|
+
loaded.to_date.should == object.to_date
|
28
|
+
end
|
29
|
+
|
30
|
+
it "returns correct Date for DateTime objects" do
|
31
|
+
object = DateTime.now
|
32
|
+
loaded = type.load(object)
|
33
|
+
loaded.should be_a(::Date)
|
34
|
+
loaded.should be(object)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'for incoming strings' do
|
40
|
+
|
41
|
+
[
|
42
|
+
'2001-02-03T04:05:06+07:00',
|
43
|
+
'Sat, 03 Feb 2001 04:05:06 GMT',
|
44
|
+
'20010203T040506+0700',
|
45
|
+
'2001-W05-6T04:05:06+07:00',
|
46
|
+
'H13.02.03T04:05:06+07:00',
|
47
|
+
'Sat, 3 Feb 2001 04:05:06 +0700',
|
48
|
+
'2013/08/23 00:39:55 +0000',
|
49
|
+
'2007-10-19T04:11:33Z',
|
50
|
+
'2001-02-03T04:05:06+07:00.123456', # custom format with microseconds
|
51
|
+
].each do |value|
|
52
|
+
|
53
|
+
it "returns correct Date for #{value.inspect}" do
|
54
|
+
type.load(value).should == Date.parse(value)
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
[
|
60
|
+
'Sat, 30 Feb 2001 04:05:06 GMT', # No such date exists
|
61
|
+
'2013/08/33 00:39:55 +0000',
|
62
|
+
'2007-10-33T04:11:33Z',
|
63
|
+
'2001-02-33T04:05:06+07:00.123456', # custom format with microseconds
|
64
|
+
].each do |value|
|
65
|
+
|
66
|
+
it "raises Attributor::AttributorException for #{value.inspect}" do
|
67
|
+
expect {
|
68
|
+
type.load(value)
|
69
|
+
}.to raise_error(Attributor::DeserializationError, /Error deserializing a String using Date/)
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
[
|
75
|
+
'',
|
76
|
+
'foobar',
|
77
|
+
'Sat, 30 Feb 2001 04:05:06 FOOBAR', # No such date format exists
|
78
|
+
].each do |value|
|
79
|
+
|
80
|
+
it "raises Attributor::AttributorException for #{value.inspect}" do
|
81
|
+
expect {
|
82
|
+
type.load(value)
|
83
|
+
}.to raise_error(Attributor::DeserializationError, /Error deserializing a String using Date/)
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
|
@@ -14,8 +14,12 @@ describe Attributor::DateTime do
|
|
14
14
|
|
15
15
|
context '.load' do
|
16
16
|
|
17
|
+
it 'returns nil for nil' do
|
18
|
+
type.load(nil).should be(nil)
|
19
|
+
end
|
20
|
+
|
17
21
|
context 'for incoming objects' do
|
18
|
-
|
22
|
+
|
19
23
|
it "returns correct DateTime for Time objects" do
|
20
24
|
object = Time.now
|
21
25
|
loaded = type.load(object)
|
@@ -35,15 +39,15 @@ describe Attributor::DateTime do
|
|
35
39
|
context 'for incoming strings' do
|
36
40
|
|
37
41
|
[
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
42
|
+
'2001-02-03T04:05:06+07:00',
|
43
|
+
'Sat, 03 Feb 2001 04:05:06 GMT',
|
44
|
+
'20010203T040506+0700',
|
45
|
+
'2001-W05-6T04:05:06+07:00',
|
46
|
+
'H13.02.03T04:05:06+07:00',
|
47
|
+
'Sat, 3 Feb 2001 04:05:06 +0700',
|
48
|
+
'2013/08/23 00:39:55 +0000',
|
49
|
+
'2007-10-19T04:11:33Z',
|
50
|
+
'2001-02-03T04:05:06+07:00.123456', # custom format with microseconds
|
47
51
|
].each do |value|
|
48
52
|
|
49
53
|
it "returns correct DateTime for #{value.inspect}" do
|
@@ -53,10 +57,10 @@ describe Attributor::DateTime do
|
|
53
57
|
end
|
54
58
|
|
55
59
|
[
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
+
'Sat, 30 Feb 2001 04:05:06 GMT', # No such date exists
|
61
|
+
'2013/08/33 00:39:55 +0000',
|
62
|
+
'2007-10-33T04:11:33Z',
|
63
|
+
'2001-02-33T04:05:06+07:00.123456', # custom format with microseconds
|
60
64
|
].each do |value|
|
61
65
|
|
62
66
|
it "raises Attributor::AttributorException for #{value.inspect}" do
|
@@ -68,9 +72,9 @@ describe Attributor::DateTime do
|
|
68
72
|
end
|
69
73
|
|
70
74
|
[
|
71
|
-
|
72
|
-
|
73
|
-
|
75
|
+
'',
|
76
|
+
'foobar',
|
77
|
+
'Sat, 30 Feb 2001 04:05:06 FOOBAR', # No such date format exists
|
74
78
|
].each do |value|
|
75
79
|
|
76
80
|
it "raises Attributor::AttributorException for #{value.inspect}" do
|