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.
@@ -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