google-api-client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +3 -0
- data/LICENSE +202 -0
- data/README +68 -0
- data/Rakefile +52 -0
- data/bin/google-api +295 -0
- data/lib/google/api_client.rb +375 -0
- data/lib/google/api_client/discovery.rb +535 -0
- data/lib/google/api_client/parsers/json_parser.rb +40 -0
- data/lib/google/api_client/version.rb +25 -0
- data/spec/google/api_client/discovery_spec.rb +428 -0
- data/spec/google/api_client/parsers/json_parser_spec.rb +51 -0
- data/spec/google/api_client_spec.rb +79 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +5 -0
- data/tasks/clobber.rake +2 -0
- data/tasks/gem.rake +76 -0
- data/tasks/git.rake +40 -0
- data/tasks/metrics.rake +22 -0
- data/tasks/rdoc.rake +26 -0
- data/tasks/spec.rake +78 -0
- data/tasks/yard.rake +26 -0
- metadata +281 -0
@@ -0,0 +1,375 @@
|
|
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 'httpadapter'
|
16
|
+
require 'json'
|
17
|
+
|
18
|
+
require 'google/api_client/discovery'
|
19
|
+
|
20
|
+
module Google
|
21
|
+
# TODO(bobaman): Document all this stuff.
|
22
|
+
|
23
|
+
##
|
24
|
+
# This class manages communication with a single API.
|
25
|
+
class APIClient
|
26
|
+
##
|
27
|
+
# An error which is raised when there is an unexpected response or other
|
28
|
+
# transport error that prevents an operation from succeeding.
|
29
|
+
class TransmissionError < StandardError
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(options={})
|
33
|
+
@options = {
|
34
|
+
# TODO: What configuration options need to go here?
|
35
|
+
}.merge(options)
|
36
|
+
# Force immediate type-checking and short-cut resolution
|
37
|
+
self.parser
|
38
|
+
self.authorization
|
39
|
+
self.http_adapter
|
40
|
+
return self
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Returns the parser used by the client.
|
45
|
+
def parser
|
46
|
+
unless @options[:parser]
|
47
|
+
require 'google/api_client/parsers/json_parser'
|
48
|
+
# NOTE: Do not rely on this default value, as it may change
|
49
|
+
@options[:parser] = JSONParser
|
50
|
+
end
|
51
|
+
return @options[:parser]
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Returns the authorization mechanism used by the client.
|
56
|
+
#
|
57
|
+
# @return [#generate_authenticated_request] The authorization mechanism.
|
58
|
+
def authorization
|
59
|
+
case @options[:authorization]
|
60
|
+
when :oauth_1, :oauth
|
61
|
+
require 'signet/oauth_1/client'
|
62
|
+
# NOTE: Do not rely on this default value, as it may change
|
63
|
+
@options[:authorization] = Signet::OAuth1::Client.new(
|
64
|
+
:temporary_credential_uri =>
|
65
|
+
'https://www.google.com/accounts/OAuthGetRequestToken',
|
66
|
+
:authorization_uri =>
|
67
|
+
'https://www.google.com/accounts/OAuthAuthorizeToken',
|
68
|
+
:token_credential_uri =>
|
69
|
+
'https://www.google.com/accounts/OAuthGetAccessToken',
|
70
|
+
:client_credential_key => 'anonymous',
|
71
|
+
:client_credential_secret => 'anonymous'
|
72
|
+
)
|
73
|
+
when nil
|
74
|
+
# No authorization mechanism
|
75
|
+
else
|
76
|
+
if !@options[:authorization].respond_to?(
|
77
|
+
:generate_authenticated_request)
|
78
|
+
raise TypeError,
|
79
|
+
'Expected authorization mechanism to respond to ' +
|
80
|
+
'#generate_authenticated_request.'
|
81
|
+
end
|
82
|
+
end
|
83
|
+
return @options[:authorization]
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Sets the authorization mechanism used by the client.
|
88
|
+
#
|
89
|
+
# @param [#generate_authenticated_request] new_authorization
|
90
|
+
# The new authorization mechanism.
|
91
|
+
def authorization=(new_authorization)
|
92
|
+
@options[:authorization] = new_authorization
|
93
|
+
return self.authorization
|
94
|
+
end
|
95
|
+
|
96
|
+
##
|
97
|
+
# Returns the HTTP adapter used by the client.
|
98
|
+
def http_adapter
|
99
|
+
return @options[:http_adapter] ||= (begin
|
100
|
+
require 'httpadapter/adapters/net_http'
|
101
|
+
@options[:http_adapter] = HTTPAdapter::NetHTTPRequestAdapter
|
102
|
+
end)
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Returns the URI for the discovery document.
|
107
|
+
#
|
108
|
+
# @return [Addressable::URI] The URI of the discovery document.
|
109
|
+
def discovery_uri
|
110
|
+
return @options[:discovery_uri] ||= (begin
|
111
|
+
if @options[:service]
|
112
|
+
service_id = @options[:service]
|
113
|
+
service_version = @options[:service_version] || 'v1'
|
114
|
+
Addressable::URI.parse(
|
115
|
+
"http://www.googleapis.com/discovery/0.1/describe" +
|
116
|
+
"?api=#{service_id}"
|
117
|
+
)
|
118
|
+
else
|
119
|
+
raise ArgumentError,
|
120
|
+
'Missing required configuration value, :discovery_uri.'
|
121
|
+
end
|
122
|
+
end)
|
123
|
+
end
|
124
|
+
|
125
|
+
##
|
126
|
+
# Returns the parsed discovery document.
|
127
|
+
#
|
128
|
+
# @return [Hash] The parsed JSON from the discovery document.
|
129
|
+
def discovery_document
|
130
|
+
return @discovery_document ||= (begin
|
131
|
+
request = ['GET', self.discovery_uri.to_s, [], []]
|
132
|
+
response = self.transmit_request(request)
|
133
|
+
status, headers, body = response
|
134
|
+
if status == 200 # TODO(bobaman) Better status code handling?
|
135
|
+
merged_body = StringIO.new
|
136
|
+
body.each do |chunk|
|
137
|
+
merged_body.write(chunk)
|
138
|
+
end
|
139
|
+
merged_body.rewind
|
140
|
+
JSON.parse(merged_body.string)
|
141
|
+
else
|
142
|
+
raise TransmissionError,
|
143
|
+
"Could not retrieve discovery document at: #{self.discovery_uri}"
|
144
|
+
end
|
145
|
+
end)
|
146
|
+
end
|
147
|
+
|
148
|
+
##
|
149
|
+
# Returns a list of services this client instance has performed discovery
|
150
|
+
# for. This may return multiple versions of the same service.
|
151
|
+
#
|
152
|
+
# @return [Array]
|
153
|
+
# A list of discovered <code>Google::APIClient::Service</code> objects.
|
154
|
+
def discovered_services
|
155
|
+
return @discovered_services ||= (begin
|
156
|
+
service_names = self.discovery_document['data'].keys()
|
157
|
+
services = []
|
158
|
+
for service_name in service_names
|
159
|
+
versions = self.discovery_document['data'][service_name]
|
160
|
+
for service_version in versions.keys()
|
161
|
+
service_description =
|
162
|
+
self.discovery_document['data'][service_name][service_version]
|
163
|
+
services << ::Google::APIClient::Service.new(
|
164
|
+
service_name,
|
165
|
+
service_version,
|
166
|
+
service_description
|
167
|
+
)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
services
|
171
|
+
end)
|
172
|
+
end
|
173
|
+
|
174
|
+
##
|
175
|
+
# Returns the service object for a given service name and service version.
|
176
|
+
#
|
177
|
+
# @param [String, Symbol] service_name The service name.
|
178
|
+
# @param [String] service_version The desired version of the service.
|
179
|
+
#
|
180
|
+
# @return [Google::APIClient::Service] The service object.
|
181
|
+
def discovered_service(service_name, service_version='v1')
|
182
|
+
if !service_name.kind_of?(String) && !service_name.kind_of?(Symbol)
|
183
|
+
raise TypeError,
|
184
|
+
"Expected String or Symbol, got #{service_name.class}."
|
185
|
+
end
|
186
|
+
service_name = service_name.to_s
|
187
|
+
for service in self.discovered_services
|
188
|
+
if service.name == service_name &&
|
189
|
+
service.version.to_s == service_version.to_s
|
190
|
+
return service
|
191
|
+
end
|
192
|
+
end
|
193
|
+
return nil
|
194
|
+
end
|
195
|
+
|
196
|
+
##
|
197
|
+
# Returns the method object for a given RPC name and service version.
|
198
|
+
#
|
199
|
+
# @param [String, Symbol] rpc_name The RPC name of the desired method.
|
200
|
+
# @param [String] service_version The desired version of the service.
|
201
|
+
#
|
202
|
+
# @return [Google::APIClient::Method] The method object.
|
203
|
+
def discovered_method(rpc_name, service_version='v1')
|
204
|
+
if !rpc_name.kind_of?(String) && !rpc_name.kind_of?(Symbol)
|
205
|
+
raise TypeError,
|
206
|
+
"Expected String or Symbol, got #{rpc_name.class}."
|
207
|
+
end
|
208
|
+
rpc_name = rpc_name.to_s
|
209
|
+
for service in self.discovered_services
|
210
|
+
# This looks kinda weird, but is not a real problem because there's
|
211
|
+
# almost always only one service, and this is memoized anyhow.
|
212
|
+
if service.version.to_s == service_version.to_s
|
213
|
+
return service.to_h[rpc_name] if service.to_h[rpc_name]
|
214
|
+
end
|
215
|
+
end
|
216
|
+
return nil
|
217
|
+
end
|
218
|
+
|
219
|
+
##
|
220
|
+
# Returns the service object with the highest version number.
|
221
|
+
#
|
222
|
+
# <em>Warning</em>: This method should be used with great care. As APIs
|
223
|
+
# are updated, minor differences between versions may cause
|
224
|
+
# incompatibilities. Requesting a specific version will avoid this issue.
|
225
|
+
#
|
226
|
+
# @param [String, Symbol] service_name The name of the service.
|
227
|
+
#
|
228
|
+
# @return [Google::APIClient::Service] The service object.
|
229
|
+
def latest_service_version(service_name)
|
230
|
+
if !service_name.kind_of?(String) && !service_name.kind_of?(Symbol)
|
231
|
+
raise TypeError,
|
232
|
+
"Expected String or Symbol, got #{service_name.class}."
|
233
|
+
end
|
234
|
+
service_name = service_name.to_s
|
235
|
+
return (self.discovered_services.select do |service|
|
236
|
+
service.name == service_name
|
237
|
+
end).sort.last
|
238
|
+
end
|
239
|
+
|
240
|
+
##
|
241
|
+
# Generates a request.
|
242
|
+
#
|
243
|
+
# @param [Google::APIClient::Method, String] api_method
|
244
|
+
# The method object or the RPC name of the method being executed.
|
245
|
+
# @param [Hash, Array] parameters
|
246
|
+
# The parameters to send to the method.
|
247
|
+
# @param [String] body The body of the request.
|
248
|
+
# @param [Hash, Array] headers The HTTP headers for the request.
|
249
|
+
# @param [Hash] options
|
250
|
+
# The configuration parameters for the request.
|
251
|
+
# - <code>:service_version</code> —
|
252
|
+
# The service version. Only used if <code>api_method</code> is a
|
253
|
+
# <code>String</code>. Defaults to <code>'v1'</code>.
|
254
|
+
# - <code>:parser</code> —
|
255
|
+
# The parser for the response.
|
256
|
+
# - <code>:authorization</code> —
|
257
|
+
# The authorization mechanism for the response. Used only if
|
258
|
+
# <code>:signed</code> is <code>true</code>.
|
259
|
+
# - <code>:signed</code> —
|
260
|
+
# <code>true</code> if the request must be signed, <code>false</code>
|
261
|
+
# otherwise. Defaults to <code>true</code> if an authorization
|
262
|
+
# mechanism has been set, <code>false</code> otherwise.
|
263
|
+
#
|
264
|
+
# @return [Array] The generated request.
|
265
|
+
#
|
266
|
+
# @example
|
267
|
+
# request = client.generate_request(
|
268
|
+
# 'chili.activities.list',
|
269
|
+
# {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
|
270
|
+
# )
|
271
|
+
# method, uri, headers, body = request
|
272
|
+
def generate_request(
|
273
|
+
api_method, parameters={}, body='', headers=[], options={})
|
274
|
+
options={
|
275
|
+
:parser => self.parser,
|
276
|
+
:service_version => 'v1',
|
277
|
+
:authorization => self.authorization
|
278
|
+
}.merge(options)
|
279
|
+
# The default value for the :signed option depends on whether an
|
280
|
+
# authorization mechanism has been set.
|
281
|
+
if options[:authorization]
|
282
|
+
options = {:signed => true}.merge(options)
|
283
|
+
else
|
284
|
+
options = {:signed => false}.merge(options)
|
285
|
+
end
|
286
|
+
if api_method.kind_of?(String) || api_method.kind_of?(Symbol)
|
287
|
+
api_method = self.discovered_method(
|
288
|
+
api_method.to_s, options[:service_version]
|
289
|
+
)
|
290
|
+
elsif !api_method.kind_of?(::Google::APIClient::Method)
|
291
|
+
raise TypeError,
|
292
|
+
"Expected String, Symbol, or Google::APIClient::Method, " +
|
293
|
+
"got #{api_method.class}."
|
294
|
+
end
|
295
|
+
unless api_method
|
296
|
+
raise ArgumentError, "API method could not be found."
|
297
|
+
end
|
298
|
+
request = api_method.generate_request(parameters, body, headers)
|
299
|
+
if options[:signed]
|
300
|
+
request = self.sign_request(request, options[:authorization])
|
301
|
+
end
|
302
|
+
return request
|
303
|
+
end
|
304
|
+
|
305
|
+
##
|
306
|
+
# Generates a request and transmits it.
|
307
|
+
#
|
308
|
+
# @param [Google::APIClient::Method, String] api_method
|
309
|
+
# The method object or the RPC name of the method being executed.
|
310
|
+
# @param [Hash, Array] parameters
|
311
|
+
# The parameters to send to the method.
|
312
|
+
# @param [String] body The body of the request.
|
313
|
+
# @param [Hash, Array] headers The HTTP headers for the request.
|
314
|
+
# @param [Hash] options
|
315
|
+
# The configuration parameters for the request.
|
316
|
+
# - <code>:service_version</code> —
|
317
|
+
# The service version. Only used if <code>api_method</code> is a
|
318
|
+
# <code>String</code>. Defaults to <code>'v1'</code>.
|
319
|
+
# - <code>:adapter</code> —
|
320
|
+
# The HTTP adapter.
|
321
|
+
# - <code>:parser</code> —
|
322
|
+
# The parser for the response.
|
323
|
+
# - <code>:authorization</code> —
|
324
|
+
# The authorization mechanism for the response. Used only if
|
325
|
+
# <code>:signed</code> is <code>true</code>.
|
326
|
+
# - <code>:signed</code> —
|
327
|
+
# <code>true</code> if the request must be signed, <code>false</code>
|
328
|
+
# otherwise. Defaults to <code>true</code>.
|
329
|
+
#
|
330
|
+
# @return [Array] The response from the API.
|
331
|
+
#
|
332
|
+
# @example
|
333
|
+
# response = client.execute(
|
334
|
+
# 'chili.activities.list',
|
335
|
+
# {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
|
336
|
+
# )
|
337
|
+
# status, headers, body = response
|
338
|
+
def execute(api_method, parameters={}, body='', headers=[], options={})
|
339
|
+
request = self.generate_request(
|
340
|
+
api_method, parameters, body, headers, options
|
341
|
+
)
|
342
|
+
return self.transmit_request(
|
343
|
+
request,
|
344
|
+
options[:adapter] || self.http_adapter
|
345
|
+
)
|
346
|
+
end
|
347
|
+
|
348
|
+
##
|
349
|
+
# Transmits the request using the current HTTP adapter.
|
350
|
+
#
|
351
|
+
# @param [Array] request The request to transmit.
|
352
|
+
# @param [#transmit] adapter The HTTP adapter.
|
353
|
+
#
|
354
|
+
# @return [Array] The response from the server.
|
355
|
+
def transmit_request(request, adapter=self.http_adapter)
|
356
|
+
::HTTPAdapter.transmit(request, adapter)
|
357
|
+
end
|
358
|
+
|
359
|
+
##
|
360
|
+
# Signs a request using the current authorization mechanism.
|
361
|
+
#
|
362
|
+
# @param [Array] request The request to sign.
|
363
|
+
# @param [#generate_authenticated_request] authorization
|
364
|
+
# The authorization mechanism.
|
365
|
+
#
|
366
|
+
# @return [Array] The signed request.
|
367
|
+
def sign_request(request, authorization=self.authorization)
|
368
|
+
return authorization.generate_authenticated_request(
|
369
|
+
:request => request
|
370
|
+
)
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
require 'google/api_client/version'
|
@@ -0,0 +1,535 @@
|
|
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 'json'
|
16
|
+
require 'addressable/uri'
|
17
|
+
require 'addressable/template'
|
18
|
+
require 'extlib/inflection'
|
19
|
+
|
20
|
+
module Google
|
21
|
+
class APIClient
|
22
|
+
##
|
23
|
+
# An exception that is raised if a method is called with missing or
|
24
|
+
# invalid parameter values.
|
25
|
+
class ValidationError < StandardError
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# A service that has been described by a discovery document.
|
30
|
+
class Service
|
31
|
+
|
32
|
+
##
|
33
|
+
# Creates a description of a particular version of a service.
|
34
|
+
#
|
35
|
+
# @param [String] service_name
|
36
|
+
# The identifier for the service. Note that while this frequently
|
37
|
+
# matches the first segment of all of the service's RPC names, this
|
38
|
+
# should not be assumed. There is no requirement that these match.
|
39
|
+
# @param [String] service_version
|
40
|
+
# The identifier for the service version.
|
41
|
+
# @param [Hash] service_description
|
42
|
+
# The section of the discovery document that applies to this service
|
43
|
+
# version.
|
44
|
+
#
|
45
|
+
# @return [Google::APIClient::Service] The constructed service object.
|
46
|
+
def initialize(service_name, service_version, service_description)
|
47
|
+
@name = service_name
|
48
|
+
@version = service_version
|
49
|
+
@description = service_description
|
50
|
+
metaclass = (class <<self; self; end)
|
51
|
+
self.resources.each do |resource|
|
52
|
+
method_name = Extlib::Inflection.underscore(resource.name).to_sym
|
53
|
+
if !self.respond_to?(method_name)
|
54
|
+
metaclass.send(:define_method, method_name) { resource }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
self.methods.each do |method|
|
58
|
+
method_name = Extlib::Inflection.underscore(method.name).to_sym
|
59
|
+
if !self.respond_to?(method_name)
|
60
|
+
metaclass.send(:define_method, method_name) { method }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Returns the identifier for the service.
|
67
|
+
#
|
68
|
+
# @return [String] The service identifier.
|
69
|
+
attr_reader :name
|
70
|
+
|
71
|
+
##
|
72
|
+
# Returns the version of the service.
|
73
|
+
#
|
74
|
+
# @return [String] The service version.
|
75
|
+
attr_reader :version
|
76
|
+
|
77
|
+
##
|
78
|
+
# Returns the parsed section of the discovery document that applies to
|
79
|
+
# this version of the service.
|
80
|
+
#
|
81
|
+
# @return [Hash] The service description.
|
82
|
+
attr_reader :description
|
83
|
+
|
84
|
+
##
|
85
|
+
# Returns the base URI for this version of the service.
|
86
|
+
#
|
87
|
+
# @return [Addressable::URI] The base URI that methods are joined to.
|
88
|
+
def base
|
89
|
+
return @base ||= Addressable::URI.parse(self.description['baseUrl'])
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# A list of resources available at the root level of this version of the
|
94
|
+
# service.
|
95
|
+
#
|
96
|
+
# @return [Array] A list of {Google::APIClient::Resource} objects.
|
97
|
+
def resources
|
98
|
+
return @resources ||= (
|
99
|
+
(self.description['resources'] || []).inject([]) do |accu, (k, v)|
|
100
|
+
accu << ::Google::APIClient::Resource.new(self.base, k, v)
|
101
|
+
accu
|
102
|
+
end
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# A list of methods available at the root level of this version of the
|
108
|
+
# service.
|
109
|
+
#
|
110
|
+
# @return [Array] A list of {Google::APIClient::Method} objects.
|
111
|
+
def methods
|
112
|
+
return @methods ||= (
|
113
|
+
(self.description['methods'] || []).inject([]) do |accu, (k, v)|
|
114
|
+
accu << ::Google::APIClient::Method.new(self.base, k, v)
|
115
|
+
accu
|
116
|
+
end
|
117
|
+
)
|
118
|
+
end
|
119
|
+
|
120
|
+
##
|
121
|
+
# Converts the service to a flat mapping of RPC names and method objects.
|
122
|
+
#
|
123
|
+
# @return [Hash] All methods available on the service.
|
124
|
+
#
|
125
|
+
# @example
|
126
|
+
# # Discover available methods
|
127
|
+
# method_names = client.discovered_service('buzz').to_h.keys
|
128
|
+
def to_h
|
129
|
+
return @hash ||= (begin
|
130
|
+
methods_hash = {}
|
131
|
+
self.methods.each do |method|
|
132
|
+
methods_hash[method.rpc_name] = method
|
133
|
+
end
|
134
|
+
self.resources.each do |resource|
|
135
|
+
methods_hash.merge!(resource.to_h)
|
136
|
+
end
|
137
|
+
methods_hash
|
138
|
+
end)
|
139
|
+
end
|
140
|
+
|
141
|
+
##
|
142
|
+
# Compares two versions of a service.
|
143
|
+
#
|
144
|
+
# @param [Object] other The service to compare.
|
145
|
+
#
|
146
|
+
# @return [Integer]
|
147
|
+
# <code>-1</code> if the service is older than <code>other</code>.
|
148
|
+
# <code>0</code> if the service is the same as <code>other</code>.
|
149
|
+
# <code>1</code> if the service is newer than <code>other</code>.
|
150
|
+
# <code>nil</code> if the service cannot be compared to
|
151
|
+
# <code>other</code>.
|
152
|
+
def <=>(other)
|
153
|
+
# We can only compare versions of the same service
|
154
|
+
if other.kind_of?(self.class) && self.name == other.name
|
155
|
+
split_version = lambda do |version|
|
156
|
+
dotted_version = version[/^v?(\d+(.\d+)*)-?(.*?)?$/, 1]
|
157
|
+
suffix = version[/^v?(\d+(.\d+)*)-?(.*?)?$/, 3]
|
158
|
+
[dotted_version.split('.').map { |v| v.to_i }, suffix]
|
159
|
+
end
|
160
|
+
self_sortable, self_suffix = split_version.call(self.version)
|
161
|
+
other_sortable, other_suffix = split_version.call(other.version)
|
162
|
+
result = self_sortable <=> other_sortable
|
163
|
+
if result != 0
|
164
|
+
return result
|
165
|
+
# If the dotted versions are equal, check the suffix.
|
166
|
+
# An omitted suffix should be sorted after an included suffix.
|
167
|
+
elsif self_suffix == ''
|
168
|
+
return 1
|
169
|
+
elsif other_suffix == ''
|
170
|
+
return -1
|
171
|
+
else
|
172
|
+
return self_suffix <=> other_suffix
|
173
|
+
end
|
174
|
+
else
|
175
|
+
return nil
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
##
|
180
|
+
# Returns a <code>String</code> representation of the service's state.
|
181
|
+
#
|
182
|
+
# @return [String] The service's state, as a <code>String</code>.
|
183
|
+
def inspect
|
184
|
+
sprintf(
|
185
|
+
"#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.name
|
186
|
+
)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
##
|
191
|
+
# A resource that has been described by a discovery document.
|
192
|
+
class Resource
|
193
|
+
|
194
|
+
##
|
195
|
+
# Creates a description of a particular version of a resource.
|
196
|
+
#
|
197
|
+
# @param [Addressable::URI] base
|
198
|
+
# The base URI for the service.
|
199
|
+
# @param [String] resource_name
|
200
|
+
# The identifier for the resource.
|
201
|
+
# @param [Hash] resource_description
|
202
|
+
# The section of the discovery document that applies to this resource.
|
203
|
+
#
|
204
|
+
# @return [Google::APIClient::Resource] The constructed resource object.
|
205
|
+
def initialize(base, resource_name, resource_description)
|
206
|
+
@base = base
|
207
|
+
@name = resource_name
|
208
|
+
@description = resource_description
|
209
|
+
metaclass = (class <<self; self; end)
|
210
|
+
self.resources.each do |resource|
|
211
|
+
method_name = Extlib::Inflection.underscore(resource.name).to_sym
|
212
|
+
if !self.respond_to?(method_name)
|
213
|
+
metaclass.send(:define_method, method_name) { resource }
|
214
|
+
end
|
215
|
+
end
|
216
|
+
self.methods.each do |method|
|
217
|
+
method_name = Extlib::Inflection.underscore(method.name).to_sym
|
218
|
+
if !self.respond_to?(method_name)
|
219
|
+
metaclass.send(:define_method, method_name) { method }
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
##
|
225
|
+
# Returns the identifier for the resource.
|
226
|
+
#
|
227
|
+
# @return [String] The resource identifier.
|
228
|
+
attr_reader :name
|
229
|
+
|
230
|
+
##
|
231
|
+
# Returns the parsed section of the discovery document that applies to
|
232
|
+
# this resource.
|
233
|
+
#
|
234
|
+
# @return [Hash] The resource description.
|
235
|
+
attr_reader :description
|
236
|
+
|
237
|
+
##
|
238
|
+
# Returns the base URI for this resource.
|
239
|
+
#
|
240
|
+
# @return [Addressable::URI] The base URI that methods are joined to.
|
241
|
+
attr_reader :base
|
242
|
+
|
243
|
+
##
|
244
|
+
# A list of sub-resources available on this resource.
|
245
|
+
#
|
246
|
+
# @return [Array] A list of {Google::APIClient::Resource} objects.
|
247
|
+
def resources
|
248
|
+
return @resources ||= (
|
249
|
+
(self.description['resources'] || []).inject([]) do |accu, (k, v)|
|
250
|
+
accu << ::Google::APIClient::Resource.new(self.base, k, v)
|
251
|
+
accu
|
252
|
+
end
|
253
|
+
)
|
254
|
+
end
|
255
|
+
|
256
|
+
##
|
257
|
+
# A list of methods available on this resource.
|
258
|
+
#
|
259
|
+
# @return [Array] A list of {Google::APIClient::Method} objects.
|
260
|
+
def methods
|
261
|
+
return @methods ||= (
|
262
|
+
(self.description['methods'] || []).inject([]) do |accu, (k, v)|
|
263
|
+
accu << ::Google::APIClient::Method.new(self.base, k, v)
|
264
|
+
accu
|
265
|
+
end
|
266
|
+
)
|
267
|
+
end
|
268
|
+
|
269
|
+
##
|
270
|
+
# Converts the resource to a flat mapping of RPC names and method
|
271
|
+
# objects.
|
272
|
+
#
|
273
|
+
# @return [Hash] All methods available on the resource.
|
274
|
+
def to_h
|
275
|
+
return @hash ||= (begin
|
276
|
+
methods_hash = {}
|
277
|
+
self.methods.each do |method|
|
278
|
+
methods_hash[method.rpc_name] = method
|
279
|
+
end
|
280
|
+
self.resources.each do |resource|
|
281
|
+
methods_hash.merge!(resource.to_h)
|
282
|
+
end
|
283
|
+
methods_hash
|
284
|
+
end)
|
285
|
+
end
|
286
|
+
|
287
|
+
##
|
288
|
+
# Returns a <code>String</code> representation of the resource's state.
|
289
|
+
#
|
290
|
+
# @return [String] The resource's state, as a <code>String</code>.
|
291
|
+
def inspect
|
292
|
+
sprintf(
|
293
|
+
"#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.name
|
294
|
+
)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
##
|
299
|
+
# A method that has been described by a discovery document.
|
300
|
+
class Method
|
301
|
+
|
302
|
+
##
|
303
|
+
# Creates a description of a particular method.
|
304
|
+
#
|
305
|
+
# @param [Addressable::URI] base
|
306
|
+
# The base URI for the service.
|
307
|
+
# @param [String] method_name
|
308
|
+
# The identifier for the method.
|
309
|
+
# @param [Hash] method_description
|
310
|
+
# The section of the discovery document that applies to this method.
|
311
|
+
#
|
312
|
+
# @return [Google::APIClient::Method] The constructed method object.
|
313
|
+
def initialize(base, method_name, method_description)
|
314
|
+
@base = base
|
315
|
+
@name = method_name
|
316
|
+
@description = method_description
|
317
|
+
end
|
318
|
+
|
319
|
+
##
|
320
|
+
# Returns the identifier for the method.
|
321
|
+
#
|
322
|
+
# @return [String] The method identifier.
|
323
|
+
attr_reader :name
|
324
|
+
|
325
|
+
##
|
326
|
+
# Returns the parsed section of the discovery document that applies to
|
327
|
+
# this method.
|
328
|
+
#
|
329
|
+
# @return [Hash] The method description.
|
330
|
+
attr_reader :description
|
331
|
+
|
332
|
+
##
|
333
|
+
# Returns the base URI for the method.
|
334
|
+
#
|
335
|
+
# @return [Addressable::URI]
|
336
|
+
# The base URI that this method will be joined to.
|
337
|
+
attr_reader :base
|
338
|
+
|
339
|
+
##
|
340
|
+
# Returns the RPC name for the method.
|
341
|
+
#
|
342
|
+
# @return [String] The RPC name.
|
343
|
+
def rpc_name
|
344
|
+
return self.description['rpcName']
|
345
|
+
end
|
346
|
+
|
347
|
+
##
|
348
|
+
# Returns the URI template for the method. A parameter list can be
|
349
|
+
# used to expand this into a URI.
|
350
|
+
#
|
351
|
+
# @return [Addressable::Template] The URI template.
|
352
|
+
def uri_template
|
353
|
+
# TODO(bobaman) We shouldn't be calling #to_s here, this should be
|
354
|
+
# a join operation on a URI, but we have to treat these as Strings
|
355
|
+
# because of the way the discovery document provides the URIs.
|
356
|
+
# This should be fixed soon.
|
357
|
+
return @uri_template ||=
|
358
|
+
Addressable::Template.new(base.to_s + self.description['pathUrl'])
|
359
|
+
end
|
360
|
+
|
361
|
+
##
|
362
|
+
# Normalizes parameters, converting to the appropriate types.
|
363
|
+
#
|
364
|
+
# @param [Hash, Array] parameters
|
365
|
+
# The parameters to normalize.
|
366
|
+
#
|
367
|
+
# @return [Hash] The normalized parameters.
|
368
|
+
def normalize_parameters(parameters={})
|
369
|
+
# Convert keys to Strings when appropriate
|
370
|
+
if parameters.kind_of?(Hash) || parameters.kind_of?(Array)
|
371
|
+
# Is a Hash or an Array a better return type? Do we ever need to
|
372
|
+
# worry about the same parameter being sent twice with different
|
373
|
+
# values?
|
374
|
+
parameters = parameters.inject({}) do |accu, (k, v)|
|
375
|
+
k = k.to_s if k.kind_of?(Symbol)
|
376
|
+
k = k.to_str if k.respond_to?(:to_str)
|
377
|
+
unless k.kind_of?(String)
|
378
|
+
raise TypeError, "Expected String, got #{k.class}."
|
379
|
+
end
|
380
|
+
accu[k] = v
|
381
|
+
accu
|
382
|
+
end
|
383
|
+
else
|
384
|
+
raise TypeError,
|
385
|
+
"Expected Hash or Array, got #{parameters.class}."
|
386
|
+
end
|
387
|
+
return parameters
|
388
|
+
end
|
389
|
+
|
390
|
+
##
|
391
|
+
# Expands the method's URI template using a parameter list.
|
392
|
+
#
|
393
|
+
# @param [Hash, Array] parameters
|
394
|
+
# The parameter list to use.
|
395
|
+
#
|
396
|
+
# @return [Addressable::URI] The URI after expansion.
|
397
|
+
def generate_uri(parameters={})
|
398
|
+
parameters = self.normalize_parameters(parameters)
|
399
|
+
self.validate_parameters(parameters)
|
400
|
+
template_variables = self.uri_template.variables
|
401
|
+
uri = self.uri_template.expand(parameters)
|
402
|
+
query_parameters = parameters.reject do |k, v|
|
403
|
+
template_variables.include?(k)
|
404
|
+
end
|
405
|
+
if query_parameters.size > 0
|
406
|
+
uri.query_values = (uri.query_values || {}).merge(query_parameters)
|
407
|
+
end
|
408
|
+
# Normalization is necessary because of undesirable percent-escaping
|
409
|
+
# during URI template expansion
|
410
|
+
return uri.normalize
|
411
|
+
end
|
412
|
+
|
413
|
+
##
|
414
|
+
# Generates an HTTP request for this method.
|
415
|
+
#
|
416
|
+
# @param [Hash, Array] parameters
|
417
|
+
# The parameters to send.
|
418
|
+
# @param [String, StringIO] body The body for the HTTP request.
|
419
|
+
# @param [Hash, Array] headers The HTTP headers for the request.
|
420
|
+
#
|
421
|
+
# @return [Array] The generated HTTP request.
|
422
|
+
def generate_request(parameters={}, body='', headers=[])
|
423
|
+
if body.respond_to?(:string)
|
424
|
+
body = body.string
|
425
|
+
elsif body.respond_to?(:to_str)
|
426
|
+
body = body.to_str
|
427
|
+
else
|
428
|
+
raise TypeError, "Expected String or StringIO, got #{body.class}."
|
429
|
+
end
|
430
|
+
if !headers.kind_of?(Array) && !headers.kind_of?(Hash)
|
431
|
+
raise TypeError, "Expected Hash or Array, got #{headers.class}."
|
432
|
+
end
|
433
|
+
method = self.description['httpMethod'] || 'GET'
|
434
|
+
uri = self.generate_uri(parameters)
|
435
|
+
headers = headers.to_a if headers.kind_of?(Hash)
|
436
|
+
return [method, uri.to_str, headers, [body]]
|
437
|
+
end
|
438
|
+
|
439
|
+
##
|
440
|
+
# Returns a <code>Hash</code> of the parameter descriptions for
|
441
|
+
# this method.
|
442
|
+
#
|
443
|
+
# @return [Hash] The parameter descriptions.
|
444
|
+
def parameter_descriptions
|
445
|
+
@parameter_descriptions ||= (
|
446
|
+
self.description['parameters'] || {}
|
447
|
+
).inject({}) { |h,(k,v)| h[k]=v; h }
|
448
|
+
end
|
449
|
+
|
450
|
+
##
|
451
|
+
# Returns an <code>Array</code> of the parameters for this method.
|
452
|
+
#
|
453
|
+
# @return [Array] The parameters.
|
454
|
+
def parameters
|
455
|
+
@parameters ||= ((
|
456
|
+
self.description['parameters'] || {}
|
457
|
+
).inject({}) { |h,(k,v)| h[k]=v; h }).keys
|
458
|
+
end
|
459
|
+
|
460
|
+
##
|
461
|
+
# Returns an <code>Array</code> of the required parameters for this
|
462
|
+
# method.
|
463
|
+
#
|
464
|
+
# @return [Array] The required parameters.
|
465
|
+
#
|
466
|
+
# @example
|
467
|
+
# # A list of all required parameters.
|
468
|
+
# method.required_parameters
|
469
|
+
def required_parameters
|
470
|
+
@required_parameters ||= ((self.parameter_descriptions.select do |k, v|
|
471
|
+
v['required']
|
472
|
+
end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
|
473
|
+
end
|
474
|
+
|
475
|
+
##
|
476
|
+
# Returns an <code>Array</code> of the optional parameters for this
|
477
|
+
# method.
|
478
|
+
#
|
479
|
+
# @return [Array] The optional parameters.
|
480
|
+
#
|
481
|
+
# @example
|
482
|
+
# # A list of all optional parameters.
|
483
|
+
# method.optional_parameters
|
484
|
+
def optional_parameters
|
485
|
+
@optional_parameters ||= ((self.parameter_descriptions.reject do |k, v|
|
486
|
+
v['required']
|
487
|
+
end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
|
488
|
+
end
|
489
|
+
|
490
|
+
##
|
491
|
+
# Verifies that the parameters are valid for this method. Raises an
|
492
|
+
# exception if validation fails.
|
493
|
+
#
|
494
|
+
# @param [Hash, Array] parameters
|
495
|
+
# The parameters to verify.
|
496
|
+
#
|
497
|
+
# @return [NilClass] <code>nil</code> if validation passes.
|
498
|
+
def validate_parameters(parameters={})
|
499
|
+
parameters = self.normalize_parameters(parameters)
|
500
|
+
required_variables = ((self.parameter_descriptions.select do |k, v|
|
501
|
+
v['required']
|
502
|
+
end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
|
503
|
+
missing_variables = required_variables - parameters.keys
|
504
|
+
if missing_variables.size > 0
|
505
|
+
raise ArgumentError,
|
506
|
+
"Missing required parameters: #{missing_variables.join(', ')}."
|
507
|
+
end
|
508
|
+
parameters.each do |k, v|
|
509
|
+
if self.parameter_descriptions[k]
|
510
|
+
pattern = self.parameter_descriptions[k]['pattern']
|
511
|
+
if pattern
|
512
|
+
regexp = Regexp.new("^#{pattern}$")
|
513
|
+
if v !~ regexp
|
514
|
+
raise ArgumentError,
|
515
|
+
"Parameter '#{k}' has an invalid value: #{v}. " +
|
516
|
+
"Must match: /^#{pattern}$/."
|
517
|
+
end
|
518
|
+
end
|
519
|
+
end
|
520
|
+
end
|
521
|
+
return nil
|
522
|
+
end
|
523
|
+
|
524
|
+
##
|
525
|
+
# Returns a <code>String</code> representation of the method's state.
|
526
|
+
#
|
527
|
+
# @return [String] The method's state, as a <code>String</code>.
|
528
|
+
def inspect
|
529
|
+
sprintf(
|
530
|
+
"#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.rpc_name
|
531
|
+
)
|
532
|
+
end
|
533
|
+
end
|
534
|
+
end
|
535
|
+
end
|