attributor 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,50 @@
1
+
2
+ module Attributor
3
+ class Struct < Attributor::Model
4
+
5
+
6
+ # Construct a new subclass, using attribute_definition to define attributes.
7
+ def self.construct(attribute_definition, options={})
8
+ # if we're in a subclass of Struct, but not attribute_definition is provided, we're
9
+ # not REALLY trying to define a new struct. more than likely Collection is calling
10
+ # construct on us.
11
+ unless self == Attributor::Struct || attribute_definition.nil?
12
+ raise AttributorException, 'can not construct from already-constructed Struct'
13
+ end
14
+
15
+ # TODO: massage the options here to pull out only the relevant ones
16
+
17
+ # simply return Struct if we don't specify any sub-attributes....
18
+ return self if attribute_definition.nil?
19
+
20
+ if options[:reference]
21
+ options.merge!(options[:reference].options) do |key, oldval, newval|
22
+ oldval
23
+ end
24
+ end
25
+
26
+ Class.new(self) do
27
+ attributes options, &attribute_definition
28
+ end
29
+
30
+ end
31
+
32
+
33
+ def self.definition
34
+ # Could probably do this better, but its use should be memoized in the enclosing Attribute
35
+ if self == Attributor::Struct
36
+ raise AttributorException, "Can not use a pure Struct without defining sub-attributes"
37
+ else
38
+ super
39
+ end
40
+ end
41
+
42
+
43
+ # Two structs are equal if their attributes are equal
44
+ def ==(other_object)
45
+ return false if other_object == nil
46
+ self.attributes == other_object.attributes
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,36 @@
1
+ require 'tempfile'
2
+
3
+ module Attributor
4
+ class Tempfile
5
+ include Attributor::Type
6
+
7
+ def self.native_type
8
+ return ::Tempfile
9
+ end
10
+
11
+ def self.example(context=Attributor::DEFAULT_ROOT_CONTEXT, options:{})
12
+ ::Tempfile.new(Attributor.humanize_context(context))
13
+ end
14
+
15
+ def self.dump(value, **opts)
16
+ value.path
17
+ end
18
+
19
+ def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
20
+ # TODO: handle additional cases that make sense
21
+ case value
22
+ when ::String
23
+ name = Attributor.humanize_context(context)
24
+
25
+ file = ::Tempfile.new(name)
26
+ file.write(value)
27
+ file.rewind
28
+ return file
29
+ end
30
+
31
+ super
32
+ end
33
+
34
+
35
+ end
36
+ end
@@ -0,0 +1,3 @@
1
+ module Attributor
2
+ VERSION = "2.1.0"
3
+ end
@@ -0,0 +1,227 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
+
3
+
4
+ describe Attributor::AttributeResolver do
5
+ let(:value) { /\w+/.gen }
6
+
7
+ context 'registering and querying simple values' do
8
+ let(:name) { "string_value" }
9
+ before { subject.register(name,value) }
10
+
11
+ it 'works' do
12
+ subject.query(name).should be value
13
+ end
14
+ end
15
+
16
+
17
+ context 'querying and registering nested values' do
18
+ let(:one) { double(:two => value) }
19
+ let(:key) { "one.two" }
20
+ before { subject.register("one", one) }
21
+
22
+ it 'works' do
23
+ subject.query(key).should be value
24
+ end
25
+ end
26
+
27
+
28
+ context 'querying nested values from models' do
29
+ let(:instance) { double("instance", :ssh_key => ssh_key) }
30
+ let(:ssh_key) { double("ssh_key", :name => value) }
31
+ let(:key) { "instance.ssh_key.name" }
32
+
33
+ before { subject.register('instance', instance) }
34
+
35
+ it 'works' do
36
+ subject.query("instance").should be instance
37
+ subject.query("instance.ssh_key").should be ssh_key
38
+ subject.query(key).should be value
39
+ end
40
+
41
+
42
+ context 'with a prefix' do
43
+ let(:key) { "name" }
44
+ let(:prefix) { "$.instance.ssh_key"}
45
+ let(:value) { 'some_name' }
46
+ it 'works' do
47
+ subject.query(key,prefix).should be(value)
48
+ end
49
+ end
50
+
51
+ end
52
+
53
+
54
+ context 'querying values that do not exist' do
55
+ context 'for a straight key' do
56
+ let(:key) { "missing" }
57
+ it 'returns nil' do
58
+ subject.query(key).should be_nil
59
+ end
60
+ end
61
+ context 'for a nested key' do
62
+ let(:key) { "nested.missing" }
63
+ it 'returns nil' do
64
+ subject.query(key).should be_nil
65
+ end
66
+ end
67
+ end
68
+
69
+
70
+ context 'checking attribute conditions' do
71
+ let(:key) { "instance.ssh_key.name" }
72
+ let(:ssh_key) { double("ssh_key", :name => value) }
73
+ let(:instance_id) { 123 }
74
+ let(:instance) { double("instance", ssh_key: ssh_key, id: instance_id) }
75
+
76
+ let(:context) { '$' }
77
+
78
+ before { subject.register('instance', instance) }
79
+
80
+ let(:present_key) { key }
81
+ let(:missing_key) { 'instance.ssh_key.something_else' }
82
+
83
+ context 'with no condition' do
84
+ let(:condition) { nil }
85
+ before { ssh_key.should_receive(:something_else).and_return(nil) }
86
+ it 'works' do
87
+ subject.check(context, present_key, condition).should be true
88
+ subject.check(context, missing_key, condition).should be false
89
+ end
90
+ end
91
+
92
+
93
+ context 'with a string condition' do
94
+ let(:passing_condition) { value }
95
+ let(:failing_condition) { /\w+/.gen }
96
+
97
+ it 'works' do
98
+ subject.check(context, key, passing_condition).should be true
99
+ subject.check(context, key, failing_condition).should be false
100
+ end
101
+ end
102
+
103
+
104
+ context 'with a regex condition' do
105
+ let(:passing_condition) { /\w+/ }
106
+ let(:failing_condition) { /\d+/ }
107
+
108
+ it 'works' do
109
+ subject.check(context, key, passing_condition).should be true
110
+ subject.check(context, key, failing_condition).should be false
111
+ end
112
+
113
+ end
114
+
115
+ context 'with an integer condition' do
116
+ let(:key) { "instance.id" }
117
+ let(:passing_condition) { instance_id }
118
+ let(:failing_condition) { /\w+/.gen }
119
+
120
+ it 'works' do
121
+ subject.check(context, key, passing_condition).should be true
122
+ subject.check(context, key, failing_condition).should be false
123
+ end
124
+ end
125
+
126
+ context 'with a hash condition' do
127
+ end
128
+
129
+ context 'with a proc condition' do
130
+ let(:passing_condition) { Proc.new { |test_value| test_value == value } }
131
+ let(:failing_condition) { Proc.new { |test_value| test_value != value } }
132
+
133
+ it 'works' do
134
+ expect(subject.check(context, key, passing_condition)).to eq(true)
135
+ expect(subject.check(context, key, failing_condition)).to eq(false)
136
+ end
137
+
138
+ end
139
+
140
+ context 'with an unsupported condition type' do
141
+ let(:condition) { double("weird condition type") }
142
+ it 'raises an error' do
143
+ expect { subject.check(context, present_key, condition) }.to raise_error
144
+ end
145
+ end
146
+
147
+ context 'with a condition that asserts something IS nil' do
148
+ let(:ssh_key) { double("ssh_key", :name => nil) }
149
+ it 'can be done using the almighty Proc' do
150
+ cond = Proc.new { |value| !value.nil? }
151
+ subject.check(context, key, cond).should be false
152
+ end
153
+ end
154
+
155
+ context 'with a relative path' do
156
+ let(:context) { "$.instance.ssh_key"}
157
+ let(:key) { "name" }
158
+
159
+ it 'works' do
160
+ subject.check(context, key, value).should be true
161
+ end
162
+
163
+ end
164
+
165
+ end
166
+
167
+
168
+ # context 'with context stuff...' do
169
+
170
+ # let(:ssh_key) { double("ssh_key", name:value) }
171
+ # let(:instance) { double("instance", ssh_key:ssh_key) }
172
+
173
+ # let(:key) { "ssh_key.name" }
174
+ # let(:key) { "$.payload" }
175
+ # let(:key) { "ssh_key.name" } # no $ == current object
176
+ # let(:key) { "@.ssh_key" } # @ is current object
177
+
178
+ # before { subject.register('instance', instance) }
179
+
180
+ # it 'works?' do
181
+ # # check dependency for 'instance'
182
+ # resolver.with 'instance' do |res|
183
+ # res.check(key)
184
+ # '$.payload'
185
+ # end
186
+
187
+ # end
188
+
189
+ # end
190
+
191
+
192
+ # context 'integration with attributes that have sub-attributes' do
193
+ #when you start to parse... do you set the root in the resolver?
194
+ # end
195
+ #
196
+ # context 'actually using the thing' do
197
+
198
+
199
+ # # we'll always want to add... right? never really remove?
200
+ # # at least not remove for the duration of a given resolver...
201
+ # # which will last for one request.
202
+ # #
203
+ # # could the resolver be an identity-map of sorts for the request?
204
+ # # how much overlap is there in there?
205
+ # #
206
+ # #
207
+
208
+ # it 'is really actually quite useful' do
209
+ # #attribute = Attributor::Attribute.new ::String, required_if: { "instance.ssh_key.name" : Proc.new { |value| value.nil? } }
210
+
211
+ # resolver = Attributor::AttributeResolver.new
212
+
213
+ # resolver.register '$.parsed_params', parsed_params
214
+ # resolver.register '$.payload', payload
215
+
216
+ # resolver.query '$.parsed_params.account_id'
217
+
218
+
219
+ # end
220
+
221
+
222
+ # end
223
+
224
+ end
225
+
226
+
227
+
@@ -0,0 +1,597 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
+
3
+
4
+ describe Attributor::Attribute do
5
+
6
+ let(:attribute_options) { Hash.new }
7
+ let(:type) { AttributeType }
8
+
9
+ subject(:attribute) { Attributor::Attribute.new(type, attribute_options) }
10
+
11
+ let(:context) { ["context"] }
12
+ let(:value) { "one" }
13
+
14
+ context 'initialize' do
15
+ its(:type) { should be type }
16
+ its(:options) { should be attribute_options }
17
+
18
+ it 'calls check_options!' do
19
+ Attributor::Attribute.any_instance.should_receive(:check_options!)
20
+ Attributor::Attribute.new(type, attribute_options)
21
+ end
22
+
23
+ context 'for anonymous types (aka Structs)' do
24
+ before do
25
+ Attributor.should_receive(:resolve_type).once.with(Struct,attribute_options, anything()).and_call_original
26
+ end
27
+
28
+ it 'generates the class' do
29
+ thing = Attributor::Attribute.new(Struct, attribute_options) do
30
+ attribute :id, Integer
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+
38
+ context '==' do
39
+ let(:other_attribute) { Attributor::Attribute.new(type, attribute_options) }
40
+ it { should == other_attribute}
41
+ end
42
+
43
+ context 'describe' do
44
+ let(:attribute_options) { {:required => true, :values => ["one"], :description => "something", :min => 0} }
45
+ let(:expected) do
46
+ h = {:type => {:name => type.name} }
47
+ common = attribute_options.select{|k,v| Attributor::Attribute::TOP_LEVEL_OPTIONS.include? k }
48
+ h.merge!( common )
49
+ h[:options] = {:min => 0 }
50
+ h
51
+ end
52
+
53
+
54
+ its(:describe) { should == expected }
55
+
56
+ context 'with example options' do
57
+ let(:attribute_options) { {:description=> "something", :example => "ex_def"} }
58
+ its(:describe) { should have_key(:example_definition) }
59
+ its(:describe) { should_not have_key(:example) }
60
+ it 'should have the example value in the :example_definition key' do
61
+ subject.describe[:example_definition].should == "ex_def"
62
+ end
63
+ end
64
+
65
+ context 'for an anonymous type (aka: Struct)' do
66
+ let(:attribute_options) { Hash.new }
67
+ let(:attribute) do
68
+ Attributor::Attribute.new(Struct, attribute_options) do
69
+ attribute :id, Integer
70
+ end
71
+ end
72
+
73
+
74
+ subject(:description) { attribute.describe }
75
+
76
+
77
+ it 'uses the name of the first non-anonymous ancestor' do
78
+ description[:type][:name].should == 'Struct'
79
+ end
80
+
81
+ it 'includes sub-attributes' do
82
+ description[:type][:attributes].should have_key(:id)
83
+ end
84
+
85
+ end
86
+ end
87
+
88
+
89
+ context 'parse' do
90
+ let(:loaded_object){ double("I'm loaded") }
91
+ it 'loads and validates' do
92
+ attribute.should_receive(:load).with(value,Attributor::DEFAULT_ROOT_CONTEXT).and_return(loaded_object)
93
+ attribute.should_receive(:validate).with(loaded_object,Attributor::DEFAULT_ROOT_CONTEXT).and_call_original
94
+
95
+ attribute.parse(value)
96
+ end
97
+ end
98
+
99
+
100
+ context 'checking options' do
101
+ it 'raises for invalid options' do
102
+ expect {
103
+ Attributor::Attribute.new(Integer, unknown_opt: true)
104
+ }.to raise_error(/unsupported option/)
105
+ end
106
+
107
+ it 'has a spec that we try to validate the :default value' do
108
+ expect {
109
+ Attributor::Attribute.new(Integer, default: "not an okay integer")
110
+ }.to raise_error(/Default value doesn't have the correct attribute type/)
111
+ end
112
+
113
+ end
114
+
115
+
116
+ context 'example' do
117
+ let(:example) { nil }
118
+ let(:attribute_options) { {:example => example} }
119
+
120
+ context 'with nothing specified' do
121
+ let(:attribute_options) { {} }
122
+ before do
123
+ type.should_receive(:example).and_return(example)
124
+ end
125
+
126
+ it 'defers to the type' do
127
+ attribute.example.should be example
128
+ end
129
+ end
130
+
131
+
132
+ context 'with a string' do
133
+ let(:example) { "example" }
134
+
135
+ its(:example) { should be example }
136
+ end
137
+
138
+ context 'with a regexp' do
139
+ let(:example) { /\w+/ }
140
+
141
+
142
+ it 'calls #gen on the regexp' do
143
+ example.should_receive(:gen).and_call_original
144
+ subject.example.should =~ example
145
+ end
146
+
147
+ context 'for a type with a non-String native_type' do
148
+ let(:type) { IntegerAttributeType}
149
+ let(:example) { /\d{5}/ }
150
+ it 'coerces the example value properly' do
151
+ example.should_receive(:gen).and_call_original
152
+ type.should_receive(:load).and_call_original
153
+
154
+ subject.example.should be_kind_of(type.native_type)
155
+ end
156
+ end
157
+ end
158
+
159
+
160
+ context 'with a proc' do
161
+ context 'with one argument' do
162
+ let(:example) { lambda { |obj| 'ok' } }
163
+ let(:some_object) { Object.new }
164
+
165
+ before do
166
+ example.should_receive(:call).with(some_object).and_call_original
167
+ end
168
+
169
+ it 'passes any given parent through to the example proc' do
170
+ subject.example(nil, parent: some_object).should == 'ok'
171
+ end
172
+ end
173
+
174
+ context 'with two arguments' do
175
+ let(:example) { lambda { |obj, context| "#{context} ok" } }
176
+ let(:some_object) { Object.new }
177
+ let(:some_context) { ['some_context'] }
178
+
179
+ before do
180
+ example.should_receive(:call).with(some_object, some_context).and_call_original
181
+ end
182
+
183
+ it 'passes any given parent through to the example proc' do
184
+ subject.example(some_context, parent: some_object).should == "#{some_context} ok"
185
+ end
186
+ end
187
+
188
+ end
189
+
190
+ context 'with an array' do
191
+ let(:example) { ["one", "two"] }
192
+ it 'picks a random value' do
193
+ example.should include subject.example
194
+ end
195
+ end
196
+
197
+ context 'with an attribute that has the values option set' do
198
+ let(:values) { ["one", "two"] }
199
+ let(:attribute_options) { {:values => values} }
200
+ it 'picks a random value' do
201
+ values.should include subject.example
202
+ end
203
+
204
+ end
205
+
206
+ context 'deterministic examples' do
207
+ let(:example) { /\w+/ }
208
+ it 'can take a context to pre-seed the random number generator' do
209
+ example_1 = subject.example(['context'])
210
+ example_2 = subject.example(['context'])
211
+
212
+ example_1.should eq example_2
213
+ end
214
+
215
+ it 'can take a context to pre-seed the random number generator' do
216
+ example_1 = subject.example(['context'])
217
+ example_2 = subject.example(['different context'])
218
+
219
+ example_1.should_not eq example_2
220
+ end
221
+
222
+ end
223
+ end
224
+
225
+ context 'load' do
226
+ let(:context){ ['context'] }
227
+ let(:value) { '1' }
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
+ it 'delegates to type.load' do
235
+ type.should_receive(:load).with(value,context, {})
236
+ attribute.load(value,context)
237
+ end
238
+
239
+ it 'passes options to type.load' do
240
+ type.should_receive(:load).with(value, context, foo: 'bar')
241
+ attribute.load(value, context, foo: 'bar')
242
+ end
243
+
244
+ context 'applying default values' do
245
+ let(:default_value) { "default value" }
246
+ let(:attribute_options) { {:default => default_value} }
247
+
248
+ subject(:result) { attribute.load(value) }
249
+
250
+ context 'for nil' do
251
+ let(:value) { nil }
252
+ it { should == default_value}
253
+ end
254
+
255
+
256
+ context 'for a value that the type loads as nil' do
257
+ let(:value) { "not nil"}
258
+ before do
259
+ type.should_receive(:load).and_return(nil)
260
+ end
261
+ it { should == default_value}
262
+ end
263
+
264
+ end
265
+
266
+ context 'validating a value' do
267
+
268
+ context '#validate' do
269
+ context 'applying attribute options' do
270
+ context ':required' do
271
+ let(:attribute_options) { {:required => true} }
272
+ context 'with a nil value' do
273
+ let(:value) { nil }
274
+ it 'returns an error' do
275
+ attribute.validate(value, context).first.should == 'Attribute context is required'
276
+ end
277
+ end
278
+ end
279
+
280
+ context ':values' do
281
+ let(:values) { ['one','two'] }
282
+ let(:attribute_options) { {:values => values} }
283
+ let(:value) { nil }
284
+
285
+ subject(:errors) { attribute.validate(value, context)}
286
+
287
+ context 'with a value that is allowed' do
288
+ let(:value) { "one" }
289
+ it 'returns no errors' do
290
+ errors.should be_empty
291
+ end
292
+ end
293
+
294
+ context 'with a value that is not allowed' do
295
+ let(:value) { "three" }
296
+ it 'returns an error indicating the problem' do
297
+ errors.first.should =~ /is not within the allowed values/
298
+ end
299
+
300
+ end
301
+ end
302
+
303
+
304
+ end
305
+
306
+ it 'calls the right validate_X methods?' do
307
+ attribute.should_receive(:validate_type).with(value, context).and_call_original
308
+ attribute.should_not_receive(:validate_dependency)
309
+ type.should_receive(:validate).and_call_original
310
+ attribute.validate(value, context)
311
+ end
312
+
313
+ end
314
+
315
+ context '#validate_type' do
316
+ subject(:errors) { attribute.validate_type(value, context)}
317
+
318
+ context 'with a value of the right type' do
319
+ let(:value) { "one" }
320
+ it 'returns no errors' do
321
+ errors.should be_empty
322
+ end
323
+ end
324
+
325
+ context 'with a value of a value different than the native_type' do
326
+ let(:value) { 1 }
327
+
328
+ it 'returns errors' do
329
+ errors.should_not be_empty
330
+ errors.first.should =~ /is of the wrong type/
331
+ end
332
+
333
+ end
334
+
335
+
336
+ end
337
+
338
+ context '#validate_missing_value' do
339
+ let(:key) { "$.instance.ssh_key.name" }
340
+ let(:value) { /\w+/.gen }
341
+
342
+ let(:attribute_options) { {:required_if => key} }
343
+
344
+ let(:ssh_key) { double("ssh_key", :name => value) }
345
+ let(:instance) { double("instance", :ssh_key => ssh_key) }
346
+
347
+ before { Attributor::AttributeResolver.current.register('instance', instance) }
348
+
349
+ let(:attribute_context) { ['$','params','key_material'] }
350
+ subject(:errors) { attribute.validate_missing_value(attribute_context) }
351
+
352
+
353
+ context 'for a simple dependency without a predicate' do
354
+ context 'that is satisfied' do
355
+ it { should_not be_empty }
356
+ end
357
+
358
+ context 'that is missing' do
359
+ let(:value) { nil }
360
+ it { should be_empty }
361
+ end
362
+ end
363
+
364
+ context 'with a dependency that has a predicate' do
365
+ let(:value) { "default_ssh_key_name" }
366
+ #subject(:errors) { attribute.validate_missing_value('') }
367
+
368
+ context 'where the target attribute exists, and matches the predicate' do
369
+ let(:attribute_options) { {:required_if => {key => /default/} } }
370
+
371
+ it { should_not be_empty }
372
+
373
+ its(:first) { should =~ /Attribute #{Regexp.quote(Attributor.humanize_context( attribute_context ))} is required when #{Regexp.quote(key)} matches/ }
374
+ end
375
+
376
+ context 'where the target attribute exists, but does not match the predicate' do
377
+ let(:attribute_options) { {:required_if => {key => /other/} } }
378
+
379
+ it { should be_empty }
380
+ end
381
+
382
+ context 'where the target attribute does not exist' do
383
+ let(:attribute_options) { {:required_if => {key => /default/} } }
384
+ let(:ssh_key) { double("ssh_key", :name => nil) }
385
+
386
+ it { should be_empty }
387
+ end
388
+ end
389
+
390
+ end
391
+
392
+ end
393
+
394
+
395
+ context 'for an attribute for a subclass of Model' do
396
+ let(:type) { Chicken }
397
+ let(:type_options) { Chicken.options }
398
+
399
+ subject(:attribute) { Attributor::Attribute.new(type, attribute_options) }
400
+
401
+ it 'has attributes' do
402
+ attribute.attributes.should == type.attributes
403
+ end
404
+
405
+ #it 'has compiled_definition' do
406
+ # attribute.compiled_definition.should == type.definition
407
+ #end
408
+
409
+
410
+ it 'merges its options with those of the compiled_definition' do
411
+ attribute.options.should == attribute_options.merge(type_options)
412
+ end
413
+
414
+ it 'describe handles sub-attributes nicely' do
415
+ describe = attribute.describe(false)
416
+
417
+ describe[:type][:name].should == type.name
418
+ common_options = attribute_options.select{|k,v| Attributor::Attribute.TOP_LEVEL_OPTIONS.include? k }
419
+ special_options = attribute_options.reject{|k,v| Attributor::Attribute.TOP_LEVEL_OPTIONS.include? k }
420
+ common_options.each do |k,v|
421
+ describe[k].should == v
422
+ end
423
+ special_options.each do |k,v|
424
+ describe[:options][k].should == v
425
+ end
426
+ type_options.each do |k,v|
427
+ describe[:options][k].should == v
428
+ end
429
+
430
+
431
+ attribute.attributes.each do |name, attr|
432
+ describe[:type][:attributes].should have_key(name)
433
+ end
434
+
435
+ end
436
+
437
+ it 'supports deterministic examples' do
438
+ example_1 = attribute.example(["Chicken context"])
439
+ example_2 = attribute.example(["Chicken context"])
440
+
441
+ example_1.attributes.should eq(example_2.attributes)
442
+ end
443
+
444
+ context '#validate' do
445
+ let(:chicken) { Chicken.example }
446
+ let(:type_attributes) { type.attributes }
447
+
448
+ it 'validates sub-attributes' do
449
+ errors = attribute.validate(chicken)
450
+ errors.should be_empty
451
+ end
452
+
453
+ context 'with a failing validation' do
454
+ subject(:chicken) { Chicken.example(age: 150, email: "foo") }
455
+ let(:email_validation_response) { ["$.email value \(#{chicken.email}\) does not match regexp (/@/)"] }
456
+ let(:age_validation_response) { ["$.age value \(#{chicken.age}\) is larger than the allowed max (120)"] }
457
+
458
+ it 'collects sub-attribute validation errors' do
459
+ errors = attribute.validate(chicken)
460
+ errors.should =~ (age_validation_response | email_validation_response)
461
+ end
462
+ end
463
+
464
+ end
465
+
466
+
467
+ context '#validate_missing_value' do
468
+ let(:type) { Duck }
469
+ let(:attribute_name) { nil }
470
+ let(:attribute) { Duck.attributes[attribute_name] }
471
+
472
+ let(:attribute_context) { ['$','duck',"#{attribute_name}"] }
473
+ subject(:errors) { attribute.validate_missing_value(attribute_context) }
474
+
475
+ before do
476
+ Attributor::AttributeResolver.current.register('duck', duck)
477
+ end
478
+
479
+ context 'for a dependency with no predicate' do
480
+ let(:attribute_name) { :email }
481
+
482
+ let(:duck) do
483
+ d = Duck.new
484
+ d.age = 1
485
+ d.name = 'Donald'
486
+ d
487
+ end
488
+
489
+ context 'where the target attribute exists, and matches the predicate' do
490
+ it { should_not be_empty }
491
+ its(:first) { should == "Attribute $.duck.email is required when name (for $.duck) is present." }
492
+ end
493
+ context 'where the target attribute does not exist' do
494
+ before do
495
+ duck.name = nil
496
+ end
497
+ it { should be_empty }
498
+ end
499
+ end
500
+
501
+
502
+ context 'for a dependency with a predicate' do
503
+ let(:attribute_name) { :age }
504
+
505
+ let(:duck) do
506
+ d = Duck.new
507
+ d.name = 'Daffy'
508
+ d.email = 'daffy@darkwing.uoregon.edu' # he's a duck,get it?
509
+ d
510
+ end
511
+
512
+ context 'where the target attribute exists, and matches the predicate' do
513
+ it { should_not be_empty }
514
+ its(:first) { should =~ /Attribute #{Regexp.quote('$.duck.age')} is required when name #{Regexp.quote('(for $.duck)')} matches/ }
515
+ end
516
+
517
+ context 'where the target attribute exists, and does not match the predicate' do
518
+ before do
519
+ duck.name = 'Donald'
520
+ end
521
+ it { should be_empty }
522
+ end
523
+
524
+ context 'where the target attribute does not exist' do
525
+ before do
526
+ duck.name = nil
527
+ end
528
+ it { should be_empty }
529
+ end
530
+
531
+ end
532
+
533
+ end
534
+
535
+ end
536
+ end
537
+
538
+ context 'for a Collection' do
539
+ context 'of non-Model (or Struct) type' do
540
+ let(:member_type) { Attributor::Integer }
541
+ let(:type) { Attributor::Collection.of(member_type)}
542
+ let(:member_options) { {:max => 10} }
543
+ let(:attribute_options) { {:member_options => member_options} }
544
+
545
+ context 'the member_attribute of that type' do
546
+ subject(:member_attribute) { attribute.type.member_attribute }
547
+
548
+ it { should be_kind_of(Attributor::Attribute)}
549
+ its(:type) { should be(member_type) }
550
+ its(:options) { should eq(member_options) }
551
+ end
552
+
553
+ context "working with members" do
554
+ let(:values) { ['1',2,12] }
555
+
556
+ it 'loads' do
557
+ attribute.load(values).should =~ [1,2,12]
558
+ end
559
+
560
+ it 'validates' do
561
+ errors = attribute.validate(values)
562
+ errors.should_not be_empty
563
+ errors[0].should =~ /of the wrong type/
564
+ errors[1].should =~ /value \(12\) is larger/
565
+ end
566
+
567
+
568
+ end
569
+
570
+
571
+ end
572
+
573
+ context 'of a Model (or Struct) type' do
574
+ subject(:attribute) { Attributor::Attribute.new(type, attribute_options, &attribute_block) }
575
+
576
+ let(:attribute_block) { Proc.new{ attribute :angry , required: true } }
577
+ let(:attribute_options) { {reference: Chicken, member_options: member_options} }
578
+ let(:member_type) { Attributor::Struct }
579
+ let(:type) { Attributor::Collection.of(member_type) }
580
+ let(:member_options) { {} }
581
+
582
+
583
+ context 'the member_attribute of that type' do
584
+ subject(:member_attribute) { attribute.type.member_attribute }
585
+ it { should be_kind_of(Attributor::Attribute)}
586
+ its(:options) { should eq(member_options.merge(reference: Chicken, identity: :email)) }
587
+ its(:attributes) { should have_key :angry }
588
+ it 'inherited the type and options from the reference' do
589
+ member_attribute.attributes[:angry].type.should be(Chicken.attributes[:angry].type)
590
+ member_attribute.attributes[:angry].options.should eq(Chicken.attributes[:angry].options.merge(required: true))
591
+ end
592
+ end
593
+
594
+ end
595
+ end
596
+
597
+ end