google-api-client 0.1.3 → 0.2.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 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