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.
16 gem 'faraday', '~> 0.8.1'
18 require 'faraday/utils'
20 require 'compat/multi_json'
21 require 'addressable/uri'
23 require 'google/api_client/discovery'
25 # TODO - needs some serious cleanup
30 MULTIPART_BOUNDARY = "-----------RubyApiMultipartPost".freeze
32 def initialize(options={})
33 # We only need this to do lookups on method ID String values
34 # It's optional, but method ID lookups will fail if the client is
36 @client = options[:client]
37 @version = options[:version] || 'v1'
39 self.connection = options[:connection] || Faraday.default_connection
40 self.authorization = options[:authorization]
41 self.api_method = options[:api_method]
42 self.parameters = options[:parameters] || {}
43 # These parameters are handled differently because they're not
44 # parameters to the API method, but rather to the API system.
45 if self.parameters.kind_of?(Array)
47 self.parameters.reject! { |k, _| k == 'key' }
48 self.parameters << ['key', options[:key]]
51 self.parameters.reject! { |k, _| k == 'userIp' }
52 self.parameters << ['userIp', options[:user_ip]]
54 elsif self.parameters.kind_of?(Hash)
55 self.parameters['key'] ||= options[:key] if options[:key]
56 self.parameters['userIp'] ||= options[:user_ip] if options[:user_ip]
57 # Convert to Array, because they're easier to work with when
58 # repeated parameters are an issue.
59 self.parameters = self.parameters.to_a
62 "Expected Array or Hash, got #{self.parameters.class}."
64 self.headers = options[:headers] || {}
66 self.media = options[:media]
67 upload_type = self.parameters.find { |(k, _)| ['uploadType', 'upload_type'].include?(k) }.last
70 if options[:body] || options[:body_object]
72 "Can not specify body & body object for simple uploads."
74 self.headers['Content-Type'] ||= self.media.content_type
75 self.body = self.media
77 unless options[:body_object]
78 raise ArgumentError, "Multipart requested but no body object."
80 # This is all a bit of a hack due to Signet requiring body to be a
81 # string. Ideally, update Signet to delay serialization so we can
82 # just pass streams all the way down through to the HTTP library.
83 metadata = StringIO.new(serialize_body(options[:body_object]))
87 "multipart/related;boundary=#{MULTIPART_BOUNDARY}"
89 :request => {:boundary => MULTIPART_BOUNDARY}
91 multipart = Faraday::Request::Multipart.new
92 self.body = multipart.create_multipart(env, [
93 [nil, Faraday::UploadIO.new(
94 metadata, 'application/json', 'file.json'
97 self.headers.update(env[:request_headers])
99 file_length = self.media.length
100 self.headers['X-Upload-Content-Type'] = self.media.content_type
101 self.headers['X-Upload-Content-Length'] = file_length.to_s
102 if options[:body_object]
103 self.headers['Content-Type'] ||= 'application/json'
104 self.body = serialize_body(options[:body_object])
109 raise ArgumentError, "Invalid uploadType for media."
112 self.body = options[:body]
113 elsif options[:body_object]
114 self.headers['Content-Type'] ||= 'application/json'
115 self.body = serialize_body(options[:body_object])
119 unless self.api_method
120 self.http_method = options[:http_method] || 'GET'
121 self.uri = options[:uri]
122 unless self.parameters.empty?
123 query_values = (self.uri.query_values(Array) || [])
124 self.uri.query = Addressable::URI.form_encode(
125 (query_values + self.parameters).sort
127 self.uri.query = nil if self.uri.query == ""
132 def serialize_body(body)
133 return body.to_json if body.respond_to?(:to_json)
134 return MultiJson.dump(options[:body_object].to_hash) if body.respond_to?(:to_hash)
135 raise TypeError, 'Could not convert body object to JSON.' +
136 'Must respond to :to_json or :to_hash.'
148 return @authorization
151 def authorization=(new_authorization)
152 @authorization = new_authorization
159 def connection=(new_connection)
160 if new_connection.kind_of?(Faraday::Connection)
161 @connection = new_connection
164 "Expected Faraday::Connection, got #{new_connection.class}."
172 def api_method=(new_api_method)
173 if new_api_method.kind_of?(Google::APIClient::Method) ||
174 new_api_method == nil
175 @api_method = new_api_method
176 elsif new_api_method.respond_to?(:to_str) ||
177 new_api_method.kind_of?(Symbol)
180 "API method lookup impossible without client instance."
182 new_api_method = new_api_method.to_s
183 # This method of guessing the API is unreliable. This will fail for
184 # APIs where the first segment of the RPC name does not match the
185 # service name. However, this is a fallback mechanism anyway.
186 # Developers should be passing in a reference to the method, rather
187 # than passing in a string or symbol. This should raise an error
188 # in the case of a mismatch.
189 api = new_api_method[/^([^.]+)\./, 1]
190 @api_method = @client.discovered_method(
191 new_api_method, api, @version
194 # Ditch the client reference, we won't need it again.
197 raise ArgumentError, "API method could not be found."
201 "Expected Google::APIClient::Method, got #{new_api_method.class}."
209 def parameters=(new_parameters)
210 # No type-checking needed, the Method class handles this.
211 @parameters = new_parameters
219 if new_body.respond_to?(:to_str)
220 @body = new_body.to_str
221 elsif new_body.respond_to?(:read)
222 @body = new_body.read()
223 elsif new_body.respond_to?(:inject)
224 @body = (new_body.inject(StringIO.new) do |accu, chunk|
230 "Expected body to be String, IO, or Enumerable chunks."
235 return @headers ||= {}
238 def headers=(new_headers)
239 if new_headers.kind_of?(Array) || new_headers.kind_of?(Hash)
240 @headers = new_headers
242 raise TypeError, "Expected Hash or Array, got #{new_headers.class}."
247 return @http_method ||= self.api_method.http_method
250 def http_method=(new_http_method)
251 if new_http_method.kind_of?(Symbol)
252 @http_method = new_http_method.to_s.upcase
253 elsif new_http_method.respond_to?(:to_str)
254 @http_method = new_http_method.to_str.upcase
257 "Expected String or Symbol, got #{new_http_method.class}."
262 return @uri ||= self.api_method.generate_uri(self.parameters)
266 @uri = Addressable::URI.parse(new_uri)
271 return self.api_method.generate_request(
272 self.parameters, self.body, self.headers,
273 :connection => self.connection
276 return self.connection.build_request(
277 self.http_method.to_s.downcase.to_sym
279 req.url(Addressable::URI.parse(self.uri).normalize.to_s)
280 req.headers = Faraday::Utils::Headers.new(self.headers)
289 options[:api_method] = self.api_method
290 options[:parameters] = self.parameters
292 options[:http_method] = self.http_method
293 options[:uri] = self.uri
295 options[:headers] = self.headers
296 options[:body] = self.body
297 options[:connection] = self.connection
298 unless self.authorization.nil?
299 options[:authorization] = self.authorization