Merge branch 'schemas'
[arvados.git] / lib / google / api_client / discovery / method.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 'addressable/uri'
17 require 'addressable/template'
18
19 require 'google/api_client/errors'
20
21 module Google
22   class APIClient
23     ##
24     # A method that has been described by a discovery document.
25     class Method
26
27       ##
28       # Creates a description of a particular method.
29       #
30       # @param [Addressable::URI] method_base
31       #   The base URI for the service.
32       # @param [String] method_name
33       #   The identifier for the method.
34       # @param [Hash] method_description
35       #   The section of the discovery document that applies to this method.
36       #
37       # @return [Google::APIClient::Method] The constructed method object.
38       def initialize(api, method_base, method_name, discovery_document)
39         @api = api
40         @method_base = method_base
41         @name = method_name
42         @discovery_document = discovery_document
43       end
44
45       ##
46       # Returns the identifier for the method.
47       #
48       # @return [String] The method identifier.
49       attr_reader :name
50
51       ##
52       # Returns the parsed section of the discovery document that applies to
53       # this method.
54       #
55       # @return [Hash] The method description.
56       attr_reader :description
57
58       ##
59       # Returns the base URI for the method.
60       #
61       # @return [Addressable::URI]
62       #   The base URI that this method will be joined to.
63       attr_reader :method_base
64
65       ##
66       # Updates the method with the new base.
67       #
68       # @param [Addressable::URI, #to_str, String] new_base
69       #   The new base URI to use for the method.
70       def method_base=(new_method_base)
71         @method_base = Addressable::URI.parse(new_method_base)
72         @uri_template = nil
73       end
74
75       ##
76       # Returns the method ID.
77       #
78       # @return [String] The method identifier.
79       def id
80         return @discovery_document['id']
81       end
82
83       ##
84       # Returns the HTTP method or 'GET' if none is specified.
85       #
86       # @return [String] The HTTP method that will be used in the request.
87       def http_method
88         return @discovery_document['httpMethod'] || 'GET'
89       end
90
91       ##
92       # Returns the URI template for the method.  A parameter list can be
93       # used to expand this into a URI.
94       #
95       # @return [Addressable::Template] The URI template.
96       def uri_template
97         # TODO(bobaman) We shouldn't be calling #to_s here, this should be
98         # a join operation on a URI, but we have to treat these as Strings
99         # because of the way the discovery document provides the URIs.
100         # This should be fixed soon.
101         return @uri_template ||= Addressable::Template.new(
102           self.method_base + @discovery_document['path']
103         )
104       end
105
106       ##
107       # Returns the Schema object for the method's request, if any.
108       #
109       # @return [Google::APIClient::Schema] The request schema.
110       def request_schema
111         if @discovery_document['request']
112           schema_name = @discovery_document['request']['$ref']
113           return @api.schemas[schema_name]
114         else
115           return nil
116         end
117       end
118
119       ##
120       # Returns the Schema object for the method's response, if any.
121       #
122       # @return [Google::APIClient::Schema] The response schema.
123       def response_schema
124         if @discovery_document['response']
125           schema_name = @discovery_document['response']['$ref']
126           return @api.schemas[schema_name]
127         else
128           return nil
129         end
130       end
131
132       ##
133       # Normalizes parameters, converting to the appropriate types.
134       #
135       # @param [Hash, Array] parameters
136       #   The parameters to normalize.
137       #
138       # @return [Hash] The normalized parameters.
139       def normalize_parameters(parameters={})
140         # Convert keys to Strings when appropriate
141         if parameters.kind_of?(Hash) || parameters.kind_of?(Array)
142           # Is a Hash or an Array a better return type?  Do we ever need to
143           # worry about the same parameter being sent twice with different
144           # values?
145           parameters = parameters.inject({}) do |accu, (k, v)|
146             k = k.to_s if k.kind_of?(Symbol)
147             k = k.to_str if k.respond_to?(:to_str)
148             unless k.kind_of?(String)
149               raise TypeError, "Expected String, got #{k.class}."
150             end
151             accu[k] = v
152             accu
153           end
154         else
155           raise TypeError,
156             "Expected Hash or Array, got #{parameters.class}."
157         end
158         return parameters
159       end
160
161       ##
162       # Expands the method's URI template using a parameter list.
163       #
164       # @param [Hash, Array] parameters
165       #   The parameter list to use.
166       #
167       # @return [Addressable::URI] The URI after expansion.
168       def generate_uri(parameters={})
169         parameters = self.normalize_parameters(parameters)
170         self.validate_parameters(parameters)
171         template_variables = self.uri_template.variables
172         uri = self.uri_template.expand(parameters)
173         query_parameters = parameters.reject do |k, v|
174           template_variables.include?(k)
175         end
176         if query_parameters.size > 0
177           uri.query_values = (uri.query_values || {}).merge(query_parameters)
178         end
179         # Normalization is necessary because of undesirable percent-escaping
180         # during URI template expansion
181         return uri.normalize
182       end
183
184       ##
185       # Generates an HTTP request for this method.
186       #
187       # @param [Hash, Array] parameters
188       #   The parameters to send.
189       # @param [String, StringIO] body The body for the HTTP request.
190       # @param [Hash, Array] headers The HTTP headers for the request.
191       #
192       # @return [Array] The generated HTTP request.
193       def generate_request(parameters={}, body='', headers=[])
194         if body.respond_to?(:string)
195           body = body.string
196         elsif body.respond_to?(:to_str)
197           body = body.to_str
198         else
199           raise TypeError, "Expected String or StringIO, got #{body.class}."
200         end
201         if !headers.kind_of?(Array) && !headers.kind_of?(Hash)
202           raise TypeError, "Expected Hash or Array, got #{headers.class}."
203         end
204         method = self.http_method
205         uri = self.generate_uri(parameters)
206         headers = headers.to_a if headers.kind_of?(Hash)
207         return [method, uri.to_str, headers, [body]]
208       end
209
210       ##
211       # Returns a <code>Hash</code> of the parameter descriptions for
212       # this method.
213       #
214       # @return [Hash] The parameter descriptions.
215       def parameter_descriptions
216         @parameter_descriptions ||= (
217           @discovery_document['parameters'] || {}
218         ).inject({}) { |h,(k,v)| h[k]=v; h }
219       end
220
221       ##
222       # Returns an <code>Array</code> of the parameters for this method.
223       #
224       # @return [Array] The parameters.
225       def parameters
226         @parameters ||= ((
227           @discovery_document['parameters'] || {}
228         ).inject({}) { |h,(k,v)| h[k]=v; h }).keys
229       end
230
231       ##
232       # Returns an <code>Array</code> of the required parameters for this
233       # method.
234       #
235       # @return [Array] The required parameters.
236       #
237       # @example
238       #   # A list of all required parameters.
239       #   method.required_parameters
240       def required_parameters
241         @required_parameters ||= ((self.parameter_descriptions.select do |k, v|
242           v['required']
243         end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
244       end
245
246       ##
247       # Returns an <code>Array</code> of the optional parameters for this
248       # method.
249       #
250       # @return [Array] The optional parameters.
251       #
252       # @example
253       #   # A list of all optional parameters.
254       #   method.optional_parameters
255       def optional_parameters
256         @optional_parameters ||= ((self.parameter_descriptions.reject do |k, v|
257           v['required']
258         end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
259       end
260
261       ##
262       # Verifies that the parameters are valid for this method.  Raises an
263       # exception if validation fails.
264       #
265       # @param [Hash, Array] parameters
266       #   The parameters to verify.
267       #
268       # @return [NilClass] <code>nil</code> if validation passes.
269       def validate_parameters(parameters={})
270         parameters = self.normalize_parameters(parameters)
271         required_variables = ((self.parameter_descriptions.select do |k, v|
272           v['required']
273         end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
274         missing_variables = required_variables - parameters.keys
275         if missing_variables.size > 0
276           raise ArgumentError,
277             "Missing required parameters: #{missing_variables.join(', ')}."
278         end
279         parameters.each do |k, v|
280           if self.parameter_descriptions[k]
281             enum = self.parameter_descriptions[k]['enum']
282             if enum && !enum.include?(v)
283               raise ArgumentError,
284                 "Parameter '#{k}' has an invalid value: #{v}. " +
285                 "Must be one of #{enum.inspect}."
286             end
287             pattern = self.parameter_descriptions[k]['pattern']
288             if pattern
289               regexp = Regexp.new("^#{pattern}$")
290               if v !~ regexp
291                 raise ArgumentError,
292                   "Parameter '#{k}' has an invalid value: #{v}. " +
293                   "Must match: /^#{pattern}$/."
294               end
295             end
296           end
297         end
298         return nil
299       end
300
301       ##
302       # Returns a <code>String</code> representation of the method's state.
303       #
304       # @return [String] The method's state, as a <code>String</code>.
305       def inspect
306         sprintf(
307           "#<%s:%#0x ID:%s>",
308           self.class.to_s, self.object_id, self.id
309         )
310       end
311     end
312   end
313 end