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,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