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