Merge pull request #20 from simplymeasured/feature/make-autorefresh-of-token-optional
[arvados.git] / lib / google / api_client.rb
index ab97de9429c6e52e5c445653ccfd3c827e5a9876..4c9ee03c7a9a7b1657f5f0418f884512b5f3326a 100644 (file)
 # limitations under the License.
 
 
-gem 'faraday', '~> 0.7.0'
 require 'faraday'
 require 'faraday/utils'
 require 'multi_json'
+require 'compat/multi_json'
 require 'stringio'
 
 require 'google/api_client/version'
+require 'google/api_client/logging'
 require 'google/api_client/errors'
 require 'google/api_client/environment'
 require 'google/api_client/discovery'
+require 'google/api_client/request'
 require 'google/api_client/reference'
 require 'google/api_client/result'
-
+require 'google/api_client/media'
+require 'google/api_client/service_account'
+require 'google/api_client/batch'
+require 'google/api_client/railtie' if defined?(Rails)
 
 module Google
-  # TODO(bobaman): Document all this stuff.
-
 
   ##
   # This class manages APIs communication.
   class APIClient
+    include Google::APIClient::Logging
+    
     ##
     # Creates a new Google API client.
     #
@@ -47,6 +52,10 @@ module Google
     #     <li><code>:oauth_1</code></li>
     #     <li><code>:oauth_2</code></li>
     #   </ul>
+    # @option options [Boolean] :auto_refresh_token (true)
+    #   The setting that controls whether or not the api client attempts to
+    #   refresh authorization when a 401 is hit in #execute. If the token does 
+    #   not support it, this option is ignored.
     # @option options [String] :application_name
     #   The name of the application using the client.
     # @option options [String] :application_version
@@ -62,37 +71,43 @@ module Google
     # @option options [String] :discovery_path ("/discovery/v1")
     #   The discovery base path. This rarely needs to be changed.
     def initialize(options={})
+      logger.debug { "#{self.class} - Initializing client with options #{options}" }
+      
       # Normalize key to String to allow indifferent access.
       options = options.inject({}) do |accu, (key, value)|
-        accu[key.to_s] = value
+        accu[key.to_sym] = value
         accu
       end
       # Almost all API usage will have a host of 'www.googleapis.com'.
-      self.host = options["host"] || 'www.googleapis.com'
-      self.port = options["port"] || 443
-      self.discovery_path = options["discovery_path"] || '/discovery/v1'
+      self.host = options[:host] || 'www.googleapis.com'
+      self.port = options[:port] || 443
+      self.discovery_path = options[:discovery_path] || '/discovery/v1'
 
       # Most developers will want to leave this value alone and use the
       # application_name option.
-      application_string = (
-        options["application_name"] ? (
-          "#{options["application_name"]}/" +
-          "#{options["application_version"] || '0.0.0'}"
-        ) : ""
-      )
-      self.user_agent = options["user_agent"] || (
+      if options[:application_name]
+        app_name = options[:application_name]
+        app_version = options[:application_version]
+        application_string = "#{app_name}/#{app_version || '0.0.0'}"
+      else
+        logger.warn { "#{self.class} - Please provide :application_name and :application_version when initializing the client" }
+      end
+      self.user_agent = options[:user_agent] || (
         "#{application_string} " +
-        "google-api-ruby-client/#{VERSION::STRING} " +
+        "google-api-ruby-client/#{Google::APIClient::VERSION::STRING} " +
          ENV::OS_VERSION
       ).strip
       # The writer method understands a few Symbols and will generate useful
       # default authentication mechanisms.
-      self.authorization = options.key?("authorization") ? options["authorization"] : :oauth_2
-      self.key = options["key"]
-      self.user_ip = options["user_ip"]
+      self.authorization =
+        options.key?(:authorization) ? options[:authorization] : :oauth_2
+      self.auto_refresh_token = options.fetch(:auto_refresh_token) { true }
+      self.key = options[:key]
+      self.user_ip = options[:user_ip]
       @discovery_uris = {}
       @discovery_documents = {}
       @discovered_apis = {}
+            
       return self
     end
 
@@ -110,7 +125,6 @@ module Google
     def authorization=(new_authorization)
       case new_authorization
       when :oauth_1, :oauth
-        gem 'signet', '~> 0.3.0'
         require 'signet/oauth_1/client'
         # NOTE: Do not rely on this default value, as it may change
         new_authorization = Signet::OAuth1::Client.new(
@@ -124,7 +138,6 @@ module Google
           :client_credential_secret => 'anonymous'
         )
       when :two_legged_oauth_1, :two_legged_oauth
-        gem 'signet', '~> 0.3.0'
         require 'signet/oauth_1/client'
         # NOTE: Do not rely on this default value, as it may change
         new_authorization = Signet::OAuth1::Client.new(
@@ -133,7 +146,6 @@ module Google
           :two_legged => true
         )
       when :oauth_2
-        gem 'signet', '~> 0.3.0'
         require 'signet/oauth_2/client'
         # NOTE: Do not rely on this default value, as it may change
         new_authorization = Signet::OAuth2::Client.new(
@@ -155,6 +167,13 @@ module Google
       return @authorization
     end
 
+    ##
+    # The setting that controls whether or not the api client attempts to
+    # refresh authorization when a 401 is hit in #execute. 
+    #
+    # @return [Boolean]
+    attr_accessor :auto_refresh_token
+
     ##
     # The application's API key issued by the API console.
     #
@@ -195,31 +214,6 @@ module Google
     #   The base path. Should almost always be '/discovery/v1'.
     attr_accessor :discovery_path
 
-    ##
-    # Resolves a URI template against the client's configured base.
-    #
-    # @param [String, Addressable::URI, Addressable::Template] template
-    #   The template to resolve.
-    # @param [Hash] mapping The mapping that corresponds to the template.
-    # @return [Addressable::URI] The expanded URI.
-    def resolve_uri(template, mapping={})
-      @base_uri ||= Addressable::URI.new(
-        :scheme => 'https',
-        :host => self.host,
-        :port => self.port
-      ).normalize
-      template = if template.kind_of?(Addressable::Template)
-        template.pattern
-      elsif template.respond_to?(:to_str)
-        template.to_str
-      else
-        raise TypeError,
-          "Expected String, Addressable::URI, or Addressable::Template, " +
-          "got #{template.class}."
-      end
-      return Addressable::Template.new(@base_uri + template).expand(mapping)
-    end
-
     ##
     # Returns the URI for the directory document.
     #
@@ -280,7 +274,7 @@ module Google
           "Expected String or StringIO, got #{discovery_document.class}."
       end
       @discovery_documents["#{api}:#{version}"] =
-        MultiJson.decode(discovery_document)
+        MultiJson.load(discovery_document)
     end
 
     ##
@@ -289,27 +283,12 @@ module Google
     # @return [Hash] The parsed JSON from the directory document.
     def directory_document
       return @directory_document ||= (begin
-        request = self.generate_request(
+        response = self.execute!(
           :http_method => :get,
           :uri => self.directory_uri,
           :authenticated => false
         )
-        response = self.transmit(:request => request)
-        if response.status >= 200 && response.status < 300
-          MultiJson.decode(response.body)
-        elsif response.status >= 400
-          case response.status
-          when 400...500
-            exception_type = ClientError
-          when 500...600
-            exception_type = ServerError
-          else
-            exception_type = TransmissionError
-          end
-          url = request.to_env(Faraday.default_connection)[:url]
-          raise exception_type,
-            "Could not retrieve directory document at: #{url}"
-        end
+        response.data
       end)
     end
 
@@ -323,27 +302,12 @@ module Google
       api = api.to_s
       version = version || 'v1'
       return @discovery_documents["#{api}:#{version}"] ||= (begin
-        request = self.generate_request(
+        response = self.execute!(
           :http_method => :get,
           :uri => self.discovery_uri(api, version),
           :authenticated => false
         )
-        response = self.transmit(:request => request)
-        if response.status >= 200 && response.status < 300
-          MultiJson.decode(response.body)
-        elsif response.status >= 400
-          case response.status
-          when 400...500
-            exception_type = ClientError
-          when 500...600
-            exception_type = ServerError
-          else
-            exception_type = TransmissionError
-          end
-          url = request.to_env(Faraday.default_connection)[:url]
-          raise exception_type,
-            "Could not retrieve discovery document at: #{url}"
-        end
+        response.data
       end)
     end
 
@@ -399,7 +363,7 @@ module Google
     # Returns the method object for a given RPC name and service version.
     #
     # @param [String, Symbol] rpc_name The RPC name of the desired method.
-    # @param [String, Symbol] rpc_name The API the method is within.
+    # @param [String, Symbol] api The API the method is within.
     # @param [String] version The desired version of the API.
     #
     # @return [Google::APIClient::Method] The method object.
@@ -445,7 +409,6 @@ module Google
     # an ID token supplied by an untrusted client-side mechanism is valid.
     # Raises an error if the token is invalid or missing.
     def verify_id_token!
-      gem 'jwt', '~> 0.1.4'
       require 'jwt'
       require 'openssl'
       @certificates ||= {}
@@ -475,31 +438,16 @@ module Google
         if check_cached_certs.call()
           return true
         end
-        request = self.generate_request(
+        response = self.execute!(
           :http_method => :get,
           :uri => 'https://www.googleapis.com/oauth2/v1/certs',
           :authenticated => false
         )
-        response = self.transmit(:request => request)
-        if response.status >= 200 && response.status < 300
-          @certificates.merge!(
-            Hash[MultiJson.decode(response.body).map do |key, cert|
-              [key, OpenSSL::X509::Certificate.new(cert)]
-            end]
-          )
-        elsif response.status >= 400
-          case response.status
-          when 400...500
-            exception_type = ClientError
-          when 500...600
-            exception_type = ServerError
-          else
-            exception_type = TransmissionError
-          end
-          url = request.to_env(Faraday.default_connection)[:url]
-          raise exception_type,
-            "Could not retrieve certificates from: #{url}"
-        end
+        @certificates.merge!(
+          Hash[MultiJson.load(response.body).map do |key, cert|
+            [key, OpenSSL::X509::Certificate.new(cert)]
+          end]
+        )
         if check_cached_certs.call()
           return true
         else
@@ -513,7 +461,7 @@ module Google
     ##
     # Generates a request.
     #
-    # @option options [Google::APIClient::Method, String] :api_method
+    # @option options [Google::APIClient::Method] :api_method
     #   The method object or the RPC name of the method being executed.
     # @option options [Hash, Array] :parameters
     #   The parameters to send to the method.
@@ -528,7 +476,7 @@ module Google
     #   `true` if the request must be signed or somehow
     #   authenticated, `false` otherwise.
     #
-    # @return [Faraday::Request] The generated request.
+    # @return [Google::APIClient::Reference] The generated request.
     #
     # @example
     #   request = client.generate_request(
@@ -537,178 +485,93 @@ module Google
     #       {'collection' => 'public', 'userId' => 'me'}
     #   )
     def generate_request(options={})
-      # Note: The merge method on a Hash object will coerce an API Reference
-      # object into a Hash and merge with the default options.
-      options={
-        :version => 'v1',
-        :authorization => self.authorization,
-        :key => self.key,
-        :user_ip => self.user_ip,
-        :connection => Faraday.default_connection
+      options = {
+        :api_client => self
       }.merge(options)
-      # The Reference object is going to need this to do method ID lookups.
-      options[:client] = self
-      # The default value for the :authenticated option depends on whether an
-      # authorization mechanism has been set.
-      if options[:authorization]
-        options = {:authenticated => true}.merge(options)
-      else
-        options = {:authenticated => false}.merge(options)
-      end
-      reference = Google::APIClient::Reference.new(options)
-      request = reference.to_request
-      if options[:authenticated]
-        request = self.generate_authenticated_request(
-          :request => request,
-          :connection => options[:connection]
-        )
-      end
-      return request
+      return Google::APIClient::Request.new(options)
     end
 
     ##
-    # Signs a request using the current authorization mechanism.
+    # Executes a request, wrapping it in a Result object.
     #
-    # @param [Hash] options a customizable set of options
+    # @param [Google::APIClient::Request, Hash, Array] params
+    #   Either a Google::APIClient::Request, a Hash, or an Array.
     #
-    # @return [Faraday::Request] The signed or otherwise authenticated request.
-    def generate_authenticated_request(options={})
-      return authorization.generate_authenticated_request(options)
-    end
-
-    ##
-    # Transmits the request using the current HTTP adapter.
-    #
-    # @option options [Array, Faraday::Request] :request
-    #   The HTTP request to transmit.
-    # @option options [String, Symbol] :method
-    #   The method for the HTTP request.
-    # @option options [String, Addressable::URI] :uri
-    #   The URI for the HTTP request.
-    # @option options [Array, Hash] :headers
-    #   The headers for the HTTP request.
-    # @option options [String] :body
-    #   The body for the HTTP request.
-    # @option options [Faraday::Connection] :connection
-    #   The HTTP connection to use.
-    #
-    # @return [Faraday::Response] The response from the server.
-    def transmit(options={})
-      options[:connection] ||= Faraday.default_connection
-      if options[:request]
-        if options[:request].kind_of?(Array)
-          method, uri, headers, body = options[:request]
-        elsif options[:request].kind_of?(Faraday::Request)
-          unless options[:connection]
-            raise ArgumentError,
-              "Faraday::Request used, requires a connection to be provided."
-          end
-          method = options[:request].method.to_s.downcase.to_sym
-          uri = options[:connection].build_url(
-            options[:request].path, options[:request].params
-          )
-          headers = options[:request].headers || {}
-          body = options[:request].body || ''
-        end
-      else
-        method = options[:method] || :get
-        uri = options[:uri]
-        headers = options[:headers] || []
-        body = options[:body] || ''
-      end
-      headers = headers.to_a if headers.kind_of?(Hash)
-      request_components = {
-        :method => method,
-        :uri => uri,
-        :headers => headers,
-        :body => body
-      }
-      # Verify that we have all pieces required to transmit an HTTP request
-      request_components.each do |(key, value)|
-        unless value
-          raise ArgumentError, "Missing :#{key} parameter."
-        end
-      end
-
-      if self.user_agent != nil
-        # If there's no User-Agent header, set one.
-        unless headers.kind_of?(Enumerable)
-          # We need to use some Enumerable methods, relying on the presence of
-          # the #each method.
-          class << headers
-            include Enumerable
-          end
-        end
-        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
-
-      request = Faraday::Request.create(method.to_s.downcase.to_sym) do |req|
-        req.url(Addressable::URI.parse(uri))
-        req.headers = Faraday::Utils::Headers.new(headers)
-        req.body = body
-      end
-      request_env = request.to_env(options[:connection])
-      response = options[:connection].app.call(request_env)
-      return response
-    end
-
-    ##
-    # Executes a request, wrapping it in a Result object.
+    #   If a Google::APIClient::Request, no other parameters are expected.
     #
-    # @param [Google::APIClient::Method, String] api_method
-    #   The method object or the RPC name of the method being executed.
-    # @param [Hash, Array] parameters
-    #   The parameters to send to the method.
-    # @param [String] body The body of the request.
-    # @param [Hash, Array] headers The HTTP headers for the request.
-    # @option options [String] :version ("v1")
-    #   The service version. Only used if `api_method` is a `String`.
-    # @option options [#generate_authenticated_request] :authorization
-    #   The authorization mechanism for the response. Used only if
-    #   `:authenticated` is `true`.
-    # @option options [TrueClass, FalseClass] :authenticated (true)
-    #   `true` if the request must be signed or somehow
-    #   authenticated, `false` otherwise.
+    #   If a Hash, the below parameters are handled. If an Array, the
+    #   parameters are assumed to be in the below order:
+    #
+    #   - (Google::APIClient::Method) api_method:
+    #     The method object or the RPC name of the method being executed.
+    #   - (Hash, Array) parameters:
+    #     The parameters to send to the method.
+    #   - (String) body: The body of the request.
+    #   - (Hash, Array) headers: The HTTP headers for the request.
+    #   - (Hash) options: A set of options for the request, of which:
+    #     - (#generate_authenticated_request) :authorization (default: true) -
+    #       The authorization mechanism for the response. Used only if
+    #       `:authenticated` is `true`.
+    #     - (TrueClass, FalseClass) :authenticated (default: true) -
+    #       `true` if the request must be signed or somehow
+    #       authenticated, `false` otherwise.
     #
-    # @return [Google::APIClient::Result] The result from the API.
+    # @return [Google::APIClient::Result] The result from the API, nil if batch.
     #
     # @example
+    #   result = client.execute(batch_request)
+    #
+    # @example
+    #   plus = client.discovered_api('plus')
     #   result = client.execute(
-    #     :api_method => 'plus.activities.list',
+    #     :api_method => plus.activities.list,
     #     :parameters => {'collection' => 'public', 'userId' => 'me'}
     #   )
     #
     # @see Google::APIClient#generate_request
     def execute(*params)
-      # This block of code allows us to accept multiple parameter passing
-      # styles, and maintaining some backwards compatibility.
-      #
-      # Note: I'm extremely tempted to deprecate this style of execute call.
-      if params.last.respond_to?(:to_hash) && params.size == 1
-        options = params.pop
-      else
+      if params.last.kind_of?(Google::APIClient::Request) &&
+          params.size == 1
+        request = params.pop
         options = {}
+      else
+        # This block of code allows us to accept multiple parameter passing
+        # styles, and maintaining some backwards compatibility.
+        #
+        # Note: I'm extremely tempted to deprecate this style of execute call.
+        if params.last.respond_to?(:to_hash) && params.size == 1
+          options = params.pop
+        else
+          options = {}
+        end
+
+        options[:api_method] = params.shift if params.size > 0
+        options[:parameters] = params.shift if params.size > 0
+        options[:body] = params.shift if params.size > 0
+        options[:headers] = params.shift if params.size > 0
+        options.update(params.shift) if params.size > 0
+        request = self.generate_request(options)
       end
-      options[:api_method] = params.shift if params.size > 0
-      options[:parameters] = params.shift if params.size > 0
-      options[:body] = params.shift if params.size > 0
-      options[:headers] = params.shift if params.size > 0
-      options[:client] = self
-
-      reference = Google::APIClient::Reference.new(options)
-      request = self.generate_request(reference)
-      response = self.transmit(
-        :request => request,
-        :connection => options[:connection]
-      )
-      return Google::APIClient::Result.new(reference, request, response)
+      
+      request.headers['User-Agent'] ||= '' + self.user_agent unless self.user_agent.nil?
+      request.parameters['key'] ||= self.key unless self.key.nil?
+      request.parameters['userIp'] ||= self.user_ip unless self.user_ip.nil?
+
+      connection = options[:connection] || Faraday.default_connection
+      request.authorization = options[:authorization] || self.authorization unless options[:authenticated] == false
+
+      result = request.send(connection)
+      if result.status == 401 && authorization.respond_to?(:refresh_token) && auto_refresh_token
+        begin
+          logger.debug("Attempting refresh of access token & retry of request")
+          authorization.fetch_access_token!
+          result = request.send(connection)
+        rescue Signet::AuthorizationError
+           # Ignore since we want the original error
+        end
+      end
+      
+      return result
     end
 
     ##
@@ -718,30 +581,52 @@ module Google
     # @see Google::APIClient#execute
     def execute!(*params)
       result = self.execute(*params)
-      if result.data.respond_to?(:error) &&
-          result.data.error.respond_to?(:message)
-        # You're going to get a terrible error message if the response isn't
-        # parsed successfully as an error.
-        error_message = result.data.error.message
-      elsif result.data['error'] && result.data['error']['message']
-        error_message = result.data['error']['message']
-      end
-      if result.response.status >= 400
+      if result.error?
+        error_message = result.error_message
         case result.response.status
-        when 400...500
-          exception_type = ClientError
-          error_message ||= "A client error has occurred."
-        when 500...600
-          exception_type = ServerError
-          error_message ||= "A server error has occurred."
-        else
-          exception_type = TransmissionError
-          error_message ||= "A transmission error has occurred."
+          when 400...500
+            exception_type = ClientError
+            error_message ||= "A client error has occurred."
+          when 500...600
+            exception_type = ServerError
+            error_message ||= "A server error has occurred."
+          else
+            exception_type = TransmissionError
+            error_message ||= "A transmission error has occurred."
         end
         raise exception_type, error_message
       end
       return result
     end
+    
+    protected
+    
+    ##
+    # Resolves a URI template against the client's configured base.
+    #
+    # @api private
+    # @param [String, Addressable::URI, Addressable::Template] template
+    #   The template to resolve.
+    # @param [Hash] mapping The mapping that corresponds to the template.
+    # @return [Addressable::URI] The expanded URI.
+    def resolve_uri(template, mapping={})
+      @base_uri ||= Addressable::URI.new(
+        :scheme => 'https',
+        :host => self.host,
+        :port => self.port
+      ).normalize
+      template = if template.kind_of?(Addressable::Template)
+        template.pattern
+      elsif template.respond_to?(:to_str)
+        template.to_str
+      else
+        raise TypeError,
+          "Expected String, Addressable::URI, or Addressable::Template, " +
+          "got #{template.class}."
+      end
+      return Addressable::Template.new(@base_uri + template).expand(mapping)
+    end
+    
   end
 end