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