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,286 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb')
|
2
|
+
|
3
|
+
describe Attributor::Collection do
|
4
|
+
|
5
|
+
subject(:type) { Attributor::Collection }
|
6
|
+
|
7
|
+
context '.of' do
|
8
|
+
|
9
|
+
[Attributor::Integer, Attributor::Struct].each do |member_type|
|
10
|
+
it "returns an anonymous class with correct member_attribute of type #{member_type}" do
|
11
|
+
klass = type.of(member_type)
|
12
|
+
klass.should be_a(::Class)
|
13
|
+
klass.member_type.should == member_type
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
[
|
18
|
+
# FIXME: https://github.com/rightscale/attributor/issues/24
|
19
|
+
#::Integer,
|
20
|
+
#::String,
|
21
|
+
#::Object
|
22
|
+
].each do |member_type|
|
23
|
+
it "raises when given invalid element type #{member_type}" do
|
24
|
+
expect { klass = type.of(member_type) }.to raise_error(Attributor::AttributorException)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context '.construct' do
|
30
|
+
|
31
|
+
# context 'with a Model (or Struct) member_type' do
|
32
|
+
# let(:member_type) { Attributor::Struct }
|
33
|
+
|
34
|
+
# it 'calls construct on that type' do
|
35
|
+
# member_type.should_receive(:construct).and_call_original
|
36
|
+
|
37
|
+
# Attributor::Collection.of(member_type)
|
38
|
+
# end
|
39
|
+
|
40
|
+
# end
|
41
|
+
|
42
|
+
# context 'with a non-Model (or Struct) member_type' do
|
43
|
+
# let(:member_type) { Attributor::Integer }
|
44
|
+
|
45
|
+
# it 'does not call construct on that type' do
|
46
|
+
# member_type.should_receive(:respond_to?).with(:construct).and_return(false)
|
47
|
+
# member_type.should_not_receive(:construct)
|
48
|
+
|
49
|
+
# Attributor::Collection.of(member_type)
|
50
|
+
# end
|
51
|
+
|
52
|
+
# end
|
53
|
+
|
54
|
+
context 'with :member_options option' do
|
55
|
+
let(:element_options) { {:identity => 'name'} }
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
context '.native_type' do
|
61
|
+
it "returns Array" do
|
62
|
+
type.native_type.should be(::Array)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context '.decode_json' do
|
67
|
+
context 'for valid JSON strings' do
|
68
|
+
[
|
69
|
+
'[]',
|
70
|
+
'[1,2,3]',
|
71
|
+
'["alpha", "omega", "gamma"]',
|
72
|
+
'["alpha", 2, 3.0]'
|
73
|
+
].each do |value|
|
74
|
+
it "parses JSON string as array when incoming value is #{value.inspect}" do
|
75
|
+
type.decode_json(value).should == JSON.parse(value)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context 'for invalid JSON strings' do
|
81
|
+
[
|
82
|
+
'{}',
|
83
|
+
'foobar',
|
84
|
+
'2',
|
85
|
+
'',
|
86
|
+
2,
|
87
|
+
nil
|
88
|
+
].each do |value|
|
89
|
+
it "parses JSON string as array when incoming value is #{value.inspect}" do
|
90
|
+
expect {
|
91
|
+
type.decode_json(value)
|
92
|
+
}.to raise_error(Attributor::AttributorException)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
context '.load' do
|
100
|
+
context 'from a Set' do
|
101
|
+
let(:values) { [1,2,3]}
|
102
|
+
let(:value) { Set.new(values) }
|
103
|
+
it 'loads properly' do
|
104
|
+
type.load(value).should =~ values
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
context 'with unspecified element type' do
|
110
|
+
context 'for valid values' do
|
111
|
+
[
|
112
|
+
nil,
|
113
|
+
[],
|
114
|
+
[1,2,3],
|
115
|
+
[Object.new, [1,2], nil, true]
|
116
|
+
].each do |value|
|
117
|
+
it "returns value when incoming value is #{value.inspect}" do
|
118
|
+
type.load(value).should == value
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context 'for invalid values' do
|
124
|
+
let(:context){ ['root','subattr'] }
|
125
|
+
[1, Object.new, false, true, 3.0].each do |value|
|
126
|
+
it "raises error when incoming value is #{value.inspect} (propagating the context)" do
|
127
|
+
expect { type.load(value,context).should == value }.to raise_error(Attributor::IncompatibleTypeError,/#{context.join('.')}/)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context 'with Attributor::Type element type' do
|
134
|
+
context 'for valid values' do
|
135
|
+
{
|
136
|
+
Attributor::String => ["foo", "bar"],
|
137
|
+
Attributor::Integer => [1, "2", 3],
|
138
|
+
Attributor::Float => [1.0, "2.0", Math::PI, Math::E],
|
139
|
+
Attributor::DateTime => ["2001-02-03T04:05:06+07:00", "Sat, 3 Feb 2001 04:05:06 +0700"],
|
140
|
+
::Chicken => [::Chicken.new, ::Chicken.new]
|
141
|
+
}.each do |member_type, value|
|
142
|
+
it "returns loaded value when member_type is #{member_type} and value is #{value.inspect}" do
|
143
|
+
expected_result = value.map {|v| member_type.load(v)}
|
144
|
+
type.of(member_type).load(value).should == expected_result
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
context 'for invalid values' do
|
150
|
+
{
|
151
|
+
# FIXME: https://github.com/rightscale/attributor/issues/24
|
152
|
+
#::String => ["foo", "bar"],
|
153
|
+
#::Object => [::Object.new]
|
154
|
+
::Chicken => [::Turkey.new]
|
155
|
+
}.each do |member_type, value|
|
156
|
+
it "raises error when member_type is #{member_type} and value is #{value.inspect}" do
|
157
|
+
expect { type.of(member_type).load(value).should == value }.to raise_error(Attributor::AttributorException)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context 'with Attributor::Struct element type' do
|
164
|
+
|
165
|
+
# FIXME: Raise in all cases of empty Structs
|
166
|
+
# context 'for empty structs' do
|
167
|
+
# let(:attribute_definition) do
|
168
|
+
# Proc.new do
|
169
|
+
# end
|
170
|
+
# end
|
171
|
+
|
172
|
+
# let(:empty_struct) { Attributor::Struct.construct(attribute_definition) }
|
173
|
+
|
174
|
+
# # FIXME: these values are only valid because they aren't values at all.
|
175
|
+
# # which isn't so bad, since a straight Struct can't have any values.
|
176
|
+
# # so I suppose this is all correct, but not entirely valuable or obvious.
|
177
|
+
# context 'for valid struct values' do
|
178
|
+
# [
|
179
|
+
# [],
|
180
|
+
# [nil],
|
181
|
+
# [nil, nil],
|
182
|
+
# [{}],
|
183
|
+
# ["{}", "{}"],
|
184
|
+
# ].each do |value|
|
185
|
+
# it "returns value when incoming value is #{value.inspect}" do
|
186
|
+
# #pending
|
187
|
+
# expected_value = value.map {|v| empty_struct.load(v)}
|
188
|
+
# type.of(Struct).load(value).should == expected_value
|
189
|
+
# end
|
190
|
+
# end
|
191
|
+
# end
|
192
|
+
|
193
|
+
# context 'for invalid struct values' do
|
194
|
+
# [
|
195
|
+
# [{"name" => "value"}, {"foo" => "another_value"}], # Ruby hash
|
196
|
+
# ['{"bar":"value"}'], # JSON hash
|
197
|
+
# ].each do |value|
|
198
|
+
# it "raises when incoming value is #{value.inspect}" do
|
199
|
+
# expect {
|
200
|
+
# type.of(empty_struct).load(value)
|
201
|
+
# }.to raise_error(Attributor::AttributorException)
|
202
|
+
# end
|
203
|
+
# end
|
204
|
+
# end
|
205
|
+
|
206
|
+
|
207
|
+
# end
|
208
|
+
|
209
|
+
|
210
|
+
context 'for simple structs' do
|
211
|
+
let(:attribute_definition) do
|
212
|
+
Proc.new do
|
213
|
+
attribute :name, Attributor::String
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
let(:simple_struct) { Attributor::Struct.construct(attribute_definition) }
|
218
|
+
|
219
|
+
context 'for valid struct values' do
|
220
|
+
[
|
221
|
+
[{"name" => "value"}, {"name" => "another_value"}], # Ruby hash
|
222
|
+
['{"name":"value"}'], # JSON hash
|
223
|
+
].each do |value|
|
224
|
+
it "returns value when incoming value is #{value.inspect}" do
|
225
|
+
expected_value = value.map {|v| simple_struct.load(v.clone)}
|
226
|
+
type.of(simple_struct).load(value).should == expected_value
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
context 'for invalid struct values' do
|
232
|
+
[
|
233
|
+
[{"name" => "value"}, {"foo" => "another_value"}], # Ruby hash
|
234
|
+
['{"bar":"value"}'], # JSON hash
|
235
|
+
[1,2],
|
236
|
+
].each do |value|
|
237
|
+
it "raises when incoming value is #{value.inspect}" do
|
238
|
+
expect {
|
239
|
+
type.of(simple_struct).load(value)
|
240
|
+
}.to raise_error(Attributor::AttributorException)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
context '.validate' do
|
249
|
+
let(:collection_members) { [1, 2, 'three'] }
|
250
|
+
let(:expected_errors) { ["error 1", "error 2", "error 3"]}
|
251
|
+
|
252
|
+
before do
|
253
|
+
collection_members.zip(expected_errors).each do |member, expected_error|
|
254
|
+
type.member_attribute.should_receive(:validate).
|
255
|
+
with(member,an_instance_of(Array)). # we don't care about the exact context here
|
256
|
+
and_return([expected_error])
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
it 'validates members' do
|
261
|
+
type.validate(collection_members).should =~ expected_errors
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
|
266
|
+
context '.example' do
|
267
|
+
it "returns an Array" do
|
268
|
+
value = type.example
|
269
|
+
value.should be_a(::Array)
|
270
|
+
end
|
271
|
+
|
272
|
+
[
|
273
|
+
Attributor::Integer,
|
274
|
+
Attributor::String,
|
275
|
+
Attributor::Boolean,
|
276
|
+
Attributor::DateTime,
|
277
|
+
Attributor::Float,
|
278
|
+
Attributor::Object
|
279
|
+
].each do |member_type|
|
280
|
+
it "returns an Array of native types of #{member_type}" do
|
281
|
+
value = Attributor::Collection.of(member_type).example
|
282
|
+
value.all? { |element| member_type.valid_type?(element) }.should be_true
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '../spec_helper.rb')
|
2
|
+
|
3
|
+
|
4
|
+
describe Attributor::Container do
|
5
|
+
|
6
|
+
context '.decode_json' do
|
7
|
+
|
8
|
+
let(:mock_type) do
|
9
|
+
Class.new do
|
10
|
+
include Attributor::Container
|
11
|
+
def self.native_type
|
12
|
+
::Hash
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
context 'for valid JSON strings' do
|
17
|
+
it "parses JSON string into the native type" do
|
18
|
+
a_hash = {"a" => 1, "b" => 2}
|
19
|
+
json_hash = JSON.dump( a_hash )
|
20
|
+
mock_type.decode_json(json_hash).should == a_hash
|
21
|
+
end
|
22
|
+
it 'complains when trying to decode a non-String value' do
|
23
|
+
expect{
|
24
|
+
mock_type.decode_json(Object.new)
|
25
|
+
}.to raise_error(Attributor::DeserializationError, /Error deserializing a Object using JSON/)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'complains when the deserialized value is not of the native_type' do
|
29
|
+
expect{
|
30
|
+
mock_type.decode_json("[1,2,3]")
|
31
|
+
}.to raise_error(Attributor::CoercionError, /Error coercing from Array/)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'complains if there is a error deserializing the string' do
|
35
|
+
expect{
|
36
|
+
mock_type.decode_json("{invalid_json}")
|
37
|
+
}.to raise_error(Attributor::DeserializationError, /Error deserializing a String using JSON/)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'uses the passed context in the output error' do
|
41
|
+
expect{
|
42
|
+
mock_type.decode_json("{invalid_json}",["my_context","attribute_name"])
|
43
|
+
}.to raise_error(Attributor::DeserializationError, /Error deserializing a String using JSON.* while loading my_context.attribute_name/)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb')
|
2
|
+
|
3
|
+
describe Attributor::CSV do
|
4
|
+
|
5
|
+
subject!(:csv) { Attributor::CSV.of(Integer) }
|
6
|
+
|
7
|
+
context '.load' do
|
8
|
+
let!(:array) { (1..10).to_a }
|
9
|
+
let!(:value) { array.join(',') }
|
10
|
+
|
11
|
+
it 'parses the value and returns an array with the right types' do
|
12
|
+
csv.load(value).should eq(array)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb')
|
2
|
+
|
3
|
+
describe Attributor::DateTime do
|
4
|
+
|
5
|
+
subject(:type) { Attributor::DateTime }
|
6
|
+
|
7
|
+
context '.native_type' do
|
8
|
+
its(:native_type) { should be(::DateTime) }
|
9
|
+
end
|
10
|
+
|
11
|
+
context '.example' do
|
12
|
+
its(:example) { should be_a(::DateTime) }
|
13
|
+
end
|
14
|
+
|
15
|
+
context '.load' do
|
16
|
+
|
17
|
+
context 'for incoming objects' do
|
18
|
+
|
19
|
+
it "returns correct DateTime for Time objects" do
|
20
|
+
object = Time.now
|
21
|
+
loaded = type.load(object)
|
22
|
+
loaded.should be_a(::DateTime)
|
23
|
+
loaded.to_time.should == object
|
24
|
+
end
|
25
|
+
|
26
|
+
it "returns correct DateTime for DateTime objects" do
|
27
|
+
object = DateTime.now
|
28
|
+
loaded = type.load(object)
|
29
|
+
loaded.should be_a(::DateTime)
|
30
|
+
loaded.should be( object )
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'for incoming strings' do
|
36
|
+
|
37
|
+
[
|
38
|
+
'2001-02-03T04:05:06+07:00',
|
39
|
+
'Sat, 03 Feb 2001 04:05:06 GMT',
|
40
|
+
'20010203T040506+0700',
|
41
|
+
'2001-W05-6T04:05:06+07:00',
|
42
|
+
'H13.02.03T04:05:06+07:00',
|
43
|
+
'Sat, 3 Feb 2001 04:05:06 +0700',
|
44
|
+
'2013/08/23 00:39:55 +0000', # Right API 1.5
|
45
|
+
'2007-10-19T04:11:33Z', # Right API 1.0
|
46
|
+
'2001-02-03T04:05:06+07:00.123456', # custom format with microseconds
|
47
|
+
].each do |value|
|
48
|
+
|
49
|
+
it "returns correct DateTime for #{value.inspect}" do
|
50
|
+
type.load(value).should == DateTime.parse(value)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
[
|
56
|
+
'Sat, 30 Feb 2001 04:05:06 GMT', # No such date exists
|
57
|
+
'2013/08/33 00:39:55 +0000', # Right API 1.5
|
58
|
+
'2007-10-33T04:11:33Z', # Right API 1.0
|
59
|
+
'2001-02-33T04:05:06+07:00.123456', # custom format with microseconds
|
60
|
+
].each do |value|
|
61
|
+
|
62
|
+
it "raises Attributor::AttributorException for #{value.inspect}" do
|
63
|
+
expect {
|
64
|
+
type.load(value)
|
65
|
+
}.to raise_error(Attributor::DeserializationError, /Error deserializing a String using DateTime/)
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
[
|
71
|
+
'',
|
72
|
+
'foobar',
|
73
|
+
'Sat, 30 Feb 2001 04:05:06 FOOBAR', # No such date format exists
|
74
|
+
].each do |value|
|
75
|
+
|
76
|
+
it "raises Attributor::AttributorException for #{value.inspect}" do
|
77
|
+
expect {
|
78
|
+
type.load(value)
|
79
|
+
}.to raise_error(Attributor::DeserializationError, /Error deserializing a String using DateTime/)
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
|