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