Fixed extlib/activesupport conflict. Seriously people, thou shalt not monkey-patch!
[arvados.git] / lib / google / api_client / discovery.rb
1 # Copyright 2010 Google Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #      http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 require 'json'
16 require 'addressable/uri'
17 require 'addressable/template'
18
19 require 'google/inflection'
20
21 module Google
22   class APIClient
23     ##
24     # An exception that is raised if a method is called with missing or
25     # invalid parameter values.
26     class ValidationError < StandardError
27     end
28
29     ##
30     # A service that has been described by a discovery document.
31     class Service
32
33       ##
34       # Creates a description of a particular version of a service.
35       #
36       # @param [String] service_name
37       #   The identifier for the service.  Note that while this frequently
38       #   matches the first segment of all of the service's RPC names, this
39       #   should not be assumed.  There is no requirement that these match.
40       # @param [String] service_version
41       #   The identifier for the service version.
42       # @param [Hash] service_description
43       #   The section of the discovery document that applies to this service
44       #   version.
45       #
46       # @return [Google::APIClient::Service] The constructed service object.
47       def initialize(service_name, service_version, service_description)
48         @name = service_name
49         @version = service_version
50         @description = service_description
51         metaclass = (class <<self; self; end)
52         self.resources.each do |resource|
53           method_name = Google::INFLECTOR.underscore(resource.name).to_sym
54           if !self.respond_to?(method_name)
55             metaclass.send(:define_method, method_name) { resource }
56           end
57         end
58         self.methods.each do |method|
59           method_name = Google::INFLECTOR.underscore(method.name).to_sym
60           if !self.respond_to?(method_name)
61             metaclass.send(:define_method, method_name) { method }
62           end
63         end
64       end
65
66       ##
67       # Returns the identifier for the service.
68       #
69       # @return [String] The service identifier.
70       attr_reader :name
71
72       ##
73       # Returns the version of the service.
74       #
75       # @return [String] The service version.
76       attr_reader :version
77
78       ##
79       # Returns the parsed section of the discovery document that applies to
80       # this version of the service.
81       #
82       # @return [Hash] The service description.
83       attr_reader :description
84
85       ##
86       # Returns the base URI for this version of the service.
87       #
88       # @return [Addressable::URI] The base URI that methods are joined to.
89       def base
90         return @base ||= Addressable::URI.parse(self.description['baseUrl'])
91       end
92
93       ##
94       # Updates the hierarchy of resources and methods with the new base.
95       #
96       # @param [Addressable::URI, #to_str, String] new_base
97       #   The new base URI to use for the service.
98       def base=(new_base)
99         @base = Addressable::URI.parse(new_base)
100         self.resources.each do |resource|
101           resource.base = @base
102         end
103         self.methods.each do |method|
104           method.base = @base
105         end
106       end
107
108       ##
109       # A list of resources available at the root level of this version of the
110       # service.
111       #
112       # @return [Array] A list of {Google::APIClient::Resource} objects.
113       def resources
114         return @resources ||= (
115           (self.description['resources'] || []).inject([]) do |accu, (k, v)|
116             accu << ::Google::APIClient::Resource.new(self.base, k, v)
117             accu
118           end
119         )
120       end
121
122       ##
123       # A list of methods available at the root level of this version of the
124       # service.
125       #
126       # @return [Array] A list of {Google::APIClient::Method} objects.
127       def methods
128         return @methods ||= (
129           (self.description['methods'] || []).inject([]) do |accu, (k, v)|
130             accu << ::Google::APIClient::Method.new(self.base, k, v)
131             accu
132           end
133         )
134       end
135
136       ##
137       # Converts the service to a flat mapping of RPC names and method objects.
138       #
139       # @return [Hash] All methods available on the service.
140       #
141       # @example
142       #   # Discover available methods
143       #   method_names = client.discovered_service('buzz').to_h.keys
144       def to_h
145         return @hash ||= (begin
146           methods_hash = {}
147           self.methods.each do |method|
148             methods_hash[method.rpc_name] = method
149           end
150           self.resources.each do |resource|
151             methods_hash.merge!(resource.to_h)
152           end
153           methods_hash
154         end)
155       end
156
157       ##
158       # Compares two versions of a service.
159       #
160       # @param [Object] other The service to compare.
161       #
162       # @return [Integer]
163       #   <code>-1</code> if the service is older than <code>other</code>.
164       #   <code>0</code> if the service is the same as <code>other</code>.
165       #   <code>1</code> if the service is newer than <code>other</code>.
166       #   <code>nil</code> if the service cannot be compared to
167       #   <code>other</code>.
168       def <=>(other)
169         # We can only compare versions of the same service
170         if other.kind_of?(self.class) && self.name == other.name
171           split_version = lambda do |version|
172             dotted_version = version[/^v?(\d+(.\d+)*)-?(.*?)?$/, 1]
173             suffix = version[/^v?(\d+(.\d+)*)-?(.*?)?$/, 3]
174             if dotted_version && suffix
175               [dotted_version.split('.').map { |v| v.to_i }, suffix]
176             else
177               [[-1], version]
178             end
179           end
180           self_sortable, self_suffix = split_version.call(self.version)
181           other_sortable, other_suffix = split_version.call(other.version)
182           result = self_sortable <=> other_sortable
183           if result != 0
184             return result
185           # If the dotted versions are equal, check the suffix.
186           # An omitted suffix should be sorted after an included suffix.
187           elsif self_suffix == ''
188             return 1
189           elsif other_suffix == ''
190             return -1
191           else
192             return self_suffix <=> other_suffix
193           end
194         else
195           return nil
196         end
197       end
198
199       ##
200       # Returns a <code>String</code> representation of the service's state.
201       #
202       # @return [String] The service's state, as a <code>String</code>.
203       def inspect
204         sprintf(
205           "#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.name
206         )
207       end
208     end
209
210     ##
211     # A resource that has been described by a discovery document.
212     class Resource
213
214       ##
215       # Creates a description of a particular version of a resource.
216       #
217       # @param [Addressable::URI] base
218       #   The base URI for the service.
219       # @param [String] resource_name
220       #   The identifier for the resource.
221       # @param [Hash] resource_description
222       #   The section of the discovery document that applies to this resource.
223       #
224       # @return [Google::APIClient::Resource] The constructed resource object.
225       def initialize(base, resource_name, resource_description)
226         @base = base
227         @name = resource_name
228         @description = resource_description
229         metaclass = (class <<self; self; end)
230         self.resources.each do |resource|
231           method_name = Google::INFLECTOR.underscore(resource.name).to_sym
232           if !self.respond_to?(method_name)
233             metaclass.send(:define_method, method_name) { resource }
234           end
235         end
236         self.methods.each do |method|
237           method_name = Google::INFLECTOR.underscore(method.name).to_sym
238           if !self.respond_to?(method_name)
239             metaclass.send(:define_method, method_name) { method }
240           end
241         end
242       end
243
244       ##
245       # Returns the identifier for the resource.
246       #
247       # @return [String] The resource identifier.
248       attr_reader :name
249
250       ##
251       # Returns the parsed section of the discovery document that applies to
252       # this resource.
253       #
254       # @return [Hash] The resource description.
255       attr_reader :description
256
257       ##
258       # Returns the base URI for this resource.
259       #
260       # @return [Addressable::URI] The base URI that methods are joined to.
261       attr_reader :base
262
263       ##
264       # Updates the hierarchy of resources and methods with the new base.
265       #
266       # @param [Addressable::URI, #to_str, String] new_base
267       #   The new base URI to use for the resource.
268       def base=(new_base)
269         @base = Addressable::URI.parse(new_base)
270         self.resources.each do |resource|
271           resource.base = @base
272         end
273         self.methods.each do |method|
274           method.base = @base
275         end
276       end
277
278       ##
279       # A list of sub-resources available on this resource.
280       #
281       # @return [Array] A list of {Google::APIClient::Resource} objects.
282       def resources
283         return @resources ||= (
284           (self.description['resources'] || []).inject([]) do |accu, (k, v)|
285             accu << ::Google::APIClient::Resource.new(self.base, k, v)
286             accu
287           end
288         )
289       end
290
291       ##
292       # A list of methods available on this resource.
293       #
294       # @return [Array] A list of {Google::APIClient::Method} objects.
295       def methods
296         return @methods ||= (
297           (self.description['methods'] || []).inject([]) do |accu, (k, v)|
298             accu << ::Google::APIClient::Method.new(self.base, k, v)
299             accu
300           end
301         )
302       end
303
304       ##
305       # Converts the resource to a flat mapping of RPC names and method
306       # objects.
307       #
308       # @return [Hash] All methods available on the resource.
309       def to_h
310         return @hash ||= (begin
311           methods_hash = {}
312           self.methods.each do |method|
313             methods_hash[method.rpc_name] = method
314           end
315           self.resources.each do |resource|
316             methods_hash.merge!(resource.to_h)
317           end
318           methods_hash
319         end)
320       end
321
322       ##
323       # Returns a <code>String</code> representation of the resource's state.
324       #
325       # @return [String] The resource's state, as a <code>String</code>.
326       def inspect
327         sprintf(
328           "#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.name
329         )
330       end
331     end
332
333     ##
334     # A method that has been described by a discovery document.
335     class Method
336
337       ##
338       # Creates a description of a particular method.
339       #
340       # @param [Addressable::URI] base
341       #   The base URI for the service.
342       # @param [String] method_name
343       #   The identifier for the method.
344       # @param [Hash] method_description
345       #   The section of the discovery document that applies to this method.
346       #
347       # @return [Google::APIClient::Method] The constructed method object.
348       def initialize(base, method_name, method_description)
349         @base = base
350         @name = method_name
351         @description = method_description
352       end
353
354       ##
355       # Returns the identifier for the method.
356       #
357       # @return [String] The method identifier.
358       attr_reader :name
359
360       ##
361       # Returns the parsed section of the discovery document that applies to
362       # this method.
363       #
364       # @return [Hash] The method description.
365       attr_reader :description
366
367       ##
368       # Returns the base URI for the method.
369       #
370       # @return [Addressable::URI]
371       #   The base URI that this method will be joined to.
372       attr_reader :base
373
374       ##
375       # Updates the method with the new base.
376       #
377       # @param [Addressable::URI, #to_str, String] new_base
378       #   The new base URI to use for the method.
379       def base=(new_base)
380         @base = Addressable::URI.parse(new_base)
381         @uri_template = nil
382       end
383
384       ##
385       # Returns the RPC name for the method.
386       #
387       # @return [String] The RPC name.
388       def rpc_name
389         return self.description['rpcName']
390       end
391
392       ##
393       # Returns the URI template for the method.  A parameter list can be
394       # used to expand this into a URI.
395       #
396       # @return [Addressable::Template] The URI template.
397       def uri_template
398         # TODO(bobaman) We shouldn't be calling #to_s here, this should be
399         # a join operation on a URI, but we have to treat these as Strings
400         # because of the way the discovery document provides the URIs.
401         # This should be fixed soon.
402         return @uri_template ||=
403           Addressable::Template.new(base.to_s + self.description['pathUrl'])
404       end
405
406       ##
407       # Normalizes parameters, converting to the appropriate types.
408       #
409       # @param [Hash, Array] parameters
410       #   The parameters to normalize.
411       #
412       # @return [Hash] The normalized parameters.
413       def normalize_parameters(parameters={})
414         # Convert keys to Strings when appropriate
415         if parameters.kind_of?(Hash) || parameters.kind_of?(Array)
416           # Is a Hash or an Array a better return type?  Do we ever need to
417           # worry about the same parameter being sent twice with different
418           # values?
419           parameters = parameters.inject({}) do |accu, (k, v)|
420             k = k.to_s if k.kind_of?(Symbol)
421             k = k.to_str if k.respond_to?(:to_str)
422             unless k.kind_of?(String)
423               raise TypeError, "Expected String, got #{k.class}."
424             end
425             accu[k] = v
426             accu
427           end
428         else
429           raise TypeError,
430             "Expected Hash or Array, got #{parameters.class}."
431         end
432         return parameters
433       end
434
435       ##
436       # Expands the method's URI template using a parameter list.
437       #
438       # @param [Hash, Array] parameters
439       #   The parameter list to use.
440       #
441       # @return [Addressable::URI] The URI after expansion.
442       def generate_uri(parameters={})
443         parameters = self.normalize_parameters(parameters)
444         self.validate_parameters(parameters)
445         template_variables = self.uri_template.variables
446         uri = self.uri_template.expand(parameters)
447         query_parameters = parameters.reject do |k, v|
448           template_variables.include?(k)
449         end
450         if query_parameters.size > 0
451           uri.query_values = (uri.query_values || {}).merge(query_parameters)
452         end
453         # Normalization is necessary because of undesirable percent-escaping
454         # during URI template expansion
455         return uri.normalize
456       end
457
458       ##
459       # Generates an HTTP request for this method.
460       #
461       # @param [Hash, Array] parameters
462       #   The parameters to send.
463       # @param [String, StringIO] body The body for the HTTP request.
464       # @param [Hash, Array] headers The HTTP headers for the request.
465       #
466       # @return [Array] The generated HTTP request.
467       def generate_request(parameters={}, body='', headers=[])
468         if body.respond_to?(:string)
469           body = body.string
470         elsif body.respond_to?(:to_str)
471           body = body.to_str
472         else
473           raise TypeError, "Expected String or StringIO, got #{body.class}."
474         end
475         if !headers.kind_of?(Array) && !headers.kind_of?(Hash)
476           raise TypeError, "Expected Hash or Array, got #{headers.class}."
477         end
478         method = self.description['httpMethod'] || 'GET'
479         uri = self.generate_uri(parameters)
480         headers = headers.to_a if headers.kind_of?(Hash)
481         return [method, uri.to_str, headers, [body]]
482       end
483
484       ##
485       # Returns a <code>Hash</code> of the parameter descriptions for
486       # this method.
487       #
488       # @return [Hash] The parameter descriptions.
489       def parameter_descriptions
490         @parameter_descriptions ||= (
491           self.description['parameters'] || {}
492         ).inject({}) { |h,(k,v)| h[k]=v; h }
493       end
494
495       ##
496       # Returns an <code>Array</code> of the parameters for this method.
497       #
498       # @return [Array] The parameters.
499       def parameters
500         @parameters ||= ((
501           self.description['parameters'] || {}
502         ).inject({}) { |h,(k,v)| h[k]=v; h }).keys
503       end
504
505       ##
506       # Returns an <code>Array</code> of the required parameters for this
507       # method.
508       #
509       # @return [Array] The required parameters.
510       #
511       # @example
512       #   # A list of all required parameters.
513       #   method.required_parameters
514       def required_parameters
515         @required_parameters ||= ((self.parameter_descriptions.select do |k, v|
516           v['required']
517         end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
518       end
519
520       ##
521       # Returns an <code>Array</code> of the optional parameters for this
522       # method.
523       #
524       # @return [Array] The optional parameters.
525       #
526       # @example
527       #   # A list of all optional parameters.
528       #   method.optional_parameters
529       def optional_parameters
530         @optional_parameters ||= ((self.parameter_descriptions.reject do |k, v|
531           v['required']
532         end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
533       end
534
535       ##
536       # Verifies that the parameters are valid for this method.  Raises an
537       # exception if validation fails.
538       #
539       # @param [Hash, Array] parameters
540       #   The parameters to verify.
541       #
542       # @return [NilClass] <code>nil</code> if validation passes.
543       def validate_parameters(parameters={})
544         parameters = self.normalize_parameters(parameters)
545         required_variables = ((self.parameter_descriptions.select do |k, v|
546           v['required']
547         end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
548         missing_variables = required_variables - parameters.keys
549         if missing_variables.size > 0
550           raise ArgumentError,
551             "Missing required parameters: #{missing_variables.join(', ')}."
552         end
553         parameters.each do |k, v|
554           if self.parameter_descriptions[k]
555             pattern = self.parameter_descriptions[k]['pattern']
556             if pattern
557               regexp = Regexp.new("^#{pattern}$")
558               if v !~ regexp
559                 raise ArgumentError,
560                   "Parameter '#{k}' has an invalid value: #{v}. " +
561                   "Must match: /^#{pattern}$/."
562               end
563             end
564           end
565         end
566         return nil
567       end
568
569       ##
570       # Returns a <code>String</code> representation of the method's state.
571       #
572       # @return [String] The method's state, as a <code>String</code>.
573       def inspect
574         sprintf(
575           "#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.rpc_name
576         )
577       end
578     end
579   end
580 end