right_cloud_api_base 0.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/HISTORY +2 -0
- data/LICENSE +19 -0
- data/README.md +14 -0
- data/Rakefile +37 -0
- data/lib/base/api_manager.rb +707 -0
- data/lib/base/helpers/cloud_api_logger.rb +214 -0
- data/lib/base/helpers/http_headers.rb +239 -0
- data/lib/base/helpers/http_parent.rb +103 -0
- data/lib/base/helpers/http_request.rb +173 -0
- data/lib/base/helpers/http_response.rb +122 -0
- data/lib/base/helpers/net_http_patch.rb +31 -0
- data/lib/base/helpers/query_api_patterns.rb +862 -0
- data/lib/base/helpers/support.rb +270 -0
- data/lib/base/helpers/support.xml.rb +306 -0
- data/lib/base/helpers/utils.rb +380 -0
- data/lib/base/manager.rb +122 -0
- data/lib/base/parsers/json.rb +38 -0
- data/lib/base/parsers/plain.rb +36 -0
- data/lib/base/parsers/rexml.rb +83 -0
- data/lib/base/parsers/sax.rb +200 -0
- data/lib/base/routines/cache_validator.rb +184 -0
- data/lib/base/routines/connection_proxies/net_http_persistent_proxy.rb +194 -0
- data/lib/base/routines/connection_proxies/right_http_connection_proxy.rb +224 -0
- data/lib/base/routines/connection_proxy.rb +66 -0
- data/lib/base/routines/request_analyzer.rb +122 -0
- data/lib/base/routines/request_generator.rb +48 -0
- data/lib/base/routines/request_initializer.rb +52 -0
- data/lib/base/routines/response_analyzer.rb +152 -0
- data/lib/base/routines/response_parser.rb +79 -0
- data/lib/base/routines/result_wrapper.rb +75 -0
- data/lib/base/routines/retry_manager.rb +106 -0
- data/lib/base/routines/routine.rb +98 -0
- data/lib/right_cloud_api_base.rb +72 -0
- data/lib/right_cloud_api_base_version.rb +37 -0
- data/right_cloud_api_base.gemspec +63 -0
- data/spec/helpers/query_api_pattern_spec.rb +312 -0
- data/spec/helpers/support_spec.rb +211 -0
- data/spec/helpers/support_xml_spec.rb +207 -0
- data/spec/helpers/utils_spec.rb +179 -0
- data/spec/routines/connection_proxies/test_net_http_persistent_proxy_spec.rb +143 -0
- data/spec/routines/test_cache_validator_spec.rb +152 -0
- data/spec/routines/test_connection_proxy_spec.rb +44 -0
- data/spec/routines/test_request_analyzer_spec.rb +106 -0
- data/spec/routines/test_response_analyzer_spec.rb +132 -0
- data/spec/routines/test_response_parser_spec.rb +228 -0
- data/spec/routines/test_result_wrapper_spec.rb +63 -0
- data/spec/routines/test_retry_manager_spec.rb +84 -0
- data/spec/spec_helper.rb +15 -0
- metadata +215 -0
@@ -0,0 +1,862 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2013 RightScale, Inc.
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# 'Software'), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
18
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
module RightScale
|
25
|
+
module CloudApi
|
26
|
+
|
27
|
+
# Mixins namespace
|
28
|
+
#
|
29
|
+
# @api public
|
30
|
+
#
|
31
|
+
module Mixin
|
32
|
+
|
33
|
+
# Query API namespace
|
34
|
+
module QueryApiPatterns
|
35
|
+
|
36
|
+
# Standard included
|
37
|
+
#
|
38
|
+
# @return [void]
|
39
|
+
# @example
|
40
|
+
# # no example
|
41
|
+
#
|
42
|
+
def self.included(base)
|
43
|
+
base.extend(ClassMethods)
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
# Query API patterns help one to simulate the Query API type through the REST API
|
48
|
+
#
|
49
|
+
# When the REST API is powerfull enough it is not easy to code it becaue one have to worry
|
50
|
+
# about the path, the URL parameters, the headers and the body, when in the QUERY API
|
51
|
+
# all you need to worry about are the URL parameters.
|
52
|
+
#
|
53
|
+
# The patterns described below help you to build methods that will take a linear set of
|
54
|
+
# parameters (usially) a hash and put then into the proper positions into the URL, headers or
|
55
|
+
# body.
|
56
|
+
#
|
57
|
+
# TODO :add an example that would compare REST vs QUERY calls
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
# # Add a QUERY methods pattern:
|
61
|
+
#
|
62
|
+
# query_api_pattern 'MethodName', :verb, 'path', UnifiedParams+:params+:headers+:body+:options+:before+:after do |args|
|
63
|
+
# puts args # where args is a Hash: { :verb, :path, :opts, :manager }
|
64
|
+
# ...
|
65
|
+
# return args # where args is a Hash: { :verb, :path, :opts [, :manager] }
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# There are 2 ways to define a Query API pattern:
|
69
|
+
#
|
70
|
+
# 1. Manager class level:
|
71
|
+
# We could use this when we define a new cloud handler. I dont see any
|
72
|
+
# use case right now because we can implement all we need now using the
|
73
|
+
# second way and Wrappers.
|
74
|
+
#
|
75
|
+
# @example
|
76
|
+
# module MyCoolCloud
|
77
|
+
# class ApiManager < CloudApi::ApiManager
|
78
|
+
# query_api_pattern 'ListBuckets', :get
|
79
|
+
#
|
80
|
+
# query_api_pattern 'PutObject', :put, '{:Bucket}/{:Object}',
|
81
|
+
# :body => Utils::MUST_BE_SET,
|
82
|
+
# :headers => { 'content-type' => ['application/octet-stream'] }
|
83
|
+
# ..
|
84
|
+
# end
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# 2. Manager instance level: this is where Wrappers come.
|
88
|
+
#
|
89
|
+
# @example
|
90
|
+
# module MyCoolCloud
|
91
|
+
# module API_DEFAULT
|
92
|
+
# def self.extended(base)
|
93
|
+
# base.query_api_pattern 'ListBuckets', :get
|
94
|
+
#
|
95
|
+
# base.query_api_pattern 'ListMyCoolBucket', :get do |args|
|
96
|
+
# args[:path] = 'my-cool-bucket'
|
97
|
+
# args
|
98
|
+
# end
|
99
|
+
#
|
100
|
+
# base.query_api_pattern 'PutObject', :put, '{:Bucket:}/{:Object:}',
|
101
|
+
# :body => Utils::MUST_BE_SET,
|
102
|
+
# :headers => { 'content-type' => ['application/octet-stream'] }
|
103
|
+
#
|
104
|
+
# base.query_api_pattern 'UploadPartCopy', :put,'{:DestinationBucket}/{:DestinationObject}',
|
105
|
+
# :params => { 'partNumber' => :PartNumber, 'uploadId' => :UploadId },
|
106
|
+
# :headers => { 'x-amz-copy-source' => '{:SourceBucket}/{:SourceObject}' }
|
107
|
+
# ..
|
108
|
+
# end
|
109
|
+
# end
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# @example
|
113
|
+
# Use case examples:
|
114
|
+
# s3.MethodName(UnifiedParams+:params+:headers+:body+:options+:path+:verb)
|
115
|
+
#
|
116
|
+
# @example
|
117
|
+
# # List all buckets
|
118
|
+
# s3.ListBuckets
|
119
|
+
#
|
120
|
+
# @example
|
121
|
+
# # Put object binary
|
122
|
+
# s3.PutObject({'Bucket' => 'xxx', 'Object => 'yyy'}, :body => 'Hahaha')
|
123
|
+
#
|
124
|
+
# @example
|
125
|
+
# # UploadCopy
|
126
|
+
# s3.UploadPartCopy( 'SourceBucket' => 'xxx',
|
127
|
+
# 'SourceObject => 'yyy',
|
128
|
+
# 'DestinationBucket' => 'aaa',
|
129
|
+
# 'DestinationObject' => 'bbb',
|
130
|
+
# 'PartNumber' => 134,
|
131
|
+
# 'UploadId' => '111111',
|
132
|
+
# 'foo_param' => 'foo',
|
133
|
+
# 'bar_param' => 'bar' )
|
134
|
+
#
|
135
|
+
module ClassMethods
|
136
|
+
|
137
|
+
# The method returns a list of pattternd defined in the current class
|
138
|
+
#
|
139
|
+
# @return [Array] The arrays of patterns.
|
140
|
+
# @example
|
141
|
+
# # no example
|
142
|
+
#
|
143
|
+
def query_api_patterns
|
144
|
+
@query_api_patterns ||= {}
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
# Defines a new query pattern
|
149
|
+
#
|
150
|
+
# @param [String] method_name The name of the new QUERY-like method;
|
151
|
+
# @param [String] verb The HTTP verb.
|
152
|
+
# @param [String] path The path pattern.
|
153
|
+
# @param [Hash] opts A set of extra parameters.
|
154
|
+
# @option opts [Hash] :params Url parameters pattern.
|
155
|
+
# @option opts [Hash] :headers HTTP headers pattern.
|
156
|
+
# @option opts [Hash] :body HTTP request body pattern.
|
157
|
+
# @option opts [Proc] :before Before callback.
|
158
|
+
# @option opts [Hash] :after After callback.
|
159
|
+
# @option opts [Hash] :defaults A set of default variables.
|
160
|
+
# @option opts [Hash] :options A set of extra options.
|
161
|
+
#
|
162
|
+
# TODO: :explain options, callbacks, etc
|
163
|
+
#
|
164
|
+
# @return [void]
|
165
|
+
# @example
|
166
|
+
# # no example
|
167
|
+
#
|
168
|
+
def query_api_pattern(method_name, verb, path='', opts={}, storage=nil, &block)
|
169
|
+
opts = opts.dup
|
170
|
+
method_name = method_name.to_s
|
171
|
+
storage ||= query_api_patterns
|
172
|
+
before = opts.delete(:before)
|
173
|
+
after = opts.delete(:after) || block
|
174
|
+
defaults = opts.delete(:defaults) || {}
|
175
|
+
params = opts.delete(:params) || {}
|
176
|
+
headers = opts.delete(:headers) || {}
|
177
|
+
options = opts.delete(:options) || {}
|
178
|
+
body = opts.delete(:body) || nil
|
179
|
+
# Complain if there are any unused keys left.
|
180
|
+
fail(Error.new("#{method_name.inspect} pattern: unsupported key(s): #{opts.keys.map{|k| k.inspect}.join(',')}")) if opts.any?
|
181
|
+
# Store the new pattern.
|
182
|
+
storage[method_name] = {
|
183
|
+
:verb => verb.to_s.downcase.to_sym,
|
184
|
+
:path => path.to_s,
|
185
|
+
:before => before,
|
186
|
+
:after => after,
|
187
|
+
:defaults => defaults,
|
188
|
+
:params => params,
|
189
|
+
:headers => HTTPHeaders::new(headers),
|
190
|
+
:options => options,
|
191
|
+
:body => body }
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
# Returns the list of current patterns
|
197
|
+
#
|
198
|
+
# The patterns can be defined at the class levels or/and in special Wrapper modules.
|
199
|
+
# The patterns defined at the class levels are always inherited by the instances of this
|
200
|
+
# class, when the wrapper defined patterns are applied to this particular object only.
|
201
|
+
#
|
202
|
+
# This allows one to define generic patterns at the class level and somehow specific
|
203
|
+
# at the level of wrappers.
|
204
|
+
#
|
205
|
+
# @return [Array] The has of QUERY-like method patterns.
|
206
|
+
# @example
|
207
|
+
# # no example
|
208
|
+
#
|
209
|
+
# P.S. The method is usually called in Wrapper modules (see S3 default wrapper)
|
210
|
+
#
|
211
|
+
def query_api_patterns
|
212
|
+
@query_api_patterns ||= {}
|
213
|
+
# The current set of patterns may override whatever is defined at the class level.
|
214
|
+
self.class.query_api_patterns.merge(@query_api_patterns)
|
215
|
+
end
|
216
|
+
|
217
|
+
|
218
|
+
# Explains the given pattern by name
|
219
|
+
#
|
220
|
+
# (Displays the pattern definition)
|
221
|
+
#
|
222
|
+
# @param [String] pattern_name The pattern method name.
|
223
|
+
# @return [Stringq]
|
224
|
+
#
|
225
|
+
# @example
|
226
|
+
# puts open_stack.explain_query_api_pattern('AttachVolume') #=>
|
227
|
+
# AttachVolume: POST 'servers/{:id}/os-volume_attachments'
|
228
|
+
# - body : {:volumeAttachment=>{"volumeId"=>:volumeId, "device"=>:device}}
|
229
|
+
#
|
230
|
+
def explain_query_api_pattern(pattern_name)
|
231
|
+
pattern_name = pattern_name.to_s
|
232
|
+
result = "#{pattern_name}: "
|
233
|
+
pattern = query_api_patterns[pattern_name]
|
234
|
+
unless pattern
|
235
|
+
result << 'does not exist'
|
236
|
+
else
|
237
|
+
result << "#{pattern[:verb].to_s.upcase} '#{pattern[:path]}'"
|
238
|
+
[:params, :headers, :options, :body, :before, :after].each do |key|
|
239
|
+
result << ("\n - %-8s: #{pattern[key].inspect}" % key.to_s) unless pattern[key]._blank?
|
240
|
+
end
|
241
|
+
end
|
242
|
+
result
|
243
|
+
end
|
244
|
+
|
245
|
+
|
246
|
+
# Set object specific QUERY-like pattern
|
247
|
+
#
|
248
|
+
# This guy is usually called from Wrapper's module from self.extended method (see S3 default wrapper)
|
249
|
+
#
|
250
|
+
# @return [void]
|
251
|
+
# @example
|
252
|
+
# # no example
|
253
|
+
#
|
254
|
+
def query_api_pattern(method_name, verb, path='', opts={}, &block)
|
255
|
+
self.class.query_api_pattern(method_name, verb, path, opts, @query_api_patterns, &block)
|
256
|
+
end
|
257
|
+
|
258
|
+
|
259
|
+
# Build request based on the given set of variables and QUERY-like api pattern
|
260
|
+
#
|
261
|
+
# @api private
|
262
|
+
#
|
263
|
+
# @param [String] query_pattern_name The QUERY-like pattern name.
|
264
|
+
# @param [param_type] query_params A set of options.
|
265
|
+
#
|
266
|
+
# @yield [block_params] block_description
|
267
|
+
# @yieldreturn [block_return_type] block_return_description
|
268
|
+
#
|
269
|
+
# @return [return] return_description
|
270
|
+
# @example
|
271
|
+
# # no example
|
272
|
+
#
|
273
|
+
# @raise [error] raise_description
|
274
|
+
#
|
275
|
+
def compute_query_api_pattern_based_params(query_pattern_name, query_params={})
|
276
|
+
# fix a method name
|
277
|
+
pattern = query_api_patterns[query_pattern_name.to_s]
|
278
|
+
# Complain if we dont know the method
|
279
|
+
raise PatternNotFoundError::new("#{query_pattern_name.inspect} pattern not found") unless pattern
|
280
|
+
# Make sure we got what we expected
|
281
|
+
query_params ||= {}
|
282
|
+
raise Error::new("Params must be Hash but #{query_params.class.name} received.") unless query_params.is_a?(Hash)
|
283
|
+
# Make a new Hash instance from the incoming Hash.
|
284
|
+
# Do not clone because we don't want to have HashWithIndifferentAccess instance or
|
285
|
+
# something similar because we need to have Symbols and Strings separated.
|
286
|
+
query_params = Hash[query_params]
|
287
|
+
opts = {}
|
288
|
+
opts[:body] = query_params.delete(:body)
|
289
|
+
opts[:headers] = query_params.delete(:headers) || {}
|
290
|
+
opts[:options] = query_params.delete(:options) || {}
|
291
|
+
opts[:params] = query_params._stringify_keys
|
292
|
+
opts[:manager] = self
|
293
|
+
request_opts = compute_query_api_pattern_request_data(query_pattern_name, pattern, opts)
|
294
|
+
# Try to use custom :process_rest_api_request method first because some auth things
|
295
|
+
# may be required.
|
296
|
+
# (see OpenStack case) otherwise use standard :process_api_request method
|
297
|
+
{ :method => respond_to?(:process_rest_api_request) ? :process_rest_api_request : :process_api_request,
|
298
|
+
:verb => request_opts.delete(:verb),
|
299
|
+
:path => request_opts.delete(:path),
|
300
|
+
:opts => request_opts }
|
301
|
+
end
|
302
|
+
private :compute_query_api_pattern_based_params
|
303
|
+
|
304
|
+
|
305
|
+
# Execute pattered method if it exists
|
306
|
+
#
|
307
|
+
# @raise [PatternNotFoundError]
|
308
|
+
#
|
309
|
+
# @return [Object]
|
310
|
+
# @example
|
311
|
+
# # no example
|
312
|
+
#
|
313
|
+
def invoke_query_api_pattern_method(method_name, *args, &block)
|
314
|
+
computed_data = compute_query_api_pattern_based_params(method_name, args.first)
|
315
|
+
# Make an API call:
|
316
|
+
__send__(computed_data[:method],
|
317
|
+
computed_data[:verb],
|
318
|
+
computed_data[:path],
|
319
|
+
computed_data[:opts],
|
320
|
+
&block)
|
321
|
+
end
|
322
|
+
|
323
|
+
|
324
|
+
# Create custom method_missing method
|
325
|
+
#
|
326
|
+
# If the called method is not explicitly defined then it tries to find the method definition
|
327
|
+
# in the QUERY-like patterns. And if the method is there it builds a request based on the
|
328
|
+
# pattern definition.
|
329
|
+
#
|
330
|
+
# @return [Object]
|
331
|
+
# @example
|
332
|
+
# # no example
|
333
|
+
#
|
334
|
+
def method_missing(method_name, *args, &block)
|
335
|
+
begin
|
336
|
+
invoke_query_api_pattern_method(method_name, *args, &block)
|
337
|
+
rescue PatternNotFoundError
|
338
|
+
super
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
|
343
|
+
FIND_KEY_REGEXP = /\{:([a-zA-Z0-9_]+)\}/
|
344
|
+
FIND_COLLECTION_1_REGEXP = /\[\{:([a-zA-Z0-9_]+)\}\]/
|
345
|
+
FIND_COLLECTION_2_REGEXP = /^([^\[]+)\[\]/
|
346
|
+
FIND_REPLACEMENT_REGEXP = /\{:([a-zA-Z0-9_]+)\}(?!\])/
|
347
|
+
FIND_BLANK_KEYS_TO_REMOVE = /\{!remove-if-blank\}/
|
348
|
+
|
349
|
+
|
350
|
+
# Prepares patters params
|
351
|
+
#
|
352
|
+
# @api private
|
353
|
+
#
|
354
|
+
# Returns a hash of parameters (:params, :options, :body, :headers, etc) that will
|
355
|
+
# used for making an API request.
|
356
|
+
#
|
357
|
+
# @return [Hash]
|
358
|
+
# @example
|
359
|
+
# # no example
|
360
|
+
#
|
361
|
+
def compute_query_api_pattern_request_data(method_name, pattern, opts={}) # :nodoc:
|
362
|
+
container = opts.dup
|
363
|
+
container[:verb] ||= pattern[:verb]
|
364
|
+
container[:path] ||= pattern[:path]
|
365
|
+
container[:error] ||= Error
|
366
|
+
[ :params, :headers, :options, :defaults ].each do |key|
|
367
|
+
container[key] ||= {}
|
368
|
+
container[key] = (pattern[key] || {}).merge(container[key])
|
369
|
+
end
|
370
|
+
container[:defaults] = container[:defaults]._stringify_keys
|
371
|
+
container[:headers] = HTTPHeaders::new(container[:headers])
|
372
|
+
# Call "before" callback (if it is)
|
373
|
+
pattern[:before].call(container) if pattern[:before].is_a?(Proc)
|
374
|
+
# Mix default variables into the given set of variables and
|
375
|
+
# initialize the list of used variables.
|
376
|
+
container[:params_with_defaults] = container[:defaults].merge(container[:params])
|
377
|
+
used_params = []
|
378
|
+
# Compute: Path, UrlParams,Headers and Body
|
379
|
+
compute_query_api_pattern_path(method_name, container, used_params)
|
380
|
+
compute_query_api_pattern_headers(method_name, container, used_params)
|
381
|
+
compute_query_api_pattern_body(method_name, container, used_params, pattern)
|
382
|
+
compute_query_api_pattern_params(method_name, container, used_params)
|
383
|
+
# Delete used query params. The params that are left will go into URL params set later.
|
384
|
+
used_params.each do |key|
|
385
|
+
container[:params].delete(key.to_s)
|
386
|
+
container[:params].delete(key.to_sym)
|
387
|
+
end
|
388
|
+
container.delete(:params_with_defaults)
|
389
|
+
# Call "after" callback (if it is)
|
390
|
+
pattern[:after].call(container) if pattern[:after].is_a?(Proc)
|
391
|
+
# Remove temporary variables.
|
392
|
+
container.delete(:error)
|
393
|
+
container.delete(:manager)
|
394
|
+
#
|
395
|
+
container
|
396
|
+
end
|
397
|
+
private :compute_query_api_pattern_request_data
|
398
|
+
|
399
|
+
|
400
|
+
# Computes the path for the API request
|
401
|
+
#
|
402
|
+
# @api private
|
403
|
+
#
|
404
|
+
# @param [String] query_api_method_name Auery API like pattern name.
|
405
|
+
# @param [Hash] container The container for final parameters.
|
406
|
+
# @param [Hash] used_query_params The list of used variables.
|
407
|
+
#
|
408
|
+
# @return [String] The path.
|
409
|
+
# @example
|
410
|
+
# # no example
|
411
|
+
#
|
412
|
+
def compute_query_api_pattern_path(query_api_method_name, container, used_query_params)
|
413
|
+
container[:path] = compute_query_api_pattern_param(query_api_method_name, container[:path], container[:params_with_defaults], used_query_params)
|
414
|
+
end
|
415
|
+
private :compute_query_api_pattern_path
|
416
|
+
|
417
|
+
|
418
|
+
# Computes the set of URL params for the API request
|
419
|
+
#
|
420
|
+
# @api private
|
421
|
+
#
|
422
|
+
# @param [String] query_api_method_name Auery API like pattern name.
|
423
|
+
# @param [Hash] container The container for final parameters.
|
424
|
+
# @param [Hash] used_query_params The list of used variables.
|
425
|
+
#
|
426
|
+
# @return [Hash] The set of URL params.
|
427
|
+
# @example
|
428
|
+
# # no example
|
429
|
+
#
|
430
|
+
def compute_query_api_pattern_params(query_api_method_name, container, used_query_params)
|
431
|
+
container[:params] = compute_query_api_pattern_param(query_api_method_name, container[:params], container[:params_with_defaults], used_query_params)
|
432
|
+
end
|
433
|
+
private :compute_query_api_pattern_params
|
434
|
+
|
435
|
+
|
436
|
+
# Computes the set of headers for the API request
|
437
|
+
#
|
438
|
+
# @api private
|
439
|
+
#
|
440
|
+
# @param [String] query_api_method_name Auery API like pattern name.
|
441
|
+
# @param [Hash] container The container for final parameters.
|
442
|
+
# @param [Hash] used_query_params The list of used variables.
|
443
|
+
#
|
444
|
+
# @return [Hash] The set of HTTP headers.
|
445
|
+
# @example
|
446
|
+
# # no example
|
447
|
+
#
|
448
|
+
def compute_query_api_pattern_headers(query_api_method_name, container, used_query_params)
|
449
|
+
container[:headers].dup.each do |header, header_values|
|
450
|
+
container[:headers][header].each_with_index do |header_value, idx|
|
451
|
+
container[:headers][header] = container[:headers][header].dup
|
452
|
+
container[:headers][header][idx] = compute_query_api_pattern_param(query_api_method_name, header_value, container[:params_with_defaults], used_query_params)
|
453
|
+
container[:headers][header].delete_at(idx) if container[:headers][header][idx] == Utils::NONE
|
454
|
+
end
|
455
|
+
end
|
456
|
+
end
|
457
|
+
private :compute_query_api_pattern_headers
|
458
|
+
|
459
|
+
|
460
|
+
# Computes the body value for the API request
|
461
|
+
#
|
462
|
+
# @api private
|
463
|
+
#
|
464
|
+
# @param [String] query_api_method_name Auery API like pattern name.
|
465
|
+
# @param [Hash] container The container for final parameters.
|
466
|
+
# @param [Hash] used_query_params The list of used variables.
|
467
|
+
# @param [Hash] pattern The pattern.
|
468
|
+
#
|
469
|
+
# @return [Hash,String] The HTTP request body..
|
470
|
+
# @example
|
471
|
+
# # no example
|
472
|
+
#
|
473
|
+
def compute_query_api_pattern_body(query_api_method_name, container, used_query_params, pattern)
|
474
|
+
if container[:body].nil? && !pattern[:body].nil?
|
475
|
+
# Make sure body is not left blank when it must be set
|
476
|
+
fail(Error::new("#{query_api_method_name}: body parameter must be set")) if pattern[:body] == Utils::MUST_BE_SET
|
477
|
+
container[:body] = compute_query_api_pattern_param(query_api_method_name, pattern[:body], container[:params_with_defaults], used_query_params)
|
478
|
+
end
|
479
|
+
end
|
480
|
+
private :compute_query_api_pattern_body
|
481
|
+
|
482
|
+
|
483
|
+
# Computes single Query API pattern parameter
|
484
|
+
#
|
485
|
+
# @param [String] query_api_method_name Auery API like pattern name.
|
486
|
+
# @param [Hash] source The param to compute/parse.
|
487
|
+
# @param [Hash] used_query_params The list of used variables.
|
488
|
+
# @param [Hash] params_with_defaults The set of parameters passed by a user + all the default
|
489
|
+
# values defined in wrappers.
|
490
|
+
#
|
491
|
+
# @return [Object]
|
492
|
+
# @example
|
493
|
+
# # no example
|
494
|
+
#
|
495
|
+
def compute_query_api_pattern_param(query_api_method_name, source, params_with_defaults, used_query_params) # :nodoc:
|
496
|
+
case
|
497
|
+
when source.is_a?(Hash) then compute_query_api_pattern_hash_data(query_api_method_name, source, params_with_defaults, used_query_params)
|
498
|
+
when source.is_a?(Array) then compute_query_api_pattern_array_data(query_api_method_name, source, params_with_defaults, used_query_params)
|
499
|
+
when source.is_a?(Symbol) then compute_query_api_pattern_symbol_data(query_api_method_name, source, params_with_defaults, used_query_params)
|
500
|
+
when source.is_a?(String) then compute_query_api_pattern_string_data(query_api_method_name, source, params_with_defaults, used_query_params)
|
501
|
+
else source
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
|
506
|
+
#-----------------------------------------
|
507
|
+
# Query API pattents: HASH
|
508
|
+
#-----------------------------------------
|
509
|
+
|
510
|
+
# Parses Query API replacements
|
511
|
+
#
|
512
|
+
# @api private
|
513
|
+
#
|
514
|
+
# You may define a key so that is has a default value but you may override it if you
|
515
|
+
# provide another "replacement" key.
|
516
|
+
#
|
517
|
+
# The replacement key is defined as "KeyToSentToCloud{:ReplacementKeyName}" string and
|
518
|
+
# it will send 'KeyToSentToCloud' with the value taken from 'ReplacementKeyName' if
|
519
|
+
# 'ReplacementKeyName' is provided.
|
520
|
+
#
|
521
|
+
# @param [Hash] params_with_defaults A set API call parameters.
|
522
|
+
# @param [Array] used_params An array that lists all the paramaters names who were already
|
523
|
+
# somehow used for this api call. All the unused params wil go into URL params
|
524
|
+
# @param [String] key The current key.
|
525
|
+
# @param [Object] value The current value,
|
526
|
+
# @param [Hash] result The resulting hash that has all the transformed params.
|
527
|
+
#
|
528
|
+
# @return [Array] The updated key name and its value
|
529
|
+
#
|
530
|
+
# @example:
|
531
|
+
# # Example 1: simple case.
|
532
|
+
# query_api_pattern 'CreateServer', :post, 'servers',
|
533
|
+
# :body => {
|
534
|
+
# Something{:Replacemet} => {'X' => 1, 'Y' => 2}
|
535
|
+
# }
|
536
|
+
#
|
537
|
+
# # 1.a
|
538
|
+
# api.CreateServer #=>
|
539
|
+
# # it will set request body to:
|
540
|
+
# # { Something => {'X' => 1, 'Y' => 2} }
|
541
|
+
#
|
542
|
+
# # 1.b
|
543
|
+
# api.CreateServer('Replacement' => 'hahaha' ) #=>
|
544
|
+
# # it will set request body to:
|
545
|
+
# # { Something => 'hahaha' }
|
546
|
+
#
|
547
|
+
# # Example 2: complex case:
|
548
|
+
# query_api_pattern :MyApiCallName, :get, '',
|
549
|
+
# :body => {
|
550
|
+
# 'Key1' => :Value1,
|
551
|
+
# 'Collections{:Replacement}' => { # <-- The key with Replacement
|
552
|
+
# 'Collection[{:Items}]' => {
|
553
|
+
# 'Name' => :Name,
|
554
|
+
# 'Value' => :Value
|
555
|
+
# }
|
556
|
+
# }
|
557
|
+
# },
|
558
|
+
# :defaults => {
|
559
|
+
# :Key1 => 'hoho',
|
560
|
+
# :Collections => Utils::NONE
|
561
|
+
# }
|
562
|
+
#
|
563
|
+
# # 2.a No parameters are provided
|
564
|
+
# api.MyApiCallName #=>
|
565
|
+
# # it will set request body to:
|
566
|
+
# # { 'Key1' => 'hoho' }
|
567
|
+
#
|
568
|
+
# # 2.b Some parameters are provided:
|
569
|
+
# api.MyApiCallName('Key1' => 'woohoo', 'Items' => [ {'Name' => 'a', 'Value' => 'b'},
|
570
|
+
# {'Name' => 'b', 'Value' => 'c'} ]) #=>
|
571
|
+
# # it will set request body to:
|
572
|
+
# # { 'Key1' => 'woohoo',
|
573
|
+
# # 'Collections' =>
|
574
|
+
# # {'Collection' =>
|
575
|
+
# # [ {'Name' => 'a', 'Value' => 'b'},
|
576
|
+
# # {'Name' => 'c', 'Value' => 'd'} ] } }
|
577
|
+
# #
|
578
|
+
#
|
579
|
+
# # 2.c Areplacement key is provided:
|
580
|
+
# api.MyApiCallName('Key1' => 'ahaha', 'Replacement' => 'oooops') #=>
|
581
|
+
# # it will set request body to:
|
582
|
+
# # { 'Key1' => 'ahaha',
|
583
|
+
# 'Collections' => 'oooops' }
|
584
|
+
#
|
585
|
+
def parse_query_api_pattern_replacements(params_with_defaults, used_params, key, value, result)
|
586
|
+
# Test the current key if it has a replacement mark or not.
|
587
|
+
# If not then we do nothing.
|
588
|
+
replacement_key = key[FIND_REPLACEMENT_REGEXP] && $1
|
589
|
+
if replacement_key
|
590
|
+
# If it is a key with a possible replacement then we should exract the replacement
|
591
|
+
# variable name from the key:
|
592
|
+
# so that 'CloudKeyName{:ReplacementKeyName}' should transform into:
|
593
|
+
# key -> 'CloudKeyName' and replacement_key -> 'ReplacementKeyName'.
|
594
|
+
#
|
595
|
+
result.delete(key)
|
596
|
+
key = key.sub(FIND_REPLACEMENT_REGEXP, '')
|
597
|
+
if params_with_defaults.has_key?(replacement_key)
|
598
|
+
# If We have 'ReplacementKeyName' passed by a user or set by default then we should use
|
599
|
+
# its value otherwise we keep the original value that was defined for 'CloudKeyName'.
|
600
|
+
#
|
601
|
+
# Anyway the final key name is 'CloudKeyName'.
|
602
|
+
#
|
603
|
+
value = params_with_defaults[replacement_key]
|
604
|
+
used_params << replacement_key
|
605
|
+
end
|
606
|
+
end
|
607
|
+
[key, value]
|
608
|
+
end
|
609
|
+
private :parse_query_api_pattern_replacements
|
610
|
+
|
611
|
+
|
612
|
+
# Collections
|
613
|
+
#
|
614
|
+
# @api private
|
615
|
+
#
|
616
|
+
# The simple definition delow tells us that parameters will have a key named "CloudKeyName"
|
617
|
+
# which will point to an Array of Hashes. Where every hash will have keys: 'Key' and 'Value'
|
618
|
+
#
|
619
|
+
# @param [String] method_name The name of the pattern.
|
620
|
+
# @param [Hash] params_with_defaults A set API call parameters.
|
621
|
+
# @param [Array] used_params An array that lists all the paramaters names who were already
|
622
|
+
# somehow used for this api call. All the unused params wil go into URL params
|
623
|
+
# @param [String] key The current key.
|
624
|
+
# @param [Object] value The current value,
|
625
|
+
# @param [Hash] result The resulting hash that has all the transformed params.
|
626
|
+
#
|
627
|
+
# @return [Array] The updated key name and its value
|
628
|
+
#
|
629
|
+
# @raise [Error] If things go wrong in the method.
|
630
|
+
#
|
631
|
+
# @example
|
632
|
+
# # Example 1: Simple Collection definition:
|
633
|
+
#
|
634
|
+
# query_api_pattern :MyApiCallName, :get, '',
|
635
|
+
# :body => {
|
636
|
+
# 'CloudKeyName[]' => {
|
637
|
+
# 'Name' => :Key,
|
638
|
+
# 'State' => :Value
|
639
|
+
# }
|
640
|
+
# }
|
641
|
+
#
|
642
|
+
# api.MyApiCallName('CloudKeyName' => [{'Key' => 1, 'Value' => 2},
|
643
|
+
# {'Key' => 3, 'Value' => 4}]) #=>
|
644
|
+
# # it will set request body to:
|
645
|
+
# # 'CloudKeyName' => [
|
646
|
+
# # {'Name' => 1, 'State' => 2},
|
647
|
+
# # {'Name' => 3, 'State' => 4} ]
|
648
|
+
#
|
649
|
+
# The collection may comsume values from a parameter that has name differet from the
|
650
|
+
# 'CloudKeyName' in the example above:
|
651
|
+
#
|
652
|
+
# @example
|
653
|
+
# # Example 2: Simple Collection definition:
|
654
|
+
#
|
655
|
+
# query_api_pattern :MyApiCallName, :get, '',
|
656
|
+
# :body => {
|
657
|
+
# 'CloudKeyName[{:Something}]' => {
|
658
|
+
# 'Name' => :Key,
|
659
|
+
# 'State' => :Value
|
660
|
+
# }
|
661
|
+
# }
|
662
|
+
#
|
663
|
+
# api.MyApiCallName('Something' => [{'Key' => 1, 'Value' => 2},
|
664
|
+
# {'Key' => 3, 'Value' => 4}]) #=>
|
665
|
+
# # it will set request body to the same value as above:
|
666
|
+
# # 'CloudKeyName' => [
|
667
|
+
# # {'Name' => 1, 'State' => 2},
|
668
|
+
# # {'Name' => 3, 'State' => 4} ]
|
669
|
+
#
|
670
|
+
# You can nest the collections:
|
671
|
+
#
|
672
|
+
# @example
|
673
|
+
# query_api_pattern :MyApiCallName, :get, '',
|
674
|
+
# :body => {
|
675
|
+
# 'CloudKeyName[]' => {
|
676
|
+
# 'Name' => :Key,
|
677
|
+
# 'States[]' => {
|
678
|
+
# 'SubState' => :SubKey,
|
679
|
+
# 'FixedKey' => 13
|
680
|
+
# }
|
681
|
+
# }
|
682
|
+
# }
|
683
|
+
#
|
684
|
+
# api.MyApiCallName('CloudKeyName' => [{'Key' => 1,
|
685
|
+
# 'States' => [{'SubState' => 'x'},
|
686
|
+
# {'SubState' => 'y'},
|
687
|
+
# {'SubState' => 'y'}]},
|
688
|
+
# {'Key' => 3,
|
689
|
+
# 'States' => {'SubState' => 'a'}}])
|
690
|
+
#
|
691
|
+
# If a collection was defined with the default value == Utils::NONE it will remove
|
692
|
+
# the collection key from the final hash of params unless any collection items were passed.
|
693
|
+
#
|
694
|
+
# query_api_pattern :MyApiCallName, :get, '',
|
695
|
+
# :body => {
|
696
|
+
# 'CloudKeyName[]' => {
|
697
|
+
# 'Name' => :Key,
|
698
|
+
# 'State' => :Value
|
699
|
+
# }
|
700
|
+
# },
|
701
|
+
# :defaults => Utils::NONE
|
702
|
+
#
|
703
|
+
# api.MyApiCallName() #=>
|
704
|
+
# # it will set request body to: {}
|
705
|
+
#
|
706
|
+
def parse_query_api_pattern_collections(method_name, params_with_defaults, used_params, key, value, result)
|
707
|
+
# Parse complex collection: KeyName[{:VarName}]'
|
708
|
+
collection_key = key[FIND_COLLECTION_1_REGEXP] && $1
|
709
|
+
# Parse simple collection: KeyName[]'
|
710
|
+
collection_key ||= key[FIND_COLLECTION_2_REGEXP] && $1
|
711
|
+
# Do nothing unless there is a collection key detected
|
712
|
+
if collection_key
|
713
|
+
# Delete the original key from the resulting hash because it has collection crap in it.
|
714
|
+
sub_pattern = result.delete(key)
|
715
|
+
# Extract the real key from the original mixed collection key.
|
716
|
+
# in the case of:
|
717
|
+
# - 'KeyName[{:VarName}]' the real key is 'KeyName' and the collection key is 'VarName';
|
718
|
+
# - 'KeyName[]' the real key and the collection key are both 'KeyName'.
|
719
|
+
key = key[/^[^\[]*/]
|
720
|
+
# If a user did not pass collection key and the key has not been given a default value
|
721
|
+
# when the current pattern was defined we should fail.
|
722
|
+
fail Error::new("#{method_name}: #{collection_key.inspect} is required") unless params_with_defaults.has_key?(collection_key)
|
723
|
+
# Grab the values for the collection from what the user sent of from the default defs.
|
724
|
+
value = params_with_defaults[collection_key]
|
725
|
+
# Walk through all the collection items and substitule required values into it.
|
726
|
+
if value.is_a?(Array) || value.is_a?(Hash)
|
727
|
+
value = value.dup
|
728
|
+
value = [ value ] if value.is_a?(Hash)
|
729
|
+
value.each_with_index do |item_params, idx|
|
730
|
+
# The values given by the user (or the default ones) must be defined as hashes.
|
731
|
+
fail Error::new("#{method_name}: Collection items must be Hash instances but #{item_params.inspect} is provided") unless item_params.is_a?(Hash)
|
732
|
+
# Recursively pdate them all.
|
733
|
+
value[idx] = compute_query_api_pattern_param(method_name, sub_pattern, params_with_defaults.merge(item_params), used_params)
|
734
|
+
end
|
735
|
+
end
|
736
|
+
# Mark the collection key as the one that has been used already.
|
737
|
+
used_params << collection_key
|
738
|
+
else
|
739
|
+
value = compute_query_api_pattern_param(method_name, value, params_with_defaults, used_params) unless value == Utils::NONE
|
740
|
+
end
|
741
|
+
value == Utils::NONE ? result.delete(key) : result[key] = value
|
742
|
+
[key, value]
|
743
|
+
end
|
744
|
+
private :parse_query_api_pattern_collections
|
745
|
+
|
746
|
+
|
747
|
+
# Deals with blank values.
|
748
|
+
#
|
749
|
+
# @api private
|
750
|
+
#
|
751
|
+
# If the given key responds to "blank? and it is true and it is marked as to be removed if
|
752
|
+
# it is blank then we remove it in this method.
|
753
|
+
#
|
754
|
+
# @param [String] key The current key.
|
755
|
+
# @param [Object] value The current value,
|
756
|
+
# @param [Hash] result The resulting hash that has all the transformed params.
|
757
|
+
#
|
758
|
+
# @return [Array] The updated key name and its value.
|
759
|
+
#
|
760
|
+
def parse_query_api_pattern_remove_blank_key( key, value, result)
|
761
|
+
# 'KeyName{!remove-if-blank}'
|
762
|
+
blank_key_sign = key[FIND_BLANK_KEYS_TO_REMOVE]
|
763
|
+
if blank_key_sign
|
764
|
+
# Delete the original key from the resulting hash.
|
765
|
+
result.delete(key)
|
766
|
+
# But if its value is not blank then fix the key name (get rid of {!remove-if-blank}) and
|
767
|
+
# put it back.
|
768
|
+
unless value.respond_to?(:_blank?) && value._blank?
|
769
|
+
key = key.sub(blank_key_sign, '')
|
770
|
+
result[key] = value
|
771
|
+
end
|
772
|
+
end
|
773
|
+
[key, value]
|
774
|
+
end
|
775
|
+
private :parse_query_api_pattern_remove_blank_key
|
776
|
+
|
777
|
+
|
778
|
+
# Parses Hash objects
|
779
|
+
#
|
780
|
+
# @api private
|
781
|
+
#
|
782
|
+
# @return [Hash]
|
783
|
+
#
|
784
|
+
def compute_query_api_pattern_hash_data(method_name, source, params_with_defaults, used_params)
|
785
|
+
result = source.dup
|
786
|
+
source.dup.each do |key, value|
|
787
|
+
# Make sure key is a String
|
788
|
+
key = key.to_s.dup
|
789
|
+
# Subsets replacement
|
790
|
+
key, value = *parse_query_api_pattern_replacements(params_with_defaults, used_params, key, value, result)
|
791
|
+
# Collections replacement
|
792
|
+
key, value = *parse_query_api_pattern_collections(method_name, params_with_defaults, used_params, key, value, result)
|
793
|
+
# Remove empty keys
|
794
|
+
parse_query_api_pattern_remove_blank_key(key, value, result)
|
795
|
+
end
|
796
|
+
result
|
797
|
+
end
|
798
|
+
private :compute_query_api_pattern_hash_data
|
799
|
+
|
800
|
+
|
801
|
+
#-----------------------------------------
|
802
|
+
# Query API pattern: ARRAY
|
803
|
+
#-----------------------------------------
|
804
|
+
|
805
|
+
# Parses Array objects
|
806
|
+
#
|
807
|
+
# @return [Array]
|
808
|
+
#
|
809
|
+
def compute_query_api_pattern_array_data(query_api_method_name, source, params_with_defaults, used_query_params)
|
810
|
+
result = source.dup
|
811
|
+
source.dup.each_with_index do |item, idx|
|
812
|
+
value = compute_query_api_pattern_param(query_api_method_name, item, params_with_defaults, used_query_params)
|
813
|
+
value == Utils::NONE ? result.delete_at(idx) : result[idx] = value
|
814
|
+
end
|
815
|
+
result
|
816
|
+
end
|
817
|
+
|
818
|
+
|
819
|
+
#-----------------------------------------
|
820
|
+
# Query API pattern: STRING
|
821
|
+
#-----------------------------------------
|
822
|
+
|
823
|
+
# Parses String objects
|
824
|
+
#
|
825
|
+
# @return [String]
|
826
|
+
#
|
827
|
+
# @raise [Error]
|
828
|
+
#
|
829
|
+
def compute_query_api_pattern_string_data(query_api_method_name, source, params_with_defaults, used_query_params)
|
830
|
+
result = source.dup
|
831
|
+
result.scan(FIND_KEY_REGEXP).flatten.each do |key|
|
832
|
+
fail Error::new("#{query_api_method_name}: #{key.inspect} is required") unless params_with_defaults.has_key?(key)
|
833
|
+
value = params_with_defaults[key]
|
834
|
+
result.gsub!("{:#{key}}", value == Utils::NONE ? '' : value.to_s)
|
835
|
+
used_query_params << key
|
836
|
+
end
|
837
|
+
result
|
838
|
+
end
|
839
|
+
|
840
|
+
|
841
|
+
#-----------------------------------------
|
842
|
+
# Query API pattern: SYMBOL
|
843
|
+
#-----------------------------------------
|
844
|
+
|
845
|
+
# Parses Symbol objects
|
846
|
+
#
|
847
|
+
# @return [String]
|
848
|
+
#
|
849
|
+
# @raise [Error]
|
850
|
+
#
|
851
|
+
def compute_query_api_pattern_symbol_data(query_api_method_name, source, params_with_defaults, used_query_params)
|
852
|
+
key = source.to_s
|
853
|
+
fail Error::new("#{query_api_method_name}: #{key.inspect} is required") unless params_with_defaults.has_key?(key)
|
854
|
+
result = params_with_defaults[key]
|
855
|
+
used_query_params << key
|
856
|
+
result
|
857
|
+
end
|
858
|
+
|
859
|
+
end
|
860
|
+
end
|
861
|
+
end
|
862
|
+
end
|