20862: Improve exception messages.
[arvados.git] / sdk / ruby-google-api-client / lib / google / api_client / request.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 require 'faraday'
16 require 'compat/multi_json'
17 require 'addressable/uri'
18 require 'stringio'
19 require 'google/api_client/discovery'
20 require 'google/api_client/logging'
21
22 module Google
23   class APIClient
24
25     ##
26     # Represents an API request.
27     class Request
28       include Google::APIClient::Logging
29
30       MULTIPART_BOUNDARY = "-----------RubyApiMultipartPost".freeze
31
32       # @return [Hash] Request parameters
33       attr_reader :parameters
34       # @return [Hash] Additional HTTP headers
35       attr_reader :headers
36       # @return [Google::APIClient::Method] API method to invoke
37       attr_reader :api_method
38       # @return [Google::APIClient::UploadIO] File to upload
39       attr_accessor :media
40       # @return [#generated_authenticated_request] User credentials
41       attr_accessor :authorization
42       # @return [TrueClass,FalseClass] True if request should include credentials
43       attr_accessor :authenticated
44       # @return [#read, #to_str] Request body
45       attr_accessor :body
46
47       ##
48       # Build a request
49       #
50       # @param [Hash] options
51       # @option options [Hash, Array] :parameters
52       #   Request parameters for the API method.
53       # @option options [Google::APIClient::Method] :api_method
54       #   API method to invoke. Either :api_method or :uri must be specified
55       # @option options [TrueClass, FalseClass] :authenticated
56       #   True if request should include credentials. Implicitly true if
57       #   unspecified and :authorization present
58       # @option options [#generate_signed_request] :authorization
59       #   OAuth credentials
60       # @option options [Google::APIClient::UploadIO] :media
61       #   File to upload, if media upload request
62       # @option options [#to_json, #to_hash] :body_object
63       #   Main body of the API request. Typically hash or object that can
64       #   be serialized to JSON
65       # @option options [#read, #to_str] :body
66       #   Raw body to send in POST/PUT requests
67       # @option options [String, Addressable::URI] :uri
68       #   URI to request. Either :api_method or :uri must be specified
69       # @option options [String, Symbol] :http_method
70       #   HTTP method when requesting a URI
71       def initialize(options={})
72         @parameters = Faraday::Utils::ParamsHash.new
73         @headers = Faraday::Utils::Headers.new
74
75         self.parameters.merge!(options[:parameters]) unless options[:parameters].nil?
76         self.headers.merge!(options[:headers]) unless options[:headers].nil?
77         self.api_method = options[:api_method]
78         self.authenticated = options[:authenticated]
79         self.authorization = options[:authorization]
80
81         # These parameters are handled differently because they're not
82         # parameters to the API method, but rather to the API system.
83         self.parameters['key'] ||= options[:key] if options[:key]
84         self.parameters['userIp'] ||= options[:user_ip] if options[:user_ip]
85
86         if options[:media]
87           self.initialize_media_upload(options)
88         elsif options[:body]
89           self.body = options[:body]
90         elsif options[:body_object]
91           self.headers['Content-Type'] ||= 'application/json'
92           self.body = serialize_body(options[:body_object])
93         else
94           self.body = ''
95         end
96
97         unless self.api_method
98           self.http_method = options[:http_method] || 'GET'
99           self.uri = options[:uri]
100         end
101       end
102
103       # @!attribute [r] upload_type
104       # @return [String] protocol used for upload
105       def upload_type
106         return self.parameters['uploadType'] || self.parameters['upload_type']
107       end
108
109       # @!attribute http_method
110       # @return [Symbol] HTTP method if invoking a URI
111       def http_method
112         return @http_method ||= self.api_method.http_method.to_s.downcase.to_sym
113       end
114
115       def http_method=(new_http_method)
116         if new_http_method.kind_of?(Symbol)
117           @http_method = new_http_method.to_s.downcase.to_sym
118         elsif new_http_method.respond_to?(:to_str)
119           @http_method = new_http_method.to_s.downcase.to_sym
120         else
121           raise TypeError,
122             "Expected String or Symbol, got #{new_http_method.class}."
123         end
124       end
125
126       def api_method=(new_api_method)
127         if new_api_method.nil? || new_api_method.kind_of?(Google::APIClient::Method)
128           @api_method = new_api_method
129         else
130           raise TypeError,
131             "Expected Google::APIClient::Method, got #{new_api_method.class}."
132         end
133       end
134
135       # @!attribute uri
136       # @return [Addressable::URI] URI to send request
137       def uri
138         return @uri ||= self.api_method.generate_uri(self.parameters)
139       end
140
141       def uri=(new_uri)
142         @uri = Addressable::URI.parse(new_uri)
143         @parameters.update(@uri.query_values) unless @uri.query_values.nil?
144       end
145
146
147       # Transmits the request with the given connection
148       #
149       # @api private
150       #
151       # @param [Faraday::Connection] connection
152       #   the connection to transmit with
153       # @param [TrueValue,FalseValue] is_retry
154       #   True if request has been previous sent
155       #
156       # @return [Google::APIClient::Result]
157       #   result of API request
158       def send(connection, is_retry = false)
159         self.body.rewind if is_retry && self.body.respond_to?(:rewind)
160         env = self.to_env(connection)
161         logger.debug  { "#{self.class} Sending API request #{env[:method]} #{env[:url].to_s} #{env[:request_headers]}" }
162         http_response = connection.app.call(env)
163         result = self.process_http_response(http_response)
164
165         logger.debug { "#{self.class} Result: #{result.status} #{result.headers}" }
166
167         # Resumamble slightly different than other upload protocols in that it requires at least
168         # 2 requests.
169         if result.status == 200 && self.upload_type == 'resumable' && self.media
170           upload = result.resumable_upload
171           unless upload.complete?
172             logger.debug { "#{self.class} Sending upload body" }
173             result = upload.send(connection)
174           end
175         end
176         return result
177       end
178
179       # Convert to an HTTP request. Returns components in order of method, URI,
180       # request headers, and body
181       #
182       # @api private
183       #
184       # @return [Array<(Symbol, Addressable::URI, Hash, [#read,#to_str])>]
185       def to_http_request
186         request = (
187           if self.api_method
188             self.api_method.generate_request(self.parameters, self.body, self.headers)
189           elsif self.uri
190             unless self.parameters.empty?
191               self.uri.query = Addressable::URI.form_encode(self.parameters)
192             end
193             [self.http_method, self.uri.to_s, self.headers, self.body]
194           end)
195         return request
196       end
197
198       ##
199       # Hashified verison of the API request
200       #
201       # @return [Hash]
202       def to_hash
203         options = {}
204         if self.api_method
205           options[:api_method] = self.api_method
206           options[:parameters] = self.parameters
207         else
208           options[:http_method] = self.http_method
209           options[:uri] = self.uri
210         end
211         options[:headers] = self.headers
212         options[:body] = self.body
213         options[:media] = self.media
214         unless self.authorization.nil?
215           options[:authorization] = self.authorization
216         end
217         return options
218       end
219
220       ##
221       # Prepares the request for execution, building a hash of parts
222       # suitable for sending to Faraday::Connection.
223       #
224       # @api private
225       #
226       # @param [Faraday::Connection] connection
227       #   Connection for building the request
228       #
229       # @return [Hash]
230       #   Encoded request
231       def to_env(connection)
232         method, uri, headers, body = self.to_http_request
233         http_request = connection.build_request(method) do |req|
234           req.url(uri.to_s)
235           req.headers.update(headers)
236           req.body = body
237         end
238
239         if self.authorization.respond_to?(:generate_authenticated_request)
240           http_request = self.authorization.generate_authenticated_request(
241             :request => http_request,
242             :connection => connection
243           )
244         end
245
246         http_request.to_env(connection)
247       end
248
249       ##
250       # Convert HTTP response to an API Result
251       #
252       # @api private
253       #
254       # @param [Faraday::Response] response
255       #   HTTP response
256       #
257       # @return [Google::APIClient::Result]
258       #   Processed API response
259       def process_http_response(response)
260         Result.new(self, response)
261       end
262
263       protected
264
265       ##
266       # Adjust headers & body for media uploads
267       #
268       # @api private
269       #
270       # @param [Hash] options
271       # @option options [Hash, Array] :parameters
272       #   Request parameters for the API method.
273       # @option options [Google::APIClient::UploadIO] :media
274       #   File to upload, if media upload request
275       # @option options [#to_json, #to_hash] :body_object
276       #   Main body of the API request. Typically hash or object that can
277       #   be serialized to JSON
278       # @option options [#read, #to_str] :body
279       #   Raw body to send in POST/PUT requests
280       def initialize_media_upload(options)
281         raise "media upload not supported by arvados-google-api-client"
282       end
283
284       ##
285       # Assemble a multipart message from a set of parts
286       #
287       # @api private
288       #
289       # @param [Array<[#read,#to_str]>] parts
290       #   Array of parts to encode.
291       # @param [String] mime_type
292       #   MIME type of the message
293       # @param [String] boundary
294       #   Boundary for separating each part of the message
295       def build_multipart(parts, mime_type = 'multipart/related', boundary = MULTIPART_BOUNDARY)
296         raise "multipart upload not supported by arvados-google-api-client"
297       end
298
299       ##
300       # Serialize body object to JSON
301       #
302       # @api private
303       #
304       # @param [#to_json,#to_hash] body
305       #   object to serialize
306       #
307       # @return [String]
308       #   JSON
309       def serialize_body(body)
310         return body.to_json if body.respond_to?(:to_json)
311         return MultiJson.dump(body.to_hash) if body.respond_to?(:to_hash)
312         raise TypeError, 'Could not convert body object to JSON.' +
313                          'Must respond to :to_json or :to_hash.'
314       end
315
316     end
317   end
318 end