1 # Copyright 2010 Google Inc.
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
17 require 'faraday/utils'
19 require 'compat/multi_json'
20 require 'addressable/uri'
22 require 'google/api_client/discovery'
24 # TODO - needs some serious cleanup
29 MULTIPART_BOUNDARY = "-----------RubyApiMultipartPost".freeze
31 def initialize(options={})
32 # We only need this to do lookups on method ID String values
33 # It's optional, but method ID lookups will fail if the client is
35 @client = options[:client]
36 @version = options[:version] || 'v1'
38 self.connection = options[:connection] || Faraday.default_connection
39 self.authorization = options[:authorization]
40 self.api_method = options[:api_method]
41 self.parameters = options[:parameters] || {}
42 # These parameters are handled differently because they're not
43 # parameters to the API method, but rather to the API system.
44 if self.parameters.kind_of?(Array)
46 self.parameters.reject! { |k, _| k == 'key' }
47 self.parameters << ['key', options[:key]]
50 self.parameters.reject! { |k, _| k == 'userIp' }
51 self.parameters << ['userIp', options[:user_ip]]
53 elsif self.parameters.kind_of?(Hash)
54 self.parameters['key'] ||= options[:key] if options[:key]
55 self.parameters['userIp'] ||= options[:user_ip] if options[:user_ip]
56 # Convert to Array, because they're easier to work with when
57 # repeated parameters are an issue.
58 self.parameters = self.parameters.to_a
61 "Expected Array or Hash, got #{self.parameters.class}."
63 self.headers = options[:headers] || {}
65 self.media = options[:media]
66 upload_type = self.parameters.find { |(k, _)| ['uploadType', 'upload_type'].include?(k) }.last
69 if options[:body] || options[:body_object]
71 "Can not specify body & body object for simple uploads."
73 self.headers['Content-Type'] ||= self.media.content_type
74 self.body = self.media
76 unless options[:body_object]
77 raise ArgumentError, "Multipart requested but no body object."
79 # This is all a bit of a hack due to Signet requiring body to be a
80 # string. Ideally, update Signet to delay serialization so we can
81 # just pass streams all the way down through to the HTTP library.
82 metadata = StringIO.new(serialize_body(options[:body_object]))
86 "multipart/related;boundary=#{MULTIPART_BOUNDARY}"
88 :request => {:boundary => MULTIPART_BOUNDARY}
90 multipart = Faraday::Request::Multipart.new
91 self.body = multipart.create_multipart(env, [
92 [nil, Faraday::UploadIO.new(
93 metadata, 'application/json', 'file.json'
96 self.headers.update(env[:request_headers])
98 file_length = self.media.length
99 self.headers['X-Upload-Content-Type'] = self.media.content_type
100 self.headers['X-Upload-Content-Length'] = file_length.to_s
101 if options[:body_object]
102 self.headers['Content-Type'] ||= 'application/json'
103 self.body = serialize_body(options[:body_object])
108 raise ArgumentError, "Invalid uploadType for media."
111 self.body = options[:body]
112 elsif options[:body_object]
113 self.headers['Content-Type'] ||= 'application/json'
114 self.body = serialize_body(options[:body_object])
118 unless self.api_method
119 self.http_method = options[:http_method] || 'GET'
120 self.uri = options[:uri]
121 unless self.parameters.empty?
122 query_values = (self.uri.query_values(Array) || [])
123 self.uri.query = Addressable::URI.form_encode(
124 (query_values + self.parameters).sort
126 self.uri.query = nil if self.uri.query == ""
131 def serialize_body(body)
132 return body.to_json if body.respond_to?(:to_json)
133 return MultiJson.dump(options[:body_object].to_hash) if body.respond_to?(:to_hash)
134 raise TypeError, 'Could not convert body object to JSON.' +
135 'Must respond to :to_json or :to_hash.'
147 return @authorization
150 def authorization=(new_authorization)
151 @authorization = new_authorization
158 def connection=(new_connection)
159 if new_connection.kind_of?(Faraday::Connection)
160 @connection = new_connection
163 "Expected Faraday::Connection, got #{new_connection.class}."
171 def api_method=(new_api_method)
172 if new_api_method.kind_of?(Google::APIClient::Method) ||
173 new_api_method == nil
174 @api_method = new_api_method
175 elsif new_api_method.respond_to?(:to_str) ||
176 new_api_method.kind_of?(Symbol)
179 "API method lookup impossible without client instance."
181 new_api_method = new_api_method.to_s
182 # This method of guessing the API is unreliable. This will fail for
183 # APIs where the first segment of the RPC name does not match the
184 # service name. However, this is a fallback mechanism anyway.
185 # Developers should be passing in a reference to the method, rather
186 # than passing in a string or symbol. This should raise an error
187 # in the case of a mismatch.
188 api = new_api_method[/^([^.]+)\./, 1]
189 @api_method = @client.discovered_method(
190 new_api_method, api, @version
193 # Ditch the client reference, we won't need it again.
196 raise ArgumentError, "API method could not be found."
200 "Expected Google::APIClient::Method, got #{new_api_method.class}."
208 def parameters=(new_parameters)
209 # No type-checking needed, the Method class handles this.
210 @parameters = new_parameters
218 if new_body.respond_to?(:to_str)
219 @body = new_body.to_str
220 elsif new_body.respond_to?(:read)
221 @body = new_body.read()
222 elsif new_body.respond_to?(:inject)
223 @body = (new_body.inject(StringIO.new) do |accu, chunk|
229 "Expected body to be String, IO, or Enumerable chunks."
234 return @headers ||= {}
237 def headers=(new_headers)
238 if new_headers.kind_of?(Array) || new_headers.kind_of?(Hash)
239 @headers = new_headers
241 raise TypeError, "Expected Hash or Array, got #{new_headers.class}."
246 return @http_method ||= self.api_method.http_method
249 def http_method=(new_http_method)
250 if new_http_method.kind_of?(Symbol)
251 @http_method = new_http_method.to_s.upcase
252 elsif new_http_method.respond_to?(:to_str)
253 @http_method = new_http_method.to_str.upcase
256 "Expected String or Symbol, got #{new_http_method.class}."
261 return @uri ||= self.api_method.generate_uri(self.parameters)
265 @uri = Addressable::URI.parse(new_uri)
270 return self.api_method.generate_request(
271 self.parameters, self.body, self.headers,
272 :connection => self.connection
275 return self.connection.build_request(
276 self.http_method.to_s.downcase.to_sym
278 req.url(Addressable::URI.parse(self.uri).normalize.to_s)
279 req.headers = Faraday::Utils::Headers.new(self.headers)
288 options[:api_method] = self.api_method
289 options[:parameters] = self.parameters
291 options[:http_method] = self.http_method
292 options[:uri] = self.uri
294 options[:headers] = self.headers
295 options[:body] = self.body
296 options[:connection] = self.connection
297 unless self.authorization.nil?
298 options[:authorization] = self.authorization