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