google-api-client 0.1.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 +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
|