diff --git a/CHANGELOG b/CHANGELOG index c2e195de1..a0e4d30e3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 diff --git a/README b/README index b7e47e5bd..6e7557f45 100644 --- a/README +++ b/README @@ -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( diff --git a/bin/google-api b/bin/google-api index b7f2f4f24..0af0f6b40 100755 --- a/bin/google-api +++ b/bin/google-api @@ -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 ", String, - "Set the 2-legged OAuth key") do |k| + "--client-id ", String, + "Set the OAuth client id or key") do |k| options[:client_credential_key] = k end opts.on( "--client-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 ", String, - "Perform discovery on service") do |s| - options[:service_name] = s + "--api ", String, + "Perform discovery on API") do |s| + options[:api] = s end opts.on( "--service-version ", String, "Select service version") do |id| - options[:service_version] = id + options[:version] = id end opts.on( "--content-type ", 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__) diff --git a/examples/sinatra/explorer.rb b/examples/sinatra/explorer.rb index c1f03d94d..7ca699853 100644 --- a/examples/sinatra/explorer.rb +++ b/examples/sinatra/explorer.rb @@ -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 diff --git a/lib/google/api_client.rb b/lib/google/api_client.rb index 78eeb351c..07f3a13ab 100644 --- a/lib/google/api_client.rb +++ b/lib/google/api_client.rb @@ -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: + #
    + #
  • :two_legged_oauth_1
  • + #
  • :oauth_1
  • + #
  • :oauth_2
  • + #
+ # @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 + # #serialize and a #parse method may be used. + # If nil, 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 + # #serialize and a #parse method may be used. + # If nil, 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 Google::APIClient::Service 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. # - # Warning: This method should be used with great care. As APIs - # are updated, minor differences between versions may cause + # @note Warning: 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. - # - :service_version — + # - :version — # The service version. Only used if api_method is a # String. Defaults to 'v1'. # - :parser — # The parser for the response. # - :authorization — # The authorization mechanism for the response. Used only if - # :signed is true. - # - :signed — - # true if the request must be signed, false + # :authenticated is true. + # - :authenticated — + # true if the request must be signed or otherwise + # authenticated, false # otherwise. Defaults to true if an authorization # mechanism has been set, false 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. - # - :service_version — + # - :version — # The service version. Only used if api_method is a # String. Defaults to 'v1'. # - :adapter — @@ -365,9 +517,10 @@ module Google # The parser for the response. # - :authorization — # The authorization mechanism for the response. Used only if - # :signed is true. - # - :signed — - # true if the request must be signed, false + # :authenticated is true. + # - :authenticated — + # true if the request must be signed or otherwise + # authenticated, false # otherwise. Defaults to true. # # @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 diff --git a/lib/google/api_client/discovery.rb b/lib/google/api_client/discovery.rb index 192c3e924..88a0a6ef8 100644 --- a/lib/google/api_client/discovery.rb +++ b/lib/google/api_client/discovery.rb @@ -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 <-1 if the service is older than other. - # 0 if the service is the same as other. - # 1 if the service is newer than other. - # nil if the service cannot be compared to - # other. - 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 String 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 <String. 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 diff --git a/lib/google/api_client/errors.rb b/lib/google/api_client/errors.rb new file mode 100644 index 000000000..00d1995e9 --- /dev/null +++ b/lib/google/api_client/errors.rb @@ -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 diff --git a/lib/google/api_client/parsers/json_parser.rb b/lib/google/api_client/parsers/json_parser.rb index 902668e1b..f686ddfd5 100644 --- a/lib/google/api_client/parsers/json_parser.rb +++ b/lib/google/api_client/parsers/json_parser.rb @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. + require 'json' module Google diff --git a/lib/google/api_client/version.rb b/lib/google/api_client/version.rb index 48b01c73a..f42feedd0 100644 --- a/lib/google/api_client/version.rb +++ b/lib/google/api_client/version.rb @@ -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 diff --git a/lib/google/inflection.rb b/lib/google/inflection.rb index 0724e2c15..6b3fa726b 100644 --- a/lib/google/inflection.rb +++ b/lib/google/inflection.rb @@ -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 diff --git a/spec/google/api_client/discovery_spec.rb b/spec/google/api_client/discovery_spec.rb index 25f53ce94..fce06fd49 100644 --- a/spec/google/api_client/discovery_spec.rb +++ b/spec/google/api_client/discovery_spec.rb @@ -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 diff --git a/spec/google/api_client_spec.rb b/spec/google/api_client_spec.rb index e5ee0d7da..30bfa1d10 100644 --- a/spec/google/api_client_spec.rb +++ b/spec/google/api_client_spec.rb @@ -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 diff --git a/tasks/gem.rake b/tasks/gem.rake index efdc382fb..5c4e31ce1 100644 --- a/tasks/gem.rake +++ b/tasks/gem.rake @@ -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