Merge pull request #20 from simplymeasured/feature/make-autorefresh-of-token-optional
[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/utils'
17 require 'multi_json'
18 require 'compat/multi_json'
19 require 'addressable/uri'
20 require 'stringio'
21 require 'google/api_client/discovery'
22 require 'google/api_client/logging'
23
24 module Google
25   class APIClient
26
27     ##
28     # Represents an API request.
29     class Request
30       include Google::APIClient::Logging
31       
32       MULTIPART_BOUNDARY = "-----------RubyApiMultipartPost".freeze
33
34       # @return [Hash] Request parameters
35       attr_reader :parameters
36       # @return [Hash] Additional HTTP headers
37       attr_reader :headers
38       # @return [Google::APIClient::Method] API method to invoke
39       attr_reader :api_method
40       # @return [Google::APIClient::UploadIO] File to upload
41       attr_accessor :media
42       # @return [#generated_authenticated_request] User credentials
43       attr_accessor :authorization
44       # @return [TrueClass,FalseClass] True if request should include credentials
45       attr_accessor :authenticated
46       # @return [#read, #to_str] Request body
47       attr_accessor :body
48
49       ##
50       # Build a request
51       #
52       # @param [Hash] options
53       # @option options [Hash, Array] :parameters
54       #   Request parameters for the API method.
55       # @option options [Google::APIClient::Method] :api_method
56       #   API method to invoke. Either :api_method or :uri must be specified
57       # @option options [TrueClass, FalseClass] :authenticated
58       #   True if request should include credentials. Implicitly true if
59       #   unspecified and :authorization present
60       # @option options [#generate_signed_request] :authorization
61       #   OAuth credentials
62       # @option options [Google::APIClient::UploadIO] :media
63       #   File to upload, if media upload request
64       # @option options [#to_json, #to_hash] :body_object
65       #   Main body of the API request. Typically hash or object that can
66       #   be serialized to JSON
67       # @option options [#read, #to_str] :body
68       #   Raw body to send in POST/PUT requests
69       # @option options [String, Addressable::URI] :uri
70       #   URI to request. Either :api_method or :uri must be specified
71       # @option options [String, Symbol] :http_method
72       #   HTTP method when requesting a URI
73       def initialize(options={})
74         @parameters = Hash[options[:parameters] || {}]
75         @headers = Faraday::Utils::Headers.new
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       #
154       # @return [Google::APIClient::Result]
155       #   result of API request
156       def send(connection)
157         env = self.to_env(connection)
158         logger.debug  { "#{self.class} Sending API request #{env[:method]} #{env[:url].to_s} #{env[:request_headers]}" }
159         http_response = connection.app.call(env)
160         result = self.process_http_response(http_response)
161
162         logger.debug { "#{self.class} Result: #{result.status} #{result.headers}" }
163
164         # Resumamble slightly different than other upload protocols in that it requires at least
165         # 2 requests.
166         if self.upload_type == 'resumable'
167           upload =  result.resumable_upload
168           unless upload.complete?
169             logger.debug { "#{self.class} Sending upload body" }
170             result = upload.send(connection)
171           end
172         end
173         return result
174       end
175
176       # Convert to an HTTP request. Returns components in order of method, URI,
177       # request headers, and body
178       #
179       # @api private
180       #
181       # @return [Array<(Symbol, Addressable::URI, Hash, [#read,#to_str])>]
182       def to_http_request
183         request = (
184           if self.uri
185             unless self.parameters.empty?
186               self.uri.query = Addressable::URI.form_encode(self.parameters)
187             end
188             [self.http_method, self.uri.to_s, self.headers, self.body]
189           else
190             self.api_method.generate_request(self.parameters, self.body, self.headers)
191           end)
192       end
193
194       ##
195       # Hashified verison of the API request
196       #
197       # @return [Hash]
198       def to_hash
199         options = {}
200         if self.api_method
201           options[:api_method] = self.api_method
202           options[:parameters] = self.parameters
203         else
204           options[:http_method] = self.http_method
205           options[:uri] = self.uri
206         end
207         options[:headers] = self.headers
208         options[:body] = self.body
209         options[:media] = self.media
210         unless self.authorization.nil?
211           options[:authorization] = self.authorization
212         end
213         return options
214       end
215
216       ##
217       # Prepares the request for execution, building a hash of parts
218       # suitable for sending to Faraday::Connection.
219       #
220       # @api private
221       #
222       # @param [Faraday::Connection] connection
223       #   Connection for building the request
224       #
225       # @return [Hash]
226       #   Encoded request
227       def to_env(connection)
228         method, uri, headers, body = self.to_http_request
229         http_request = connection.build_request(method) do |req|
230           req.url(uri)
231           req.headers.update(headers)
232           req.body = body
233         end
234
235         if self.authorization.respond_to?(:generate_authenticated_request)
236           http_request = self.authorization.generate_authenticated_request(
237             :request => http_request,
238             :connection => connection
239           )
240         end
241
242         request_env = http_request.to_env(connection)
243       end
244
245       ##
246       # Convert HTTP response to an API Result
247       #
248       # @api private
249       #
250       # @param [Faraday::Response] response
251       #   HTTP response
252       #
253       # @return [Google::APIClient::Result]
254       #   Processed API response
255       def process_http_response(response)
256         Result.new(self, response)
257       end
258
259       protected
260
261       ##
262       # Adjust headers & body for media uploads
263       #
264       # @api private
265       #
266       # @param [Hash] options
267       # @option options [Hash, Array] :parameters
268       #   Request parameters for the API method.
269       # @option options [Google::APIClient::UploadIO] :media
270       #   File to upload, if media upload request
271       # @option options [#to_json, #to_hash] :body_object
272       #   Main body of the API request. Typically hash or object that can
273       #   be serialized to JSON
274       # @option options [#read, #to_str] :body
275       #   Raw body to send in POST/PUT requests
276       def initialize_media_upload(options)
277         self.media = options[:media]
278         case self.upload_type
279         when "media"
280           if options[:body] || options[:body_object]
281             raise ArgumentError, "Can not specify body & body object for simple uploads"
282           end
283           self.headers['Content-Type'] ||= self.media.content_type
284           self.body = self.media
285         when "multipart"
286           unless options[:body_object]
287             raise ArgumentError, "Multipart requested but no body object"
288           end
289           metadata = StringIO.new(serialize_body(options[:body_object]))
290           build_multipart([Faraday::UploadIO.new(metadata, 'application/json', 'file.json'), self.media])
291         when "resumable"
292           file_length = self.media.length
293           self.headers['X-Upload-Content-Type'] = self.media.content_type
294           self.headers['X-Upload-Content-Length'] = file_length.to_s
295           if options[:body_object]
296             self.headers['Content-Type'] ||= 'application/json'
297             self.body = serialize_body(options[:body_object])
298           else
299             self.body = ''
300           end
301         end
302       end
303
304       ##
305       # Assemble a multipart message from a set of parts
306       #
307       # @api private
308       #
309       # @param [Array<[#read,#to_str]>] parts
310       #   Array of parts to encode.
311       # @param [String] mime_type
312       #   MIME type of the message
313       # @param [String] boundary
314       #   Boundary for separating each part of the message
315       def build_multipart(parts, mime_type = 'multipart/related', boundary = MULTIPART_BOUNDARY)
316         env = {
317           :request_headers => {'Content-Type' => "#{mime_type};boundary=#{boundary}"},
318           :request => { :boundary => boundary }
319         }
320         multipart = Faraday::Request::Multipart.new
321         self.body = multipart.create_multipart(env, parts.map {|part| [nil, part]})
322         self.headers.update(env[:request_headers])
323       end
324
325       ##
326       # Serialize body object to JSON
327       #
328       # @api private
329       #
330       # @param [#to_json,#to_hash] body
331       #   object to serialize
332       #
333       # @return [String]
334       #   JSON
335       def serialize_body(body)
336         return body.to_json if body.respond_to?(:to_json)
337         return MultiJson.dump(body.to_hash) if body.respond_to?(:to_hash)
338         raise TypeError, 'Could not convert body object to JSON.' +
339                          'Must respond to :to_json or :to_hash.'
340       end
341
342     end
343   end
344 end