right_cloud_api_base 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,380 @@
|
|
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
|
+
class BlankSlate
|
28
|
+
instance_methods.each { |m| undef_method m unless m =~ /(^__)|(^object_id$)/ }
|
29
|
+
end
|
30
|
+
|
31
|
+
module Utils
|
32
|
+
|
33
|
+
ONE_DAY_OF_SECONDS = 60*60*24
|
34
|
+
MUST_BE_SET = :__MUST_BE_SET__
|
35
|
+
NONE = :__NONE__
|
36
|
+
|
37
|
+
# URL encodes a string.
|
38
|
+
#
|
39
|
+
# @param [String] str A string to escape.
|
40
|
+
#
|
41
|
+
# @return [String] The escaped string.
|
42
|
+
#
|
43
|
+
# P.S. URI.escape is deprecated in ruby 1.9.2+
|
44
|
+
#
|
45
|
+
def self.url_encode(str)
|
46
|
+
CGI::escape(str.to_s).gsub("+", "%20")
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.base64en(string)
|
50
|
+
Base64::encode64(string.to_s).strip
|
51
|
+
end
|
52
|
+
|
53
|
+
# Makes a URL params string from a given hash. If block is given the it invokes the block on
|
54
|
+
# every value so that one could escape it in his own way. If there is no block it url_encode
|
55
|
+
# values automatically.
|
56
|
+
#
|
57
|
+
# @param [Hash] params A set of URL params => values.
|
58
|
+
#
|
59
|
+
# @yield [String] Current value.
|
60
|
+
# @yieldreturn [String] The escaped value.
|
61
|
+
#
|
62
|
+
# @return [String] The result.
|
63
|
+
#
|
64
|
+
# @example
|
65
|
+
# RightScale::CloudApi::Utils::params_to_urn(
|
66
|
+
# "InstanceId.1" => 'i-12345678',
|
67
|
+
# "InstanceName" => "",
|
68
|
+
# "Reboot" => nil,
|
69
|
+
# "XLogPath" => "/mypath/log file.txt") #=> "InstanceId.1=i-12345678&InstanceName=&Reboot&XLogPath=%2Fmypath%2Flog%20with%20spaces.txt"
|
70
|
+
#
|
71
|
+
# Block can be used to url encode keys and values:
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
# RightScale::CloudApi::Utils::params_to_urn(
|
75
|
+
# "InstanceId.1" => 'i-12345678',
|
76
|
+
# "InstanceName" => "",
|
77
|
+
# "Reboot" => nil,
|
78
|
+
# "XLogPath" => "/mypath/log with spaces.txt") do |value|
|
79
|
+
# RightScale::CloudApi::AWS::Utils::amz_escape(value) #=> "InstanceId.1=i-12345678&InstanceName=&Reboot&XLogPath=%2Fmypath%2Flog%20with%20spaces.txt"
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
# P.S. Nil values are interpreted as valuesless ones and "" are as blank values
|
83
|
+
#
|
84
|
+
def self.params_to_urn(params={}, &block)
|
85
|
+
block ||= Proc::new { |value| url_encode(value) }
|
86
|
+
params.keys.sort.map do |name|
|
87
|
+
value, name = params[name], block.call(name)
|
88
|
+
value.nil? ? name : [value].flatten.inject([]) { |m, v| m << "#{name}=#{block.call(v)}"; m }.join('&')
|
89
|
+
end.compact.join('&')
|
90
|
+
end
|
91
|
+
|
92
|
+
# Joins pathes and URN params into a well formed URN.
|
93
|
+
# Block can be used to url encode URN param's keys and values.
|
94
|
+
#
|
95
|
+
# @param [String] absolute Absolute path.
|
96
|
+
# @param [Array] relatives Relative pathes (Strings) and URL params (Hash) as a very last item.
|
97
|
+
#
|
98
|
+
# @yield [String] Current URL param value.
|
99
|
+
# @yieldreturn [String] The escaped URL param value.
|
100
|
+
#
|
101
|
+
# @example
|
102
|
+
# join_urn(absolute, [relative1, [..., [relativeN, [urn_params, [&block]]]]])
|
103
|
+
#
|
104
|
+
# @example
|
105
|
+
# RightScale::CloudApi::Utils::join_urn(
|
106
|
+
# "service/v1.0",
|
107
|
+
# "servers/index",
|
108
|
+
# "blah-bllah",
|
109
|
+
# "InstanceId.1" => 'i-12345678',
|
110
|
+
# "InstanceName" => "",
|
111
|
+
# "Reboot" => nil,
|
112
|
+
# "XLogPath" => "/mypath/log with spaces.txt") #=>
|
113
|
+
# "/service/v1.0/servers/index/blah-bllah?InstanceId.1=i-12345678&InstanceName=&Reboot&XLogPath=%2Fmypath%2Flog%20with%20spaces.txt"
|
114
|
+
#
|
115
|
+
def self.join_urn(absolute, *relatives, &block)
|
116
|
+
# Fix absolute path
|
117
|
+
absolute = absolute.to_s
|
118
|
+
result = absolute[/^\//] ? absolute.dup : "/#{absolute}"
|
119
|
+
# Extract urn_params if they are
|
120
|
+
urn_params = relatives.last.is_a?(Hash) ? relatives.pop : {}
|
121
|
+
# Add relative pathes
|
122
|
+
relatives.each do |relative|
|
123
|
+
relative = relative.to_s
|
124
|
+
# skip relative path if is blank
|
125
|
+
next if relative._blank?
|
126
|
+
# KD: small hack if relative starts with '/' it should override everything before and become a absolute path
|
127
|
+
if relative[/^\//]
|
128
|
+
result = relative
|
129
|
+
else
|
130
|
+
result << (result[/\/$/] ? relative : "/#{relative}")
|
131
|
+
end
|
132
|
+
end
|
133
|
+
# Add there a list of params
|
134
|
+
urn_params = params_to_urn(urn_params, &block)
|
135
|
+
urn_params._blank? ? result : "#{result}?#{urn_params}"
|
136
|
+
end
|
137
|
+
|
138
|
+
# Get a hash of URL parameters from URL string.
|
139
|
+
#
|
140
|
+
# @param [String] url The URL.
|
141
|
+
#
|
142
|
+
# @return [Hash] A hash with parameters parsed from the URL.
|
143
|
+
#
|
144
|
+
# @example
|
145
|
+
# parse_url_params('https://ec2.amazonaws.com/?w=1&x=3&y&z') #=> {"z"=>nil, "y"=>nil, "x"=>"3", "w"=>"1"}
|
146
|
+
#
|
147
|
+
# @example
|
148
|
+
# parse_url_params('https://ec2.amazonaws.com') #=> {}
|
149
|
+
#
|
150
|
+
def self.extract_url_params(url)
|
151
|
+
URI::parse(url).query.to_s.split('&').map{|i| i.split('=')}.inject({}){|result, i| result[i[0]] = i[1]; result }
|
152
|
+
end
|
153
|
+
|
154
|
+
#-------------------------------------------------------------------------
|
155
|
+
# Other Patterns
|
156
|
+
#-------------------------------------------------------------------------
|
157
|
+
|
158
|
+
# Checks if a response/request data matches to the pattern
|
159
|
+
# Returns true | nil
|
160
|
+
#
|
161
|
+
# Pattern is a Hash:
|
162
|
+
# :verb => Condition, # HTTP verb: get|post|put etc
|
163
|
+
# :path => Condition, # Request path must match Condition
|
164
|
+
# :request => Condition, # Request body must match Condition
|
165
|
+
# :code => Condition, # Response code must match Condition
|
166
|
+
# :response => Condition, # Response body must match Condition
|
167
|
+
# :path! => Condition, # Request path must not match Condition
|
168
|
+
# :request! => Condition, # Request body must not match Condition
|
169
|
+
# :code! => Condition, # Response code must not match Condition
|
170
|
+
# :response! => Condition, # Response body must not match Condition
|
171
|
+
# :if => Proc::new{ |opts| do something } # Extra condition: should return true | false
|
172
|
+
#
|
173
|
+
# (Condition above is /RegExp/ or String or Symbol)
|
174
|
+
#
|
175
|
+
# Opts is a Hash:
|
176
|
+
# :request => Object, # HTTP request instance
|
177
|
+
# :response => Object, # HTTP response instance
|
178
|
+
# :verb => String, # HTTP verb
|
179
|
+
# :params => Hash, # Initial request params Hash
|
180
|
+
#
|
181
|
+
def self.pattern_matches?(pattern, opts={})
|
182
|
+
request, response, verb = opts[:request], opts[:response], opts[:verb].to_s.downcase
|
183
|
+
mapping = { :verb => verb,
|
184
|
+
:path => request.path,
|
185
|
+
:request => request.body,
|
186
|
+
:code => response && response.code,
|
187
|
+
:response => response && response.body }
|
188
|
+
# Should not match cases (return immediatelly if any of the conditions matches)
|
189
|
+
mapping.each do |key, value|
|
190
|
+
key = "#{key}!".to_sym # Make key negative
|
191
|
+
condition = pattern[key]
|
192
|
+
next unless condition
|
193
|
+
return nil if case
|
194
|
+
when condition.is_a?(Regexp) then value[condition]
|
195
|
+
when condition.is_a?(Proc) then condition.call(value)
|
196
|
+
else condition.to_s.downcase == value.to_s.downcase
|
197
|
+
end
|
198
|
+
end
|
199
|
+
# Should match cases (return immediatelly if any of the conditions does not match)
|
200
|
+
mapping.each do |key, value|
|
201
|
+
condition = pattern[key]
|
202
|
+
next unless condition
|
203
|
+
return nil unless case
|
204
|
+
when condition.is_a?(Regexp) then value[condition]
|
205
|
+
when condition.is_a?(Proc) then condition.call(value)
|
206
|
+
else condition.to_s.downcase == value.to_s.downcase
|
207
|
+
end
|
208
|
+
end
|
209
|
+
# Should also match
|
210
|
+
return nil if pattern[:if] && !pattern[:if].call(opts)
|
211
|
+
true
|
212
|
+
end
|
213
|
+
|
214
|
+
# Returns an Array with the current Thread and Fiber (if exists) instances.
|
215
|
+
#
|
216
|
+
# @return [Array] The first item is the current Thread instance and the second item
|
217
|
+
# is the current Fiber instance. For ruby 1.8.7 may return only the first item.
|
218
|
+
#
|
219
|
+
def self::current_thread_and_fiber
|
220
|
+
if defined?(::Fiber) && ::Fiber::respond_to?(:current)
|
221
|
+
[ Thread::current, Fiber::current ]
|
222
|
+
else
|
223
|
+
[ Thread::current ]
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# Storage is an Hash: [Thread, Fiber|nil] => something
|
228
|
+
def self::remove_dead_fibers_and_threads_from_storage(storage)
|
229
|
+
storage.keys.each do |thread_and_fiber|
|
230
|
+
thread, fiber = *thread_and_fiber
|
231
|
+
unless (thread.alive? && (!fiber || (fiber && fiber.alive?)))
|
232
|
+
storage.delete(thread_and_fiber)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Transforms body (when it is a Hash) into String
|
238
|
+
#
|
239
|
+
# @param [Hash] body The request body as a Hash instance.
|
240
|
+
# @param [String] content_type The required content type ( only XML and JSON formats are supported).
|
241
|
+
#
|
242
|
+
# @return [String] The body as a String.
|
243
|
+
#
|
244
|
+
# @raise [RightScale::CloudApi::Error] When the content_type is not supported
|
245
|
+
#
|
246
|
+
# @example
|
247
|
+
# RightScale::CloudApi::Utils.contentify_body(@body,'json') #=>
|
248
|
+
# '{"1":"2"}'
|
249
|
+
#
|
250
|
+
# @example
|
251
|
+
# RightScale::CloudApi::Utils.contentify_body(@body,'xml') #=>
|
252
|
+
# "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<1>2</1>"
|
253
|
+
#
|
254
|
+
def self.contentify_body(body, content_type)
|
255
|
+
return body unless body.is_a?(Hash)
|
256
|
+
# Transform
|
257
|
+
case dearrayify(content_type).to_s
|
258
|
+
when /json/ then body.to_json
|
259
|
+
when /xml/ then body._to_xml!
|
260
|
+
else fail Error::new("Can't transform body from Hash into #{content_type.inspect} type String")
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
# Generates a unique token (Uses UUID when possible)
|
265
|
+
#
|
266
|
+
# @return [String] A random 28-symbols string.
|
267
|
+
#
|
268
|
+
# @example
|
269
|
+
# Utils::generate_token => "1f4a91d7-3650-7b22-f401-6f9c54bcf5e5" # if UUID
|
270
|
+
#
|
271
|
+
# @example
|
272
|
+
# Utils::generate_token => "062e32337633070448fd0d284c46c2b9a41b" # otherwise
|
273
|
+
#
|
274
|
+
def self.generate_token
|
275
|
+
# Use UUID gem if it is
|
276
|
+
if defined?(UUID) && UUID::respond_to?(:new)
|
277
|
+
uuid = UUID::new
|
278
|
+
return uuid.generate if uuid.respond_to?(:generate)
|
279
|
+
end
|
280
|
+
# Otherwise generate a random token
|
281
|
+
time = Time::now.utc
|
282
|
+
token = "%.2x" % (time.to_i % 256 )
|
283
|
+
token << "%.6x" % ((rand(256)*1000000 + time.usec) % 16777216 )
|
284
|
+
# [4, 4, 4, 12].each{ |count| token << "-#{random(count)}" } # UUID like
|
285
|
+
token << random(28)
|
286
|
+
token
|
287
|
+
end
|
288
|
+
|
289
|
+
# Generates a random sequence.
|
290
|
+
#
|
291
|
+
# @param [Integer] size The length of the random string.
|
292
|
+
# @param [Hash] options A set of options
|
293
|
+
# @option options [Integer] :base (is 16 by default to generate HEX output)
|
294
|
+
# @option options [Integer] :offset (is 0 by default)
|
295
|
+
#
|
296
|
+
# @return [String] A random string.
|
297
|
+
#
|
298
|
+
# @example
|
299
|
+
# Utils::random(28) #=> "d8b2292c8de43256b6eaf91129d3" # (0-9 + a-f)
|
300
|
+
#
|
301
|
+
# @example
|
302
|
+
# Utils::random(28, :base => 26, :offset => 10) #=> "jaunwhhdameatxilyavsnnnwpets" # (a-z)
|
303
|
+
#
|
304
|
+
# @example
|
305
|
+
# Utils::random(28, :base => 10) #=> "4330946481889419283880628515" # (0-9)
|
306
|
+
#
|
307
|
+
def self.random(size=1, options={})
|
308
|
+
options[:base] ||= 16
|
309
|
+
options[:offset] ||= 0
|
310
|
+
result = ''
|
311
|
+
size.times{ result << (rand(options[:base]) + options[:offset]).to_s(options[:base] + options[:offset]) }
|
312
|
+
result
|
313
|
+
end
|
314
|
+
|
315
|
+
# Arrayifies the given object unless it is an Array already.
|
316
|
+
#
|
317
|
+
# @param [Object] object Any possible object.
|
318
|
+
#
|
319
|
+
# @return [Array] It wraps the given object into Array.
|
320
|
+
#
|
321
|
+
def self.arrayify(object)
|
322
|
+
object.is_a?(Array) ? object : [ object ]
|
323
|
+
end
|
324
|
+
|
325
|
+
# De-Arrayifies the given object.
|
326
|
+
#
|
327
|
+
# @param [Object] object Any possible object.
|
328
|
+
#
|
329
|
+
# @return [Object] If the object is not an Array instance it just returns the object.
|
330
|
+
# But if it is an Array the method returns the first element of the array.
|
331
|
+
#
|
332
|
+
def self.dearrayify(object)
|
333
|
+
object.is_a?(Array) ? object.first : object
|
334
|
+
end
|
335
|
+
|
336
|
+
# Returns an XML parser by its string name.
|
337
|
+
# If the name is empty it returns the default parser (Parser::Sax).
|
338
|
+
#
|
339
|
+
# @param [String] xml_parser_name The name of the parser ('sax' or 'rexml').
|
340
|
+
#
|
341
|
+
# @return [Class] The parser class
|
342
|
+
#
|
343
|
+
# @raise [RightScale::CloudApi::Error] When an unexpected name is passed.
|
344
|
+
#
|
345
|
+
def self.get_xml_parser_class(xml_parser_name)
|
346
|
+
case xml_parser_name.to_s.strip
|
347
|
+
when 'sax','' then Parser::Sax # the default one
|
348
|
+
when 'rexml' then Parser::ReXml
|
349
|
+
else fail Error::new("Unknown parser: #{xml_parser_name.inspect}")
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
# Get attribute value(s) or try to inherit it from superclasses.
|
354
|
+
#
|
355
|
+
# @param [Class] klass The source class.
|
356
|
+
# @param [String,Symbol] attribute The name of the attribute reader.
|
357
|
+
# @param [Array] values Some extra data to be returned with the calculated results.
|
358
|
+
#
|
359
|
+
# @return [Array] An array of values in the next formt:
|
360
|
+
# [ ..., SuperSuperClass.attribute, ..., self.attribute, values[0], ..., values[last]]
|
361
|
+
#
|
362
|
+
# @yield [Any] The block is called with "current attribute" so the block can modify it.
|
363
|
+
# @yieldreturn [Any] When block is passed then it should return a "modified" attribute.
|
364
|
+
#
|
365
|
+
def self.inheritance_chain(klass, attribute, *values, &block) # :nodoc:
|
366
|
+
chain, origin = [], klass
|
367
|
+
while origin.respond_to?(attribute) do
|
368
|
+
values.unshift(origin.__send__(attribute))
|
369
|
+
origin = origin.superclass
|
370
|
+
end
|
371
|
+
values.each do |value|
|
372
|
+
value = block ? block.call(value) : value
|
373
|
+
chain << value if value
|
374
|
+
end
|
375
|
+
chain
|
376
|
+
end
|
377
|
+
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
data/lib/base/manager.rb
ADDED
@@ -0,0 +1,122 @@
|
|
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
|
+
# Standard error
|
28
|
+
class Error < StandardError
|
29
|
+
end
|
30
|
+
|
31
|
+
# Cache hit error
|
32
|
+
class CacheHit < Error
|
33
|
+
end
|
34
|
+
|
35
|
+
# Cloud specific errors should be raised with this guy
|
36
|
+
class CloudError < Error
|
37
|
+
end
|
38
|
+
|
39
|
+
# Low level connection errors
|
40
|
+
class ConnectionError < CloudError
|
41
|
+
end
|
42
|
+
|
43
|
+
# HTTP Error
|
44
|
+
class HttpError < CloudError
|
45
|
+
attr_reader :code
|
46
|
+
|
47
|
+
def initialize(code, message)
|
48
|
+
@code = code.to_s
|
49
|
+
super(message)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Missing Query API pattern Error
|
54
|
+
class PatternNotFoundError < Error
|
55
|
+
end
|
56
|
+
|
57
|
+
# Retry Attempt exception
|
58
|
+
class RetryAttempt < Error # :nodoc:
|
59
|
+
end
|
60
|
+
|
61
|
+
# The class is the parent class for all the cloud based thread-safe managers.
|
62
|
+
#
|
63
|
+
# The main purpose of the manager is to check if the current thread or fiber has a thread-unsafe
|
64
|
+
# ApiManager instance created or not. If not them the manager creates than instance of ApiManager
|
65
|
+
# in the current thread/fiber and feeds the method and parameters to it.
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
#
|
69
|
+
# module RightScale
|
70
|
+
# module CloudApi
|
71
|
+
# module MyCoolCloudNamespace
|
72
|
+
# class Manager < CloudApi::Manager
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
# end
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# # Create an instance of MyCoolCloudNamespace manager.
|
79
|
+
# my_cloud = RightScale::CloudApi::MyCoolCloudNamespace::Manager.new(my_cool_creds)
|
80
|
+
#
|
81
|
+
# # Make an API call.
|
82
|
+
# # The call below creates an instance of RightScale::CloudApi::YourCoolCloudNamespace::ApiManager in the
|
83
|
+
# # current thread and invokes "ListMyCoolResources" method on it.
|
84
|
+
# my_cloud.ListMyCoolResources #=> cloud response
|
85
|
+
#
|
86
|
+
# @see ApiManager
|
87
|
+
#
|
88
|
+
class Manager
|
89
|
+
|
90
|
+
# The initializer.
|
91
|
+
#
|
92
|
+
# @param [Any] args Usually a set of credentials.
|
93
|
+
#
|
94
|
+
# @yield [Any] Optional: the block will be passed to ApiManager on its initialization.
|
95
|
+
#
|
96
|
+
def initialize(*args, &block)
|
97
|
+
@args, @block = args, block
|
98
|
+
options = args.last.is_a?(Hash) ? args.last : {}
|
99
|
+
@api_manager_class = options[:api_manager_class] || self.class.name.sub(/::Manager$/, '::ApiManager')._constantize
|
100
|
+
@api_manager_storage = {}
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns the an instance of ApiManager for the current thread/fiber.
|
104
|
+
# The method creates a new ApiManager instance ubless it exist.
|
105
|
+
#
|
106
|
+
# @return [..::MyCoolCloudNamespace::ApiManager]
|
107
|
+
#
|
108
|
+
def api_manager
|
109
|
+
# Delete dead threads and their managers from the list.
|
110
|
+
Utils::remove_dead_fibers_and_threads_from_storage(@api_manager_storage)
|
111
|
+
@api_manager_storage[Utils::current_thread_and_fiber] ||= @api_manager_class::new(*@args, &@block)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Feeds all unknown methods to the ApiManager instance.
|
115
|
+
#
|
116
|
+
def method_missing(m, *args, &block)
|
117
|
+
api_manager.__send__(m, *args, &block)
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,38 @@
|
|
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
|
+
require "json"
|
25
|
+
|
26
|
+
module RightScale
|
27
|
+
module CloudApi
|
28
|
+
module Parser
|
29
|
+
|
30
|
+
class Json
|
31
|
+
def self.parse(text, options = {})
|
32
|
+
text && ::JSON::parse(text)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,36 @@
|
|
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
|
+
module Parser
|
27
|
+
|
28
|
+
class Plain
|
29
|
+
def self.parse(text, options = {})
|
30
|
+
text
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,83 @@
|
|
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
|
+
require "rexml/document"
|
25
|
+
|
26
|
+
module RightScale
|
27
|
+
module CloudApi
|
28
|
+
module Parser
|
29
|
+
|
30
|
+
class ReXml
|
31
|
+
def self.parse(body, options = {})
|
32
|
+
parse_rexml_node ::REXML::Document::new(body).root
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.parse_rexml_node(node)
|
36
|
+
attributes = {}
|
37
|
+
children = {}
|
38
|
+
text = ''
|
39
|
+
|
40
|
+
# Parse Attributes
|
41
|
+
node.attributes.each do |name, value|
|
42
|
+
attributes["@#{name}"] = value
|
43
|
+
end
|
44
|
+
|
45
|
+
# Parse child nodes
|
46
|
+
node.each_element do |child|
|
47
|
+
if child.has_elements? || child.has_text?
|
48
|
+
response = parse_rexml_node(child)
|
49
|
+
unless children["#{child.name}"]
|
50
|
+
# This is a first child - keep it as is
|
51
|
+
children["#{child.name}"] = response
|
52
|
+
else
|
53
|
+
# This is a second+ child: make sure we put them in an Array
|
54
|
+
children["#{child.name}"] = [ children["#{child.name}"] ] unless children["#{child.name}"].is_a?(Array)
|
55
|
+
children["#{child.name}"] << response
|
56
|
+
end
|
57
|
+
else
|
58
|
+
# Don't lose blank elements
|
59
|
+
children["#{child.name}"] = nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Parse Text
|
64
|
+
text << node.texts.join('')
|
65
|
+
|
66
|
+
# Merge results
|
67
|
+
if attributes._blank? && children._blank?
|
68
|
+
result = text._blank? ? nil : text
|
69
|
+
else
|
70
|
+
result = attributes.merge(children)
|
71
|
+
result.merge!("@@text" => text) unless text._blank?
|
72
|
+
end
|
73
|
+
|
74
|
+
# Build a root key when necessary
|
75
|
+
result = { node.name => result } if node.parent.is_a?(REXML::Document)
|
76
|
+
|
77
|
+
result
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|