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