Merge pull request #4 from vapir/baseURI_and_fixups
[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         # encode all non-template parameters
176         params = ""
177         unless query_parameters.empty?
178           params = "?" + Addressable::URI.form_encode(query_parameters)
179         end
180         # Normalization is necessary because of undesirable percent-escaping
181         # during URI template expansion
182         return uri.normalize + params
183       end
184
185       ##
186       # Generates an HTTP request for this method.
187       #
188       # @param [Hash, Array] parameters
189       #   The parameters to send.
190       # @param [String, StringIO] body The body for the HTTP request.
191       # @param [Hash, Array] headers The HTTP headers for the request.
192       #
193       # @return [Array] The generated HTTP request.
194       def generate_request(parameters={}, body='', headers=[])
195         if body.respond_to?(:string)
196           body = body.string
197         elsif body.respond_to?(:to_str)
198           body = body.to_str
199         else
200           raise TypeError, "Expected String or StringIO, got #{body.class}."
201         end
202         if !headers.kind_of?(Array) && !headers.kind_of?(Hash)
203           raise TypeError, "Expected Hash or Array, got #{headers.class}."
204         end
205         method = self.http_method
206         uri = self.generate_uri(parameters)
207         headers = headers.to_a if headers.kind_of?(Hash)
208         return Faraday::Request.create(method.to_s.downcase.to_sym) do |req|
209           req.url(Addressable::URI.parse(uri))
210           req.headers = Faraday::Utils::Headers.new(headers)
211           req.body = body
212         end
213       end
214
215       ##
216       # Returns a <code>Hash</code> of the parameter descriptions for
217       # this method.
218       #
219       # @return [Hash] The parameter descriptions.
220       def parameter_descriptions
221         @parameter_descriptions ||= (
222           @discovery_document['parameters'] || {}
223         ).inject({}) { |h,(k,v)| h[k]=v; h }
224       end
225
226       ##
227       # Returns an <code>Array</code> of the parameters for this method.
228       #
229       # @return [Array] The parameters.
230       def parameters
231         @parameters ||= ((
232           @discovery_document['parameters'] || {}
233         ).inject({}) { |h,(k,v)| h[k]=v; h }).keys
234       end
235
236       ##
237       # Returns an <code>Array</code> of the required parameters for this
238       # method.
239       #
240       # @return [Array] The required parameters.
241       #
242       # @example
243       #   # A list of all required parameters.
244       #   method.required_parameters
245       def required_parameters
246         @required_parameters ||= ((self.parameter_descriptions.select do |k, v|
247           v['required']
248         end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
249       end
250
251       ##
252       # Returns an <code>Array</code> of the optional parameters for this
253       # method.
254       #
255       # @return [Array] The optional parameters.
256       #
257       # @example
258       #   # A list of all optional parameters.
259       #   method.optional_parameters
260       def optional_parameters
261         @optional_parameters ||= ((self.parameter_descriptions.reject do |k, v|
262           v['required']
263         end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
264       end
265
266       ##
267       # Verifies that the parameters are valid for this method.  Raises an
268       # exception if validation fails.
269       #
270       # @param [Hash, Array] parameters
271       #   The parameters to verify.
272       #
273       # @return [NilClass] <code>nil</code> if validation passes.
274       def validate_parameters(parameters={})
275         parameters = self.normalize_parameters(parameters)
276         required_variables = ((self.parameter_descriptions.select do |k, v|
277           v['required']
278         end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
279         missing_variables = required_variables - parameters.map(&:first)
280         if missing_variables.size > 0
281           raise ArgumentError,
282             "Missing required parameters: #{missing_variables.join(', ')}."
283         end
284         parameters.each do |k, v|
285           if self.parameter_descriptions[k]
286             enum = self.parameter_descriptions[k]['enum']
287             if enum && !enum.include?(v)
288               raise ArgumentError,
289                 "Parameter '#{k}' has an invalid value: #{v}. " +
290                 "Must be one of #{enum.inspect}."
291             end
292             pattern = self.parameter_descriptions[k]['pattern']
293             if pattern
294               regexp = Regexp.new("^#{pattern}$")
295               if v !~ regexp
296                 raise ArgumentError,
297                   "Parameter '#{k}' has an invalid value: #{v}. " +
298                   "Must match: /^#{pattern}$/."
299               end
300             end
301           end
302         end
303         return nil
304       end
305
306       ##
307       # Returns a <code>String</code> representation of the method's state.
308       #
309       # @return [String] The method's state, as a <code>String</code>.
310       def inspect
311         sprintf(
312           "#<%s:%#0x ID:%s>",
313           self.class.to_s, self.object_id, self.id
314         )
315       end
316     end
317   end
318 end