Merge branch 'master' of https://code.google.com/p/google-api-ruby-client
[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         return @uri_template ||= Addressable::Template.new(
99           self.method_base.join(Addressable::URI.parse(@discovery_document['path']))
100         )
101       end
102
103       ##
104       # Returns media upload information for this method, if supported
105       #
106       # @return [Google::APIClient::MediaUpload] Description of upload endpoints
107       def media_upload
108         if @discovery_document['mediaUpload']
109           return @media_upload ||= Google::APIClient::MediaUpload.new(self, self.method_base, @discovery_document['mediaUpload'])
110         else
111           return nil
112         end
113       end
114       
115       ##
116       # Returns the Schema object for the method's request, if any.
117       #
118       # @return [Google::APIClient::Schema] The request schema.
119       def request_schema
120         if @discovery_document['request']
121           schema_name = @discovery_document['request']['$ref']
122           return @api.schemas[schema_name]
123         else
124           return nil
125         end
126       end
127
128       ##
129       # Returns the Schema object for the method's response, if any.
130       #
131       # @return [Google::APIClient::Schema] The response schema.
132       def response_schema
133         if @discovery_document['response']
134           schema_name = @discovery_document['response']['$ref']
135           return @api.schemas[schema_name]
136         else
137           return nil
138         end
139       end
140
141       ##
142       # Normalizes parameters, converting to the appropriate types.
143       #
144       # @param [Hash, Array] parameters
145       #   The parameters to normalize.
146       #
147       # @return [Hash] The normalized parameters.
148       def normalize_parameters(parameters={})
149         # Convert keys to Strings when appropriate
150         if parameters.kind_of?(Hash) || parameters.kind_of?(Array)
151           # Returning an array since parameters can be repeated (ie, Adsense Management API)
152           parameters = parameters.inject([]) do |accu, (k, v)|
153             k = k.to_s if k.kind_of?(Symbol)
154             k = k.to_str if k.respond_to?(:to_str)
155             unless k.kind_of?(String)
156               raise TypeError, "Expected String, got #{k.class}."
157             end
158             accu << [k,v]
159             accu
160           end
161         else
162           raise TypeError,
163             "Expected Hash or Array, got #{parameters.class}."
164         end
165         return parameters
166       end
167
168       ##
169       # Expands the method's URI template using a parameter list.
170       #
171       # @param [Hash, Array] parameters
172       #   The parameter list to use.
173       #
174       # @return [Addressable::URI] The URI after expansion.
175       def generate_uri(parameters={})
176         parameters = self.normalize_parameters(parameters)
177         self.validate_parameters(parameters)
178         template_variables = self.uri_template.variables
179         upload_type = parameters.assoc('uploadType') || parameters.assoc('upload_type')
180         if upload_type
181           unless self.media_upload
182             raise ArgumentException, "Media upload not supported for this method"
183           end
184           case upload_type.last
185           when 'media', 'multipart', 'resumable'
186             uri = self.media_upload.uri_template.expand(parameters)            
187           else
188             raise ArgumentException, "Invalid uploadType '#{upload_type}'"
189           end
190         else
191           uri = self.uri_template.expand(parameters)
192         end
193         query_parameters = parameters.reject do |k, v|
194           template_variables.include?(k)
195         end
196         # encode all non-template parameters
197         params = ""
198         unless query_parameters.empty?
199           params = "?" + Addressable::URI.form_encode(query_parameters)
200         end
201         # Normalization is necessary because of undesirable percent-escaping
202         # during URI template expansion
203         return uri.normalize + params
204       end
205
206       ##
207       # Generates an HTTP request for this method.
208       #
209       # @param [Hash, Array] parameters
210       #   The parameters to send.
211       # @param [String, StringIO] body The body for the HTTP request.
212       # @param [Hash, Array] headers The HTTP headers for the request.
213       #
214       # @return [Array] The generated HTTP request.
215       def generate_request(parameters={}, body='', headers=[])
216         if body.respond_to?(:string)
217           body = body.string
218         elsif body.respond_to?(:to_str)
219           body = body.to_str
220         else
221           raise TypeError, "Expected String or StringIO, got #{body.class}."
222         end
223         if !headers.kind_of?(Array) && !headers.kind_of?(Hash)
224           raise TypeError, "Expected Hash or Array, got #{headers.class}."
225         end
226         method = self.http_method
227         uri = self.generate_uri(parameters)
228         headers = headers.to_a if headers.kind_of?(Hash)
229         return Faraday::Request.create(method.to_s.downcase.to_sym) do |req|
230           req.url(Addressable::URI.parse(uri))
231           req.headers = Faraday::Utils::Headers.new(headers)
232           req.body = body
233         end
234       end
235       
236
237       ##
238       # Returns a <code>Hash</code> of the parameter descriptions for
239       # this method.
240       #
241       # @return [Hash] The parameter descriptions.
242       def parameter_descriptions
243         @parameter_descriptions ||= (
244           @discovery_document['parameters'] || {}
245         ).inject({}) { |h,(k,v)| h[k]=v; h }
246       end
247
248       ##
249       # Returns an <code>Array</code> of the parameters for this method.
250       #
251       # @return [Array] The parameters.
252       def parameters
253         @parameters ||= ((
254           @discovery_document['parameters'] || {}
255         ).inject({}) { |h,(k,v)| h[k]=v; h }).keys
256       end
257
258       ##
259       # Returns an <code>Array</code> of the required parameters for this
260       # method.
261       #
262       # @return [Array] The required parameters.
263       #
264       # @example
265       #   # A list of all required parameters.
266       #   method.required_parameters
267       def required_parameters
268         @required_parameters ||= ((self.parameter_descriptions.select do |k, v|
269           v['required']
270         end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
271       end
272
273       ##
274       # Returns an <code>Array</code> of the optional parameters for this
275       # method.
276       #
277       # @return [Array] The optional parameters.
278       #
279       # @example
280       #   # A list of all optional parameters.
281       #   method.optional_parameters
282       def optional_parameters
283         @optional_parameters ||= ((self.parameter_descriptions.reject do |k, v|
284           v['required']
285         end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
286       end
287
288       ##
289       # Verifies that the parameters are valid for this method.  Raises an
290       # exception if validation fails.
291       #
292       # @param [Hash, Array] parameters
293       #   The parameters to verify.
294       #
295       # @return [NilClass] <code>nil</code> if validation passes.
296       def validate_parameters(parameters={})
297         parameters = self.normalize_parameters(parameters)
298         required_variables = ((self.parameter_descriptions.select do |k, v|
299           v['required']
300         end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
301         missing_variables = required_variables - parameters.map(&:first)
302         if missing_variables.size > 0
303           raise ArgumentError,
304             "Missing required parameters: #{missing_variables.join(', ')}."
305         end
306         parameters.each do |k, v|
307           if self.parameter_descriptions[k]
308             enum = self.parameter_descriptions[k]['enum']
309             if enum && !enum.include?(v)
310               raise ArgumentError,
311                 "Parameter '#{k}' has an invalid value: #{v}. " +
312                 "Must be one of #{enum.inspect}."
313             end
314             pattern = self.parameter_descriptions[k]['pattern']
315             if pattern
316               regexp = Regexp.new("^#{pattern}$")
317               if v !~ regexp
318                 raise ArgumentError,
319                   "Parameter '#{k}' has an invalid value: #{v}. " +
320                   "Must match: /^#{pattern}$/."
321               end
322             end
323           end
324         end
325         return nil
326       end
327
328       ##
329       # Returns a <code>String</code> representation of the method's state.
330       #
331       # @return [String] The method's state, as a <code>String</code>.
332       def inspect
333         sprintf(
334           "#<%s:%#0x ID:%s>",
335           self.class.to_s, self.object_id, self.id
336         )
337       end
338     end
339   end
340 end