A significant update of the client functionality.
authorBob Aman <bobaman@google.com>
Wed, 4 May 2011 11:44:35 +0000 (11:44 +0000)
committerBob Aman <bobaman@google.com>
Wed, 4 May 2011 11:44:35 +0000 (11:44 +0000)
* 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

13 files changed:
CHANGELOG
README
bin/google-api
examples/sinatra/explorer.rb
lib/google/api_client.rb
lib/google/api_client/discovery.rb
lib/google/api_client/errors.rb [new file with mode: 0644]
lib/google/api_client/parsers/json_parser.rb
lib/google/api_client/version.rb
lib/google/inflection.rb
spec/google/api_client/discovery_spec.rb
spec/google/api_client_spec.rb
tasks/gem.rake

index c2e195de13a76215dd42a1884e5a14dceb494ff8..a0e4d30e328bbbccf6ad13effb2a9777be5501d9 100644 (file)
--- 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 b7e47e5bdb9f323d98679533e35114462a7d705d..6e7557f45ef1e3b2dece52652140b98f645961b1 100644 (file)
--- 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(
index b7f2f4f247de423ca8f25765b643196877312c3e..0af0f6b408c4d9cd1040e771aaf84ebb14e5d996 100755 (executable)
@@ -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__)
index c1f03d94dc3c64e5b44d396b731da90afed72efa..7ca699853ce56b3094fd1328cbecee9fd8ea79ec 100644 (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
index 78eeb351c26854db413705cdb7025f9a3ebafc23..07f3a13ab8680caed77d3e0c4c67d363aec3c4c5 100644 (file)
 # 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,177 @@ 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]
+      @authorization = new_authorization
+      return @authorization
     end
 
     ##
-    # Sets the authorization mechanism used by the client.
+    # Returns the HTTP adapter used by the client.
     #
-    # @param [#generate_authenticated_request] new_authorization
-    #   The new authorization mechanism.
-    def authorization=(new_authorization)
-      @options[:authorization] = new_authorization
-      return self.authorization
-    end
+    # @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.
-    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.
+    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 directory document.
+    #
+    # @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?
+          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 the parsed discovery document.
     #
+    # @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
-      return @discovery_document ||= (begin
-        request = ['GET', self.discovery_uri.to_s, [], []]
+    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?
@@ -160,124 +325,102 @@ 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.
-    #
-    # @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
+    # 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
index 192c3e9245e06fda016b89aa6ddd4368d00f63fc..88a0a6ef82ba23db13656e0662786b2ca014126b 100644 (file)
 # 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
diff --git a/lib/google/api_client/errors.rb b/lib/google/api_client/errors.rb
new file mode 100644 (file)
index 0000000..00d1995
--- /dev/null
@@ -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
index 902668e1bca7f430ccfaf860676343e61dae5869..f686ddfd555c82ba2fc89cb0e14e646cd923d202 100644 (file)
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+
 require 'json'
 
 module Google
index 48b01c73af5d5483f06456ac89d03f04bbbb7ccc..f42feedd07aeef9bfebf92782c31c616b4f9acc9 100644 (file)
 # 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
index 0724e2c15d2ee786c16c72a2fa22085b57d4c135..6b3fa726b3be6dc6be6573d368106fb66b439371 100644 (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
index 25f53ce945f3c8b0799d10e4ca9113af17afd7ad..fce06fd49bcc8866340719d4451205853c83338b 100644 (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)
-  end
-end
-
-describe Google::APIClient, 'configured for bogus authorization' do
-  it 'should raise a type error' do
     (lambda do
-      Google::APIClient.new(:service => 'prediction', :authorization => 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
+      @client.discovered_api('bogus')
+    end).should raise_error(Google::APIClient::TransmissionError)
   end
 
   it 'should raise an error for bogus services' do
     (lambda do
-      @client.discovered_service(42)
+      @client.discovered_api(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)
+      @client.preferred_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')
-  end
-
-  it 'should correctly determine the discovery URI' do
-    @client.discovery_uri.should ===
-      'http://www.googleapis.com/discovery/0.1/describe?api=buzz'
-  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
-
-  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
-    # TODO(bobaman) Fix this when the RPC names are correct
-    @client.discovered_method('chili.activities.list').name.should == 'list'
-  end
-
-  it 'should not find methods that are not in the discovery document' do
-    @client.discovered_method('buzz.bogus').should == nil
-  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 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 not return a preferred version for bogus service names' do
+    @client.preferred_version('bogus').should == nil
+  end
+
+  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
+
+  describe 'with the buzz API' do
+    before do
+      @client.authorization = nil
+      @buzz = @client.discovered_api('buzz')
+    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 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 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 not find methods that are not in the discovery document' do
+      @client.discovered_method('buzz.bogus', 'buzz').should == nil
+    end
+
+    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
-
-  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'},
+      method, uri, headers, body = request
+      uri.should ==
+        'https://www.googleapis.com/buzz/v1/activities/hikingfan/@public'
+    end
+
+    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)
-  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
+      status, headers, body = response
+      status.should == 200
+    end
+  end
+
+  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
index e5ee0d7daf8fcfe8a0cca6054521891e18d7cca0..30bfa1d10d831e1642ea947225481bc03037d96e 100644 (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
+  describe 'configured for OAuth 1' do
+    before do
+      @client.authorization = :oauth_1
+    end
 
-  it 'should make its version number available' do
-    ::Google::APIClient::VERSION::STRING.should be_instance_of(String)
-  end
+    it 'should use the default 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 use the default JSON parser' do
-    @client.parser.should be(Google::APIClient::JSONParser)
+    it_should_behave_like 'configurable user agent'
   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'
+  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'
-end
+  describe 'with custom pluggable parser' do
+    before do
+      class FakeJsonParser
+        def serialize(value)
+          return "42"
+        end
 
-describe Google::APIClient, 'with custom pluggable parser' do
-  before do
-    class FakeJsonParser
+        def parse(value)
+          return 42
+        end
+      end
+
+      @client.parser = FakeJsonParser.new
     end
 
-    @client = Google::APIClient.new(:parser => FakeJsonParser.new)
-  end
+    it 'should use the custom parser' do
+      @client.parser.should be_instance_of(FakeJsonParser)
+    end
 
-  it 'should use the custom parser' do
-    @client.parser.should be_instance_of(FakeJsonParser)
+    it_should_behave_like 'configurable user agent'
   end
-
-  it_should_behave_like 'configurable user agent'
 end
index efdc382fbae9ab893d94cb4ebfe7260821f83a1e..5c4e31ce10875e46c470a91662d5333ec0b10338 100644 (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