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