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