Fix duplication of parameters in qeuery string
[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.api_method
185             self.api_method.generate_request(self.parameters, self.body, self.headers)
186           elsif self.uri
187             unless self.parameters.empty?
188               self.uri.query = Addressable::URI.form_encode(self.parameters)
189             end
190             [self.http_method, self.uri.to_s, self.headers, self.body]
191           end)
192         return request
193       end
194
195       ##
196       # Hashified verison of the API request
197       #
198       # @return [Hash]
199       def to_hash
200         options = {}
201         if self.api_method
202           options[:api_method] = self.api_method
203           options[:parameters] = self.parameters
204         else
205           options[:http_method] = self.http_method
206           options[:uri] = self.uri
207         end
208         options[:headers] = self.headers
209         options[:body] = self.body
210         options[:media] = self.media
211         unless self.authorization.nil?
212           options[:authorization] = self.authorization
213         end
214         return options
215       end
216
217       ##
218       # Prepares the request for execution, building a hash of parts
219       # suitable for sending to Faraday::Connection.
220       #
221       # @api private
222       #
223       # @param [Faraday::Connection] connection
224       #   Connection for building the request
225       #
226       # @return [Hash]
227       #   Encoded request
228       def to_env(connection)
229         method, uri, headers, body = self.to_http_request
230         http_request = connection.build_request(method) do |req|
231           req.url(uri.to_s)
232           req.headers.update(headers)
233           req.body = body
234         end
235
236         if self.authorization.respond_to?(:generate_authenticated_request)
237           http_request = self.authorization.generate_authenticated_request(
238             :request => http_request,
239             :connection => connection
240           )
241         end
242
243         request_env = http_request.to_env(connection)
244       end
245
246       ##
247       # Convert HTTP response to an API Result
248       #
249       # @api private
250       #
251       # @param [Faraday::Response] response
252       #   HTTP response
253       #
254       # @return [Google::APIClient::Result]
255       #   Processed API response
256       def process_http_response(response)
257         Result.new(self, response)
258       end
259
260       protected
261
262       ##
263       # Adjust headers & body for media uploads
264       #
265       # @api private
266       #
267       # @param [Hash] options
268       # @option options [Hash, Array] :parameters
269       #   Request parameters for the API method.
270       # @option options [Google::APIClient::UploadIO] :media
271       #   File to upload, if media upload request
272       # @option options [#to_json, #to_hash] :body_object
273       #   Main body of the API request. Typically hash or object that can
274       #   be serialized to JSON
275       # @option options [#read, #to_str] :body
276       #   Raw body to send in POST/PUT requests
277       def initialize_media_upload(options)
278         self.media = options[:media]
279         case self.upload_type
280         when "media"
281           if options[:body] || options[:body_object]
282             raise ArgumentError, "Can not specify body & body object for simple uploads"
283           end
284           self.headers['Content-Type'] ||= self.media.content_type
285           self.body = self.media
286         when "multipart"
287           unless options[:body_object]
288             raise ArgumentError, "Multipart requested but no body object"
289           end
290           metadata = StringIO.new(serialize_body(options[:body_object]))
291           build_multipart([Faraday::UploadIO.new(metadata, 'application/json', 'file.json'), self.media])
292         when "resumable"
293           file_length = self.media.length
294           self.headers['X-Upload-Content-Type'] = self.media.content_type
295           self.headers['X-Upload-Content-Length'] = file_length.to_s
296           if options[:body_object]
297             self.headers['Content-Type'] ||= 'application/json'
298             self.body = serialize_body(options[:body_object])
299           else
300             self.body = ''
301           end
302         end
303       end
304
305       ##
306       # Assemble a multipart message from a set of parts
307       #
308       # @api private
309       #
310       # @param [Array<[#read,#to_str]>] parts
311       #   Array of parts to encode.
312       # @param [String] mime_type
313       #   MIME type of the message
314       # @param [String] boundary
315       #   Boundary for separating each part of the message
316       def build_multipart(parts, mime_type = 'multipart/related', boundary = MULTIPART_BOUNDARY)
317         env = {
318           :request_headers => {'Content-Type' => "#{mime_type};boundary=#{boundary}"},
319           :request => { :boundary => boundary }
320         }
321         multipart = Faraday::Request::Multipart.new
322         self.body = multipart.create_multipart(env, parts.map {|part| [nil, part]})
323         self.headers.update(env[:request_headers])
324       end
325
326       ##
327       # Serialize body object to JSON
328       #
329       # @api private
330       #
331       # @param [#to_json,#to_hash] body
332       #   object to serialize
333       #
334       # @return [String]
335       #   JSON
336       def serialize_body(body)
337         return body.to_json if body.respond_to?(:to_json)
338         return MultiJson.dump(body.to_hash) if body.respond_to?(:to_hash)
339         raise TypeError, 'Could not convert body object to JSON.' +
340                          'Must respond to :to_json or :to_hash.'
341       end
342
343     end
344   end
345 end