attributor 5.5 → 6.1
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 +5 -5
- data/CHANGELOG.md +17 -0
- data/lib/attributor/attribute.rb +67 -79
- data/lib/attributor/hash_dsl_compiler.rb +6 -5
- data/lib/attributor/type.rb +3 -0
- data/lib/attributor/types/collection.rb +4 -1
- data/lib/attributor/types/hash.rb +24 -16
- data/lib/attributor/types/model.rb +9 -21
- data/lib/attributor/version.rb +1 -1
- data/lib/attributor.rb +2 -6
- data/spec/attribute_spec.rb +114 -121
- data/spec/hash_dsl_compiler_spec.rb +5 -5
- data/spec/spec_helper.rb +0 -2
- data/spec/support/models.rb +7 -7
- data/spec/types/hash_spec.rb +63 -22
- data/spec/types/model_spec.rb +7 -1
- metadata +3 -7
- data/lib/attributor/attribute_resolver.rb +0 -111
- data/spec/attribute_resolver_spec.rb +0 -237
@@ -1,111 +0,0 @@
|
|
1
|
-
require 'ostruct'
|
2
|
-
|
3
|
-
module Attributor
|
4
|
-
class AttributeResolver
|
5
|
-
ROOT_PREFIX = '$'.freeze
|
6
|
-
COLLECTION_INDEX_KEY = /^at\((\d+)\)$/
|
7
|
-
|
8
|
-
class Data < ::Hash
|
9
|
-
include Hashie::Extensions::MethodReader
|
10
|
-
end
|
11
|
-
|
12
|
-
attr_reader :data
|
13
|
-
|
14
|
-
def initialize
|
15
|
-
@data = Data.new
|
16
|
-
end
|
17
|
-
|
18
|
-
def query!(key_path, path_prefix = ROOT_PREFIX)
|
19
|
-
# If the incoming key_path is not an absolute path, append the given prefix
|
20
|
-
# NOTE: Need to index key_path by range here because Ruby 1.8 returns a
|
21
|
-
# FixNum for the ASCII code, not the actual character, when indexing by a number.
|
22
|
-
unless key_path[0..0] == ROOT_PREFIX
|
23
|
-
# TODO: prepend path_prefix to path_prefix if it did not include it? hm.
|
24
|
-
key_path = path_prefix + SEPARATOR + key_path
|
25
|
-
end
|
26
|
-
|
27
|
-
# Discard the initial element, which should always be ROOT_PREFIX at this point
|
28
|
-
_root, *path = key_path.split(SEPARATOR)
|
29
|
-
|
30
|
-
# Follow the hierarchy path to the requested node and return it:
|
31
|
-
# Example path => ["instance", "ssh_key", "name"]
|
32
|
-
# Example @data => {"instance" => { "ssh_key" => { "name" => "foobar" } }}
|
33
|
-
#
|
34
|
-
# at(n) is a collection index:
|
35
|
-
# Example path => ["filters", "at(0)", "type"]
|
36
|
-
# Example data => {"filters" => [{ "type" => "instance:tag" }]}
|
37
|
-
#
|
38
|
-
result = path.inject(@data) do |hash, key|
|
39
|
-
return nil if hash.nil?
|
40
|
-
if (match = key.match(COLLECTION_INDEX_KEY))
|
41
|
-
hash[match[1].to_i]
|
42
|
-
else
|
43
|
-
hash.send key
|
44
|
-
end
|
45
|
-
end
|
46
|
-
result
|
47
|
-
end
|
48
|
-
|
49
|
-
# Query for a certain key in the attribute hierarchy
|
50
|
-
#
|
51
|
-
# @param [String] key_path The name of the key to query and its path
|
52
|
-
# @param [String] path_prefix
|
53
|
-
#
|
54
|
-
# @return [String] The value of the specified attribute/key
|
55
|
-
#
|
56
|
-
def query(key_path, path_prefix = ROOT_PREFIX)
|
57
|
-
query!(key_path, path_prefix)
|
58
|
-
rescue NoMethodError
|
59
|
-
nil
|
60
|
-
end
|
61
|
-
|
62
|
-
def register(key_path, value)
|
63
|
-
if key_path.split(SEPARATOR).size > 1
|
64
|
-
raise AttributorException, "can only register top-level attributes. got: #{key_path}"
|
65
|
-
end
|
66
|
-
|
67
|
-
@data[key_path] = value
|
68
|
-
end
|
69
|
-
|
70
|
-
# Checks that the the condition is met. This means the attribute identified
|
71
|
-
# by path_prefix and key_path satisfies the optional predicate, which when
|
72
|
-
# nil simply checks for existence.
|
73
|
-
#
|
74
|
-
# @param path_prefix [String]
|
75
|
-
# @param key_path [String]
|
76
|
-
# @param predicate [String|Regexp|Proc|NilClass]
|
77
|
-
#
|
78
|
-
# @returns [Boolean] True if :required_if condition is met, false otherwise
|
79
|
-
#
|
80
|
-
# @raise [AttributorException] When an unsupported predicate is passed
|
81
|
-
#
|
82
|
-
def check(path_prefix, key_path, predicate = nil)
|
83
|
-
value = query(key_path, path_prefix)
|
84
|
-
|
85
|
-
# we have a value, any value, which is good enough given no predicate
|
86
|
-
return true if !value.nil? && predicate.nil?
|
87
|
-
|
88
|
-
case predicate
|
89
|
-
when ::String, ::Regexp, ::Integer, ::Float, ::DateTime, true, false
|
90
|
-
return predicate === value
|
91
|
-
when ::Proc
|
92
|
-
# Cannot use === here as above due to different behavior in Ruby 1.8
|
93
|
-
return predicate.call(value)
|
94
|
-
when nil
|
95
|
-
return !value.nil?
|
96
|
-
else
|
97
|
-
raise AttributorException, "predicate not supported: #{predicate.inspect}"
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
# TODO: kill this when we also kill Taylor's IdentityMap.current
|
102
|
-
def self.current=(resolver)
|
103
|
-
Thread.current[:_attributor_attribute_resolver] = resolver
|
104
|
-
end
|
105
|
-
|
106
|
-
def self.current
|
107
|
-
raise AttributorException, 'No AttributeResolver set.' unless Thread.current[:_attributor_attribute_resolver]
|
108
|
-
Thread.current[:_attributor_attribute_resolver]
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
@@ -1,237 +0,0 @@
|
|
1
|
-
require File.join(File.dirname(__FILE__), 'spec_helper.rb')
|
2
|
-
|
3
|
-
describe Attributor::AttributeResolver do
|
4
|
-
let(:value) { /\w+/.gen }
|
5
|
-
|
6
|
-
context 'registering and querying simple values' do
|
7
|
-
let(:name) { 'string_value' }
|
8
|
-
before { subject.register(name, value) }
|
9
|
-
|
10
|
-
it 'works' do
|
11
|
-
expect(subject.query(name)).to be value
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
context 'querying and registering nested values' do
|
16
|
-
let(:one) { double(two: value) }
|
17
|
-
let(:key) { 'one.two' }
|
18
|
-
before { subject.register('one', one) }
|
19
|
-
|
20
|
-
it 'works' do
|
21
|
-
expect(subject.query(key)).to be value
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
context 'querying nested values from models' do
|
26
|
-
let(:instance) { double('instance', ssh_key: ssh_key) }
|
27
|
-
let(:ssh_key) { double('ssh_key', name: value) }
|
28
|
-
let(:key) { 'instance.ssh_key.name' }
|
29
|
-
|
30
|
-
before { subject.register('instance', instance) }
|
31
|
-
|
32
|
-
it 'works' do
|
33
|
-
expect(subject.query('instance')).to be instance
|
34
|
-
expect(subject.query('instance.ssh_key')).to be ssh_key
|
35
|
-
expect(subject.query(key)).to be value
|
36
|
-
end
|
37
|
-
|
38
|
-
context 'with a prefix' do
|
39
|
-
let(:key) { 'name' }
|
40
|
-
let(:prefix) { '$.instance.ssh_key' }
|
41
|
-
let(:value) { 'some_name' }
|
42
|
-
it 'works' do
|
43
|
-
expect(subject.query(key, prefix)).to be(value)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
context 'querying values that do not exist' do
|
49
|
-
context 'for a straight key' do
|
50
|
-
let(:key) { 'missing' }
|
51
|
-
it 'returns nil' do
|
52
|
-
expect(subject.query(key)).to be_nil
|
53
|
-
end
|
54
|
-
end
|
55
|
-
context 'for a nested key' do
|
56
|
-
let(:key) { 'nested.missing' }
|
57
|
-
it 'returns nil' do
|
58
|
-
expect(subject.query(key)).to be_nil
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
context 'querying collection indices from models' do
|
64
|
-
let(:instances) { [instance1, instance2] }
|
65
|
-
let(:instance1) { double('instance1', ssh_key: ssh_key1) }
|
66
|
-
let(:instance2) { double('instance2', ssh_key: ssh_key2) }
|
67
|
-
let(:ssh_key1) { double('ssh_key', name: value) }
|
68
|
-
let(:ssh_key2) { double('ssh_key', name: 'second') }
|
69
|
-
let(:args) { [path, prefix].compact }
|
70
|
-
|
71
|
-
before { subject.register('instances', instances) }
|
72
|
-
|
73
|
-
it 'resolves the index to the correct member of the collection' do
|
74
|
-
expect(subject.query('instances')).to be instances
|
75
|
-
expect(subject.query('instances.at(1).ssh_key')).to be ssh_key2
|
76
|
-
expect(subject.query('instances.at(0).ssh_key.name')).to be value
|
77
|
-
end
|
78
|
-
|
79
|
-
it 'returns nil for index out of range' do
|
80
|
-
expect(subject.query('instances.at(2)')).to be(nil)
|
81
|
-
expect(subject.query('instances.at(-1)')).to be(nil)
|
82
|
-
end
|
83
|
-
|
84
|
-
context 'with a prefix' do
|
85
|
-
let(:key) { 'name' }
|
86
|
-
let(:prefix) { '$.instances.at(0).ssh_key' }
|
87
|
-
let(:value) { 'some_name' }
|
88
|
-
|
89
|
-
it 'resolves the index to the correct member of the collection' do
|
90
|
-
expect(subject.query(key, prefix)).to be(value)
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
context 'checking attribute conditions' do
|
96
|
-
let(:key) { 'instance.ssh_key.name' }
|
97
|
-
let(:ssh_key) { double('ssh_key', name: value) }
|
98
|
-
let(:instance_id) { 123 }
|
99
|
-
let(:instance) { double('instance', ssh_key: ssh_key, id: instance_id) }
|
100
|
-
|
101
|
-
let(:context) { '$' }
|
102
|
-
|
103
|
-
before { subject.register('instance', instance) }
|
104
|
-
|
105
|
-
let(:present_key) { key }
|
106
|
-
let(:missing_key) { 'instance.ssh_key.something_else' }
|
107
|
-
|
108
|
-
context 'with no condition' do
|
109
|
-
let(:condition) { nil }
|
110
|
-
before { expect(ssh_key).to receive(:something_else).and_return(nil) }
|
111
|
-
it 'works' do
|
112
|
-
expect(subject.check(context, present_key, condition)).to be true
|
113
|
-
expect(subject.check(context, missing_key, condition)).to be false
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
context 'with a string condition' do
|
118
|
-
let(:passing_condition) { value }
|
119
|
-
let(:failing_condition) { /\w+/.gen }
|
120
|
-
|
121
|
-
it 'works' do
|
122
|
-
expect(subject.check(context, key, passing_condition)).to be true
|
123
|
-
expect(subject.check(context, key, failing_condition)).to be false
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
context 'with a regex condition' do
|
128
|
-
let(:passing_condition) { /\w+/ }
|
129
|
-
let(:failing_condition) { /\d+/ }
|
130
|
-
|
131
|
-
it 'works' do
|
132
|
-
expect(subject.check(context, key, passing_condition)).to be true
|
133
|
-
expect(subject.check(context, key, failing_condition)).to be false
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
context 'with an integer condition' do
|
138
|
-
let(:key) { 'instance.id' }
|
139
|
-
let(:passing_condition) { instance_id }
|
140
|
-
let(:failing_condition) { /\w+/.gen }
|
141
|
-
|
142
|
-
it 'works' do
|
143
|
-
expect(subject.check(context, key, passing_condition)).to be true
|
144
|
-
expect(subject.check(context, key, failing_condition)).to be false
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
skip 'with a hash condition' do
|
149
|
-
end
|
150
|
-
|
151
|
-
context 'with a proc condition' do
|
152
|
-
let(:passing_condition) { proc { |test_value| test_value == value } }
|
153
|
-
let(:failing_condition) { proc { |test_value| test_value != value } }
|
154
|
-
|
155
|
-
it 'works' do
|
156
|
-
expect(subject.check(context, key, passing_condition)).to eq(true)
|
157
|
-
expect(subject.check(context, key, failing_condition)).to eq(false)
|
158
|
-
end
|
159
|
-
end
|
160
|
-
|
161
|
-
context 'with an unsupported condition type' do
|
162
|
-
let(:condition) { double('weird condition type') }
|
163
|
-
it 'raises an error' do
|
164
|
-
expect { subject.check(context, present_key, condition) }.to raise_error(Attributor::AttributorException)
|
165
|
-
end
|
166
|
-
end
|
167
|
-
|
168
|
-
context 'with a condition that asserts something IS nil' do
|
169
|
-
let(:ssh_key) { double('ssh_key', name: nil) }
|
170
|
-
it 'can be done using the almighty Proc' do
|
171
|
-
cond = proc { |value| !value.nil? }
|
172
|
-
expect(subject.check(context, key, cond)).to be false
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
context 'with a relative path' do
|
177
|
-
let(:context) { '$.instance.ssh_key' }
|
178
|
-
let(:key) { 'name' }
|
179
|
-
|
180
|
-
it 'works' do
|
181
|
-
expect(subject.check(context, key, value)).to be true
|
182
|
-
end
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
# context 'with context stuff...' do
|
187
|
-
|
188
|
-
# let(:ssh_key) { double("ssh_key", name:value) }
|
189
|
-
# let(:instance) { double("instance", ssh_key:ssh_key) }
|
190
|
-
|
191
|
-
# let(:key) { "ssh_key.name" }
|
192
|
-
# let(:key) { "$.payload" }
|
193
|
-
# let(:key) { "ssh_key.name" } # no $ == current object
|
194
|
-
# let(:key) { "@.ssh_key" } # @ is current object
|
195
|
-
|
196
|
-
# before { subject.register('instance', instance) }
|
197
|
-
|
198
|
-
# it 'works?' do
|
199
|
-
# # check dependency for 'instance'
|
200
|
-
# resolver.with 'instance' do |res|
|
201
|
-
# res.check(key)
|
202
|
-
# '$.payload'
|
203
|
-
# end
|
204
|
-
|
205
|
-
# end
|
206
|
-
|
207
|
-
# end
|
208
|
-
|
209
|
-
# context 'integration with attributes that have sub-attributes' do
|
210
|
-
# when you start to parse... do you set the root in the resolver?
|
211
|
-
# end
|
212
|
-
#
|
213
|
-
# context 'actually using the thing' do
|
214
|
-
|
215
|
-
# # we'll always want to add... right? never really remove?
|
216
|
-
# # at least not remove for the duration of a given resolver...
|
217
|
-
# # which will last for one request.
|
218
|
-
# #
|
219
|
-
# # could the resolver be an identity-map of sorts for the request?
|
220
|
-
# # how much overlap is there in there?
|
221
|
-
# #
|
222
|
-
# #
|
223
|
-
|
224
|
-
# it 'is really actually quite useful' do
|
225
|
-
# #attribute = Attributor::Attribute.new ::String, required_if: { "instance.ssh_key.name" : Proc.new { |value| value.nil? } }
|
226
|
-
|
227
|
-
# resolver = Attributor::AttributeResolver.new
|
228
|
-
|
229
|
-
# resolver.register '$.parsed_params', parsed_params
|
230
|
-
# resolver.register '$.payload', payload
|
231
|
-
|
232
|
-
# resolver.query '$.parsed_params.account_id'
|
233
|
-
|
234
|
-
# end
|
235
|
-
|
236
|
-
# end
|
237
|
-
end
|