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