google-api-client 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,11 @@
1
+ == 0.2.0
2
+
3
+ * updated to use v1 of the discovery API
4
+ * updated to use httpadapter 1.0.0
5
+ * added OAuth 2 support to the command line tool
6
+ * renamed some switches in the command line tool
7
+ * added additional configuration capabilities
8
+
1
9
  == 0.1.3
2
10
 
3
11
  * added support for manual overrides of the discovery URI
data/README CHANGED
@@ -44,7 +44,7 @@ APIs.
44
44
  client.authorization.fetch_token_credential!(:verifier => '12345')
45
45
 
46
46
  # Discover available methods
47
- method_names = client.discovered_service('buzz').to_h.keys
47
+ method_names = client.discovered_api('buzz').to_h.keys
48
48
 
49
49
  # Make an API call
50
50
  response = client.execute(
@@ -26,7 +26,8 @@ module Google
26
26
 
27
27
  def do_GET(request, response)
28
28
  $verifier ||= Addressable::URI.unencode_component(
29
- request.request_uri.to_s[/\?.*oauth_verifier=([^&$]+)(&|$)/, 1]
29
+ request.request_uri.to_s[/\?.*oauth_verifier=([^&$]+)(&|$)/, 1] ||
30
+ request.request_uri.to_s[/\?.*code=([^&$]+)(&|$)/, 1]
30
31
  )
31
32
  response.status = WEBrick::HTTPStatus::RC_ACCEPTED
32
33
  # This javascript will auto-close the tab after the
@@ -92,24 +93,24 @@ HTML
92
93
  options[:scope] = s
93
94
  end
94
95
  opts.on(
95
- "--client-key <key>", String,
96
- "Set the 2-legged OAuth key") do |k|
96
+ "--client-id <key>", String,
97
+ "Set the OAuth client id or key") do |k|
97
98
  options[:client_credential_key] = k
98
99
  end
99
100
  opts.on(
100
101
  "--client-secret <secret>", String,
101
- "Set the 2-legged OAuth secret") do |s|
102
+ "Set the OAuth client secret") do |s|
102
103
  options[:client_credential_secret] = s
103
104
  end
104
105
  opts.on(
105
- "-s", "--service <name>", String,
106
- "Perform discovery on service") do |s|
107
- options[:service_name] = s
106
+ "--api <name>", String,
107
+ "Perform discovery on API") do |s|
108
+ options[:api] = s
108
109
  end
109
110
  opts.on(
110
111
  "--service-version <id>", String,
111
112
  "Select service version") do |id|
112
- options[:service_version] = id
113
+ options[:version] = id
113
114
  end
114
115
  opts.on(
115
116
  "--content-type <format>", String,
@@ -162,10 +163,11 @@ HTML
162
163
 
163
164
  opts.separator(
164
165
  "\nAvailable commands:\n" +
165
- " oauth-login Log a user into an API\n" +
166
- " list List the methods available for a service\n" +
167
- " execute Execute a method on the API\n" +
168
- " irb Start an interactive client session"
166
+ " oauth-1-login Log a user into an API with OAuth 1.0a\n" +
167
+ " oauth-2-login Log a user into an API with OAuth 2.0 d10\n" +
168
+ " list List the methods available for a service\n" +
169
+ " execute Execute a method on the API\n" +
170
+ " irb Start an interactive client session"
169
171
  )
170
172
  end
171
173
  end
@@ -180,23 +182,98 @@ HTML
180
182
  self.send(symbol)
181
183
  end
182
184
 
185
+ def client
186
+ require 'signet/oauth_1/client'
187
+ require 'yaml'
188
+ require 'irb'
189
+ config_file = File.expand_path('~/.google-api.yaml')
190
+ authorization = nil
191
+ if File.exist?(config_file)
192
+ config = open(config_file, 'r') { |file| YAML.load(file.read) }
193
+ else
194
+ config = {}
195
+ end
196
+ if config["mechanism"]
197
+ authorization = config["mechanism"].to_sym
198
+ end
199
+
200
+ client = Google::APIClient.new(:authorization => authorization)
201
+
202
+ case authorization
203
+ when :oauth_1
204
+ if client.authorization &&
205
+ !client.authorization.kind_of?(Signet::OAuth1::Client)
206
+ STDERR.puts(
207
+ "Unexpected authorization mechanism: " +
208
+ "#{client.authorization.class}"
209
+ )
210
+ exit(1)
211
+ end
212
+ config = open(config_file, 'r') { |file| YAML.load(file.read) }
213
+ client.authorization.client_credential_key =
214
+ config["client_credential_key"]
215
+ client.authorization.client_credential_secret =
216
+ config["client_credential_secret"]
217
+ client.authorization.token_credential_key =
218
+ config["token_credential_key"]
219
+ client.authorization.token_credential_secret =
220
+ config["token_credential_secret"]
221
+ when :oauth_2
222
+ if client.authorization &&
223
+ !client.authorization.kind_of?(Signet::OAuth2::Client)
224
+ STDERR.puts(
225
+ "Unexpected authorization mechanism: " +
226
+ "#{client.authorization.class}"
227
+ )
228
+ exit(1)
229
+ end
230
+ config = open(config_file, 'r') { |file| YAML.load(file.read) }
231
+ client.authorization.scope = options[:scope]
232
+ client.authorization.client_id = config["client_id"]
233
+ client.authorization.client_secret = config["client_secret"]
234
+ client.authorization.access_token = config["access_token"]
235
+ client.authorization.refresh_token = config["refresh_token"]
236
+ else
237
+ # Dunno?
238
+ end
239
+
240
+ if options[:discovery_uri]
241
+ client.discovery_uri = options[:discovery_uri]
242
+ end
243
+
244
+ return client
245
+ end
246
+
247
+ def api_version(api, version)
248
+ v = version
249
+ if !version
250
+ if client.preferred_version(api)
251
+ v = client.preferred_version(api).version
252
+ else
253
+ v = 'v1'
254
+ end
255
+ end
256
+ return v
257
+ end
258
+
183
259
  COMMANDS = [
184
- :oauth_login,
260
+ :oauth_1_login,
261
+ :oauth_2_login,
185
262
  :list,
186
263
  :execute,
187
264
  :irb,
188
265
  :fuzz
189
266
  ]
190
267
 
191
- def oauth_login
268
+ def oauth_1_login
192
269
  require 'signet/oauth_1/client'
193
270
  require 'launchy'
194
271
  require 'yaml'
195
272
  if options[:client_credential_key] &&
196
273
  options[:client_credential_secret]
197
- scope = options[:scope]
198
274
  config = {
199
- "scope" => nil,
275
+ "mechanism" => "oauth_1",
276
+ "scope" => options[:scope],
200
277
  "client_credential_key" => options[:client_credential_key],
201
278
  "client_credential_secret" => options[:client_credential_secret],
202
279
  "token_credential_key" => nil,
@@ -229,19 +306,8 @@ HTML
229
306
  :client_credential_secret => 'anonymous',
230
307
  :callback => "http://localhost:#{OAUTH_SERVER_PORT}/"
231
308
  )
232
- scope = options[:scope]
233
- # Special cases
234
- case scope
235
- when "https://www.googleapis.com/auth/buzz",
236
- "https://www.googleapis.com/auth/buzz.readonly"
237
- oauth_client.authorization_uri =
238
- 'https://www.google.com/buzz/api/auth/OAuthAuthorizeToken?' +
239
- "domain=#{oauth_client.client_credential_key}&" +
240
- "scope=#{scope}&" +
241
- "xoauth_displayname=Google%20API%20Client"
242
- end
243
309
  oauth_client.fetch_temporary_credential!(:additional_parameters => {
244
- :scope => scope,
310
+ :scope => options[:scope],
245
311
  :xoauth_displayname => 'Google API Client'
246
312
  })
247
313
 
@@ -251,7 +317,7 @@ HTML
251
317
  server.start
252
318
  oauth_client.fetch_token_credential!(:verifier => $verifier)
253
319
  config = {
254
- "scope" => scope,
320
+ "scope" => options[:scope],
255
321
  "client_credential_key" =>
256
322
  oauth_client.client_credential_key,
257
323
  "client_credential_secret" =>
@@ -267,34 +333,90 @@ HTML
267
333
  end
268
334
  end
269
335
 
336
+ def oauth_2_login
337
+ require 'signet/oauth_2/client'
338
+ require 'launchy'
339
+ require 'yaml'
340
+ if !options[:client_credential_key] ||
341
+ !options[:client_credential_secret]
342
+ STDERR.puts('No client ID and secret supplied.')
343
+ exit(1)
344
+ end
345
+ if options[:access_token]
346
+ config = {
347
+ "mechanism" => "oauth_2",
348
+ "scope" => options[:scope],
349
+ "client_id" => options[:client_credential_key],
350
+ "client_secret" => options[:client_credential_secret],
351
+ "access_token" => options[:access_token],
352
+ "refresh_token" => options[:refresh_token]
353
+ }
354
+ config_file = File.expand_path('~/.google-api.yaml')
355
+ open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
356
+ exit(0)
357
+ else
358
+ $verifier = nil
359
+ # TODO(bobaman): Cross-platform?
360
+ logger = WEBrick::Log.new('/dev/null')
361
+ server = WEBrick::HTTPServer.new(
362
+ :Port => OAUTH_SERVER_PORT,
363
+ :Logger => logger,
364
+ :AccessLog => logger
365
+ )
366
+ trap("INT") { server.shutdown }
367
+
368
+ server.mount("/", OAuthVerifierServlet)
369
+
370
+ oauth_client = Signet::OAuth2::Client.new(
371
+ :authorization_uri =>
372
+ 'https://www.google.com/accounts/o8/oauth2/authorization',
373
+ :token_credential_uri =>
374
+ 'https://www.google.com/accounts/o8/oauth2/token',
375
+ :client_id => options[:client_credential_key],
376
+ :client_secret => options[:client_credential_secret],
377
+ :redirect_uri => "http://localhost:#{OAUTH_SERVER_PORT}/",
378
+ :scope => options[:scope]
379
+ )
380
+
381
+ # Launch browser
382
+ Launchy::Browser.run(oauth_client.authorization_uri.to_s)
383
+
384
+ server.start
385
+ oauth_client.code = $verifier
386
+ oauth_client.fetch_access_token!
387
+ config = {
388
+ "mechanism" => "oauth_2",
389
+ "scope" => options[:scope],
390
+ "client_id" => oauth_client.client_id,
391
+ "client_secret" => oauth_client.client_secret,
392
+ "access_token" => oauth_client.access_token,
393
+ "refresh_token" => oauth_client.refresh_token
394
+ }
395
+ config_file = File.expand_path('~/.google-api.yaml')
396
+ open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
397
+ exit(0)
398
+ end
399
+ end
400
+
270
401
  def list
271
- service_name = options[:service_name]
272
- unless service_name
273
- STDERR.puts('No service name supplied.')
402
+ api = options[:api]
403
+ unless api
404
+ STDERR.puts('No API name supplied.')
274
405
  exit(1)
275
406
  end
276
- client = Google::APIClient.new(
277
- :service => service_name,
278
- :authorization => nil
279
- )
407
+ client = Google::APIClient.new(:authorization => nil)
280
408
  if options[:discovery_uri]
281
409
  client.discovery_uri = options[:discovery_uri]
282
410
  end
283
- service_version =
284
- options[:service_version] ||
285
- client.latest_service_version(service_name).version
286
- service = client.discovered_service(service_name, service_version)
411
+ version = api_version(api, options[:version])
412
+ service = client.discovered_api(api, version)
287
413
  rpcnames = service.to_h.keys
288
414
  puts rpcnames.sort.join("\n")
289
415
  exit(0)
290
416
  end
291
417
 
292
418
  def execute
293
- require 'signet/oauth_1/client'
294
- require 'yaml'
295
- config_file = File.expand_path('~/.google-api.yaml')
296
- signed = File.exist?(config_file)
297
- authorization_type = :oauth_1
419
+ client = self.client
298
420
 
299
421
  # Setup HTTP request data
300
422
  request_body = ''
@@ -308,28 +430,6 @@ HTML
308
430
  headers << ['Content-Type', 'application/json']
309
431
  end
310
432
 
311
- configure_authorization = lambda do |client|
312
- if !client.authorization.kind_of?(Signet::OAuth1::Client)
313
- STDERR.puts(
314
- "Unexpected authorization mechanism: " +
315
- "#{client.authorization.class}"
316
- )
317
- exit(1)
318
- end
319
- config = open(config_file, 'r') { |file| YAML.load(file.read) }
320
- client.authorization.client_credential_key =
321
- config["client_credential_key"]
322
- client.authorization.client_credential_secret =
323
- config["client_credential_secret"]
324
- client.authorization.token_credential_key =
325
- config["token_credential_key"]
326
- client.authorization.token_credential_secret =
327
- config["token_credential_secret"]
328
- if client.authorization.token_credential == nil
329
- authorization_type = :two_legged_oauth_1
330
- end
331
- end
332
-
333
433
  if options[:uri]
334
434
  # Make request with URI manually specified
335
435
  uri = Addressable::URI.parse(options[:uri])
@@ -345,13 +445,8 @@ HTML
345
445
  method = options[:http_method]
346
446
  method ||= request_body == '' ? 'GET' : 'POST'
347
447
  method.upcase!
348
- client = Google::APIClient.new(:authorization => authorization_type)
349
- if options[:discovery_uri]
350
- client.discovery_uri = options[:discovery_uri]
351
- end
352
- configure_authorization.call(client) if signed
353
448
  request = [method, uri.to_str, headers, [request_body]]
354
- request = client.sign_request(request)
449
+ request = client.generate_authenticated_request(:request => request)
355
450
  response = client.transmit_request(request)
356
451
  status, headers, body = response
357
452
  puts body
@@ -362,25 +457,14 @@ HTML
362
457
  STDERR.puts('No rpcname supplied.')
363
458
  exit(1)
364
459
  end
365
- service_name =
366
- options[:service_name] || self.rpcname[/^([^\.]+)\./, 1]
367
- client = Google::APIClient.new(
368
- :service => service_name,
369
- :authorization => authorization_type
370
- )
371
- if options[:discovery_uri]
372
- client.discovery_uri = options[:discovery_uri]
373
- end
374
- configure_authorization.call(client) if signed
375
- service_version =
376
- options[:service_version] ||
377
- client.latest_service_version(service_name).version
378
- service = client.discovered_service(service_name, service_version)
460
+ api = options[:api] || self.rpcname[/^([^\.]+)\./, 1]
461
+ version = api_version(api, options[:version])
462
+ service = client.discovered_api(api, version)
379
463
  method = service.to_h[self.rpcname]
380
464
  if !method
381
465
  STDERR.puts(
382
466
  "Method #{self.rpcname} does not exist for " +
383
- "#{service_name}-#{service_version}."
467
+ "#{api}-#{version}."
384
468
  )
385
469
  exit(1)
386
470
  end
@@ -394,7 +478,7 @@ HTML
394
478
  end
395
479
  begin
396
480
  response = client.execute(
397
- method, parameters, request_body, headers, {:signed => signed}
481
+ method, parameters, request_body, headers
398
482
  )
399
483
  status, headers, body = response
400
484
  puts body
@@ -407,37 +491,7 @@ HTML
407
491
  end
408
492
 
409
493
  def irb
410
- require 'signet/oauth_1/client'
411
- require 'yaml'
412
- require 'irb'
413
- config_file = File.expand_path('~/.google-api.yaml')
414
- signed = File.exist?(config_file)
415
-
416
- $client = Google::APIClient.new(
417
- :service => options[:service_name],
418
- :authorization => (signed ? :oauth_1 : nil)
419
- )
420
-
421
- if signed
422
- if $client.authorization &&
423
- !$client.authorization.kind_of?(Signet::OAuth1::Client)
424
- STDERR.puts(
425
- "Unexpected authorization mechanism: " +
426
- "#{$client.authorization.class}"
427
- )
428
- exit(1)
429
- end
430
- config = open(config_file, 'r') { |file| YAML.load(file.read) }
431
- $client.authorization.client_credential_key =
432
- config["client_credential_key"]
433
- $client.authorization.client_credential_secret =
434
- config["client_credential_secret"]
435
- $client.authorization.token_credential_key =
436
- config["token_credential_key"]
437
- $client.authorization.token_credential_secret =
438
- config["token_credential_secret"]
439
- end
440
-
494
+ $client = self.client
441
495
  # Otherwise IRB will misinterpret command-line options
442
496
  ARGV.clear
443
497
  IRB.start(__FILE__)
@@ -12,56 +12,132 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+
15
16
  require 'httpadapter'
16
17
  require 'json'
17
18
  require 'stringio'
18
19
 
20
+ require 'google/api_client/errors'
21
+ require 'google/api_client/environment'
19
22
  require 'google/api_client/discovery'
20
23
 
21
24
  module Google
22
25
  # TODO(bobaman): Document all this stuff.
23
26
 
27
+
24
28
  ##
25
- # This class manages communication with a single API.
29
+ # This class manages APIs communication.
26
30
  class APIClient
27
31
  ##
28
- # An error which is raised when there is an unexpected response or other
29
- # transport error that prevents an operation from succeeding.
30
- class TransmissionError < StandardError
31
- end
32
-
32
+ # Creates a new Google API client.
33
+ #
34
+ # @param [Hash] options The configuration parameters for the client.
35
+ # @option options [Symbol, #generate_authenticated_request] :authorization
36
+ # (:oauth_1)
37
+ # The authorization mechanism used by the client. The following
38
+ # mechanisms are supported out-of-the-box:
39
+ # <ul>
40
+ # <li><code>:two_legged_oauth_1</code></li>
41
+ # <li><code>:oauth_1</code></li>
42
+ # <li><code>:oauth_2</code></li>
43
+ # </ul>
44
+ # @option options [String] :host ("www.googleapis.com")
45
+ # The API hostname used by the client. This rarely needs to be changed.
46
+ # @option options [String] :application_name
47
+ # The name and version of the application using the client. This should
48
+ # be given in the form `"{name}/{version}"`.
49
+ # @option options [String] :user_agent
50
+ # ("{app_name} google-api-ruby-client/{version} {os_name}/{os_version}")
51
+ # The user agent used by the client. Most developers will want to
52
+ # leave this value alone and use the `:application_name` option instead.
33
53
  def initialize(options={})
34
- @options = {
35
- # TODO: What configuration options need to go here?
36
- }.merge(options)
37
- # Force immediate type-checking and short-cut resolution
38
- self.parser
39
- self.authorization
40
- self.http_adapter
54
+ # Normalize key to String to allow indifferent access.
55
+ options = options.inject({}) do |accu, (key, value)|
56
+ accu[key.to_s] = value
57
+ accu
58
+ end
59
+ # Almost all API usage will have a host of 'www.googleapis.com'.
60
+ self.host = options["host"] || 'www.googleapis.com'
61
+ # Most developers will want to leave this value alone and use the
62
+ # application_name option.
63
+ self.user_agent = options["user_agent"] || (
64
+ (options["application_name"] || '')
65
+ 'google-api-ruby-client/' + VERSION::STRING +
66
+ ' ' + ENV::OS_VERSION
67
+ ).strip
68
+ # This is mostly a default for the sake of convenience.
69
+ # Unlike most other options, this one may be nil, so we check for
70
+ # the presence of the key rather than checking the value.
71
+ if options.has_key?("parser")
72
+ self.parser = options["parser"]
73
+ else
74
+ require 'google/api_client/parsers/json_parser'
75
+ # NOTE: Do not rely on this default value, as it may change
76
+ self.parser = Google::APIClient::JSONParser
77
+ end
78
+ # The writer method understands a few Symbols and will generate useful
79
+ # default authentication mechanisms.
80
+ self.authorization = options["authorization"] || :oauth_2
81
+ # The HTTP adapter controls all of the HTTP traffic the client generates.
82
+ # By default, Net::HTTP is used, but adding support for other clients
83
+ # is trivial.
84
+ if options["http_adapter"]
85
+ self.http_adapter = options["http_adapter"]
86
+ else
87
+ require 'httpadapter/adapters/net_http'
88
+ # NOTE: Do not rely on this default value, as it may change
89
+ self.http_adapter = HTTPAdapter::NetHTTPAdapter.new
90
+ end
91
+ @discovery_uris = {}
92
+ @discovery_documents = {}
93
+ @discovered_apis = {}
41
94
  return self
42
95
  end
43
96
 
97
+
44
98
  ##
45
99
  # Returns the parser used by the client.
46
- def parser
47
- unless @options[:parser]
48
- require 'google/api_client/parsers/json_parser'
49
- # NOTE: Do not rely on this default value, as it may change
50
- @options[:parser] = JSONParser
100
+ #
101
+ # @return [#serialize, #parse]
102
+ # The parser used by the client. Any object that implements both a
103
+ # <code>#serialize</code> and a <code>#parse</code> method may be used.
104
+ # If <code>nil</code>, no parsing will be done.
105
+ attr_reader :parser
106
+
107
+ ##
108
+ # Sets the parser used by the client.
109
+ #
110
+ # @param [#serialize, #parse] new_parser
111
+ # The parser used by the client. Any object that implements both a
112
+ # <code>#serialize</code> and a <code>#parse</code> method may be used.
113
+ # If <code>nil</code>, no parsing will be done.
114
+ def parser=(new_parser)
115
+ if new_parser &&
116
+ !new_parser.respond_to?(:serialize) &&
117
+ !new_parser.respond_to?(:parse)
118
+ raise TypeError,
119
+ 'Expected parser object to respond to #serialize and #parse.'
51
120
  end
52
- return @options[:parser]
121
+ @parser = new_parser
53
122
  end
54
123
 
55
124
  ##
56
125
  # Returns the authorization mechanism used by the client.
57
126
  #
58
127
  # @return [#generate_authenticated_request] The authorization mechanism.
59
- def authorization
60
- case @options[:authorization]
128
+ attr_reader :authorization
129
+
130
+ ##
131
+ # Sets the authorization mechanism used by the client.
132
+ #
133
+ # @param [#generate_authenticated_request] new_authorization
134
+ # The new authorization mechanism.
135
+ def authorization=(new_authorization)
136
+ case new_authorization
61
137
  when :oauth_1, :oauth
62
138
  require 'signet/oauth_1/client'
63
139
  # NOTE: Do not rely on this default value, as it may change
64
- @options[:authorization] = Signet::OAuth1::Client.new(
140
+ new_authorization = Signet::OAuth1::Client.new(
65
141
  :temporary_credential_uri =>
66
142
  'https://www.google.com/accounts/OAuthGetRequestToken',
67
143
  :authorization_uri =>
@@ -74,79 +150,175 @@ module Google
74
150
  when :two_legged_oauth_1, :two_legged_oauth
75
151
  require 'signet/oauth_1/client'
76
152
  # NOTE: Do not rely on this default value, as it may change
77
- @options[:authorization] = Signet::OAuth1::Client.new(
153
+ new_authorization = Signet::OAuth1::Client.new(
78
154
  :client_credential_key => nil,
79
155
  :client_credential_secret => nil,
80
156
  :two_legged => true
81
157
  )
158
+ when :oauth_2
159
+ require 'signet/oauth_2/client'
160
+ # NOTE: Do not rely on this default value, as it may change
161
+ new_authorization = Signet::OAuth2::Client.new(
162
+ :authorization_uri =>
163
+ 'https://accounts.google.com/o/oauth2/auth',
164
+ :token_credential_uri =>
165
+ 'https://accounts.google.com/o/oauth2/token'
166
+ )
82
167
  when nil
83
168
  # No authorization mechanism
84
169
  else
85
- if !@options[:authorization].respond_to?(
86
- :generate_authenticated_request)
170
+ if !new_authorization.respond_to?(:generate_authenticated_request)
87
171
  raise TypeError,
88
172
  'Expected authorization mechanism to respond to ' +
89
173
  '#generate_authenticated_request.'
90
174
  end
91
175
  end
92
- return @options[:authorization]
176
+ @authorization = new_authorization
177
+ return @authorization
93
178
  end
94
179
 
95
180
  ##
96
- # Sets the authorization mechanism used by the client.
181
+ # Returns the HTTP adapter used by the client.
97
182
  #
98
- # @param [#generate_authenticated_request] new_authorization
99
- # The new authorization mechanism.
100
- def authorization=(new_authorization)
101
- @options[:authorization] = new_authorization
102
- return self.authorization
103
- end
183
+ # @return [HTTPAdapter]
184
+ # The HTTP adapter object. The object must include the
185
+ # HTTPAdapter module and conform to its interface.
186
+ attr_reader :http_adapter
104
187
 
105
188
  ##
106
189
  # Returns the HTTP adapter used by the client.
107
- def http_adapter
108
- return @options[:http_adapter] ||= (begin
109
- require 'httpadapter/adapters/net_http'
110
- @options[:http_adapter] = HTTPAdapter::NetHTTPRequestAdapter
111
- end)
190
+ #
191
+ # @return [HTTPAdapter]
192
+ # The HTTP adapter object. The object must include the
193
+ # HTTPAdapter module and conform to its interface.
194
+ def http_adapter=(new_http_adapter)
195
+ if new_http_adapter.kind_of?(HTTPAdapter)
196
+ @http_adapter = new_http_adapter
197
+ else
198
+ raise TypeError, "Expected HTTPAdapter, got #{new_http_adapter.class}."
199
+ end
200
+ end
201
+
202
+ ##
203
+ # The API hostname used by the client.
204
+ #
205
+ # @return [String]
206
+ # The API hostname. Should almost always be 'www.googleapis.com'.
207
+ attr_accessor :host
208
+
209
+ ##
210
+ # The user agent used by the client.
211
+ #
212
+ # @return [String]
213
+ # The user agent string used in the User-Agent header.
214
+ attr_accessor :user_agent
215
+
216
+ ##
217
+ # Returns the URI for the directory document.
218
+ #
219
+ # @return [Addressable::URI] The URI of the directory document.
220
+ def directory_uri
221
+ template = Addressable::Template.new(
222
+ "https://{host}/discovery/v1/apis"
223
+ )
224
+ return template.expand({"host" => self.host})
225
+ end
226
+
227
+ ##
228
+ # Manually registers a URI as a discovery document for a specific version
229
+ # of an API.
230
+ #
231
+ # @param [String, Symbol] api The service name.
232
+ # @param [String] version The desired version of the service.
233
+ # @param [Addressable::URI] uri The URI of the discovery document.
234
+ def register_discovery_uri(api, version, uri)
235
+ api = api.to_s
236
+ version = version || 'v1'
237
+ @discovery_uris["#{api}:#{version}"] = uri
112
238
  end
113
239
 
114
240
  ##
115
241
  # Returns the URI for the discovery document.
116
242
  #
243
+ # @param [String, Symbol] api The service name.
244
+ # @param [String] version The desired version of the service.
117
245
  # @return [Addressable::URI] The URI of the discovery document.
118
- def discovery_uri
119
- return @options[:discovery_uri] ||= (begin
120
- if @options[:service]
121
- service_id = @options[:service]
122
- service_version = @options[:service_version] || 'v1'
123
- Addressable::URI.parse(
124
- "http://www.googleapis.com/discovery/0.1/describe" +
125
- "?api=#{service_id}"
126
- )
127
- else
128
- raise ArgumentError,
129
- 'Missing required configuration value, :discovery_uri.'
130
- end
246
+ def discovery_uri(api, version=nil)
247
+ api = api.to_s
248
+ version = version || 'v1'
249
+ return @discovery_uris["#{api}:#{version}"] ||= (begin
250
+ template = Addressable::Template.new(
251
+ "https://{host}/discovery/v1/apis/" +
252
+ "{api}/{version}/rest"
253
+ )
254
+ template.expand({
255
+ "host" => self.host,
256
+ "api" => api,
257
+ "version" => version
258
+ })
131
259
  end)
132
260
  end
133
261
 
134
262
  ##
135
- # Sets the discovery URI for the client.
263
+ # Manually registers a pre-loaded discovery document for a specific version
264
+ # of an API.
136
265
  #
137
- # @param [Addressable::URI, #to_str, String] new_discovery_uri
138
- # The new discovery URI.
139
- def discovery_uri=(new_discovery_uri)
140
- @options[:discovery_uri] = Addressable::URI.parse(new_discovery_uri)
266
+ # @param [String, Symbol] api The service name.
267
+ # @param [String] version The desired version of the service.
268
+ # @param [String, StringIO] discovery_document
269
+ # The contents of the discovery document.
270
+ def register_discovery_document(api, version, discovery_document)
271
+ api = api.to_s
272
+ version = version || 'v1'
273
+ if discovery_document.kind_of?(StringIO)
274
+ discovery_document.rewind
275
+ discovery_document = discovery_document.string
276
+ elsif discovery_document.respond_to?(:to_str)
277
+ discovery_document = discovery_document.to_str
278
+ else
279
+ raise TypeError,
280
+ "Expected String or StringIO, got #{discovery_document.class}."
281
+ end
282
+ @discovery_documents["#{api}:#{version}"] =
283
+ JSON.parse(discovery_document)
284
+ end
285
+
286
+ ##
287
+ # Returns the parsed directory document.
288
+ #
289
+ # @return [Hash] The parsed JSON from the directory document.
290
+ def directory_document
291
+ return @directory_document ||= (begin
292
+ request_uri = self.directory_uri
293
+ request = ['GET', request_uri, [], []]
294
+ response = self.transmit_request(request)
295
+ status, headers, body = response
296
+ if status == 200 # TODO(bobaman) Better status code handling?
297
+ merged_body = StringIO.new
298
+ body.each do |chunk|
299
+ merged_body.write(chunk)
300
+ end
301
+ merged_body.rewind
302
+ JSON.parse(merged_body.string)
303
+ else
304
+ raise TransmissionError,
305
+ "Could not retrieve discovery document at: #{request_uri}"
306
+ end
307
+ end)
141
308
  end
142
309
 
143
310
  ##
144
311
  # Returns the parsed discovery document.
145
312
  #
313
+ # @param [String, Symbol] api The service name.
314
+ # @param [String] version The desired version of the service.
146
315
  # @return [Hash] The parsed JSON from the discovery document.
147
- def discovery_document
148
- return @discovery_document ||= (begin
149
- request = ['GET', self.discovery_uri.to_s, [], []]
316
+ def discovery_document(api, version=nil)
317
+ api = api.to_s
318
+ version = version || 'v1'
319
+ return @discovery_documents["#{api}:#{version}"] ||= (begin
320
+ request_uri = self.discovery_uri(api, version)
321
+ request = ['GET', request_uri, [], []]
150
322
  response = self.transmit_request(request)
151
323
  status, headers, body = response
152
324
  if status == 200 # TODO(bobaman) Better status code handling?
@@ -158,101 +330,102 @@ module Google
158
330
  JSON.parse(merged_body.string)
159
331
  else
160
332
  raise TransmissionError,
161
- "Could not retrieve discovery document at: #{self.discovery_uri}"
333
+ "Could not retrieve discovery document at: #{request_uri}"
162
334
  end
163
335
  end)
164
336
  end
165
337
 
166
338
  ##
167
- # Returns a list of services this client instance has performed discovery
168
- # for. This may return multiple versions of the same service.
169
- #
170
- # @return [Array]
171
- # A list of discovered <code>Google::APIClient::Service</code> objects.
172
- def discovered_services
173
- return @discovered_services ||= (begin
174
- service_names = self.discovery_document['data'].keys()
175
- services = []
176
- for service_name in service_names
177
- versions = self.discovery_document['data'][service_name]
178
- for service_version in versions.keys()
179
- service_description =
180
- self.discovery_document['data'][service_name][service_version]
181
- services << ::Google::APIClient::Service.new(
182
- service_name,
183
- service_version,
184
- service_description
339
+ # Returns all APIs published in the directory document.
340
+ #
341
+ # @return [Array] The list of available APIs.
342
+ def discovered_apis
343
+ @directory_apis ||= (begin
344
+ document_base = self.directory_uri
345
+ if self.directory_document && self.directory_document['items']
346
+ self.directory_document['items'].map do |discovery_document|
347
+ ::Google::APIClient::API.new(
348
+ document_base,
349
+ discovery_document
185
350
  )
186
351
  end
352
+ else
353
+ []
187
354
  end
188
- services
189
355
  end)
190
356
  end
191
357
 
192
358
  ##
193
359
  # Returns the service object for a given service name and service version.
194
360
  #
195
- # @param [String, Symbol] service_name The service name.
196
- # @param [String] service_version The desired version of the service.
361
+ # @param [String, Symbol] api The service name.
362
+ # @param [String] version The desired version of the service.
197
363
  #
198
- # @return [Google::APIClient::Service] The service object.
199
- def discovered_service(service_name, service_version='v1')
200
- if !service_name.kind_of?(String) && !service_name.kind_of?(Symbol)
364
+ # @return [Google::APIClient::API] The service object.
365
+ def discovered_api(api, version=nil)
366
+ if !api.kind_of?(String) && !api.kind_of?(Symbol)
201
367
  raise TypeError,
202
- "Expected String or Symbol, got #{service_name.class}."
368
+ "Expected String or Symbol, got #{api.class}."
203
369
  end
204
- service_name = service_name.to_s
205
- for service in self.discovered_services
206
- if service.name == service_name &&
207
- service.version.to_s == service_version.to_s
208
- return service
370
+ api = api.to_s
371
+ version = version || 'v1'
372
+ return @discovered_apis["#{api}:#{version}"] ||= begin
373
+ document_base = self.discovery_uri(api, version)
374
+ discovery_document = self.discovery_document(api, version)
375
+ if document_base && discovery_document
376
+ ::Google::APIClient::API.new(
377
+ document_base,
378
+ discovery_document
379
+ )
380
+ else
381
+ nil
209
382
  end
210
383
  end
211
- return nil
212
384
  end
213
385
 
214
386
  ##
215
387
  # Returns the method object for a given RPC name and service version.
216
388
  #
217
389
  # @param [String, Symbol] rpc_name The RPC name of the desired method.
218
- # @param [String] service_version The desired version of the service.
390
+ # @param [String] version The desired version of the service.
219
391
  #
220
392
  # @return [Google::APIClient::Method] The method object.
221
- def discovered_method(rpc_name, service_version='v1')
393
+ def discovered_method(rpc_name, api, version=nil)
222
394
  if !rpc_name.kind_of?(String) && !rpc_name.kind_of?(Symbol)
223
395
  raise TypeError,
224
396
  "Expected String or Symbol, got #{rpc_name.class}."
225
397
  end
226
398
  rpc_name = rpc_name.to_s
227
- for service in self.discovered_services
228
- # This looks kinda weird, but is not a real problem because there's
229
- # almost always only one service, and this is memoized anyhow.
230
- if service.version.to_s == service_version.to_s
231
- return service.to_h[rpc_name] if service.to_h[rpc_name]
232
- end
399
+ api = api.to_s
400
+ version = version || 'v1'
401
+ service = self.discovered_api(api, version)
402
+ if service.to_h[rpc_name]
403
+ return service.to_h[rpc_name]
404
+ else
405
+ return nil
233
406
  end
234
- return nil
235
407
  end
236
408
 
237
409
  ##
238
410
  # Returns the service object with the highest version number.
239
411
  #
240
- # <em>Warning</em>: This method should be used with great care. As APIs
241
- # are updated, minor differences between versions may cause
412
+ # @note <em>Warning</em>: This method should be used with great care.
413
+ # As APIs are updated, minor differences between versions may cause
242
414
  # incompatibilities. Requesting a specific version will avoid this issue.
243
415
  #
244
- # @param [String, Symbol] service_name The name of the service.
416
+ # @param [String, Symbol] api The name of the service.
245
417
  #
246
- # @return [Google::APIClient::Service] The service object.
247
- def latest_service_version(service_name)
248
- if !service_name.kind_of?(String) && !service_name.kind_of?(Symbol)
418
+ # @return [Google::APIClient::API] The service object.
419
+ def preferred_version(api)
420
+ if !api.kind_of?(String) && !api.kind_of?(Symbol)
249
421
  raise TypeError,
250
- "Expected String or Symbol, got #{service_name.class}."
422
+ "Expected String or Symbol, got #{api.class}."
423
+ end
424
+ api = api.to_s
425
+ # TODO(bobaman): Update to use directory API.
426
+ return self.discovered_apis.detect do |a|
427
+ a.name == api && a.preferred == true
251
428
  end
252
- service_name = service_name.to_s
253
- return (self.discovered_services.select do |service|
254
- service.name == service_name
255
- end).sort.last
256
429
  end
257
430
 
258
431
  ##
@@ -266,16 +439,17 @@ module Google
266
439
  # @param [Hash, Array] headers The HTTP headers for the request.
267
440
  # @param [Hash] options
268
441
  # The configuration parameters for the request.
269
- # - <code>:service_version</code> —
442
+ # - <code>:version</code> —
270
443
  # The service version. Only used if <code>api_method</code> is a
271
444
  # <code>String</code>. Defaults to <code>'v1'</code>.
272
445
  # - <code>:parser</code> —
273
446
  # The parser for the response.
274
447
  # - <code>:authorization</code> —
275
448
  # The authorization mechanism for the response. Used only if
276
- # <code>:signed</code> is <code>true</code>.
277
- # - <code>:signed</code> —
278
- # <code>true</code> if the request must be signed, <code>false</code>
449
+ # <code>:authenticated</code> is <code>true</code>.
450
+ # - <code>:authenticated</code> —
451
+ # <code>true</code> if the request must be signed or otherwise
452
+ # authenticated, <code>false</code>
279
453
  # otherwise. Defaults to <code>true</code> if an authorization
280
454
  # mechanism has been set, <code>false</code> otherwise.
281
455
  #
@@ -291,19 +465,27 @@ module Google
291
465
  api_method, parameters={}, body='', headers=[], options={})
292
466
  options={
293
467
  :parser => self.parser,
294
- :service_version => 'v1',
468
+ :version => 'v1',
295
469
  :authorization => self.authorization
296
470
  }.merge(options)
297
- # The default value for the :signed option depends on whether an
471
+ # The default value for the :authenticated option depends on whether an
298
472
  # authorization mechanism has been set.
299
473
  if options[:authorization]
300
- options = {:signed => true}.merge(options)
474
+ options = {:authenticated => true}.merge(options)
301
475
  else
302
- options = {:signed => false}.merge(options)
476
+ options = {:authenticated => false}.merge(options)
303
477
  end
304
478
  if api_method.kind_of?(String) || api_method.kind_of?(Symbol)
479
+ api_method = api_method.to_s
480
+ # This method of guessing the API is unreliable. This will fail for
481
+ # APIs where the first segment of the RPC name does not match the
482
+ # service name. However, this is a fallback mechanism anyway.
483
+ # Developers should be passing in a reference to the method, rather
484
+ # than passing in a string or symbol. This should raise an error
485
+ # in the case of a mismatch.
486
+ api = api_method[/^([^.]+)\./, 1]
305
487
  api_method = self.discovered_method(
306
- api_method.to_s, options[:service_version]
488
+ api_method, api, options[:version]
307
489
  )
308
490
  elsif !api_method.kind_of?(::Google::APIClient::Method)
309
491
  raise TypeError,
@@ -314,8 +496,8 @@ module Google
314
496
  raise ArgumentError, "API method could not be found."
315
497
  end
316
498
  request = api_method.generate_request(parameters, body, headers)
317
- if options[:signed]
318
- request = self.sign_request(request, options[:authorization])
499
+ if options[:authenticated]
500
+ request = self.generate_authenticated_request(:request => request)
319
501
  end
320
502
  return request
321
503
  end
@@ -331,7 +513,7 @@ module Google
331
513
  # @param [Hash, Array] headers The HTTP headers for the request.
332
514
  # @param [Hash] options
333
515
  # The configuration parameters for the request.
334
- # - <code>:service_version</code> —
516
+ # - <code>:version</code> —
335
517
  # The service version. Only used if <code>api_method</code> is a
336
518
  # <code>String</code>. Defaults to <code>'v1'</code>.
337
519
  # - <code>:adapter</code> —
@@ -340,9 +522,10 @@ module Google
340
522
  # The parser for the response.
341
523
  # - <code>:authorization</code> —
342
524
  # The authorization mechanism for the response. Used only if
343
- # <code>:signed</code> is <code>true</code>.
344
- # - <code>:signed</code> —
345
- # <code>true</code> if the request must be signed, <code>false</code>
525
+ # <code>:authenticated</code> is <code>true</code>.
526
+ # - <code>:authenticated</code> —
527
+ # <code>true</code> if the request must be signed or otherwise
528
+ # authenticated, <code>false</code>
346
529
  # otherwise. Defaults to <code>true</code>.
347
530
  #
348
531
  # @return [Array] The response from the API.
@@ -371,21 +554,36 @@ module Google
371
554
  #
372
555
  # @return [Array] The response from the server.
373
556
  def transmit_request(request, adapter=self.http_adapter)
374
- ::HTTPAdapter.transmit(request, adapter)
557
+ if self.user_agent != nil
558
+ # If there's no User-Agent header, set one.
559
+ method, uri, headers, body = request
560
+ unless headers.kind_of?(Enumerable)
561
+ # We need to use some Enumerable methods, relying on the presence of
562
+ # the #each method.
563
+ class <<headers
564
+ include Enumerable
565
+ end
566
+ end
567
+ if self.user_agent.kind_of?(String)
568
+ unless headers.any? { |k, v| k.downcase == 'User-Agent'.downcase }
569
+ headers = headers.to_a.insert(0, ['User-Agent', self.user_agent])
570
+ end
571
+ elsif self.user_agent != nil
572
+ raise TypeError,
573
+ "Expected User-Agent to be String, got #{self.user_agent.class}"
574
+ end
575
+ end
576
+ adapter.transmit([method, uri, headers, body])
375
577
  end
376
578
 
377
579
  ##
378
580
  # Signs a request using the current authorization mechanism.
379
581
  #
380
- # @param [Array] request The request to sign.
381
- # @param [#generate_authenticated_request] authorization
382
- # The authorization mechanism.
582
+ # @param [Hash] options The options to pass through.
383
583
  #
384
- # @return [Array] The signed request.
385
- def sign_request(request, authorization=self.authorization)
386
- return authorization.generate_authenticated_request(
387
- :request => request
388
- )
584
+ # @return [Array] The signed or otherwise authenticated request.
585
+ def generate_authenticated_request(options={})
586
+ return authorization.generate_authenticated_request(options)
389
587
  end
390
588
  end
391
589
  end