right_cloud_api_base 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY +2 -0
  3. data/LICENSE +19 -0
  4. data/README.md +14 -0
  5. data/Rakefile +37 -0
  6. data/lib/base/api_manager.rb +707 -0
  7. data/lib/base/helpers/cloud_api_logger.rb +214 -0
  8. data/lib/base/helpers/http_headers.rb +239 -0
  9. data/lib/base/helpers/http_parent.rb +103 -0
  10. data/lib/base/helpers/http_request.rb +173 -0
  11. data/lib/base/helpers/http_response.rb +122 -0
  12. data/lib/base/helpers/net_http_patch.rb +31 -0
  13. data/lib/base/helpers/query_api_patterns.rb +862 -0
  14. data/lib/base/helpers/support.rb +270 -0
  15. data/lib/base/helpers/support.xml.rb +306 -0
  16. data/lib/base/helpers/utils.rb +380 -0
  17. data/lib/base/manager.rb +122 -0
  18. data/lib/base/parsers/json.rb +38 -0
  19. data/lib/base/parsers/plain.rb +36 -0
  20. data/lib/base/parsers/rexml.rb +83 -0
  21. data/lib/base/parsers/sax.rb +200 -0
  22. data/lib/base/routines/cache_validator.rb +184 -0
  23. data/lib/base/routines/connection_proxies/net_http_persistent_proxy.rb +194 -0
  24. data/lib/base/routines/connection_proxies/right_http_connection_proxy.rb +224 -0
  25. data/lib/base/routines/connection_proxy.rb +66 -0
  26. data/lib/base/routines/request_analyzer.rb +122 -0
  27. data/lib/base/routines/request_generator.rb +48 -0
  28. data/lib/base/routines/request_initializer.rb +52 -0
  29. data/lib/base/routines/response_analyzer.rb +152 -0
  30. data/lib/base/routines/response_parser.rb +79 -0
  31. data/lib/base/routines/result_wrapper.rb +75 -0
  32. data/lib/base/routines/retry_manager.rb +106 -0
  33. data/lib/base/routines/routine.rb +98 -0
  34. data/lib/right_cloud_api_base.rb +72 -0
  35. data/lib/right_cloud_api_base_version.rb +37 -0
  36. data/right_cloud_api_base.gemspec +63 -0
  37. data/spec/helpers/query_api_pattern_spec.rb +312 -0
  38. data/spec/helpers/support_spec.rb +211 -0
  39. data/spec/helpers/support_xml_spec.rb +207 -0
  40. data/spec/helpers/utils_spec.rb +179 -0
  41. data/spec/routines/connection_proxies/test_net_http_persistent_proxy_spec.rb +143 -0
  42. data/spec/routines/test_cache_validator_spec.rb +152 -0
  43. data/spec/routines/test_connection_proxy_spec.rb +44 -0
  44. data/spec/routines/test_request_analyzer_spec.rb +106 -0
  45. data/spec/routines/test_response_analyzer_spec.rb +132 -0
  46. data/spec/routines/test_response_parser_spec.rb +228 -0
  47. data/spec/routines/test_result_wrapper_spec.rb +63 -0
  48. data/spec/routines/test_retry_manager_spec.rb +84 -0
  49. data/spec/spec_helper.rb +15 -0
  50. 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
@@ -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