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