google-api-client 0.4.7 → 0.5.0

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