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
* 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')
# 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
response = client.execute(

View File

@ -26,7 +26,8 @@ module Google
def do_GET(request, response)
$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
# This javascript will auto-close the tab after the
@ -92,24 +93,24 @@ HTML
options[:scope] = s
end
opts.on(
"--client-key <key>", String,
"Set the 2-legged OAuth key") do |k|
"--client-id <key>", String,
"Set the OAuth client id or key") do |k|
options[:client_credential_key] = k
end
opts.on(
"--client-secret <secret>", String,
"Set the 2-legged OAuth secret") do |s|
"Set the OAuth client secret") do |s|
options[:client_credential_secret] = s
end
opts.on(
"-s", "--service <name>", String,
"Perform discovery on service") do |s|
options[:service_name] = s
"--api <name>", String,
"Perform discovery on API") do |s|
options[:api] = s
end
opts.on(
"--service-version <id>", String,
"Select service version") do |id|
options[:service_version] = id
options[:version] = id
end
opts.on(
"--content-type <format>", String,
@ -162,10 +163,11 @@ HTML
opts.separator(
"\nAvailable commands:\n" +
" oauth-login Log a user into an API\n" +
" list List the methods available for a service\n" +
" execute Execute a method on the API\n" +
" irb Start an interactive client session"
" oauth-1-login Log a user into an API with OAuth 1.0a\n" +
" oauth-2-login Log a user into an API with OAuth 2.0 d10\n" +
" list List the methods available for a service\n" +
" execute Execute a method on the API\n" +
" irb Start an interactive client session"
)
end
end
@ -180,23 +182,98 @@ HTML
self.send(symbol)
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 = [
:oauth_login,
:oauth_1_login,
:oauth_2_login,
:list,
:execute,
:irb,
:fuzz
]
def oauth_login
def oauth_1_login
require 'signet/oauth_1/client'
require 'launchy'
require 'yaml'
if options[:client_credential_key] &&
options[:client_credential_secret]
scope = options[:scope]
config = {
"scope" => nil,
"mechanism" => "oauth_1",
"scope" => options[:scope],
"client_credential_key" => options[:client_credential_key],
"client_credential_secret" => options[:client_credential_secret],
"token_credential_key" => nil,
@ -229,19 +306,8 @@ HTML
:client_credential_secret => 'anonymous',
: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 => {
:scope => scope,
:scope => options[:scope],
:xoauth_displayname => 'Google API Client'
})
@ -251,7 +317,7 @@ HTML
server.start
oauth_client.fetch_token_credential!(:verifier => $verifier)
config = {
"scope" => scope,
"scope" => options[:scope],
"client_credential_key" =>
oauth_client.client_credential_key,
"client_credential_secret" =>
@ -267,34 +333,90 @@ HTML
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
service_name = options[:service_name]
unless service_name
api = options[:api]
unless api
STDERR.puts('No service name supplied.')
exit(1)
end
client = Google::APIClient.new(
:service => service_name,
:authorization => nil
)
client = Google::APIClient.new(:authorization => nil)
if options[:discovery_uri]
client.discovery_uri = options[:discovery_uri]
end
service_version =
options[:service_version] ||
client.latest_service_version(service_name).version
service = client.discovered_service(service_name, service_version)
version = api_version(api, options[:version])
service = client.discovered_api(api, version)
rpcnames = service.to_h.keys
puts rpcnames.sort.join("\n")
exit(0)
end
def execute
require 'signet/oauth_1/client'
require 'yaml'
config_file = File.expand_path('~/.google-api.yaml')
signed = File.exist?(config_file)
authorization_type = :oauth_1
client = self.client
# Setup HTTP request data
request_body = ''
@ -308,28 +430,6 @@ HTML
headers << ['Content-Type', 'application/json']
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]
# Make request with URI manually specified
uri = Addressable::URI.parse(options[:uri])
@ -345,13 +445,8 @@ HTML
method = options[:http_method]
method ||= request_body == '' ? 'GET' : 'POST'
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 = client.sign_request(request)
request = client.generate_authenticated_request(:request => request)
response = client.transmit_request(request)
status, headers, body = response
puts body
@ -362,25 +457,14 @@ HTML
STDERR.puts('No rpcname supplied.')
exit(1)
end
service_name =
options[:service_name] || self.rpcname[/^([^\.]+)\./, 1]
client = Google::APIClient.new(
: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)
api = options[:api] || self.rpcname[/^([^\.]+)\./, 1]
version = api_version(api, options[:version])
service = client.discovered_api(api, version)
method = service.to_h[self.rpcname]
if !method
STDERR.puts(
"Method #{self.rpcname} does not exist for " +
"#{service_name}-#{service_version}."
"#{api}-#{version}."
)
exit(1)
end
@ -394,7 +478,7 @@ HTML
end
begin
response = client.execute(
method, parameters, request_body, headers, {:signed => signed}
method, parameters, request_body, headers
)
status, headers, body = response
puts body
@ -407,37 +491,7 @@ HTML
end
def irb
require 'signet/oauth_1/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
$client = self.client
# Otherwise IRB will misinterpret command-line options
ARGV.clear
IRB.start(__FILE__)

View File

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

View File

@ -12,58 +12,125 @@
# See the License for the specific language governing permissions and
# limitations under the License.
require 'httpadapter'
require 'json'
require 'stringio'
require 'google/api_client/errors'
require 'google/api_client/discovery'
module Google
# TODO(bobaman): Document all this stuff.
##
# This class manages communication with a single API.
# This class manages APIs communication.
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
# Creates a new Google API client.
#
# @param [Hash] options The configuration parameters for the client.
# @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={})
@options = {
:user_agent => (
'google-api-ruby-client/' + Google::APIClient::VERSION::STRING
)
}.merge(options)
# Force immediate type-checking and short-cut resolution
self.parser
self.authorization
self.http_adapter
# Normalize key to String to allow indifferent access.
options = options.inject({}) do |accu, (key, value)|
accu[key.to_s] = value
accu
end
# Almost all API usage will have a host of 'www.googleapis.com'.
self.host = options["host"] || 'www.googleapis.com'
# Most developers will want to leave this value alone.
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
end
##
# Returns the parser used by the client.
def parser
unless @options[:parser]
require 'google/api_client/parsers/json_parser'
# NOTE: Do not rely on this default value, as it may change
@options[:parser] = JSONParser
#
# @return [#serialize, #parse]
# 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.
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
return @options[:parser]
@parser = new_parser
end
##
# Returns the authorization mechanism used by the client.
#
# @return [#generate_authenticated_request] The authorization mechanism.
def authorization
case @options[:authorization]
attr_reader :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
require 'signet/oauth_1/client'
# 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 =>
'https://www.google.com/accounts/OAuthGetRequestToken',
:authorization_uri =>
@ -76,79 +143,149 @@ module Google
when :two_legged_oauth_1, :two_legged_oauth
require 'signet/oauth_1/client'
# 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_secret => nil,
: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
# No authorization mechanism
else
if !@options[:authorization].respond_to?(
:generate_authenticated_request)
if !new_authorization.respond_to?(:generate_authenticated_request)
raise TypeError,
'Expected authorization mechanism to respond to ' +
'#generate_authenticated_request.'
end
end
return @options[:authorization]
end
##
# 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
@authorization = new_authorization
return @authorization
end
##
# Returns the HTTP adapter used by the client.
def http_adapter
return @options[:http_adapter] ||= (begin
require 'httpadapter/adapters/net_http'
@options[:http_adapter] = HTTPAdapter::NetHTTPRequestAdapter
end)
#
# @return [HTTPAdapter]
# The HTTP adapter object. The object must include the
# HTTPAdapter module and conform to its interface.
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
##
# 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.
def discovery_uri
return @options[:discovery_uri] ||= (begin
if @options[:service]
service_id = @options[:service]
service_version = @options[:service_version] || 'v1'
Addressable::URI.parse(
"http://www.googleapis.com/discovery/0.1/describe" +
"?api=#{service_id}"
)
else
raise ArgumentError,
'Missing required configuration value, :discovery_uri.'
end
def discovery_uri(api, version=nil)
api = api.to_s
version = version || 'v1'
return @discovery_uris["#{api}:#{version}"] ||= (begin
template = Addressable::Template.new(
"https://{host}/discovery/v0.3/describe/" +
"{api}/{version}"
)
template.expand({
"host" => self.host,
"api" => api,
"version" => version
})
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
# The new discovery URI.
def discovery_uri=(new_discovery_uri)
@options[:discovery_uri] = Addressable::URI.parse(new_discovery_uri)
# @param [String, Symbol] api The service name.
# @param [String] version The desired version of the service.
# @param [String, StringIO] discovery_document
# 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
##
# Returns the parsed discovery document.
# Returns the parsed directory document.
#
# @return [Hash] The parsed JSON from the discovery document.
def discovery_document
return @discovery_document ||= (begin
request = ['GET', self.discovery_uri.to_s, [], []]
# @return [Hash] The parsed JSON from the directory document.
def directory_document
return @directory_document ||= (begin
request_uri = self.directory_uri
request = ['GET', request_uri, [], []]
response = self.transmit_request(request)
status, headers, body = response
if status == 200 # TODO(bobaman) Better status code handling?
@ -160,124 +297,130 @@ module Google
JSON.parse(merged_body.string)
else
raise TransmissionError,
"Could not retrieve discovery document at: #{self.discovery_uri}"
"Could not retrieve discovery document at: #{request_uri}"
end
end)
end
##
# Returns a list of services this client instance has performed discovery
# for. This may return multiple versions of the same service.
# Returns the parsed discovery document.
#
# @return [Array]
# A list of discovered <code>Google::APIClient::Service</code> objects.
def discovered_services
return @discovered_services ||= (begin
service_names = self.discovery_document['data'].keys()
services = []
for service_name in service_names
versions = self.discovery_document['data'][service_name]
for service_version in versions.keys()
service_description =
self.discovery_document['data'][service_name][service_version]
services << ::Google::APIClient::Service.new(
service_name,
service_version,
service_description
# @param [String, Symbol] api The service name.
# @param [String] version The desired version of the service.
# @return [Hash] The parsed JSON from the discovery document.
def discovery_document(api, version=nil)
api = api.to_s
version = version || 'v1'
return @discovery_documents["#{api}:#{version}"] ||= (begin
request_uri = self.discovery_uri(api, version)
request = ['GET', request_uri, [], []]
response = self.transmit_request(request)
status, headers, body = response
if status == 200 # TODO(bobaman) Better status code handling?
merged_body = StringIO.new
body.each do |chunk|
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
else
[]
end
services
end)
end
##
# Returns the service object for a given service name and service version.
#
# @param [String, Symbol] service_name The service name.
# @param [String] service_version The desired version of the service.
# @param [String, Symbol] api The service name.
# @param [String] version The desired version of the service.
#
# @return [Google::APIClient::Service] The service object.
def discovered_service(service_name, service_version='v1')
if !service_name.kind_of?(String) && !service_name.kind_of?(Symbol)
# @return [Google::APIClient::API] The service object.
def discovered_api(api, version=nil)
if !api.kind_of?(String) && !api.kind_of?(Symbol)
raise TypeError,
"Expected String or Symbol, got #{service_name.class}."
"Expected String or Symbol, got #{api.class}."
end
service_name = service_name.to_s
for service in self.discovered_services
if service.name == service_name &&
service.version.to_s == service_version.to_s
return service
api = api.to_s
version = version || 'v1'
return @discovered_apis["#{api}:#{version}"] ||= begin
document_base = self.discovery_uri(api, version)
discovery_document = self.discovery_document(api, version)
if document_base && discovery_document
::Google::APIClient::API.new(
document_base,
discovery_document
)
else
nil
end
end
return nil
end
##
# 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] service_version The desired version of the service.
# @param [String] version The desired version of the service.
#
# @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)
raise TypeError,
"Expected String or Symbol, got #{rpc_name.class}."
end
rpc_name = rpc_name.to_s
for service in self.discovered_services
# This looks kinda weird, but is not a real problem because there's
# almost always only one service, and this is memoized anyhow.
if service.version.to_s == service_version.to_s
return service.to_h[rpc_name] if service.to_h[rpc_name]
end
api = api.to_s
version = version || 'v1'
service = self.discovered_api(api, version)
if service.to_h[rpc_name]
return service.to_h[rpc_name]
else
return nil
end
return nil
end
##
# Returns the service object with the highest version number.
#
# <em>Warning</em>: This method should be used with great care. As APIs
# are updated, minor differences between versions may cause
# @note <em>Warning</em>: This method should be used with great care.
# As APIs are updated, minor differences between versions may cause
# 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.
def latest_service_version(service_name)
if !service_name.kind_of?(String) && !service_name.kind_of?(Symbol)
# @return [Google::APIClient::API] The service object.
def preferred_version(api)
if !api.kind_of?(String) && !api.kind_of?(Symbol)
raise TypeError,
"Expected String or Symbol, got #{service_name.class}."
"Expected String or Symbol, got #{api.class}."
end
service_name = service_name.to_s
return (self.discovered_services.select do |service|
service.name == service_name
end).sort.last
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}."
api = api.to_s
# TODO(bobaman): Update to use directory API.
return self.discovered_apis.detect do |a|
a.name == api && a.preferred == true
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
##
@ -291,16 +434,17 @@ module Google
# @param [Hash, Array] headers The HTTP headers for the request.
# @param [Hash] options
# 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
# <code>String</code>. Defaults to <code>'v1'</code>.
# - <code>:parser</code> —
# The parser for the response.
# - <code>:authorization</code> —
# The authorization mechanism for the response. Used only if
# <code>:signed</code> is <code>true</code>.
# - <code>:signed</code> —
# <code>true</code> if the request must be signed, <code>false</code>
# <code>:authenticated</code> is <code>true</code>.
# - <code>:authenticated</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
# mechanism has been set, <code>false</code> otherwise.
#
@ -316,19 +460,27 @@ module Google
api_method, parameters={}, body='', headers=[], options={})
options={
:parser => self.parser,
:service_version => 'v1',
:version => 'v1',
:authorization => self.authorization
}.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.
if options[:authorization]
options = {:signed => true}.merge(options)
options = {:authenticated => true}.merge(options)
else
options = {:signed => false}.merge(options)
options = {:authenticated => false}.merge(options)
end
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.to_s, options[:service_version]
api_method, api, options[:version]
)
elsif !api_method.kind_of?(::Google::APIClient::Method)
raise TypeError,
@ -339,8 +491,8 @@ module Google
raise ArgumentError, "API method could not be found."
end
request = api_method.generate_request(parameters, body, headers)
if options[:signed]
request = self.sign_request(request, options[:authorization])
if options[:authenticated]
request = self.generate_authenticated_request(:request => request)
end
return request
end
@ -356,7 +508,7 @@ module Google
# @param [Hash, Array] headers The HTTP headers for the request.
# @param [Hash] options
# 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
# <code>String</code>. Defaults to <code>'v1'</code>.
# - <code>:adapter</code> —
@ -365,9 +517,10 @@ module Google
# The parser for the response.
# - <code>:authorization</code> —
# The authorization mechanism for the response. Used only if
# <code>:signed</code> is <code>true</code>.
# - <code>:signed</code> —
# <code>true</code> if the request must be signed, <code>false</code>
# <code>:authenticated</code> is <code>true</code>.
# - <code>:authenticated</code> —
# <code>true</code> if the request must be signed or otherwise
# authenticated, <code>false</code>
# otherwise. Defaults to <code>true</code>.
#
# @return [Array] The response from the API.
@ -406,25 +559,26 @@ module Google
include Enumerable
end
end
unless headers.any? { |k, v| k.downcase == 'user-agent' }
headers = headers.to_a.insert(0, ['User-Agent', self.user_agent])
if self.user_agent.kind_of?(String)
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
::HTTPAdapter.transmit([method, uri, headers, body], adapter)
adapter.transmit([method, uri, headers, body])
end
##
# Signs a request using the current authorization mechanism.
#
# @param [Array] request The request to sign.
# @param [#generate_authenticated_request] authorization
# The authorization mechanism.
# @param [Hash] options The options to pass through.
#
# @return [Array] The signed request.
def sign_request(request, authorization=self.authorization)
return authorization.generate_authenticated_request(
:request => request
)
# @return [Array] The signed or otherwise authenticated request.
def generate_authenticated_request(options={})
return authorization.generate_authenticated_request(options)
end
end
end

View File

@ -12,42 +12,37 @@
# See the License for the specific language governing permissions and
# limitations under the License.
require 'json'
require 'addressable/uri'
require 'addressable/template'
require 'google/inflection'
require 'google/api_client/errors'
module Google
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.
class Service
class API
##
# 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
# matches the first segment of all of the service's RPC names, this
# should not be assumed. There is no requirement that these match.
# @param [String] service_version
# @param [String] 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
# version.
#
# @return [Google::APIClient::Service] The constructed service object.
def initialize(service_name, service_version, service_description)
@name = service_name
@version = service_version
@description = service_description
# @return [Google::APIClient::API] The constructed service object.
def initialize(document_base, discovery_document)
@document_base = Addressable::URI.parse(document_base)
@discovery_document = discovery_document
metaclass = (class <<self; self; end)
self.resources.each do |resource|
method_name = Google::INFLECTOR.underscore(resource.name).to_sym
@ -63,31 +58,67 @@ module Google
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.
#
# @return [String] The service identifier.
attr_reader :name
def name
return @discovery_document['name']
end
##
# Returns the version of the service.
#
# @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
# this version of the service.
#
# @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.
#
# @return [Addressable::URI] The base URI that methods are joined to.
def base
return @base ||= Addressable::URI.parse(self.description['baseUrl'])
attr_reader :document_base
##
# 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
##
@ -95,13 +126,13 @@ module Google
#
# @param [Addressable::URI, #to_str, String] new_base
# The new base URI to use for the service.
def base=(new_base)
@base = Addressable::URI.parse(new_base)
def rest_base=(new_rest_base)
@rest_base = Addressable::URI.parse(new_rest_base)
self.resources.each do |resource|
resource.base = @base
resource.rest_base = @rest_base
end
self.methods.each do |method|
method.base = @base
method.rest_base = @rest_base
end
end
@ -112,8 +143,8 @@ module Google
# @return [Array] A list of {Google::APIClient::Resource} objects.
def resources
return @resources ||= (
(self.description['resources'] || []).inject([]) do |accu, (k, v)|
accu << ::Google::APIClient::Resource.new(self.base, k, v)
(@discovery_document['resources'] || []).inject([]) do |accu, (k, v)|
accu << ::Google::APIClient::Resource.new(self.rest_base, k, v)
accu
end
)
@ -126,8 +157,8 @@ module Google
# @return [Array] A list of {Google::APIClient::Method} objects.
def methods
return @methods ||= (
(self.description['methods'] || []).inject([]) do |accu, (k, v)|
accu << ::Google::APIClient::Method.new(self.base, k, v)
(@discovery_document['methods'] || []).inject([]) do |accu, (k, v)|
accu << ::Google::APIClient::Method.new(self.rest_base, k, v)
accu
end
)
@ -140,12 +171,12 @@ module Google
#
# @example
# # Discover available methods
# method_names = client.discovered_service('buzz').to_h.keys
# method_names = client.discovered_api('buzz').to_h.keys
def to_h
return @hash ||= (begin
methods_hash = {}
self.methods.each do |method|
methods_hash[method.rpc_name] = method
methods_hash[method.rpc_method] = method
end
self.resources.each do |resource|
methods_hash.merge!(resource.to_h)
@ -154,48 +185,6 @@ module Google
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.
#
@ -222,10 +211,10 @@ module Google
# The section of the discovery document that applies to this resource.
#
# @return [Google::APIClient::Resource] The constructed resource object.
def initialize(base, resource_name, resource_description)
@base = base
def initialize(rest_base, resource_name, discovery_document)
@rest_base = rest_base
@name = resource_name
@description = resource_description
@discovery_document = discovery_document
metaclass = (class <<self; self; end)
self.resources.each do |resource|
method_name = Google::INFLECTOR.underscore(resource.name).to_sym
@ -258,20 +247,20 @@ module Google
# Returns the base URI for this resource.
#
# @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.
#
# @param [Addressable::URI, #to_str, String] new_base
# The new base URI to use for the resource.
def base=(new_base)
@base = Addressable::URI.parse(new_base)
def rest_base=(new_rest_base)
@rest_base = Addressable::URI.parse(new_rest_base)
self.resources.each do |resource|
resource.base = @base
resource.rest_base = @rest_base
end
self.methods.each do |method|
method.base = @base
method.rest_base = @rest_base
end
end
@ -281,8 +270,8 @@ module Google
# @return [Array] A list of {Google::APIClient::Resource} objects.
def resources
return @resources ||= (
(self.description['resources'] || []).inject([]) do |accu, (k, v)|
accu << ::Google::APIClient::Resource.new(self.base, k, v)
(@discovery_document['resources'] || []).inject([]) do |accu, (k, v)|
accu << ::Google::APIClient::Resource.new(self.rest_base, k, v)
accu
end
)
@ -294,8 +283,8 @@ module Google
# @return [Array] A list of {Google::APIClient::Method} objects.
def methods
return @methods ||= (
(self.description['methods'] || []).inject([]) do |accu, (k, v)|
accu << ::Google::APIClient::Method.new(self.base, k, v)
(@discovery_document['methods'] || []).inject([]) do |accu, (k, v)|
accu << ::Google::APIClient::Method.new(self.rest_base, k, v)
accu
end
)
@ -310,7 +299,7 @@ module Google
return @hash ||= (begin
methods_hash = {}
self.methods.each do |method|
methods_hash[method.rpc_name] = method
methods_hash[method.rpc_method] = method
end
self.resources.each do |resource|
methods_hash.merge!(resource.to_h)
@ -337,7 +326,7 @@ module Google
##
# Creates a description of a particular method.
#
# @param [Addressable::URI] base
# @param [Addressable::URI] rest_base
# The base URI for the service.
# @param [String] method_name
# The identifier for the method.
@ -345,10 +334,10 @@ module Google
# The section of the discovery document that applies to this method.
#
# @return [Google::APIClient::Method] The constructed method object.
def initialize(base, method_name, method_description)
@base = base
def initialize(rest_base, method_name, discovery_document)
@rest_base = rest_base
@name = method_name
@description = method_description
@discovery_document = discovery_document
end
##
@ -369,15 +358,15 @@ module Google
#
# @return [Addressable::URI]
# The base URI that this method will be joined to.
attr_reader :base
attr_reader :rest_base
##
# Updates the method with the new base.
#
# @param [Addressable::URI, #to_str, String] new_base
# The new base URI to use for the method.
def base=(new_base)
@base = Addressable::URI.parse(new_base)
def rest_base=(new_rest_base)
@rest_base = Addressable::URI.parse(new_rest_base)
@uri_template = nil
end
@ -385,8 +374,8 @@ module Google
# Returns the RPC name for the method.
#
# @return [String] The RPC name.
def rpc_name
return self.description['rpcName']
def rpc_method
return @discovery_document['rpcMethod']
end
##
@ -399,8 +388,9 @@ module Google
# a join operation on a URI, but we have to treat these as Strings
# because of the way the discovery document provides the URIs.
# This should be fixed soon.
return @uri_template ||=
Addressable::Template.new(base.to_s + self.description['pathUrl'])
return @uri_template ||= Addressable::Template.new(
self.rest_base + @discovery_document['restPath']
)
end
##
@ -475,7 +465,7 @@ module Google
if !headers.kind_of?(Array) && !headers.kind_of?(Hash)
raise TypeError, "Expected Hash or Array, got #{headers.class}."
end
method = self.description['httpMethod'] || 'GET'
method = @discovery_document['httpMethod'] || 'GET'
uri = self.generate_uri(parameters)
headers = headers.to_a if headers.kind_of?(Hash)
return [method, uri.to_str, headers, [body]]
@ -488,7 +478,7 @@ module Google
# @return [Hash] The parameter descriptions.
def parameter_descriptions
@parameter_descriptions ||= (
self.description['parameters'] || {}
@discovery_document['parameters'] || {}
).inject({}) { |h,(k,v)| h[k]=v; h }
end
@ -498,7 +488,7 @@ module Google
# @return [Array] The parameters.
def parameters
@parameters ||= ((
self.description['parameters'] || {}
@discovery_document['parameters'] || {}
).inject({}) { |h,(k,v)| h[k]=v; h }).keys
end
@ -552,6 +542,12 @@ module Google
end
parameters.each do |k, v|
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']
if pattern
regexp = Regexp.new("^#{pattern}$")
@ -572,7 +568,8 @@ module Google
# @return [String] The method's state, as a <code>String</code>.
def inspect
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

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
# limitations under the License.
require 'json'
module Google

View File

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

View File

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

View File

@ -21,422 +21,422 @@ require 'google/api_client'
require 'google/api_client/version'
require 'google/api_client/parsers/json_parser'
describe Google::APIClient, 'unconfigured' do
describe Google::APIClient do
before do
@client = Google::APIClient.new
end
it 'should not be able to determine the discovery URI' do
it 'should raise a type error for bogus authorization' do
(lambda do
@client.discovery_uri
end).should raise_error(ArgumentError)
end
end
describe Google::APIClient, 'configured for a bogus API' do
before do
@client = Google::APIClient.new(:service => 'bogus')
Google::APIClient.new(:authorization => 42)
end).should raise_error(TypeError)
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
@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
end
describe Google::APIClient, 'configured for bogus authorization' do
it 'should raise a type error' do
it 'should raise an error for bogus services' do
(lambda do
Google::APIClient.new(:service => 'prediction', :authorization => 42)
@client.discovered_api(42)
end).should raise_error(TypeError)
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
(lambda do
@client.discovered_service(42)
@client.preferred_version(42)
end).should raise_error(TypeError)
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
(lambda do
@client.generate_request(42)
end).should raise_error(TypeError)
end
it 'should raise an error for bogus methods' do
(lambda do
@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')
it 'should not return a preferred version for bogus service names' do
@client.preferred_version('bogus').should == nil
end
it 'should correctly determine the discovery URI' do
@client.discovery_uri.should ===
'http://www.googleapis.com/discovery/0.1/describe?api=buzz'
describe 'with the prediction API' do
before do
@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
it 'should find APIs that are in the discovery document' do
@client.discovered_service('buzz').name.should == 'buzz'
@client.discovered_service('buzz').version.should == 'v1'
end
describe 'with the buzz API' do
before do
@client.authorization = nil
@buzz = @client.discovered_api('buzz')
end
it 'should not find APIs that are not in the discovery document' do
@client.discovered_service('bogus').should == nil
end
it 'should correctly determine the discovery URI' do
@client.discovery_uri('buzz').should ===
'https://www.googleapis.com/discovery/v0.3/describe/buzz/v1'
end
it 'should find methods that are in the discovery document' do
# TODO(bobaman) Fix this when the RPC names are correct
@client.discovered_method('chili.activities.list').name.should == 'list'
end
it 'should find APIs that are in the discovery document' do
@client.discovered_api('buzz').name.should == 'buzz'
@client.discovered_api('buzz').version.should == 'v1'
@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
@client.discovered_method('buzz.bogus').should == nil
end
it 'should find methods that are in the discovery document' do
# TODO(bobaman) Fix this when the RPC names are correct
@client.discovered_method(
'chili.activities.list', 'buzz'
).name.should == 'list'
end
it 'should generate requests against the correct URIs' do
# TODO(bobaman) Fix this when the RPC names are correct
request = @client.generate_request(
'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 not find methods that are not in the discovery document' do
@client.discovered_method('buzz.bogus', 'buzz').should == nil
end
it 'should correctly validate parameters' do
# TODO(bobaman) Fix this when the RPC names are correct
(lambda do
@client.generate_request(
'chili.activities.list',
{'alt' => 'json'},
it 'should fail for string RPC names that do not match API name' do
(lambda do
@client.generate_request(
'chili.activities.list',
{'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}
)
end).should raise_error(ArgumentError)
end
method, uri, headers, body = request
uri.should ==
'https://www.googleapis.com/buzz/v1/activities/hikingfan/@public'
end
it 'should correctly validate parameters' do
# TODO(bobaman) Fix this when the RPC names are correct
(lambda do
@client.generate_request(
'chili.activities.list',
{'userId' => 'hikingfan', 'scope' => '@bogus'},
it 'should correctly validate parameters' do
(lambda do
@client.generate_request(
@buzz.activities.list,
{'alt' => 'json'},
'',
[],
{: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}
)
end).should raise_error(ArgumentError)
status, headers, body = response
status.should == 200
end
end
it 'should be able to execute requests without authorization' do
# TODO(bobaman) Fix this when the RPC names are correct
response = @client.execute(
'chili.activities.list',
{'alt' => 'json', 'userId' => 'hikingfan', 'scope' => '@public'},
'',
[],
{:signed => false}
)
status, headers, body = response
status.should == 200
end
end
describe Google::APIClient, 'configured for the latitude API' do
before do
@client = Google::APIClient.new(:service => 'latitude')
end
it 'should correctly determine the discovery URI' do
@client.discovery_uri.should ===
'http://www.googleapis.com/discovery/0.1/describe?api=latitude'
end
it 'should find APIs that are in the discovery document' do
@client.discovered_service('latitude').name.should == 'latitude'
@client.discovered_service('latitude').version.should == 'v1'
end
it 'should not find APIs that are not in the discovery document' do
@client.discovered_service('bogus').should == nil
end
it 'should find methods that are in the discovery document' do
@client.discovered_method('latitude.currentLocation.get').name.should ==
'get'
end
it 'should not find methods that are not in the discovery document' do
@client.discovered_method('latitude.bogus').should == nil
end
it 'should generate requests against the correct URIs' do
request = @client.generate_request(
'latitude.currentLocation.get',
{},
'',
[],
{:signed => false}
)
method, uri, headers, body = request
uri.should ==
'https://www.googleapis.com/latitude/v1/currentLocation'
end
it 'should not be able to execute requests without authorization' do
response = @client.execute(
'latitude.currentLocation.get',
{},
'',
[],
{:signed => false}
)
status, headers, body = response
status.should == 401
end
end
describe Google::APIClient, 'configured for the moderator API' do
before do
@client = Google::APIClient.new(:service => 'moderator')
end
it 'should correctly determine the discovery URI' do
@client.discovery_uri.should ===
'http://www.googleapis.com/discovery/0.1/describe?api=moderator'
end
it 'should find APIs that are in the discovery document' do
@client.discovered_service('moderator').name.should == 'moderator'
@client.discovered_service('moderator').version.should == 'v1'
end
it 'should not find APIs that are not in the discovery document' do
@client.discovered_service('bogus').should == nil
end
it 'should find methods that are in the discovery document' do
@client.discovered_method('moderator.profiles.get').name.should ==
'get'
end
it 'should not find methods that are not in the discovery document' do
@client.discovered_method('moderator.bogus').should == nil
end
it 'should generate requests against the correct URIs' do
request = @client.generate_request(
'moderator.profiles.get',
{},
'',
[],
{:signed => false}
)
method, uri, headers, body = request
uri.should ==
'https://www.googleapis.com/moderator/v1/profiles/@me'
end
it 'should not be able to execute requests without authorization' do
response = @client.execute(
'moderator.profiles.get',
{},
'',
[],
{:signed => false}
)
status, headers, body = response
status.should == 401
describe 'with the latitude API' do
before do
@client.authorization = nil
@latitude = @client.discovered_api('latitude')
end
it 'should correctly determine the discovery URI' do
@client.discovery_uri('latitude').should ===
'https://www.googleapis.com/discovery/v0.3/describe/latitude/v1'
end
it 'should find APIs that are in the discovery document' do
@client.discovered_api('latitude').name.should == 'latitude'
@client.discovered_api('latitude').version.should == 'v1'
end
it 'should find methods that are in the discovery document' do
@client.discovered_method(
'latitude.currentLocation.get', 'latitude'
).name.should == 'get'
end
it 'should not find methods that are not in the discovery document' do
@client.discovered_method('latitude.bogus', 'latitude').should == nil
end
it 'should generate requests against the correct URIs' do
request = @client.generate_request(
'latitude.currentLocation.get',
{},
'',
[],
{:signed => false}
)
method, uri, headers, body = request
uri.should ==
'https://www.googleapis.com/latitude/v1/currentLocation'
end
it 'should generate requests against the correct URIs' do
request = @client.generate_request(
@latitude.current_location.get,
{},
'',
[],
{:signed => false}
)
method, uri, headers, body = request
uri.should ==
'https://www.googleapis.com/latitude/v1/currentLocation'
end
it 'should not be able to execute requests without authorization' do
response = @client.execute(
'latitude.currentLocation.get',
{},
'',
[],
{:signed => false}
)
status, headers, body = response
status.should == 401
end
end
describe 'with the moderator API' do
before do
@client.authorization = nil
@moderator = @client.discovered_api('moderator')
end
it 'should correctly determine the discovery URI' do
@client.discovery_uri('moderator').should ===
'https://www.googleapis.com/discovery/v0.3/describe/moderator/v1'
end
it 'should find APIs that are in the discovery document' do
@client.discovered_api('moderator').name.should == 'moderator'
@client.discovered_api('moderator').version.should == 'v1'
end
it 'should find methods that are in the discovery document' do
@client.discovered_method(
'moderator.profiles.get', 'moderator'
).name.should == 'get'
end
it 'should not find methods that are not in the discovery document' do
@client.discovered_method('moderator.bogus', 'moderator').should == nil
end
it 'should generate requests against the correct URIs' do
request = @client.generate_request(
'moderator.profiles.get',
{},
'',
[],
{:signed => false}
)
method, uri, headers, body = request
uri.should ==
'https://www.googleapis.com/moderator/v1/profiles/@me'
end
it 'should generate requests against the correct URIs' do
request = @client.generate_request(
@moderator.profiles.get,
{},
'',
[],
{:signed => false}
)
method, uri, headers, body = request
uri.should ==
'https://www.googleapis.com/moderator/v1/profiles/@me'
end
it 'should not be able to execute requests without authorization' do
response = @client.execute(
'moderator.profiles.get',
{},
'',
[],
{:signed => false}
)
status, headers, body = response
status.should == 401
end
end
end

View File

@ -33,17 +33,20 @@ shared_examples_for 'configurable user agent' do
@client.user_agent.should == nil
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
@client.user_agent = 42
@client.transmit_request(
['GET', 'http://www.google.com/', [], []]
)
end).should raise_error(TypeError)
end
it 'should transmit a User-Agent header when sending requests' do
@client.user_agent = 'Custom User Agent/1.2.3'
request = ['GET', 'http://www.google.com/', [], []]
adapter = HTTPAdapter::MockAdapter.request_adapter do |request, connection|
method, uri, headers, body = request
adapter = HTTPAdapter::MockAdapter.create do |request_ary, connection|
method, uri, headers, body = request_ary
headers.should be_any { |k, v| k.downcase == 'user-agent' }
headers.each do |k, v|
v.should == @client.user_agent if k.downcase == 'user-agent'
@ -54,7 +57,7 @@ shared_examples_for 'configurable user agent' do
end
end
describe Google::APIClient, 'with default configuration' do
describe Google::APIClient do
before do
@client = Google::APIClient.new
end
@ -67,52 +70,60 @@ describe Google::APIClient, 'with default configuration' do
@client.parser.should be(Google::APIClient::JSONParser)
end
it 'should not use an authorization mechanism' do
@client.authorization.should be_nil
it 'should default to OAuth 2' do
Signet::OAuth2::Client.should === @client.authorization
end
it_should_behave_like 'configurable user agent'
end
describe Google::APIClient, 'with default oauth configuration' do
before do
@client = Google::APIClient.new(: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
describe 'configured for OAuth 1' do
before do
@client.authorization = :oauth_1
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
it 'should use the custom parser' do
@client.parser.should be_instance_of(FakeJsonParser)
describe 'configured for OAuth 2' do
before do
@client.authorization = :oauth_2
end
# TODO
it_should_behave_like 'configurable user agent'
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

View File

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