A significant update of the client functionality.

* updated to use v0.3 of the discovery API
* updated to use httpadapter 1.0.0
* added OAuth 2 support to the command line tool
* renamed some switches in the command line tool
* added additional configuration capabilities

git-svn-id: https://google-api-ruby-client.googlecode.com/svn/trunk@128 c1d61fac-ed7f-fcc1-18f7-ff78120a04ef
This commit is contained in:
Bob Aman 2011-05-04 11:44:35 +00:00
parent 1dee705828
commit 286a7152f2
13 changed files with 1062 additions and 805 deletions

View File

@ -1,3 +1,11 @@
== 0.2.0
* updated to use v0.3 of the discovery API
* updated to use httpadapter 1.0.0
* added OAuth 2 support to the command line tool
* renamed some switches in the command line tool
* added additional configuration capabilities
== 0.1.3 == 0.1.3
* added support for manual overrides of the discovery URI * added support for manual overrides of the discovery URI

2
README
View File

@ -44,7 +44,7 @@ APIs.
client.authorization.fetch_token_credential!(:verifier => '12345') client.authorization.fetch_token_credential!(:verifier => '12345')
# Discover available methods # Discover available methods
method_names = client.discovered_service('buzz').to_h.keys method_names = client.discovered_api('buzz').to_h.keys
# Make an API call # Make an API call
response = client.execute( response = client.execute(

View File

@ -26,7 +26,8 @@ module Google
def do_GET(request, response) def do_GET(request, response)
$verifier ||= Addressable::URI.unencode_component( $verifier ||= Addressable::URI.unencode_component(
request.request_uri.to_s[/\?.*oauth_verifier=([^&$]+)(&|$)/, 1] request.request_uri.to_s[/\?.*oauth_verifier=([^&$]+)(&|$)/, 1] ||
request.request_uri.to_s[/\?.*code=([^&$]+)(&|$)/, 1]
) )
response.status = WEBrick::HTTPStatus::RC_ACCEPTED response.status = WEBrick::HTTPStatus::RC_ACCEPTED
# This javascript will auto-close the tab after the # This javascript will auto-close the tab after the
@ -92,24 +93,24 @@ HTML
options[:scope] = s options[:scope] = s
end end
opts.on( opts.on(
"--client-key <key>", String, "--client-id <key>", String,
"Set the 2-legged OAuth key") do |k| "Set the OAuth client id or key") do |k|
options[:client_credential_key] = k options[:client_credential_key] = k
end end
opts.on( opts.on(
"--client-secret <secret>", String, "--client-secret <secret>", String,
"Set the 2-legged OAuth secret") do |s| "Set the OAuth client secret") do |s|
options[:client_credential_secret] = s options[:client_credential_secret] = s
end end
opts.on( opts.on(
"-s", "--service <name>", String, "--api <name>", String,
"Perform discovery on service") do |s| "Perform discovery on API") do |s|
options[:service_name] = s options[:api] = s
end end
opts.on( opts.on(
"--service-version <id>", String, "--service-version <id>", String,
"Select service version") do |id| "Select service version") do |id|
options[:service_version] = id options[:version] = id
end end
opts.on( opts.on(
"--content-type <format>", String, "--content-type <format>", String,
@ -162,10 +163,11 @@ HTML
opts.separator( opts.separator(
"\nAvailable commands:\n" + "\nAvailable commands:\n" +
" oauth-login Log a user into an API\n" + " oauth-1-login Log a user into an API with OAuth 1.0a\n" +
" list List the methods available for a service\n" + " oauth-2-login Log a user into an API with OAuth 2.0 d10\n" +
" execute Execute a method on the API\n" + " list List the methods available for a service\n" +
" irb Start an interactive client session" " execute Execute a method on the API\n" +
" irb Start an interactive client session"
) )
end end
end end
@ -180,23 +182,98 @@ HTML
self.send(symbol) self.send(symbol)
end end
def client
require 'signet/oauth_1/client'
require 'yaml'
require 'irb'
config_file = File.expand_path('~/.google-api.yaml')
authorization = nil
if File.exist?(config_file)
config = open(config_file, 'r') { |file| YAML.load(file.read) }
else
config = {}
end
if config["mechanism"]
authorization = config["mechanism"].to_sym
end
client = Google::APIClient.new(:authorization => authorization)
case authorization
when :oauth_1
if client.authorization &&
!client.authorization.kind_of?(Signet::OAuth1::Client)
STDERR.puts(
"Unexpected authorization mechanism: " +
"#{client.authorization.class}"
)
exit(1)
end
config = open(config_file, 'r') { |file| YAML.load(file.read) }
client.authorization.client_credential_key =
config["client_credential_key"]
client.authorization.client_credential_secret =
config["client_credential_secret"]
client.authorization.token_credential_key =
config["token_credential_key"]
client.authorization.token_credential_secret =
config["token_credential_secret"]
when :oauth_2
if client.authorization &&
!client.authorization.kind_of?(Signet::OAuth2::Client)
STDERR.puts(
"Unexpected authorization mechanism: " +
"#{client.authorization.class}"
)
exit(1)
end
config = open(config_file, 'r') { |file| YAML.load(file.read) }
client.authorization.scope = options[:scope]
client.authorization.client_id = config["client_id"]
client.authorization.client_secret = config["client_secret"]
client.authorization.access_token = config["access_token"]
client.authorization.refresh_token = config["refresh_token"]
else
# Dunno?
end
if options[:discovery_uri]
client.discovery_uri = options[:discovery_uri]
end
return client
end
def api_version(api, version)
v = version
if !version
if client.preferred_version(api)
v = client.preferred_version(api).version
else
v = 'v1'
end
end
return v
end
COMMANDS = [ COMMANDS = [
:oauth_login, :oauth_1_login,
:oauth_2_login,
:list, :list,
:execute, :execute,
:irb, :irb,
:fuzz :fuzz
] ]
def oauth_login def oauth_1_login
require 'signet/oauth_1/client' require 'signet/oauth_1/client'
require 'launchy' require 'launchy'
require 'yaml' require 'yaml'
if options[:client_credential_key] && if options[:client_credential_key] &&
options[:client_credential_secret] options[:client_credential_secret]
scope = options[:scope]
config = { config = {
"scope" => nil, "mechanism" => "oauth_1",
"scope" => options[:scope],
"client_credential_key" => options[:client_credential_key], "client_credential_key" => options[:client_credential_key],
"client_credential_secret" => options[:client_credential_secret], "client_credential_secret" => options[:client_credential_secret],
"token_credential_key" => nil, "token_credential_key" => nil,
@ -229,19 +306,8 @@ HTML
:client_credential_secret => 'anonymous', :client_credential_secret => 'anonymous',
:callback => "http://localhost:#{OAUTH_SERVER_PORT}/" :callback => "http://localhost:#{OAUTH_SERVER_PORT}/"
) )
scope = options[:scope]
# Special cases
case scope
when /https:\/\/www\.googleapis\.com\/auth\/buzz/,
/https:\/\/www\.googleapis\.com\/auth\/buzz\.readonly/
oauth_client.authorization_uri =
'https://www.google.com/buzz/api/auth/OAuthAuthorizeToken?' +
"domain=#{oauth_client.client_credential_key}&" +
"scope=#{scope}&" +
"xoauth_displayname=Google%20API%20Client"
end
oauth_client.fetch_temporary_credential!(:additional_parameters => { oauth_client.fetch_temporary_credential!(:additional_parameters => {
:scope => scope, :scope => options[:scope],
:xoauth_displayname => 'Google API Client' :xoauth_displayname => 'Google API Client'
}) })
@ -251,7 +317,7 @@ HTML
server.start server.start
oauth_client.fetch_token_credential!(:verifier => $verifier) oauth_client.fetch_token_credential!(:verifier => $verifier)
config = { config = {
"scope" => scope, "scope" => options[:scope],
"client_credential_key" => "client_credential_key" =>
oauth_client.client_credential_key, oauth_client.client_credential_key,
"client_credential_secret" => "client_credential_secret" =>
@ -267,34 +333,90 @@ HTML
end end
end end
def oauth_2_login
require 'signet/oauth_2/client'
require 'launchy'
require 'yaml'
if !options[:client_credential_key] ||
!options[:client_credential_secret]
STDERR.puts('No client ID and secret supplied.')
exit(1)
end
if options[:access_token]
config = {
"mechanism" => "oauth_2",
"scope" => options[:scope],
"client_id" => options[:client_credential_key],
"client_secret" => options[:client_credential_secret],
"access_token" => options[:access_token],
"refresh_token" => options[:refresh_token]
}
config_file = File.expand_path('~/.google-api.yaml')
open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
exit(0)
else
$verifier = nil
# TODO(bobaman): Cross-platform?
logger = WEBrick::Log.new('/dev/null')
server = WEBrick::HTTPServer.new(
:Port => OAUTH_SERVER_PORT,
:Logger => logger,
:AccessLog => logger
)
trap("INT") { server.shutdown }
server.mount("/", OAuthVerifierServlet)
oauth_client = Signet::OAuth2::Client.new(
:authorization_uri =>
'https://www.google.com/accounts/o8/oauth2/authorization',
:token_credential_uri =>
'https://www.google.com/accounts/o8/oauth2/token',
:client_id => options[:client_credential_key],
:client_secret => options[:client_credential_secret],
:redirect_uri => "http://localhost:#{OAUTH_SERVER_PORT}/",
:scope => options[:scope]
)
# Launch browser
Launchy::Browser.run(oauth_client.authorization_uri.to_s)
server.start
oauth_client.code = $verifier
oauth_client.fetch_access_token!
config = {
"mechanism" => "oauth_2",
"scope" => options[:scope],
"client_id" => oauth_client.client_id,
"client_secret" => oauth_client.client_secret,
"access_token" => oauth_client.access_token,
"refresh_token" => oauth_client.refresh_token
}
config_file = File.expand_path('~/.google-api.yaml')
open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
exit(0)
end
end
def list def list
service_name = options[:service_name] api = options[:api]
unless service_name unless api
STDERR.puts('No service name supplied.') STDERR.puts('No service name supplied.')
exit(1) exit(1)
end end
client = Google::APIClient.new( client = Google::APIClient.new(:authorization => nil)
:service => service_name,
:authorization => nil
)
if options[:discovery_uri] if options[:discovery_uri]
client.discovery_uri = options[:discovery_uri] client.discovery_uri = options[:discovery_uri]
end end
service_version = version = api_version(api, options[:version])
options[:service_version] || service = client.discovered_api(api, version)
client.latest_service_version(service_name).version
service = client.discovered_service(service_name, service_version)
rpcnames = service.to_h.keys rpcnames = service.to_h.keys
puts rpcnames.sort.join("\n") puts rpcnames.sort.join("\n")
exit(0) exit(0)
end end
def execute def execute
require 'signet/oauth_1/client' client = self.client
require 'yaml'
config_file = File.expand_path('~/.google-api.yaml')
signed = File.exist?(config_file)
authorization_type = :oauth_1
# Setup HTTP request data # Setup HTTP request data
request_body = '' request_body = ''
@ -308,28 +430,6 @@ HTML
headers << ['Content-Type', 'application/json'] headers << ['Content-Type', 'application/json']
end end
configure_authorization = lambda do |client|
if !client.authorization.kind_of?(Signet::OAuth1::Client)
STDERR.puts(
"Unexpected authorization mechanism: " +
"#{client.authorization.class}"
)
exit(1)
end
config = open(config_file, 'r') { |file| YAML.load(file.read) }
client.authorization.client_credential_key =
config["client_credential_key"]
client.authorization.client_credential_secret =
config["client_credential_secret"]
client.authorization.token_credential_key =
config["token_credential_key"]
client.authorization.token_credential_secret =
config["token_credential_secret"]
if client.authorization.token_credential == nil
authorization_type = :two_legged_oauth_1
end
end
if options[:uri] if options[:uri]
# Make request with URI manually specified # Make request with URI manually specified
uri = Addressable::URI.parse(options[:uri]) uri = Addressable::URI.parse(options[:uri])
@ -345,13 +445,8 @@ HTML
method = options[:http_method] method = options[:http_method]
method ||= request_body == '' ? 'GET' : 'POST' method ||= request_body == '' ? 'GET' : 'POST'
method.upcase! method.upcase!
client = Google::APIClient.new(:authorization => authorization_type)
if options[:discovery_uri]
client.discovery_uri = options[:discovery_uri]
end
configure_authorization.call(client) if signed
request = [method, uri.to_str, headers, [request_body]] request = [method, uri.to_str, headers, [request_body]]
request = client.sign_request(request) request = client.generate_authenticated_request(:request => request)
response = client.transmit_request(request) response = client.transmit_request(request)
status, headers, body = response status, headers, body = response
puts body puts body
@ -362,25 +457,14 @@ HTML
STDERR.puts('No rpcname supplied.') STDERR.puts('No rpcname supplied.')
exit(1) exit(1)
end end
service_name = api = options[:api] || self.rpcname[/^([^\.]+)\./, 1]
options[:service_name] || self.rpcname[/^([^\.]+)\./, 1] version = api_version(api, options[:version])
client = Google::APIClient.new( service = client.discovered_api(api, version)
:service => service_name,
:authorization => authorization_type
)
if options[:discovery_uri]
client.discovery_uri = options[:discovery_uri]
end
configure_authorization.call(client) if signed
service_version =
options[:service_version] ||
client.latest_service_version(service_name).version
service = client.discovered_service(service_name, service_version)
method = service.to_h[self.rpcname] method = service.to_h[self.rpcname]
if !method if !method
STDERR.puts( STDERR.puts(
"Method #{self.rpcname} does not exist for " + "Method #{self.rpcname} does not exist for " +
"#{service_name}-#{service_version}." "#{api}-#{version}."
) )
exit(1) exit(1)
end end
@ -394,7 +478,7 @@ HTML
end end
begin begin
response = client.execute( response = client.execute(
method, parameters, request_body, headers, {:signed => signed} method, parameters, request_body, headers
) )
status, headers, body = response status, headers, body = response
puts body puts body
@ -407,37 +491,7 @@ HTML
end end
def irb def irb
require 'signet/oauth_1/client' $client = self.client
require 'yaml'
require 'irb'
config_file = File.expand_path('~/.google-api.yaml')
signed = File.exist?(config_file)
$client = Google::APIClient.new(
:service => options[:service_name],
:authorization => (signed ? :oauth_1 : nil)
)
if signed
if $client.authorization &&
!$client.authorization.kind_of?(Signet::OAuth1::Client)
STDERR.puts(
"Unexpected authorization mechanism: " +
"#{$client.authorization.class}"
)
exit(1)
end
config = open(config_file, 'r') { |file| YAML.load(file.read) }
$client.authorization.client_credential_key =
config["client_credential_key"]
$client.authorization.client_credential_secret =
config["client_credential_secret"]
$client.authorization.token_credential_key =
config["token_credential_key"]
$client.authorization.token_credential_secret =
config["token_credential_secret"]
end
# Otherwise IRB will misinterpret command-line options # Otherwise IRB will misinterpret command-line options
ARGV.clear ARGV.clear
IRB.start(__FILE__) IRB.start(__FILE__)

View File

@ -319,7 +319,7 @@ def service(service_name, service_version)
unless service_version unless service_version
service_version = client.latest_service_version(service_name).version service_version = client.latest_service_version(service_name).version
end end
client.discovered_service(service_name, service_version) client.discovered_api(service_name, service_version)
end end
get '/template/:service/:method/' do get '/template/:service/:method/' do

View File

@ -12,58 +12,125 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
require 'httpadapter' require 'httpadapter'
require 'json' require 'json'
require 'stringio' require 'stringio'
require 'google/api_client/errors'
require 'google/api_client/discovery' require 'google/api_client/discovery'
module Google module Google
# TODO(bobaman): Document all this stuff. # TODO(bobaman): Document all this stuff.
## ##
# This class manages communication with a single API. # This class manages APIs communication.
class APIClient class APIClient
## ##
# An error which is raised when there is an unexpected response or other # Creates a new Google API client.
# transport error that prevents an operation from succeeding. #
class TransmissionError < StandardError # @param [Hash] options The configuration parameters for the client.
end # @option options [Symbol, #generate_authenticated_request] :authorization
# (:oauth_1)
# The authorization mechanism used by the client. The following
# mechanisms are supported out-of-the-box:
# <ul>
# <li><code>:two_legged_oauth_1</code></li>
# <li><code>:oauth_1</code></li>
# <li><code>:oauth_2</code></li>
# </ul>
# @option options [String] :host ("www.googleapis.com")
# The API hostname used by the client. This rarely needs to be changed.
# @option options [String] :user_agent ("google-api-ruby-client/{version}")
# The user agent used by the client. Most developers will want to
# leave this value alone — the API key is the primary mechanism used to
# identify an application.
def initialize(options={}) def initialize(options={})
@options = { # Normalize key to String to allow indifferent access.
:user_agent => ( options = options.inject({}) do |accu, (key, value)|
'google-api-ruby-client/' + Google::APIClient::VERSION::STRING accu[key.to_s] = value
) accu
}.merge(options) end
# Force immediate type-checking and short-cut resolution # Almost all API usage will have a host of 'www.googleapis.com'.
self.parser self.host = options["host"] || 'www.googleapis.com'
self.authorization # Most developers will want to leave this value alone.
self.http_adapter self.user_agent = options["user_agent"] || (
'google-api-ruby-client/' + Google::APIClient::VERSION::STRING
)
# This is mostly a default for the sake of convenience.
# Unlike most other options, this one may be nil, so we check for
# the presence of the key rather than checking the value.
if options.has_key?("parser")
self.parser = options["parser"]
else
require 'google/api_client/parsers/json_parser'
# NOTE: Do not rely on this default value, as it may change
self.parser = Google::APIClient::JSONParser
end
# The writer method understands a few Symbols and will generate useful
# default authentication mechanisms.
self.authorization = options["authorization"] || :oauth_2
# The HTTP adapter controls all of the HTTP traffic the client generates.
# By default, Net::HTTP is used, but adding support for other clients
# is trivial.
if options["http_adapter"]
self.http_adapter = options["http_adapter"]
else
require 'httpadapter/adapters/net_http'
# NOTE: Do not rely on this default value, as it may change
self.http_adapter = HTTPAdapter::NetHTTPAdapter.new
end
@discovery_uris = {}
@discovery_documents = {}
@discovered_apis = {}
return self return self
end end
## ##
# Returns the parser used by the client. # Returns the parser used by the client.
def parser #
unless @options[:parser] # @return [#serialize, #parse]
require 'google/api_client/parsers/json_parser' # The parser used by the client. Any object that implements both a
# NOTE: Do not rely on this default value, as it may change # <code>#serialize</code> and a <code>#parse</code> method may be used.
@options[:parser] = JSONParser # If <code>nil</code>, no parsing will be done.
attr_reader :parser
##
# Sets the parser used by the client.
#
# @param [#serialize, #parse] new_parser
# The parser used by the client. Any object that implements both a
# <code>#serialize</code> and a <code>#parse</code> method may be used.
# If <code>nil</code>, no parsing will be done.
def parser=(new_parser)
if new_parser &&
!new_parser.respond_to?(:serialize) &&
!new_parser.respond_to?(:parse)
raise TypeError,
'Expected parser object to respond to #serialize and #parse.'
end end
return @options[:parser] @parser = new_parser
end end
## ##
# Returns the authorization mechanism used by the client. # Returns the authorization mechanism used by the client.
# #
# @return [#generate_authenticated_request] The authorization mechanism. # @return [#generate_authenticated_request] The authorization mechanism.
def authorization attr_reader :authorization
case @options[:authorization]
##
# Sets the authorization mechanism used by the client.
#
# @param [#generate_authenticated_request] new_authorization
# The new authorization mechanism.
def authorization=(new_authorization)
case new_authorization
when :oauth_1, :oauth when :oauth_1, :oauth
require 'signet/oauth_1/client' require 'signet/oauth_1/client'
# NOTE: Do not rely on this default value, as it may change # NOTE: Do not rely on this default value, as it may change
@options[:authorization] = Signet::OAuth1::Client.new( new_authorization = Signet::OAuth1::Client.new(
:temporary_credential_uri => :temporary_credential_uri =>
'https://www.google.com/accounts/OAuthGetRequestToken', 'https://www.google.com/accounts/OAuthGetRequestToken',
:authorization_uri => :authorization_uri =>
@ -76,79 +143,149 @@ module Google
when :two_legged_oauth_1, :two_legged_oauth when :two_legged_oauth_1, :two_legged_oauth
require 'signet/oauth_1/client' require 'signet/oauth_1/client'
# NOTE: Do not rely on this default value, as it may change # NOTE: Do not rely on this default value, as it may change
@options[:authorization] = Signet::OAuth1::Client.new( new_authorization = Signet::OAuth1::Client.new(
:client_credential_key => nil, :client_credential_key => nil,
:client_credential_secret => nil, :client_credential_secret => nil,
:two_legged => true :two_legged => true
) )
when :oauth_2
require 'signet/oauth_2/client'
# NOTE: Do not rely on this default value, as it may change
new_authorization = Signet::OAuth2::Client.new(
:authorization_uri =>
'https://accounts.google.com/o/oauth2/auth',
:token_credential_uri =>
'https://accounts.google.com/o/oauth2/token'
)
when nil when nil
# No authorization mechanism # No authorization mechanism
else else
if !@options[:authorization].respond_to?( if !new_authorization.respond_to?(:generate_authenticated_request)
:generate_authenticated_request)
raise TypeError, raise TypeError,
'Expected authorization mechanism to respond to ' + 'Expected authorization mechanism to respond to ' +
'#generate_authenticated_request.' '#generate_authenticated_request.'
end end
end end
return @options[:authorization] @authorization = new_authorization
end return @authorization
##
# Sets the authorization mechanism used by the client.
#
# @param [#generate_authenticated_request] new_authorization
# The new authorization mechanism.
def authorization=(new_authorization)
@options[:authorization] = new_authorization
return self.authorization
end end
## ##
# Returns the HTTP adapter used by the client. # Returns the HTTP adapter used by the client.
def http_adapter #
return @options[:http_adapter] ||= (begin # @return [HTTPAdapter]
require 'httpadapter/adapters/net_http' # The HTTP adapter object. The object must include the
@options[:http_adapter] = HTTPAdapter::NetHTTPRequestAdapter # HTTPAdapter module and conform to its interface.
end) attr_reader :http_adapter
##
# Returns the HTTP adapter used by the client.
#
# @return [HTTPAdapter]
# The HTTP adapter object. The object must include the
# HTTPAdapter module and conform to its interface.
def http_adapter=(new_http_adapter)
if new_http_adapter.kind_of?(HTTPAdapter)
@http_adapter = new_http_adapter
else
raise TypeError, "Expected HTTPAdapter, got #{new_http_adapter.class}."
end
end
##
# The API hostname used by the client.
#
# @return [String]
# The API hostname. Should almost always be 'www.googleapis.com'.
attr_accessor :host
##
# The user agent used by the client.
#
# @return [String]
# The user agent string used in the User-Agent header.
attr_accessor :user_agent
##
# Returns the URI for the directory document.
#
# @return [Addressable::URI] The URI of the directory document.
def directory_uri
template = Addressable::Template.new(
"https://{host}/discovery/v0.3/directory"
)
return template.expand({
"host" => self.host
})
end
##
# Manually registers a URI as a discovery document for a specific version
# of an API.
#
# @param [String, Symbol] api The service name.
# @param [String] version The desired version of the service.
# @param [Addressable::URI] uri The URI of the discovery document.
def register_discovery_uri(api, version, uri)
api = api.to_s
version = version || 'v1'
@discovery_uris["#{api}:#{version}"] = uri
end end
## ##
# Returns the URI for the discovery document. # Returns the URI for the discovery document.
# #
# @param [String, Symbol] api The service name.
# @param [String] version The desired version of the service.
# @return [Addressable::URI] The URI of the discovery document. # @return [Addressable::URI] The URI of the discovery document.
def discovery_uri def discovery_uri(api, version=nil)
return @options[:discovery_uri] ||= (begin api = api.to_s
if @options[:service] version = version || 'v1'
service_id = @options[:service] return @discovery_uris["#{api}:#{version}"] ||= (begin
service_version = @options[:service_version] || 'v1' template = Addressable::Template.new(
Addressable::URI.parse( "https://{host}/discovery/v0.3/describe/" +
"http://www.googleapis.com/discovery/0.1/describe" + "{api}/{version}"
"?api=#{service_id}" )
) template.expand({
else "host" => self.host,
raise ArgumentError, "api" => api,
'Missing required configuration value, :discovery_uri.' "version" => version
end })
end) end)
end end
## ##
# Sets the discovery URI for the client. # Manually registers a pre-loaded discovery document for a specific version
# of an API.
# #
# @param [Addressable::URI, #to_str, String] new_discovery_uri # @param [String, Symbol] api The service name.
# The new discovery URI. # @param [String] version The desired version of the service.
def discovery_uri=(new_discovery_uri) # @param [String, StringIO] discovery_document
@options[:discovery_uri] = Addressable::URI.parse(new_discovery_uri) # The contents of the discovery document.
def register_discovery_document(api, version, discovery_document)
api = api.to_s
version = version || 'v1'
if discovery_document.kind_of?(StringIO)
discovery_document.rewind
discovery_document = discovery_document.string
elsif discovery_document.respond_to?(:to_str)
discovery_document = discovery_document.to_str
else
raise TypeError,
"Expected String or StringIO, got #{discovery_document.class}."
end
@discovery_documents["#{api}:#{version}"] =
JSON.parse(discovery_document)
end end
## ##
# Returns the parsed discovery document. # Returns the parsed directory document.
# #
# @return [Hash] The parsed JSON from the discovery document. # @return [Hash] The parsed JSON from the directory document.
def discovery_document def directory_document
return @discovery_document ||= (begin return @directory_document ||= (begin
request = ['GET', self.discovery_uri.to_s, [], []] request_uri = self.directory_uri
request = ['GET', request_uri, [], []]
response = self.transmit_request(request) response = self.transmit_request(request)
status, headers, body = response status, headers, body = response
if status == 200 # TODO(bobaman) Better status code handling? if status == 200 # TODO(bobaman) Better status code handling?
@ -160,124 +297,130 @@ module Google
JSON.parse(merged_body.string) JSON.parse(merged_body.string)
else else
raise TransmissionError, raise TransmissionError,
"Could not retrieve discovery document at: #{self.discovery_uri}" "Could not retrieve discovery document at: #{request_uri}"
end end
end) end)
end end
## ##
# Returns a list of services this client instance has performed discovery # Returns the parsed discovery document.
# for. This may return multiple versions of the same service.
# #
# @return [Array] # @param [String, Symbol] api The service name.
# A list of discovered <code>Google::APIClient::Service</code> objects. # @param [String] version The desired version of the service.
def discovered_services # @return [Hash] The parsed JSON from the discovery document.
return @discovered_services ||= (begin def discovery_document(api, version=nil)
service_names = self.discovery_document['data'].keys() api = api.to_s
services = [] version = version || 'v1'
for service_name in service_names return @discovery_documents["#{api}:#{version}"] ||= (begin
versions = self.discovery_document['data'][service_name] request_uri = self.discovery_uri(api, version)
for service_version in versions.keys() request = ['GET', request_uri, [], []]
service_description = response = self.transmit_request(request)
self.discovery_document['data'][service_name][service_version] status, headers, body = response
services << ::Google::APIClient::Service.new( if status == 200 # TODO(bobaman) Better status code handling?
service_name, merged_body = StringIO.new
service_version, body.each do |chunk|
service_description merged_body.write(chunk)
end
merged_body.rewind
JSON.parse(merged_body.string)
else
raise TransmissionError,
"Could not retrieve discovery document at: #{request_uri}"
end
end)
end
##
# Returns all APIs published in the directory document.
#
# @return [Array] The list of available APIs.
def discovered_apis
@directory_apis ||= (begin
document_base = self.directory_uri
if self.directory_document && self.directory_document['items']
self.directory_document['items'].map do |discovery_document|
::Google::APIClient::API.new(
document_base,
discovery_document
) )
end end
else
[]
end end
services
end) end)
end end
## ##
# Returns the service object for a given service name and service version. # Returns the service object for a given service name and service version.
# #
# @param [String, Symbol] service_name The service name. # @param [String, Symbol] api The service name.
# @param [String] service_version The desired version of the service. # @param [String] version The desired version of the service.
# #
# @return [Google::APIClient::Service] The service object. # @return [Google::APIClient::API] The service object.
def discovered_service(service_name, service_version='v1') def discovered_api(api, version=nil)
if !service_name.kind_of?(String) && !service_name.kind_of?(Symbol) if !api.kind_of?(String) && !api.kind_of?(Symbol)
raise TypeError, raise TypeError,
"Expected String or Symbol, got #{service_name.class}." "Expected String or Symbol, got #{api.class}."
end end
service_name = service_name.to_s api = api.to_s
for service in self.discovered_services version = version || 'v1'
if service.name == service_name && return @discovered_apis["#{api}:#{version}"] ||= begin
service.version.to_s == service_version.to_s document_base = self.discovery_uri(api, version)
return service discovery_document = self.discovery_document(api, version)
if document_base && discovery_document
::Google::APIClient::API.new(
document_base,
discovery_document
)
else
nil
end end
end end
return nil
end end
## ##
# Returns the method object for a given RPC name and service version. # Returns the method object for a given RPC name and service version.
# #
# @param [String, Symbol] rpc_name The RPC name of the desired method. # @param [String, Symbol] rpc_name The RPC name of the desired method.
# @param [String] service_version The desired version of the service. # @param [String] version The desired version of the service.
# #
# @return [Google::APIClient::Method] The method object. # @return [Google::APIClient::Method] The method object.
def discovered_method(rpc_name, service_version='v1') def discovered_method(rpc_name, api, version=nil)
if !rpc_name.kind_of?(String) && !rpc_name.kind_of?(Symbol) if !rpc_name.kind_of?(String) && !rpc_name.kind_of?(Symbol)
raise TypeError, raise TypeError,
"Expected String or Symbol, got #{rpc_name.class}." "Expected String or Symbol, got #{rpc_name.class}."
end end
rpc_name = rpc_name.to_s rpc_name = rpc_name.to_s
for service in self.discovered_services api = api.to_s
# This looks kinda weird, but is not a real problem because there's version = version || 'v1'
# almost always only one service, and this is memoized anyhow. service = self.discovered_api(api, version)
if service.version.to_s == service_version.to_s if service.to_h[rpc_name]
return service.to_h[rpc_name] if service.to_h[rpc_name] return service.to_h[rpc_name]
end else
return nil
end end
return nil
end end
## ##
# Returns the service object with the highest version number. # Returns the service object with the highest version number.
# #
# <em>Warning</em>: This method should be used with great care. As APIs # @note <em>Warning</em>: This method should be used with great care.
# are updated, minor differences between versions may cause # As APIs are updated, minor differences between versions may cause
# incompatibilities. Requesting a specific version will avoid this issue. # incompatibilities. Requesting a specific version will avoid this issue.
# #
# @param [String, Symbol] service_name The name of the service. # @param [String, Symbol] api The name of the service.
# #
# @return [Google::APIClient::Service] The service object. # @return [Google::APIClient::API] The service object.
def latest_service_version(service_name) def preferred_version(api)
if !service_name.kind_of?(String) && !service_name.kind_of?(Symbol) if !api.kind_of?(String) && !api.kind_of?(Symbol)
raise TypeError, raise TypeError,
"Expected String or Symbol, got #{service_name.class}." "Expected String or Symbol, got #{api.class}."
end end
service_name = service_name.to_s api = api.to_s
return (self.discovered_services.select do |service| # TODO(bobaman): Update to use directory API.
service.name == service_name return self.discovered_apis.detect do |a|
end).sort.last a.name == api && a.preferred == true
end
##
# Returns the user agent used by the client.
#
# @return [String]
# The user agent string used in the User-Agent header.
def user_agent
return @options[:user_agent]
end
##
# Sets the user agent used by the client.
#
# @param [String, #to_str] new_user_agent
# The new user agent string to use in the User-Agent header.
def user_agent=(new_user_agent)
unless new_user_agent == nil || new_user_agent.respond_to?(:to_str)
raise TypeError, "Expected String, got #{new_user_agent.class}."
end end
new_user_agent = new_user_agent.to_str unless new_user_agent == nil
@options[:user_agent] = new_user_agent
return self.user_agent
end end
## ##
@ -291,16 +434,17 @@ module Google
# @param [Hash, Array] headers The HTTP headers for the request. # @param [Hash, Array] headers The HTTP headers for the request.
# @param [Hash] options # @param [Hash] options
# The configuration parameters for the request. # The configuration parameters for the request.
# - <code>:service_version</code> — # - <code>:version</code> —
# The service version. Only used if <code>api_method</code> is a # The service version. Only used if <code>api_method</code> is a
# <code>String</code>. Defaults to <code>'v1'</code>. # <code>String</code>. Defaults to <code>'v1'</code>.
# - <code>:parser</code> — # - <code>:parser</code> —
# The parser for the response. # The parser for the response.
# - <code>:authorization</code> — # - <code>:authorization</code> —
# The authorization mechanism for the response. Used only if # The authorization mechanism for the response. Used only if
# <code>:signed</code> is <code>true</code>. # <code>:authenticated</code> is <code>true</code>.
# - <code>:signed</code> — # - <code>:authenticated</code> —
# <code>true</code> if the request must be signed, <code>false</code> # <code>true</code> if the request must be signed or otherwise
# authenticated, <code>false</code>
# otherwise. Defaults to <code>true</code> if an authorization # otherwise. Defaults to <code>true</code> if an authorization
# mechanism has been set, <code>false</code> otherwise. # mechanism has been set, <code>false</code> otherwise.
# #
@ -316,19 +460,27 @@ module Google
api_method, parameters={}, body='', headers=[], options={}) api_method, parameters={}, body='', headers=[], options={})
options={ options={
:parser => self.parser, :parser => self.parser,
:service_version => 'v1', :version => 'v1',
:authorization => self.authorization :authorization => self.authorization
}.merge(options) }.merge(options)
# The default value for the :signed option depends on whether an # The default value for the :authenticated option depends on whether an
# authorization mechanism has been set. # authorization mechanism has been set.
if options[:authorization] if options[:authorization]
options = {:signed => true}.merge(options) options = {:authenticated => true}.merge(options)
else else
options = {:signed => false}.merge(options) options = {:authenticated => false}.merge(options)
end end
if api_method.kind_of?(String) || api_method.kind_of?(Symbol) if api_method.kind_of?(String) || api_method.kind_of?(Symbol)
api_method = api_method.to_s
# This method of guessing the API is unreliable. This will fail for
# APIs where the first segment of the RPC name does not match the
# service name. However, this is a fallback mechanism anyway.
# Developers should be passing in a reference to the method, rather
# than passing in a string or symbol. This should raise an error
# in the case of a mismatch.
api = api_method[/^([^.]+)\./, 1]
api_method = self.discovered_method( api_method = self.discovered_method(
api_method.to_s, options[:service_version] api_method, api, options[:version]
) )
elsif !api_method.kind_of?(::Google::APIClient::Method) elsif !api_method.kind_of?(::Google::APIClient::Method)
raise TypeError, raise TypeError,
@ -339,8 +491,8 @@ module Google
raise ArgumentError, "API method could not be found." raise ArgumentError, "API method could not be found."
end end
request = api_method.generate_request(parameters, body, headers) request = api_method.generate_request(parameters, body, headers)
if options[:signed] if options[:authenticated]
request = self.sign_request(request, options[:authorization]) request = self.generate_authenticated_request(:request => request)
end end
return request return request
end end
@ -356,7 +508,7 @@ module Google
# @param [Hash, Array] headers The HTTP headers for the request. # @param [Hash, Array] headers The HTTP headers for the request.
# @param [Hash] options # @param [Hash] options
# The configuration parameters for the request. # The configuration parameters for the request.
# - <code>:service_version</code> — # - <code>:version</code> —
# The service version. Only used if <code>api_method</code> is a # The service version. Only used if <code>api_method</code> is a
# <code>String</code>. Defaults to <code>'v1'</code>. # <code>String</code>. Defaults to <code>'v1'</code>.
# - <code>:adapter</code> — # - <code>:adapter</code> —
@ -365,9 +517,10 @@ module Google
# The parser for the response. # The parser for the response.
# - <code>:authorization</code> — # - <code>:authorization</code> —
# The authorization mechanism for the response. Used only if # The authorization mechanism for the response. Used only if
# <code>:signed</code> is <code>true</code>. # <code>:authenticated</code> is <code>true</code>.
# - <code>:signed</code> — # - <code>:authenticated</code> —
# <code>true</code> if the request must be signed, <code>false</code> # <code>true</code> if the request must be signed or otherwise
# authenticated, <code>false</code>
# otherwise. Defaults to <code>true</code>. # otherwise. Defaults to <code>true</code>.
# #
# @return [Array] The response from the API. # @return [Array] The response from the API.
@ -406,25 +559,26 @@ module Google
include Enumerable include Enumerable
end end
end end
unless headers.any? { |k, v| k.downcase == 'user-agent' } if self.user_agent.kind_of?(String)
headers = headers.to_a.insert(0, ['User-Agent', self.user_agent]) unless headers.any? { |k, v| k.downcase == 'User-Agent'.downcase }
headers = headers.to_a.insert(0, ['User-Agent', self.user_agent])
end
elsif self.user_agent != nil
raise TypeError,
"Expected User-Agent to be String, got #{self.user_agent.class}"
end end
end end
::HTTPAdapter.transmit([method, uri, headers, body], adapter) adapter.transmit([method, uri, headers, body])
end end
## ##
# Signs a request using the current authorization mechanism. # Signs a request using the current authorization mechanism.
# #
# @param [Array] request The request to sign. # @param [Hash] options The options to pass through.
# @param [#generate_authenticated_request] authorization
# The authorization mechanism.
# #
# @return [Array] The signed request. # @return [Array] The signed or otherwise authenticated request.
def sign_request(request, authorization=self.authorization) def generate_authenticated_request(options={})
return authorization.generate_authenticated_request( return authorization.generate_authenticated_request(options)
:request => request
)
end end
end end
end end

View File

@ -12,42 +12,37 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
require 'json' require 'json'
require 'addressable/uri' require 'addressable/uri'
require 'addressable/template' require 'addressable/template'
require 'google/inflection' require 'google/inflection'
require 'google/api_client/errors'
module Google module Google
class APIClient class APIClient
##
# An exception that is raised if a method is called with missing or
# invalid parameter values.
class ValidationError < StandardError
end
## ##
# A service that has been described by a discovery document. # A service that has been described by a discovery document.
class Service class API
## ##
# Creates a description of a particular version of a service. # Creates a description of a particular version of a service.
# #
# @param [String] service_name # @param [String] api
# The identifier for the service. Note that while this frequently # The identifier for the service. Note that while this frequently
# matches the first segment of all of the service's RPC names, this # matches the first segment of all of the service's RPC names, this
# should not be assumed. There is no requirement that these match. # should not be assumed. There is no requirement that these match.
# @param [String] service_version # @param [String] version
# The identifier for the service version. # The identifier for the service version.
# @param [Hash] service_description # @param [Hash] api_description
# The section of the discovery document that applies to this service # The section of the discovery document that applies to this service
# version. # version.
# #
# @return [Google::APIClient::Service] The constructed service object. # @return [Google::APIClient::API] The constructed service object.
def initialize(service_name, service_version, service_description) def initialize(document_base, discovery_document)
@name = service_name @document_base = Addressable::URI.parse(document_base)
@version = service_version @discovery_document = discovery_document
@description = service_description
metaclass = (class <<self; self; end) metaclass = (class <<self; self; end)
self.resources.each do |resource| self.resources.each do |resource|
method_name = Google::INFLECTOR.underscore(resource.name).to_sym method_name = Google::INFLECTOR.underscore(resource.name).to_sym
@ -63,31 +58,67 @@ module Google
end end
end end
##
# Returns the id of the service.
#
# @return [String] The service id.
def id
return @discovery_document['id']
end
## ##
# Returns the identifier for the service. # Returns the identifier for the service.
# #
# @return [String] The service identifier. # @return [String] The service identifier.
attr_reader :name def name
return @discovery_document['name']
end
## ##
# Returns the version of the service. # Returns the version of the service.
# #
# @return [String] The service version. # @return [String] The service version.
attr_reader :version def version
return @discovery_document['version']
end
## ##
# Returns the parsed section of the discovery document that applies to # Returns the parsed section of the discovery document that applies to
# this version of the service. # this version of the service.
# #
# @return [Hash] The service description. # @return [Hash] The service description.
attr_reader :description def description
return @discovery_document['description']
end
##
# Returns true if this is the preferred version of this API.
#
# @return [TrueClass, FalseClass]
# Whether or not this is the preferred version of this API.
def preferred
return @discovery_document['preferred']
end
## ##
# Returns the base URI for this version of the service. # Returns the base URI for this version of the service.
# #
# @return [Addressable::URI] The base URI that methods are joined to. # @return [Addressable::URI] The base URI that methods are joined to.
def base attr_reader :document_base
return @base ||= Addressable::URI.parse(self.description['baseUrl'])
##
# Returns the base URI for this version of the service.
#
# @return [Addressable::URI] The base URI that methods are joined to.
def rest_base
if @discovery_document['restBasePath']
return @rest_base ||= (
self.document_base +
Addressable::URI.parse(@discovery_document['restBasePath'])
).normalize
else
return nil
end
end end
## ##
@ -95,13 +126,13 @@ module Google
# #
# @param [Addressable::URI, #to_str, String] new_base # @param [Addressable::URI, #to_str, String] new_base
# The new base URI to use for the service. # The new base URI to use for the service.
def base=(new_base) def rest_base=(new_rest_base)
@base = Addressable::URI.parse(new_base) @rest_base = Addressable::URI.parse(new_rest_base)
self.resources.each do |resource| self.resources.each do |resource|
resource.base = @base resource.rest_base = @rest_base
end end
self.methods.each do |method| self.methods.each do |method|
method.base = @base method.rest_base = @rest_base
end end
end end
@ -112,8 +143,8 @@ module Google
# @return [Array] A list of {Google::APIClient::Resource} objects. # @return [Array] A list of {Google::APIClient::Resource} objects.
def resources def resources
return @resources ||= ( return @resources ||= (
(self.description['resources'] || []).inject([]) do |accu, (k, v)| (@discovery_document['resources'] || []).inject([]) do |accu, (k, v)|
accu << ::Google::APIClient::Resource.new(self.base, k, v) accu << ::Google::APIClient::Resource.new(self.rest_base, k, v)
accu accu
end end
) )
@ -126,8 +157,8 @@ module Google
# @return [Array] A list of {Google::APIClient::Method} objects. # @return [Array] A list of {Google::APIClient::Method} objects.
def methods def methods
return @methods ||= ( return @methods ||= (
(self.description['methods'] || []).inject([]) do |accu, (k, v)| (@discovery_document['methods'] || []).inject([]) do |accu, (k, v)|
accu << ::Google::APIClient::Method.new(self.base, k, v) accu << ::Google::APIClient::Method.new(self.rest_base, k, v)
accu accu
end end
) )
@ -140,12 +171,12 @@ module Google
# #
# @example # @example
# # Discover available methods # # Discover available methods
# method_names = client.discovered_service('buzz').to_h.keys # method_names = client.discovered_api('buzz').to_h.keys
def to_h def to_h
return @hash ||= (begin return @hash ||= (begin
methods_hash = {} methods_hash = {}
self.methods.each do |method| self.methods.each do |method|
methods_hash[method.rpc_name] = method methods_hash[method.rpc_method] = method
end end
self.resources.each do |resource| self.resources.each do |resource|
methods_hash.merge!(resource.to_h) methods_hash.merge!(resource.to_h)
@ -154,48 +185,6 @@ module Google
end) end)
end end
##
# Compares two versions of a service.
#
# @param [Object] other The service to compare.
#
# @return [Integer]
# <code>-1</code> if the service is older than <code>other</code>.
# <code>0</code> if the service is the same as <code>other</code>.
# <code>1</code> if the service is newer than <code>other</code>.
# <code>nil</code> if the service cannot be compared to
# <code>other</code>.
def <=>(other)
# We can only compare versions of the same service
if other.kind_of?(self.class) && self.name == other.name
split_version = lambda do |version|
dotted_version = version[/^v?(\d+(.\d+)*)-?(.*?)?$/, 1]
suffix = version[/^v?(\d+(.\d+)*)-?(.*?)?$/, 3]
if dotted_version && suffix
[dotted_version.split('.').map { |v| v.to_i }, suffix]
else
[[-1], version]
end
end
self_sortable, self_suffix = split_version.call(self.version)
other_sortable, other_suffix = split_version.call(other.version)
result = self_sortable <=> other_sortable
if result != 0
return result
# If the dotted versions are equal, check the suffix.
# An omitted suffix should be sorted after an included suffix.
elsif self_suffix == ''
return 1
elsif other_suffix == ''
return -1
else
return self_suffix <=> other_suffix
end
else
return nil
end
end
## ##
# Returns a <code>String</code> representation of the service's state. # Returns a <code>String</code> representation of the service's state.
# #
@ -222,10 +211,10 @@ module Google
# The section of the discovery document that applies to this resource. # The section of the discovery document that applies to this resource.
# #
# @return [Google::APIClient::Resource] The constructed resource object. # @return [Google::APIClient::Resource] The constructed resource object.
def initialize(base, resource_name, resource_description) def initialize(rest_base, resource_name, discovery_document)
@base = base @rest_base = rest_base
@name = resource_name @name = resource_name
@description = resource_description @discovery_document = discovery_document
metaclass = (class <<self; self; end) metaclass = (class <<self; self; end)
self.resources.each do |resource| self.resources.each do |resource|
method_name = Google::INFLECTOR.underscore(resource.name).to_sym method_name = Google::INFLECTOR.underscore(resource.name).to_sym
@ -258,20 +247,20 @@ module Google
# Returns the base URI for this resource. # Returns the base URI for this resource.
# #
# @return [Addressable::URI] The base URI that methods are joined to. # @return [Addressable::URI] The base URI that methods are joined to.
attr_reader :base attr_reader :rest_base
## ##
# Updates the hierarchy of resources and methods with the new base. # Updates the hierarchy of resources and methods with the new base.
# #
# @param [Addressable::URI, #to_str, String] new_base # @param [Addressable::URI, #to_str, String] new_base
# The new base URI to use for the resource. # The new base URI to use for the resource.
def base=(new_base) def rest_base=(new_rest_base)
@base = Addressable::URI.parse(new_base) @rest_base = Addressable::URI.parse(new_rest_base)
self.resources.each do |resource| self.resources.each do |resource|
resource.base = @base resource.rest_base = @rest_base
end end
self.methods.each do |method| self.methods.each do |method|
method.base = @base method.rest_base = @rest_base
end end
end end
@ -281,8 +270,8 @@ module Google
# @return [Array] A list of {Google::APIClient::Resource} objects. # @return [Array] A list of {Google::APIClient::Resource} objects.
def resources def resources
return @resources ||= ( return @resources ||= (
(self.description['resources'] || []).inject([]) do |accu, (k, v)| (@discovery_document['resources'] || []).inject([]) do |accu, (k, v)|
accu << ::Google::APIClient::Resource.new(self.base, k, v) accu << ::Google::APIClient::Resource.new(self.rest_base, k, v)
accu accu
end end
) )
@ -294,8 +283,8 @@ module Google
# @return [Array] A list of {Google::APIClient::Method} objects. # @return [Array] A list of {Google::APIClient::Method} objects.
def methods def methods
return @methods ||= ( return @methods ||= (
(self.description['methods'] || []).inject([]) do |accu, (k, v)| (@discovery_document['methods'] || []).inject([]) do |accu, (k, v)|
accu << ::Google::APIClient::Method.new(self.base, k, v) accu << ::Google::APIClient::Method.new(self.rest_base, k, v)
accu accu
end end
) )
@ -310,7 +299,7 @@ module Google
return @hash ||= (begin return @hash ||= (begin
methods_hash = {} methods_hash = {}
self.methods.each do |method| self.methods.each do |method|
methods_hash[method.rpc_name] = method methods_hash[method.rpc_method] = method
end end
self.resources.each do |resource| self.resources.each do |resource|
methods_hash.merge!(resource.to_h) methods_hash.merge!(resource.to_h)
@ -337,7 +326,7 @@ module Google
## ##
# Creates a description of a particular method. # Creates a description of a particular method.
# #
# @param [Addressable::URI] base # @param [Addressable::URI] rest_base
# The base URI for the service. # The base URI for the service.
# @param [String] method_name # @param [String] method_name
# The identifier for the method. # The identifier for the method.
@ -345,10 +334,10 @@ module Google
# The section of the discovery document that applies to this method. # The section of the discovery document that applies to this method.
# #
# @return [Google::APIClient::Method] The constructed method object. # @return [Google::APIClient::Method] The constructed method object.
def initialize(base, method_name, method_description) def initialize(rest_base, method_name, discovery_document)
@base = base @rest_base = rest_base
@name = method_name @name = method_name
@description = method_description @discovery_document = discovery_document
end end
## ##
@ -369,15 +358,15 @@ module Google
# #
# @return [Addressable::URI] # @return [Addressable::URI]
# The base URI that this method will be joined to. # The base URI that this method will be joined to.
attr_reader :base attr_reader :rest_base
## ##
# Updates the method with the new base. # Updates the method with the new base.
# #
# @param [Addressable::URI, #to_str, String] new_base # @param [Addressable::URI, #to_str, String] new_base
# The new base URI to use for the method. # The new base URI to use for the method.
def base=(new_base) def rest_base=(new_rest_base)
@base = Addressable::URI.parse(new_base) @rest_base = Addressable::URI.parse(new_rest_base)
@uri_template = nil @uri_template = nil
end end
@ -385,8 +374,8 @@ module Google
# Returns the RPC name for the method. # Returns the RPC name for the method.
# #
# @return [String] The RPC name. # @return [String] The RPC name.
def rpc_name def rpc_method
return self.description['rpcName'] return @discovery_document['rpcMethod']
end end
## ##
@ -399,8 +388,9 @@ module Google
# a join operation on a URI, but we have to treat these as Strings # a join operation on a URI, but we have to treat these as Strings
# because of the way the discovery document provides the URIs. # because of the way the discovery document provides the URIs.
# This should be fixed soon. # This should be fixed soon.
return @uri_template ||= return @uri_template ||= Addressable::Template.new(
Addressable::Template.new(base.to_s + self.description['pathUrl']) self.rest_base + @discovery_document['restPath']
)
end end
## ##
@ -475,7 +465,7 @@ module Google
if !headers.kind_of?(Array) && !headers.kind_of?(Hash) if !headers.kind_of?(Array) && !headers.kind_of?(Hash)
raise TypeError, "Expected Hash or Array, got #{headers.class}." raise TypeError, "Expected Hash or Array, got #{headers.class}."
end end
method = self.description['httpMethod'] || 'GET' method = @discovery_document['httpMethod'] || 'GET'
uri = self.generate_uri(parameters) uri = self.generate_uri(parameters)
headers = headers.to_a if headers.kind_of?(Hash) headers = headers.to_a if headers.kind_of?(Hash)
return [method, uri.to_str, headers, [body]] return [method, uri.to_str, headers, [body]]
@ -488,7 +478,7 @@ module Google
# @return [Hash] The parameter descriptions. # @return [Hash] The parameter descriptions.
def parameter_descriptions def parameter_descriptions
@parameter_descriptions ||= ( @parameter_descriptions ||= (
self.description['parameters'] || {} @discovery_document['parameters'] || {}
).inject({}) { |h,(k,v)| h[k]=v; h } ).inject({}) { |h,(k,v)| h[k]=v; h }
end end
@ -498,7 +488,7 @@ module Google
# @return [Array] The parameters. # @return [Array] The parameters.
def parameters def parameters
@parameters ||= (( @parameters ||= ((
self.description['parameters'] || {} @discovery_document['parameters'] || {}
).inject({}) { |h,(k,v)| h[k]=v; h }).keys ).inject({}) { |h,(k,v)| h[k]=v; h }).keys
end end
@ -552,6 +542,12 @@ module Google
end end
parameters.each do |k, v| parameters.each do |k, v|
if self.parameter_descriptions[k] if self.parameter_descriptions[k]
enum = self.parameter_descriptions[k]['enum']
if enum && !enum.include?(v)
raise ArgumentError,
"Parameter '#{k}' has an invalid value: #{v}. " +
"Must be one of #{enum.inspect}."
end
pattern = self.parameter_descriptions[k]['pattern'] pattern = self.parameter_descriptions[k]['pattern']
if pattern if pattern
regexp = Regexp.new("^#{pattern}$") regexp = Regexp.new("^#{pattern}$")
@ -572,7 +568,8 @@ module Google
# @return [String] The method's state, as a <code>String</code>. # @return [String] The method's state, as a <code>String</code>.
def inspect def inspect
sprintf( sprintf(
"#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.rpc_name "#<%s:%#0x NAME:%s>",
self.class.to_s, self.object_id, self.rpc_method
) )
end end
end end

View File

@ -0,0 +1,30 @@
# Copyright 2010 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
module Google
class APIClient
##
# An error which is raised when there is an unexpected response or other
# transport error that prevents an operation from succeeding.
class TransmissionError < StandardError
end
##
# An exception that is raised if a method is called with missing or
# invalid parameter values.
class ValidationError < StandardError
end
end
end

View File

@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
require 'json' require 'json'
module Google module Google

View File

@ -12,12 +12,13 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
module Google module Google
class APIClient class APIClient
module VERSION module VERSION
MAJOR = 0 MAJOR = 0
MINOR = 1 MINOR = 2
TINY = 3 TINY = 0
STRING = [MAJOR, MINOR, TINY].join('.') STRING = [MAJOR, MINOR, TINY].join('.')
end end

View File

@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
module Google module Google
if defined?(ActiveSupport::Inflector) if defined?(ActiveSupport::Inflector)
INFLECTOR = ActiveSupport::Inflector INFLECTOR = ActiveSupport::Inflector

View File

@ -21,422 +21,422 @@ require 'google/api_client'
require 'google/api_client/version' require 'google/api_client/version'
require 'google/api_client/parsers/json_parser' require 'google/api_client/parsers/json_parser'
describe Google::APIClient, 'unconfigured' do describe Google::APIClient do
before do before do
@client = Google::APIClient.new @client = Google::APIClient.new
end end
it 'should not be able to determine the discovery URI' do it 'should raise a type error for bogus authorization' do
(lambda do (lambda do
@client.discovery_uri Google::APIClient.new(:authorization => 42)
end).should raise_error(ArgumentError) end).should raise_error(TypeError)
end
end
describe Google::APIClient, 'configured for a bogus API' do
before do
@client = Google::APIClient.new(:service => 'bogus')
end end
it 'should not be able to retrieve the discovery document' do it 'should not be able to retrieve the discovery document for a bogus API' do
(lambda do (lambda do
@client.discovery_document @client.discovery_document('bogus')
end).should raise_error(Google::APIClient::TransmissionError)
(lambda do
@client.discovered_api('bogus')
end).should raise_error(Google::APIClient::TransmissionError) end).should raise_error(Google::APIClient::TransmissionError)
end end
end
describe Google::APIClient, 'configured for bogus authorization' do it 'should raise an error for bogus services' do
it 'should raise a type error' do
(lambda do (lambda do
Google::APIClient.new(:service => 'prediction', :authorization => 42) @client.discovered_api(42)
end).should raise_error(TypeError) end).should raise_error(TypeError)
end end
end
describe Google::APIClient, 'configured for the prediction API' do
before do
@client = Google::APIClient.new(:service => 'prediction')
end
it 'should correctly determine the discovery URI' do
@client.discovery_uri.should ===
'http://www.googleapis.com/discovery/0.1/describe?api=prediction'
end
it 'should have multiple versions available' do
@client.discovered_services.size.should > 1
end
it 'should find APIs that are in the discovery document' do
@client.discovered_service('prediction').name.should == 'prediction'
@client.discovered_service('prediction').version.should == 'v1'
@client.discovered_service(:prediction).name.should == 'prediction'
@client.discovered_service(:prediction).version.should == 'v1'
end
it 'should find API versions that are in the discovery document' do
@client.discovered_service('prediction', 'v1.1').version.should == 'v1.1'
end
it 'should not find APIs that are not in the discovery document' do
@client.discovered_service('bogus').should == nil
end
it 'should raise an error for bogus services' do it 'should raise an error for bogus services' do
(lambda do (lambda do
@client.discovered_service(42) @client.preferred_version(42)
end).should raise_error(TypeError) end).should raise_error(TypeError)
end end
it 'should find methods that are in the discovery document' do
@client.discovered_method('prediction.training.insert').name.should ==
'insert'
@client.discovered_method(:'prediction.training.insert').name.should ==
'insert'
end
it 'should find methods for versions that are in the discovery document' do
@client.discovered_method(
'prediction.training.delete', 'v1.1'
).should_not == nil
end
it 'should not find methods that are not in the discovery document' do
@client.discovered_method('prediction.training.delete', 'v1').should == nil
@client.discovered_method('prediction.bogus').should == nil
end
it 'should raise an error for bogus methods' do
(lambda do
@client.discovered_method(42)
end).should raise_error(TypeError)
end
it 'should correctly determine the latest version' do
@client.latest_service_version('prediction').version.should_not == 'v1'
@client.latest_service_version(:prediction).version.should_not == 'v1'
end
it 'should raise an error for bogus services' do
(lambda do
@client.latest_service_version(42)
end).should raise_error(TypeError)
end
it 'should correctly determine the latest version' do
# Sanity check the algorithm
@client.discovered_services.clear
@client.discovered_services <<
Google::APIClient::Service.new('magic', 'v1', {})
@client.discovered_services <<
Google::APIClient::Service.new('magic', 'v1.1', {})
@client.discovered_services <<
Google::APIClient::Service.new('magic', 'v1.10', {})
@client.discovered_services <<
Google::APIClient::Service.new('magic', 'v10.0.1', {})
@client.discovered_services <<
Google::APIClient::Service.new('magic', 'v10.1', {})
@client.discovered_services <<
Google::APIClient::Service.new('magic', 'v2.1', {})
@client.discovered_services <<
Google::APIClient::Service.new('magic', 'v10.0', {})
@client.latest_service_version('magic').version.should == 'v10.1'
end
it 'should correctly determine the latest version' do
# Sanity check the algorithm
@client.discovered_services.clear
@client.discovered_services <<
Google::APIClient::Service.new('one', 'v3', {})
@client.discovered_services <<
Google::APIClient::Service.new('two', 'v1', {})
@client.discovered_services <<
Google::APIClient::Service.new('two', 'v1.1-r1c3', {})
@client.discovered_services <<
Google::APIClient::Service.new('two', 'v2', {})
@client.discovered_services <<
Google::APIClient::Service.new('two', 'v2beta1', {})
@client.discovered_services <<
Google::APIClient::Service.new('two', 'test2', {})
@client.latest_service_version('two').version.should == 'v2'
end
it 'should return nil for bogus service names' do
# Sanity check the algorithm
@client.latest_service_version('bogus').should == nil
end
it 'should generate valid requests' do
request = @client.generate_request(
'prediction.training.insert',
{'query' => '12345'}
)
method, uri, headers, body = request
method.should == 'POST'
uri.should ==
'https://www.googleapis.com/prediction/v1/training?query=12345'
(headers.inject({}) { |h,(k,v)| h[k]=v; h }).should == {}
body.should respond_to(:each)
end
it 'should generate requests against the correct URIs' do
request = @client.generate_request(
:'prediction.training.insert',
{'query' => '12345'}
)
method, uri, headers, body = request
uri.should ==
'https://www.googleapis.com/prediction/v1/training?query=12345'
end
it 'should generate requests against the correct URIs' do
prediction = @client.discovered_service('prediction', 'v1')
request = @client.generate_request(
prediction.training.insert,
{'query' => '12345'}
)
method, uri, headers, body = request
uri.should ==
'https://www.googleapis.com/prediction/v1/training?query=12345'
end
it 'should allow modification to the base URIs for testing purposes' do
prediction = @client.discovered_service('prediction', 'v1')
prediction.base = 'https://testing-domain.googleapis.com/prediction/v1/'
request = @client.generate_request(
prediction.training.insert,
{'query' => '123'}
)
method, uri, headers, body = request
uri.should ==
'https://testing-domain.googleapis.com/prediction/v1/training?query=123'
end
it 'should generate signed requests' do
@client.authorization = :oauth_1
@client.authorization.token_credential_key = '12345'
@client.authorization.token_credential_secret = '12345'
request = @client.generate_request(
'prediction.training.insert',
{'query' => '12345'}
)
method, uri, headers, body = request
headers = headers.inject({}) { |h,(k,v)| h[k]=v; h }
headers.keys.should include('Authorization')
headers['Authorization'].should =~ /^OAuth/
end
it 'should not be able to execute improperly authorized requests' do
@client.authorization = :oauth_1
@client.authorization.token_credential_key = '12345'
@client.authorization.token_credential_secret = '12345'
response = @client.execute(
'prediction.training.insert',
{'query' => '12345'}
)
status, headers, body = response
status.should == 401
end
it 'should raise an error for bogus methods' do it 'should raise an error for bogus methods' do
(lambda do (lambda do
@client.generate_request(42) @client.generate_request(42)
end).should raise_error(TypeError) end).should raise_error(TypeError)
end end
it 'should raise an error for bogus methods' do it 'should not return a preferred version for bogus service names' do
(lambda do @client.preferred_version('bogus').should == nil
@client.generate_request(@client.discovered_service('prediction'))
end).should raise_error(TypeError)
end
end
describe Google::APIClient, 'configured for the buzz API' do
before do
@client = Google::APIClient.new(:service => 'buzz')
end end
it 'should correctly determine the discovery URI' do describe 'with the prediction API' do
@client.discovery_uri.should === before do
'http://www.googleapis.com/discovery/0.1/describe?api=buzz' @client.authorization = nil
end
it 'should correctly determine the discovery URI' do
@client.discovery_uri('prediction').should ===
'https://www.googleapis.com/discovery/v0.3/describe/prediction/v1'
end
it 'should correctly generate API objects' do
@client.discovered_api('prediction').name.should == 'prediction'
@client.discovered_api('prediction').version.should == 'v1'
@client.discovered_api(:prediction).name.should == 'prediction'
@client.discovered_api(:prediction).version.should == 'v1'
end
it 'should discover methods' do
@client.discovered_method(
'prediction.training.insert', 'prediction'
).name.should == 'insert'
@client.discovered_method(
:'prediction.training.insert', :prediction
).name.should == 'insert'
end
it 'should discover methods' do
@client.discovered_method(
'prediction.training.delete', 'prediction', 'v1.1'
).name.should == 'delete'
end
it 'should not find methods that are not in the discovery document' do
@client.discovered_method(
'prediction.training.delete', 'prediction', 'v1'
).should == nil
@client.discovered_method(
'prediction.bogus', 'prediction', 'v1'
).should == nil
end
it 'should raise an error for bogus methods' do
(lambda do
@client.discovered_method(42, 'prediction', 'v1')
end).should raise_error(TypeError)
end
it 'should raise an error for bogus methods' do
(lambda do
@client.generate_request(@client.discovered_api('prediction'))
end).should raise_error(TypeError)
end
it 'should correctly determine the preferred version' do
@client.preferred_version('prediction').version.should_not == 'v1'
@client.preferred_version(:prediction).version.should_not == 'v1'
end
it 'should generate valid requests' do
request = @client.generate_request(
'prediction.training.insert',
{'data' => '12345', }
)
method, uri, headers, body = request
method.should == 'POST'
uri.should ==
'https://www.googleapis.com/prediction/v1/training?data=12345'
(headers.inject({}) { |h,(k,v)| h[k]=v; h }).should == {}
body.should respond_to(:each)
end
it 'should generate requests against the correct URIs' do
request = @client.generate_request(
:'prediction.training.insert',
{'data' => '12345'}
)
method, uri, headers, body = request
uri.should ==
'https://www.googleapis.com/prediction/v1/training?data=12345'
end
it 'should generate requests against the correct URIs' do
prediction = @client.discovered_api('prediction', 'v1')
request = @client.generate_request(
prediction.training.insert,
{'data' => '12345'}
)
method, uri, headers, body = request
uri.should ==
'https://www.googleapis.com/prediction/v1/training?data=12345'
end
it 'should allow modification to the base URIs for testing purposes' do
prediction = @client.discovered_api('prediction', 'v1')
prediction.rest_base =
'https://testing-domain.googleapis.com/prediction/v1/'
request = @client.generate_request(
prediction.training.insert,
{'data' => '123'}
)
method, uri, headers, body = request
uri.should ==
'https://testing-domain.googleapis.com/prediction/v1/training?data=123'
end
it 'should generate OAuth 1 requests' do
@client.authorization = :oauth_1
@client.authorization.token_credential_key = '12345'
@client.authorization.token_credential_secret = '12345'
request = @client.generate_request(
'prediction.training.insert',
{'data' => '12345'}
)
method, uri, headers, body = request
headers = headers.inject({}) { |h,(k,v)| h[k]=v; h }
headers.keys.should include('Authorization')
headers['Authorization'].should =~ /^OAuth/
end
it 'should generate OAuth 2 requests' do
@client.authorization = :oauth_2
@client.authorization.access_token = '12345'
request = @client.generate_request(
'prediction.training.insert',
{'data' => '12345'}
)
method, uri, headers, body = request
headers = headers.inject({}) { |h,(k,v)| h[k]=v; h }
headers.keys.should include('Authorization')
headers['Authorization'].should =~ /^OAuth/
end
it 'should not be able to execute improperly authorized requests' do
@client.authorization = :oauth_1
@client.authorization.token_credential_key = '12345'
@client.authorization.token_credential_secret = '12345'
response = @client.execute(
'prediction.training.insert',
{'data' => '12345'}
)
status, headers, body = response
status.should == 401
end
it 'should not be able to execute improperly authorized requests' do
@client.authorization = :oauth_2
@client.authorization.access_token = '12345'
response = @client.execute(
'prediction.training.insert',
{'data' => '12345'}
)
status, headers, body = response
status.should == 401
end
end end
it 'should find APIs that are in the discovery document' do describe 'with the buzz API' do
@client.discovered_service('buzz').name.should == 'buzz' before do
@client.discovered_service('buzz').version.should == 'v1' @client.authorization = nil
end @buzz = @client.discovered_api('buzz')
end
it 'should not find APIs that are not in the discovery document' do it 'should correctly determine the discovery URI' do
@client.discovered_service('bogus').should == nil @client.discovery_uri('buzz').should ===
end 'https://www.googleapis.com/discovery/v0.3/describe/buzz/v1'
end
it 'should find methods that are in the discovery document' do it 'should find APIs that are in the discovery document' do
# TODO(bobaman) Fix this when the RPC names are correct @client.discovered_api('buzz').name.should == 'buzz'
@client.discovered_method('chili.activities.list').name.should == 'list' @client.discovered_api('buzz').version.should == 'v1'
end @client.discovered_api(:buzz).name.should == 'buzz'
@client.discovered_api(:buzz).version.should == 'v1'
end
it 'should not find methods that are not in the discovery document' do it 'should find methods that are in the discovery document' do
@client.discovered_method('buzz.bogus').should == nil # TODO(bobaman) Fix this when the RPC names are correct
end @client.discovered_method(
'chili.activities.list', 'buzz'
).name.should == 'list'
end
it 'should generate requests against the correct URIs' do it 'should not find methods that are not in the discovery document' do
# TODO(bobaman) Fix this when the RPC names are correct @client.discovered_method('buzz.bogus', 'buzz').should == nil
request = @client.generate_request( end
'chili.activities.list',
{'userId' => 'hikingfan', 'scope' => '@public'},
'',
[],
{:signed => false}
)
method, uri, headers, body = request
uri.should ==
'https://www.googleapis.com/buzz/v1/activities/hikingfan/@public'
end
it 'should correctly validate parameters' do it 'should fail for string RPC names that do not match API name' do
# TODO(bobaman) Fix this when the RPC names are correct (lambda do
(lambda do @client.generate_request(
@client.generate_request( 'chili.activities.list',
'chili.activities.list', {'alt' => 'json'},
{'alt' => 'json'}, '',
[],
{:signed => false}
)
end).should raise_error(Google::APIClient::TransmissionError)
end
it 'should generate requests against the correct URIs' do
request = @client.generate_request(
@buzz.activities.list,
{'userId' => 'hikingfan', 'scope' => '@public'},
'', '',
[], [],
{:signed => false} {:signed => false}
) )
end).should raise_error(ArgumentError) method, uri, headers, body = request
end uri.should ==
'https://www.googleapis.com/buzz/v1/activities/hikingfan/@public'
end
it 'should correctly validate parameters' do it 'should correctly validate parameters' do
# TODO(bobaman) Fix this when the RPC names are correct (lambda do
(lambda do @client.generate_request(
@client.generate_request( @buzz.activities.list,
'chili.activities.list', {'alt' => 'json'},
{'userId' => 'hikingfan', 'scope' => '@bogus'}, '',
[],
{:signed => false}
)
end).should raise_error(ArgumentError)
end
it 'should correctly validate parameters' do
(lambda do
@client.generate_request(
@buzz.activities.list,
{'userId' => 'hikingfan', 'scope' => '@bogus'},
'',
[],
{:signed => false}
)
end).should raise_error(ArgumentError)
end
it 'should be able to execute requests without authorization' do
response = @client.execute(
@buzz.activities.list,
{'alt' => 'json', 'userId' => 'hikingfan', 'scope' => '@public'},
'', '',
[], [],
{:signed => false} {:signed => false}
) )
end).should raise_error(ArgumentError) status, headers, body = response
status.should == 200
end
end end
it 'should be able to execute requests without authorization' do describe 'with the latitude API' do
# TODO(bobaman) Fix this when the RPC names are correct before do
response = @client.execute( @client.authorization = nil
'chili.activities.list', @latitude = @client.discovered_api('latitude')
{'alt' => 'json', 'userId' => 'hikingfan', 'scope' => '@public'}, end
'',
[], it 'should correctly determine the discovery URI' do
{:signed => false} @client.discovery_uri('latitude').should ===
) 'https://www.googleapis.com/discovery/v0.3/describe/latitude/v1'
status, headers, body = response end
status.should == 200
end it 'should find APIs that are in the discovery document' do
end @client.discovered_api('latitude').name.should == 'latitude'
@client.discovered_api('latitude').version.should == 'v1'
describe Google::APIClient, 'configured for the latitude API' do end
before do
@client = Google::APIClient.new(:service => 'latitude') it 'should find methods that are in the discovery document' do
end @client.discovered_method(
'latitude.currentLocation.get', 'latitude'
it 'should correctly determine the discovery URI' do ).name.should == 'get'
@client.discovery_uri.should === end
'http://www.googleapis.com/discovery/0.1/describe?api=latitude'
end it 'should not find methods that are not in the discovery document' do
@client.discovered_method('latitude.bogus', 'latitude').should == nil
it 'should find APIs that are in the discovery document' do end
@client.discovered_service('latitude').name.should == 'latitude'
@client.discovered_service('latitude').version.should == 'v1' it 'should generate requests against the correct URIs' do
end request = @client.generate_request(
'latitude.currentLocation.get',
it 'should not find APIs that are not in the discovery document' do {},
@client.discovered_service('bogus').should == nil '',
end [],
{:signed => false}
it 'should find methods that are in the discovery document' do )
@client.discovered_method('latitude.currentLocation.get').name.should == method, uri, headers, body = request
'get' uri.should ==
end 'https://www.googleapis.com/latitude/v1/currentLocation'
end
it 'should not find methods that are not in the discovery document' do
@client.discovered_method('latitude.bogus').should == nil it 'should generate requests against the correct URIs' do
end request = @client.generate_request(
@latitude.current_location.get,
it 'should generate requests against the correct URIs' do {},
request = @client.generate_request( '',
'latitude.currentLocation.get', [],
{}, {:signed => false}
'', )
[], method, uri, headers, body = request
{:signed => false} uri.should ==
) 'https://www.googleapis.com/latitude/v1/currentLocation'
method, uri, headers, body = request end
uri.should ==
'https://www.googleapis.com/latitude/v1/currentLocation' it 'should not be able to execute requests without authorization' do
end response = @client.execute(
'latitude.currentLocation.get',
it 'should not be able to execute requests without authorization' do {},
response = @client.execute( '',
'latitude.currentLocation.get', [],
{}, {:signed => false}
'', )
[], status, headers, body = response
{:signed => false} status.should == 401
) end
status, headers, body = response end
status.should == 401
end describe 'with the moderator API' do
end before do
@client.authorization = nil
describe Google::APIClient, 'configured for the moderator API' do @moderator = @client.discovered_api('moderator')
before do end
@client = Google::APIClient.new(:service => 'moderator')
end it 'should correctly determine the discovery URI' do
@client.discovery_uri('moderator').should ===
it 'should correctly determine the discovery URI' do 'https://www.googleapis.com/discovery/v0.3/describe/moderator/v1'
@client.discovery_uri.should === end
'http://www.googleapis.com/discovery/0.1/describe?api=moderator'
end it 'should find APIs that are in the discovery document' do
@client.discovered_api('moderator').name.should == 'moderator'
it 'should find APIs that are in the discovery document' do @client.discovered_api('moderator').version.should == 'v1'
@client.discovered_service('moderator').name.should == 'moderator' end
@client.discovered_service('moderator').version.should == 'v1'
end it 'should find methods that are in the discovery document' do
@client.discovered_method(
it 'should not find APIs that are not in the discovery document' do 'moderator.profiles.get', 'moderator'
@client.discovered_service('bogus').should == nil ).name.should == 'get'
end end
it 'should find methods that are in the discovery document' do it 'should not find methods that are not in the discovery document' do
@client.discovered_method('moderator.profiles.get').name.should == @client.discovered_method('moderator.bogus', 'moderator').should == nil
'get' end
end
it 'should generate requests against the correct URIs' do
it 'should not find methods that are not in the discovery document' do request = @client.generate_request(
@client.discovered_method('moderator.bogus').should == nil 'moderator.profiles.get',
end {},
'',
it 'should generate requests against the correct URIs' do [],
request = @client.generate_request( {:signed => false}
'moderator.profiles.get', )
{}, method, uri, headers, body = request
'', uri.should ==
[], 'https://www.googleapis.com/moderator/v1/profiles/@me'
{:signed => false} end
)
method, uri, headers, body = request it 'should generate requests against the correct URIs' do
uri.should == request = @client.generate_request(
'https://www.googleapis.com/moderator/v1/profiles/@me' @moderator.profiles.get,
end {},
'',
it 'should not be able to execute requests without authorization' do [],
response = @client.execute( {:signed => false}
'moderator.profiles.get', )
{}, method, uri, headers, body = request
'', uri.should ==
[], 'https://www.googleapis.com/moderator/v1/profiles/@me'
{:signed => false} end
)
status, headers, body = response it 'should not be able to execute requests without authorization' do
status.should == 401 response = @client.execute(
'moderator.profiles.get',
{},
'',
[],
{:signed => false}
)
status, headers, body = response
status.should == 401
end
end end
end end

View File

@ -33,17 +33,20 @@ shared_examples_for 'configurable user agent' do
@client.user_agent.should == nil @client.user_agent.should == nil
end end
it 'should not allow the user agent to be set to bogus values' do it 'should not allow the user agent to be used with bogus values' do
(lambda do (lambda do
@client.user_agent = 42 @client.user_agent = 42
@client.transmit_request(
['GET', 'http://www.google.com/', [], []]
)
end).should raise_error(TypeError) end).should raise_error(TypeError)
end end
it 'should transmit a User-Agent header when sending requests' do it 'should transmit a User-Agent header when sending requests' do
@client.user_agent = 'Custom User Agent/1.2.3' @client.user_agent = 'Custom User Agent/1.2.3'
request = ['GET', 'http://www.google.com/', [], []] request = ['GET', 'http://www.google.com/', [], []]
adapter = HTTPAdapter::MockAdapter.request_adapter do |request, connection| adapter = HTTPAdapter::MockAdapter.create do |request_ary, connection|
method, uri, headers, body = request method, uri, headers, body = request_ary
headers.should be_any { |k, v| k.downcase == 'user-agent' } headers.should be_any { |k, v| k.downcase == 'user-agent' }
headers.each do |k, v| headers.each do |k, v|
v.should == @client.user_agent if k.downcase == 'user-agent' v.should == @client.user_agent if k.downcase == 'user-agent'
@ -54,7 +57,7 @@ shared_examples_for 'configurable user agent' do
end end
end end
describe Google::APIClient, 'with default configuration' do describe Google::APIClient do
before do before do
@client = Google::APIClient.new @client = Google::APIClient.new
end end
@ -67,52 +70,60 @@ describe Google::APIClient, 'with default configuration' do
@client.parser.should be(Google::APIClient::JSONParser) @client.parser.should be(Google::APIClient::JSONParser)
end end
it 'should not use an authorization mechanism' do it 'should default to OAuth 2' do
@client.authorization.should be_nil Signet::OAuth2::Client.should === @client.authorization
end end
it_should_behave_like 'configurable user agent' it_should_behave_like 'configurable user agent'
end
describe Google::APIClient, 'with default oauth configuration' do describe 'configured for OAuth 1' do
before do before do
@client = Google::APIClient.new(:authorization => :oauth_1) @client.authorization = :oauth_1
end
it 'should make its version number available' do
::Google::APIClient::VERSION::STRING.should be_instance_of(String)
end
it 'should use the default JSON parser' do
@client.parser.should be(Google::APIClient::JSONParser)
end
it 'should use the default OAuth1 client configuration' do
@client.authorization.temporary_credential_uri.to_s.should ==
'https://www.google.com/accounts/OAuthGetRequestToken'
@client.authorization.authorization_uri.to_s.should include(
'https://www.google.com/accounts/OAuthAuthorizeToken'
)
@client.authorization.token_credential_uri.to_s.should ==
'https://www.google.com/accounts/OAuthGetAccessToken'
@client.authorization.client_credential_key.should == 'anonymous'
@client.authorization.client_credential_secret.should == 'anonymous'
end
it_should_behave_like 'configurable user agent'
end
describe Google::APIClient, 'with custom pluggable parser' do
before do
class FakeJsonParser
end end
@client = Google::APIClient.new(:parser => FakeJsonParser.new) it 'should use the default OAuth1 client configuration' do
@client.authorization.temporary_credential_uri.to_s.should ==
'https://www.google.com/accounts/OAuthGetRequestToken'
@client.authorization.authorization_uri.to_s.should include(
'https://www.google.com/accounts/OAuthAuthorizeToken'
)
@client.authorization.token_credential_uri.to_s.should ==
'https://www.google.com/accounts/OAuthGetAccessToken'
@client.authorization.client_credential_key.should == 'anonymous'
@client.authorization.client_credential_secret.should == 'anonymous'
end
it_should_behave_like 'configurable user agent'
end end
it 'should use the custom parser' do describe 'configured for OAuth 2' do
@client.parser.should be_instance_of(FakeJsonParser) before do
@client.authorization = :oauth_2
end
# TODO
it_should_behave_like 'configurable user agent'
end end
it_should_behave_like 'configurable user agent' describe 'with custom pluggable parser' do
before do
class FakeJsonParser
def serialize(value)
return "42"
end
def parse(value)
return 42
end
end
@client.parser = FakeJsonParser.new
end
it 'should use the custom parser' do
@client.parser.should be_instance_of(FakeJsonParser)
end
it_should_behave_like 'configurable user agent'
end
end end

View File

@ -20,10 +20,10 @@ namespace :gem do
s.rdoc_options.concat ['--main', 'README'] s.rdoc_options.concat ['--main', 'README']
# Dependencies used in the main library # Dependencies used in the main library
s.add_runtime_dependency('signet', '>= 0.1.4') s.add_runtime_dependency('signet', '~> 0.2.1')
s.add_runtime_dependency('addressable', '>= 2.2.2') s.add_runtime_dependency('addressable', '~> 2.2.2')
s.add_runtime_dependency('httpadapter', '>= 0.2.0') s.add_runtime_dependency('httpadapter', '~> 1.0.0')
s.add_runtime_dependency('json', '>= 1.1.9') s.add_runtime_dependency('json', '>= 1.5.1')
s.add_runtime_dependency('extlib', '>= 0.9.15') s.add_runtime_dependency('extlib', '>= 0.9.15')
# Dependencies used in the CLI # Dependencies used in the CLI