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,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
+
@@ -0,0 +1,6 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb')
2
+
3
+
4
+ describe Attributor::FileUpload do
5
+ it 'has specs'
6
+ end