google-api-client 0.4.7 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.md +11 -0
- data/Gemfile +6 -1
- data/Gemfile.lock +80 -0
- data/README.md +152 -45
- data/Rakefile +2 -2
- data/bin/google-api +2 -9
- data/lib/compat/multi_json.rb +2 -3
- data/lib/google/api_client.rb +87 -278
- data/lib/google/api_client/auth/jwt_asserter.rb +139 -0
- data/lib/google/api_client/auth/pkcs12.rb +48 -0
- data/lib/google/api_client/batch.rb +164 -136
- data/lib/google/api_client/client_secrets.rb +45 -1
- data/lib/google/api_client/discovery/api.rb +7 -8
- data/lib/google/api_client/discovery/method.rb +20 -27
- data/lib/google/api_client/discovery/resource.rb +16 -10
- data/lib/google/api_client/discovery/schema.rb +2 -0
- data/lib/google/api_client/media.rb +76 -64
- data/lib/google/api_client/reference.rb +7 -285
- data/lib/google/api_client/request.rb +336 -0
- data/lib/google/api_client/result.rb +147 -55
- data/lib/google/api_client/service_account.rb +2 -120
- data/lib/google/api_client/version.rb +2 -3
- data/spec/google/api_client/batch_spec.rb +9 -10
- data/spec/google/api_client/discovery_spec.rb +184 -114
- data/spec/google/api_client/media_spec.rb +27 -39
- data/spec/google/api_client/result_spec.rb +30 -11
- data/spec/google/api_client/service_account_spec.rb +38 -6
- data/spec/google/api_client_spec.rb +48 -32
- data/spec/spec_helper.rb +46 -0
- data/tasks/gem.rake +1 -0
- metadata +36 -70
@@ -0,0 +1,139 @@
|
|
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 'jwt'
|
16
|
+
require 'signet/oauth_2/client'
|
17
|
+
require 'delegate'
|
18
|
+
|
19
|
+
module Google
|
20
|
+
class APIClient
|
21
|
+
##
|
22
|
+
# Generates access tokens using the JWT assertion profile. Requires a
|
23
|
+
# service account & access to the private key.
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
#
|
27
|
+
# client = Google::APIClient.new
|
28
|
+
# key = Google::APIClient::PKCS12.load_key('client.p12', 'notasecret')
|
29
|
+
# service_account = Google::APIClient::JWTAsserter(
|
30
|
+
# '123456-abcdef@developer.gserviceaccount.com',
|
31
|
+
# 'https://www.googleapis.com/auth/prediction',
|
32
|
+
# key)
|
33
|
+
# client.authorization = service_account.authorize
|
34
|
+
# client.execute(...)
|
35
|
+
#
|
36
|
+
# @see https://developers.google.com/accounts/docs/OAuth2ServiceAccount
|
37
|
+
class JWTAsserter
|
38
|
+
# @return [String] ID/email of the issuing party
|
39
|
+
attr_accessor :issuer
|
40
|
+
# @return [Fixnum] How long, in seconds, the assertion is valid for
|
41
|
+
attr_accessor :expiry
|
42
|
+
# @return [Fixnum] Seconds to expand the issued at/expiry window to account for clock skew
|
43
|
+
attr_accessor :skew
|
44
|
+
# @return [String] Scopes to authorize
|
45
|
+
attr_reader :scope
|
46
|
+
# @return [OpenSSL::PKey] key for signing assertions
|
47
|
+
attr_writer :key
|
48
|
+
|
49
|
+
##
|
50
|
+
# Initializes the asserter for a service account.
|
51
|
+
#
|
52
|
+
# @param [String] issuer
|
53
|
+
# Name/ID of the client issuing the assertion
|
54
|
+
# @param [String, Array] scope
|
55
|
+
# Scopes to authorize. May be a space delimited string or array of strings
|
56
|
+
# @param [OpenSSL::PKey] key
|
57
|
+
# RSA private key for signing assertions
|
58
|
+
def initialize(issuer, scope, key)
|
59
|
+
self.issuer = issuer
|
60
|
+
self.scope = scope
|
61
|
+
self.expiry = 60 # 1 min default
|
62
|
+
self.skew = 60
|
63
|
+
self.key = key
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# Set the scopes to authorize
|
68
|
+
#
|
69
|
+
# @param [String, Array] new_scope
|
70
|
+
# Scopes to authorize. May be a space delimited string or array of strings
|
71
|
+
def scope=(new_scope)
|
72
|
+
case new_scope
|
73
|
+
when Array
|
74
|
+
@scope = new_scope.join(' ')
|
75
|
+
when String
|
76
|
+
@scope = new_scope
|
77
|
+
when nil
|
78
|
+
@scope = ''
|
79
|
+
else
|
80
|
+
raise TypeError, "Expected Array or String, got #{new_scope.class}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Builds & signs the assertion.
|
86
|
+
#
|
87
|
+
# @param [String] person
|
88
|
+
# Email address of a user, if requesting a token to act on their behalf
|
89
|
+
# @return [String] Encoded JWT
|
90
|
+
def to_jwt(person=nil)
|
91
|
+
now = Time.new
|
92
|
+
assertion = {
|
93
|
+
"iss" => @issuer,
|
94
|
+
"scope" => self.scope,
|
95
|
+
"aud" => "https://accounts.google.com/o/oauth2/token",
|
96
|
+
"exp" => (now + expiry).to_i,
|
97
|
+
"iat" => (now - skew).to_i
|
98
|
+
}
|
99
|
+
assertion['prn'] = person unless person.nil?
|
100
|
+
return JWT.encode(assertion, @key, "RS256")
|
101
|
+
end
|
102
|
+
|
103
|
+
##
|
104
|
+
# Request a new access token.
|
105
|
+
#
|
106
|
+
# @param [String] person
|
107
|
+
# Email address of a user, if requesting a token to act on their behalf
|
108
|
+
# @param [Hash] options
|
109
|
+
# Pass through to Signet::OAuth2::Client.fetch_access_token
|
110
|
+
# @return [Signet::OAuth2::Client] Access token
|
111
|
+
#
|
112
|
+
# @see Signet::OAuth2::Client.fetch_access_token
|
113
|
+
def authorize(person = nil, options={})
|
114
|
+
assertion = self.to_jwt(person)
|
115
|
+
authorization = Signet::OAuth2::Client.new(
|
116
|
+
:token_credential_uri => 'https://accounts.google.com/o/oauth2/token'
|
117
|
+
)
|
118
|
+
authorization.grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
|
119
|
+
authorization.extension_parameters = { :assertion => assertion }
|
120
|
+
authorization.fetch_access_token!(options)
|
121
|
+
return JWTAuthorization.new(authorization, self, person)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class JWTAuthorization < DelegateClass(Signet::OAuth2::Client)
|
126
|
+
def initialize(authorization, asserter, person = nil)
|
127
|
+
@asserter = asserter
|
128
|
+
@person = person
|
129
|
+
super(authorization)
|
130
|
+
end
|
131
|
+
|
132
|
+
def fetch_access_token!(options={})
|
133
|
+
new_authorization = @asserter.authorize(@person, options)
|
134
|
+
__setobj__(new_authorization)
|
135
|
+
self
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,48 @@
|
|
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
|
+
module Google
|
16
|
+
class APIClient
|
17
|
+
##
|
18
|
+
# Helper for loading keys from the PKCS12 files downloaded when
|
19
|
+
# setting up service accounts at the APIs Console.
|
20
|
+
#
|
21
|
+
module PKCS12
|
22
|
+
##
|
23
|
+
# Loads a key from PKCS12 file, assuming a single private key
|
24
|
+
# is present.
|
25
|
+
#
|
26
|
+
# @param [String] keyfile
|
27
|
+
# Path of the PKCS12 file to load. If not a path to an actual file,
|
28
|
+
# assumes the string is the content of the file itself.
|
29
|
+
# @param [String] passphrase
|
30
|
+
# Passphrase for unlocking the private key
|
31
|
+
#
|
32
|
+
# @return [OpenSSL::PKey] The private key for signing assertions.
|
33
|
+
def self.load_key(keyfile, passphrase)
|
34
|
+
begin
|
35
|
+
if File.exists?(keyfile)
|
36
|
+
content = File.read(keyfile)
|
37
|
+
else
|
38
|
+
content = keyfile
|
39
|
+
end
|
40
|
+
pkcs12 = OpenSSL::PKCS12.new(content, passphrase)
|
41
|
+
return pkcs12.key
|
42
|
+
rescue OpenSSL::PKCS12::PKCS12Error
|
43
|
+
raise ArgumentError.new("Invalid keyfile or passphrase")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -13,57 +13,91 @@
|
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
15
|
require 'addressable/uri'
|
16
|
+
require 'google/api_client/reference'
|
16
17
|
require 'uuidtools'
|
17
18
|
|
18
19
|
module Google
|
19
20
|
class APIClient
|
20
21
|
|
22
|
+
##
|
21
23
|
# Helper class to contain a response to an individual batched call.
|
24
|
+
#
|
25
|
+
# @api private
|
22
26
|
class BatchedCallResponse
|
27
|
+
# @return [String] UUID of the call
|
23
28
|
attr_reader :call_id
|
24
|
-
|
29
|
+
# @return [Fixnum] HTTP status code
|
30
|
+
attr_accessor :status
|
31
|
+
# @return [Hash] HTTP response headers
|
32
|
+
attr_accessor :headers
|
33
|
+
# @return [String] HTTP response body
|
34
|
+
attr_accessor :body
|
25
35
|
|
36
|
+
##
|
37
|
+
# Initialize the call response
|
38
|
+
#
|
39
|
+
# @param [String] call_id
|
40
|
+
# UUID of the original call
|
41
|
+
# @param [Fixnum] status
|
42
|
+
# HTTP status
|
43
|
+
# @param [Hash] headers
|
44
|
+
# HTTP response headers
|
45
|
+
# @param [#read, #to_str] body
|
46
|
+
# Response body
|
26
47
|
def initialize(call_id, status = nil, headers = nil, body = nil)
|
27
48
|
@call_id, @status, @headers, @body = call_id, status, headers, body
|
28
49
|
end
|
29
50
|
end
|
30
|
-
|
31
|
-
##
|
51
|
+
|
32
52
|
# Wraps multiple API calls into a single over-the-wire HTTP request.
|
33
|
-
|
34
|
-
|
53
|
+
#
|
54
|
+
# @example
|
55
|
+
#
|
56
|
+
# client = Google::APIClient.new
|
57
|
+
# urlshortener = client.discovered_api('urlshortener')
|
58
|
+
# batch = Google::APIClient::BatchRequest.new do |result|
|
59
|
+
# puts result.data
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# batch.add(:api_method=>urlshortener.url.insert, :body_object => { 'longUrl' => 'http://example.com/foo' })
|
63
|
+
# batch.add(:api_method=>urlshortener.url.insert, :body_object => { 'longUrl' => 'http://example.com/bar' })
|
64
|
+
#
|
65
|
+
# client.execute(batch)
|
66
|
+
#
|
67
|
+
|
68
|
+
class BatchRequest < Request
|
35
69
|
BATCH_BOUNDARY = "-----------RubyApiBatchRequest".freeze
|
36
70
|
|
37
|
-
|
38
|
-
|
71
|
+
# @api private
|
72
|
+
# @return [Array<(String,Google::APIClient::Request,Proc)] List of API calls in the batch
|
73
|
+
attr_reader :calls
|
39
74
|
|
40
75
|
##
|
41
76
|
# Creates a new batch request.
|
42
77
|
#
|
43
78
|
# @param [Hash] options
|
44
|
-
# Set of options for this request
|
45
|
-
# :connection, which specifies an HTTP connection to use.
|
79
|
+
# Set of options for this request.
|
46
80
|
# @param [Proc] block
|
47
81
|
# Callback for every call's response. Won't be called if a call defined
|
48
82
|
# a callback of its own.
|
49
83
|
#
|
50
|
-
# @return [Google::APIClient::BatchRequest]
|
84
|
+
# @return [Google::APIClient::BatchRequest]
|
85
|
+
# The constructed object.
|
86
|
+
#
|
87
|
+
# @yield [Google::APIClient::Result]
|
88
|
+
# block to be called when result ready
|
51
89
|
def initialize(options = {}, &block)
|
52
|
-
|
53
|
-
@options = options
|
54
|
-
# Batched calls to be made, indexed by call ID.
|
55
|
-
@calls = {}
|
56
|
-
# Callbacks per batched call, indexed by call ID.
|
57
|
-
@callbacks = {}
|
58
|
-
# Order for the call IDs, since Ruby 1.8 hashes are unordered.
|
59
|
-
@order = []
|
60
|
-
# Global callback to be used for every call. If a specific callback
|
61
|
-
# has been defined for a request, this won't be called.
|
90
|
+
@calls = []
|
62
91
|
@global_callback = block if block_given?
|
63
|
-
# The last auto generated ID.
|
64
92
|
@last_auto_id = 0
|
65
|
-
|
66
|
-
|
93
|
+
|
94
|
+
# TODO(sgomes): Use SecureRandom.uuid, drop UUIDTools when we drop 1.8
|
95
|
+
@base_id = UUIDTools::UUID.random_create.to_s
|
96
|
+
|
97
|
+
options[:uri] ||= 'https://www.googleapis.com/batch'
|
98
|
+
options[:http_method] ||= 'POST'
|
99
|
+
|
100
|
+
super options
|
67
101
|
end
|
68
102
|
|
69
103
|
##
|
@@ -72,69 +106,84 @@ module Google
|
|
72
106
|
# automatically be generated, avoiding collisions. If duplicate call IDs
|
73
107
|
# are provided, an error will be thrown.
|
74
108
|
#
|
75
|
-
# @param [Hash, Google::APIClient::
|
76
|
-
#
|
77
|
-
# @param [
|
109
|
+
# @param [Hash, Google::APIClient::Request] call
|
110
|
+
# the call to be added.
|
111
|
+
# @param [String] call_id
|
112
|
+
# the ID to be used for this call. Must be unique
|
113
|
+
# @param [Proc] block
|
114
|
+
# callback for this call's response.
|
115
|
+
#
|
116
|
+
# @return [Google::APIClient::BatchRequest]
|
117
|
+
# the BatchRequest, for chaining
|
78
118
|
#
|
79
|
-
# @
|
119
|
+
# @yield [Google::APIClient::Result]
|
120
|
+
# block to be called when result ready
|
80
121
|
def add(call, call_id = nil, &block)
|
81
122
|
unless call.kind_of?(Google::APIClient::Reference)
|
82
123
|
call = Google::APIClient::Reference.new(call)
|
83
124
|
end
|
84
|
-
|
85
|
-
|
86
|
-
end
|
87
|
-
if @calls.include?(call_id)
|
125
|
+
call_id ||= new_id
|
126
|
+
if @calls.assoc(call_id)
|
88
127
|
raise BatchError,
|
89
128
|
'A call with this ID already exists: %s' % call_id
|
90
129
|
end
|
91
|
-
|
92
|
-
@
|
93
|
-
if block_given?
|
94
|
-
@callbacks[call_id] = block
|
95
|
-
elsif @global_callback
|
96
|
-
@callbacks[call_id] = @global_callback
|
97
|
-
end
|
130
|
+
callback = block_given? ? block : @global_callback
|
131
|
+
@calls << [call_id, call, callback]
|
98
132
|
return self
|
99
133
|
end
|
100
134
|
|
101
|
-
##
|
102
|
-
# Convert this batch request into an HTTP request.
|
103
|
-
#
|
104
|
-
# @return [Array<String, String, Hash, String>]
|
105
|
-
# An array consisting of, in order: HTTP method, request path, request
|
106
|
-
# headers and request body.
|
107
|
-
def to_http_request
|
108
|
-
return ['POST', request_uri, request_headers, request_body]
|
109
|
-
end
|
110
|
-
|
111
135
|
##
|
112
136
|
# Processes the HTTP response to the batch request, issuing callbacks.
|
113
137
|
#
|
114
|
-
# @
|
115
|
-
|
138
|
+
# @api private
|
139
|
+
#
|
140
|
+
# @param [Faraday::Response] response
|
141
|
+
# the HTTP response.
|
142
|
+
def process_http_response(response)
|
116
143
|
content_type = find_header('Content-Type', response.headers)
|
117
144
|
boundary = /.*boundary=(.+)/.match(content_type)[1]
|
118
145
|
parts = response.body.split(/--#{Regexp.escape(boundary)}/)
|
119
146
|
parts = parts[1...-1]
|
120
147
|
parts.each do |part|
|
121
148
|
call_response = deserialize_call_response(part)
|
122
|
-
callback = @
|
123
|
-
|
124
|
-
result = Google::APIClient::Result.new(call, nil, call_response)
|
149
|
+
_, call, callback = @calls.assoc(call_response.call_id)
|
150
|
+
result = Google::APIClient::Result.new(call, call_response)
|
125
151
|
callback.call(result) if callback
|
126
152
|
end
|
153
|
+
Google::APIClient::Result.new(self, response)
|
127
154
|
end
|
128
155
|
|
129
|
-
|
156
|
+
##
|
157
|
+
# Return the request body for the BatchRequest's HTTP request.
|
158
|
+
#
|
159
|
+
# @api private
|
160
|
+
#
|
161
|
+
# @return [String]
|
162
|
+
# the request body.
|
163
|
+
def to_http_request
|
164
|
+
if @calls.nil? || @calls.empty?
|
165
|
+
raise BatchError, 'Cannot make an empty batch request'
|
166
|
+
end
|
167
|
+
parts = @calls.map {|(call_id, call, callback)| serialize_call(call_id, call)}
|
168
|
+
build_multipart(parts, 'multipart/mixed', BATCH_BOUNDARY)
|
169
|
+
super
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
protected
|
130
174
|
|
131
175
|
##
|
132
176
|
# Helper method to find a header from its name, regardless of case.
|
133
177
|
#
|
134
|
-
# @
|
135
|
-
#
|
178
|
+
# @api private
|
179
|
+
#
|
180
|
+
# @param [String] name
|
181
|
+
# the name of the header to find.
|
182
|
+
# @param [Hash] headers
|
183
|
+
# the hash of headers and their values.
|
136
184
|
#
|
137
|
-
# @return [String]
|
185
|
+
# @return [String]
|
186
|
+
# the value of the desired header.
|
138
187
|
def find_header(name, headers)
|
139
188
|
_, header = headers.detect do |h, v|
|
140
189
|
h.downcase == name.downcase
|
@@ -145,40 +194,29 @@ module Google
|
|
145
194
|
##
|
146
195
|
# Create a new call ID. Uses an auto-incrementing, conflict-avoiding ID.
|
147
196
|
#
|
148
|
-
# @
|
197
|
+
# @api private
|
198
|
+
#
|
199
|
+
# @return [String]
|
200
|
+
# the new, unique ID.
|
149
201
|
def new_id
|
150
202
|
@last_auto_id += 1
|
151
|
-
while @calls.
|
203
|
+
while @calls.assoc(@last_auto_id)
|
152
204
|
@last_auto_id += 1
|
153
205
|
end
|
154
206
|
return @last_auto_id.to_s
|
155
207
|
end
|
156
208
|
|
157
|
-
##
|
158
|
-
# Convert an id to a Content-ID header value.
|
159
|
-
#
|
160
|
-
# @param [String] call_id: identifier of individual call.
|
161
|
-
#
|
162
|
-
# @return [String]
|
163
|
-
# A Content-ID header with the call_id encoded into it. A UUID is
|
164
|
-
# prepended to the value because Content-ID headers are supposed to be
|
165
|
-
# universally unique.
|
166
|
-
def id_to_header(call_id)
|
167
|
-
if @base_id.nil?
|
168
|
-
# TODO(sgomes): Use SecureRandom.uuid, drop UUIDTools when we drop 1.8
|
169
|
-
@base_id = UUIDTools::UUID.random_create.to_s
|
170
|
-
end
|
171
|
-
|
172
|
-
return '<%s+%s>' % [@base_id, Addressable::URI.encode(call_id)]
|
173
|
-
end
|
174
|
-
|
175
209
|
##
|
176
210
|
# Convert a Content-ID header value to an id. Presumes the Content-ID
|
177
211
|
# header conforms to the format that id_to_header() returns.
|
178
212
|
#
|
179
|
-
# @
|
213
|
+
# @api private
|
214
|
+
#
|
215
|
+
# @param [String] header
|
216
|
+
# Content-ID header value.
|
180
217
|
#
|
181
|
-
# @return [String]
|
218
|
+
# @return [String]
|
219
|
+
# The extracted ID value.
|
182
220
|
def header_to_id(header)
|
183
221
|
if !header.start_with?('<') || !header.end_with?('>') ||
|
184
222
|
!header.include?('+')
|
@@ -189,36 +227,16 @@ module Google
|
|
189
227
|
return Addressable::URI.unencode(call_id)
|
190
228
|
end
|
191
229
|
|
192
|
-
##
|
193
|
-
# Convert a single batched call into a string.
|
194
|
-
#
|
195
|
-
# @param [Google::APIClient::Reference] call: the call to serialize.
|
196
|
-
#
|
197
|
-
# @return [String] The request as a string in application/http format.
|
198
|
-
def serialize_call(call)
|
199
|
-
http_request = call.to_request
|
200
|
-
method = http_request.method.to_s.upcase
|
201
|
-
path = http_request.path.to_s
|
202
|
-
status_line = method + " " + path + " HTTP/1.1"
|
203
|
-
serialized_call = status_line
|
204
|
-
if http_request.headers
|
205
|
-
http_request.headers.each do |header, value|
|
206
|
-
serialized_call << "\r\n%s: %s" % [header, value]
|
207
|
-
end
|
208
|
-
end
|
209
|
-
if http_request.body
|
210
|
-
serialized_call << "\r\n\r\n"
|
211
|
-
serialized_call << http_request.body
|
212
|
-
end
|
213
|
-
return serialized_call
|
214
|
-
end
|
215
|
-
|
216
230
|
##
|
217
231
|
# Auxiliary method to split the headers from the body in an HTTP response.
|
218
232
|
#
|
219
|
-
# @
|
233
|
+
# @api private
|
220
234
|
#
|
221
|
-
# @
|
235
|
+
# @param [String] response
|
236
|
+
# the response to parse.
|
237
|
+
#
|
238
|
+
# @return [Array<Hash>, String]
|
239
|
+
# the headers and the body, separately.
|
222
240
|
def split_headers_and_body(response)
|
223
241
|
headers = {}
|
224
242
|
payload = response.lstrip
|
@@ -239,10 +257,13 @@ module Google
|
|
239
257
|
##
|
240
258
|
# Convert a single batched response into a BatchedCallResponse object.
|
241
259
|
#
|
242
|
-
# @
|
260
|
+
# @api private
|
261
|
+
#
|
262
|
+
# @param [String] call_response
|
243
263
|
# the request to deserialize.
|
244
264
|
#
|
245
|
-
# @return [BatchedCallResponse]
|
265
|
+
# @return [Google::APIClient::BatchedCallResponse]
|
266
|
+
# the parsed and converted response.
|
246
267
|
def deserialize_call_response(call_response)
|
247
268
|
outer_headers, outer_body = split_headers_and_body(call_response)
|
248
269
|
status_line, payload = outer_body.split("\n", 2)
|
@@ -255,42 +276,49 @@ module Google
|
|
255
276
|
end
|
256
277
|
|
257
278
|
##
|
258
|
-
#
|
279
|
+
# Serialize a single batched call for assembling the multipart message
|
259
280
|
#
|
260
|
-
# @
|
261
|
-
def request_headers
|
262
|
-
return {
|
263
|
-
'Content-Type' => 'multipart/mixed; boundary=%s' % BATCH_BOUNDARY
|
264
|
-
}
|
265
|
-
end
|
266
|
-
|
267
|
-
##
|
268
|
-
# Return the request path for the BatchRequest's HTTP request.
|
281
|
+
# @api private
|
269
282
|
#
|
270
|
-
# @
|
271
|
-
|
272
|
-
|
273
|
-
|
283
|
+
# @param [Google::APIClient::Request] call
|
284
|
+
# the call to serialize.
|
285
|
+
#
|
286
|
+
# @return [Faraday::UploadIO]
|
287
|
+
# the serialized request
|
288
|
+
def serialize_call(call_id, call)
|
289
|
+
method, uri, headers, body = call.to_http_request
|
290
|
+
request = "#{method.to_s.upcase} #{Addressable::URI.parse(uri).path} HTTP/1.1"
|
291
|
+
headers.each do |header, value|
|
292
|
+
request << "\r\n%s: %s" % [header, value]
|
293
|
+
end
|
294
|
+
if body
|
295
|
+
# TODO - CompositeIO if body is a stream
|
296
|
+
request << "\r\n\r\n"
|
297
|
+
if body.respond_to?(:read)
|
298
|
+
request << body.read
|
299
|
+
else
|
300
|
+
request << body.to_s
|
301
|
+
end
|
274
302
|
end
|
275
|
-
|
276
|
-
return @calls.first[1].api_method.api.batch_path
|
303
|
+
Faraday::UploadIO.new(StringIO.new(request), 'application/http', 'ruby-api-request', 'Content-ID' => id_to_header(call_id))
|
277
304
|
end
|
278
|
-
|
305
|
+
|
279
306
|
##
|
280
|
-
#
|
307
|
+
# Convert an id to a Content-ID header value.
|
281
308
|
#
|
282
|
-
# @
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
return
|
309
|
+
# @api private
|
310
|
+
#
|
311
|
+
# @param [String] call_id
|
312
|
+
# identifier of individual call.
|
313
|
+
#
|
314
|
+
# @return [String]
|
315
|
+
# A Content-ID header with the call_id encoded into it. A UUID is
|
316
|
+
# prepended to the value because Content-ID headers are supposed to be
|
317
|
+
# universally unique.
|
318
|
+
def id_to_header(call_id)
|
319
|
+
return '<%s+%s>' % [@base_id, Addressable::URI.encode(call_id)]
|
293
320
|
end
|
321
|
+
|
294
322
|
end
|
295
323
|
end
|
296
324
|
end
|