attributor 2.2.1 → 2.3.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 +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
|