Fixed some YARD documentation issues and changed how the user-agent is built.
[arvados.git] / lib / google / api_client.rb
index 07f3a13ab8680caed77d3e0c4c67d363aec3c4c5..f01a8d93ec619b56718ea972a3f9712df4f1fd4d 100644 (file)
@@ -17,8 +17,13 @@ require 'httpadapter'
 require 'json'
 require 'stringio'
 
+require 'google/api_client/version'
 require 'google/api_client/errors'
+require 'google/api_client/environment'
 require 'google/api_client/discovery'
+require 'google/api_client/reference'
+require 'google/api_client/result'
+
 
 module Google
   # TODO(bobaman): Document all this stuff.
@@ -42,10 +47,14 @@ module Google
     #   </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}")
+    # @option options [String] :application_name
+    #   The name of the application using the client.
+    # @option options [String] :application_version
+    #   The version number of the application using the client.
+    # @option options [String] :user_agent
+    #   ("{app_name} google-api-ruby-client/{version} {os_name}/{os_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.
+    #   leave this value alone and use the `:application_name` option instead.
     def initialize(options={})
       # Normalize key to String to allow indifferent access.
       options = options.inject({}) do |accu, (key, value)|
@@ -54,23 +63,24 @@ module Google
       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
+      # 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'}"
+        ) : ""
       )
-      # 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
+      self.user_agent = options["user_agent"] || (
+        "#{application_string} " +
+        "google-api-ruby-client/#{VERSION::STRING} " +
+         ENV::OS_VERSION
+      ).strip
       # The writer method understands a few Symbols and will generate useful
       # default authentication mechanisms.
       self.authorization = options["authorization"] || :oauth_2
+      self.key = options["key"]
+      self.user_ip = options["user_ip"]
       # 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.
@@ -87,33 +97,6 @@ module Google
       return self
     end
 
-
-    ##
-    # Returns the parser used by the client.
-    #
-    # @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
-      @parser = new_parser
-    end
-
     ##
     # Returns the authorization mechanism used by the client.
     #
@@ -170,6 +153,18 @@ module Google
       return @authorization
     end
 
+    ##
+    # The application's API key issued by the API console.
+    #
+    # @return [String] The API key.
+    attr_accessor :key
+
+    ##
+    # The IP address of the user this request is being performed on behalf of.
+    #
+    # @return [String] The user's IP address.
+    attr_accessor :user_ip
+
     ##
     # Returns the HTTP adapter used by the client.
     #
@@ -212,11 +207,9 @@ module Google
     # @return [Addressable::URI] The URI of the directory document.
     def directory_uri
       template = Addressable::Template.new(
-        "https://{host}/discovery/v0.3/directory"
+        "https://{host}/discovery/v1/apis"
       )
-      return template.expand({
-        "host" => self.host
-      })
+      return template.expand({"host" => self.host})
     end
 
     ##
@@ -243,8 +236,8 @@ module Google
       version = version || 'v1'
       return @discovery_uris["#{api}:#{version}"] ||= (begin
         template = Addressable::Template.new(
-          "https://{host}/discovery/v0.3/describe/" +
-          "{api}/{version}"
+          "https://{host}/discovery/v1/apis/" +
+          "{api}/{version}/rest"
         )
         template.expand({
           "host" => self.host,
@@ -275,7 +268,7 @@ module Google
           "Expected String or StringIO, got #{discovery_document.class}."
       end
       @discovery_documents["#{api}:#{version}"] =
-        JSON.parse(discovery_document)
+        ::JSON.parse(discovery_document)
     end
 
     ##
@@ -284,18 +277,30 @@ module Google
     # @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)
+        request = self.generate_request(
+          :http_method => 'GET',
+          :uri => self.directory_uri,
+          :authenticated => false
+        )
+        response = self.transmit(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)
+        if status >= 200 && status < 300
+          # TODO(bobaman) Better status code handling?
+          merged_body = body.inject(StringIO.new) do |accu, chunk|
+            accu.write(chunk)
+            accu
           end
-          merged_body.rewind
-          JSON.parse(merged_body.string)
-        else
+          ::JSON.parse(merged_body.string)
+        elsif status >= 400 && status < 500
+          _, request_uri, _, _ = request
+          raise ClientError,
+            "Could not retrieve discovery document at: #{request_uri}"
+        elsif status >= 500 && status < 600
+          _, request_uri, _, _ = request
+          raise ServerError,
+            "Could not retrieve discovery document at: #{request_uri}"
+        elsif status > 600
+          _, request_uri, _, _ = request
           raise TransmissionError,
             "Could not retrieve discovery document at: #{request_uri}"
         end
@@ -312,18 +317,30 @@ module Google
       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)
+        request = self.generate_request(
+          :http_method => 'GET',
+          :uri => self.discovery_uri(api, version),
+          :authenticated => false
+        )
+        response = self.transmit(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)
+        if status >= 200 && status < 300
+          # TODO(bobaman) Better status code handling?
+          merged_body = body.inject(StringIO.new) do |accu, chunk|
+            accu.write(chunk)
+            accu
           end
-          merged_body.rewind
-          JSON.parse(merged_body.string)
-        else
+          ::JSON.parse(merged_body.string)
+        elsif status >= 400 && status < 500
+          _, request_uri, _, _ = request
+          raise ClientError,
+            "Could not retrieve discovery document at: #{request_uri}"
+        elsif status >= 500 && status < 600
+          _, request_uri, _, _ = request
+          raise ServerError,
+            "Could not retrieve discovery document at: #{request_uri}"
+        elsif status > 600
+          _, request_uri, _, _ = request
           raise TransmissionError,
             "Could not retrieve discovery document at: #{request_uri}"
         end
@@ -339,7 +356,7 @@ module Google
         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(
+            Google::APIClient::API.new(
               document_base,
               discovery_document
             )
@@ -368,7 +385,7 @@ module Google
         document_base = self.discovery_uri(api, version)
         discovery_document = self.discovery_document(api, version)
         if document_base && discovery_document
-          ::Google::APIClient::API.new(
+          Google::APIClient::API.new(
             document_base,
             discovery_document
           )
@@ -437,8 +454,6 @@ module Google
     #   - <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>:authenticated</code> is <code>true</code>.
@@ -452,17 +467,22 @@ module Google
     #
     # @example
     #   request = client.generate_request(
-    #     'chili.activities.list',
-    #     {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
+    #     :api_method => 'chili.activities.list',
+    #     :parameters =>
+    #       {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
     #   )
     #   method, uri, headers, body = request
-    def generate_request(
-        api_method, parameters={}, body='', headers=[], options={})
+    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={
-        :parser => self.parser,
         :version => 'v1',
-        :authorization => self.authorization
+        :authorization => self.authorization,
+        :key => self.key,
+        :user_ip => self.user_ip
       }.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]
@@ -470,27 +490,8 @@ module Google
       else
         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, api, options[:version]
-        )
-      elsif !api_method.kind_of?(::Google::APIClient::Method)
-        raise TypeError,
-          "Expected String, Symbol, or Google::APIClient::Method, " +
-          "got #{api_method.class}."
-      end
-      unless api_method
-        raise ArgumentError, "API method could not be found."
-      end
-      request = api_method.generate_request(parameters, body, headers)
+      reference = Google::APIClient::Reference.new(options)
+      request = reference.to_request
       if options[:authenticated]
         request = self.generate_authenticated_request(:request => request)
       end
@@ -498,47 +499,13 @@ module Google
     end
 
     ##
-    # Generates a request and transmits it.
-    #
-    # @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.
-    # @param [Hash] options
-    #   The configuration parameters for the request.
-    #   - <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> — 
-    #     The HTTP adapter.
-    #   - <code>:parser</code> — 
-    #     The parser for the response.
-    #   - <code>:authorization</code> — 
-    #     The authorization mechanism for the response.  Used only if
-    #     <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>.
+    # Signs a request using the current authorization mechanism.
     #
-    # @return [Array] The response from the API.
+    # @param [Hash] options The options to pass through.
     #
-    # @example
-    #   response = client.execute(
-    #     'chili.activities.list',
-    #     {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
-    #   )
-    #   status, headers, body = response
-    def execute(api_method, parameters={}, body='', headers=[], options={})
-      request = self.generate_request(
-        api_method, parameters, body, headers, options
-      )
-      return self.transmit_request(
-        request,
-        options[:adapter] || self.http_adapter
-      )
+    # @return [Array] The signed or otherwise authenticated request.
+    def generate_authenticated_request(options={})
+      return authorization.generate_authenticated_request(options)
     end
 
     ##
@@ -548,7 +515,7 @@ module Google
     # @param [#transmit] adapter The HTTP adapter.
     #
     # @return [Array] The response from the server.
-    def transmit_request(request, adapter=self.http_adapter)
+    def transmit(request, adapter=self.http_adapter)
       if self.user_agent != nil
         # If there's no User-Agent header, set one.
         method, uri, headers, body = request
@@ -572,13 +539,89 @@ module Google
     end
 
     ##
-    # Signs a request using the current authorization mechanism.
+    # Executes a request, wrapping it in a Result object.
     #
-    # @param [Hash] options The options to pass through.
+    # @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.
+    # @param [Hash] options
+    #   The configuration parameters for the request.
+    #   - <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> — 
+    #     The HTTP adapter.
+    #   - <code>:authorization</code> — 
+    #     The authorization mechanism for the response.  Used only if
+    #     <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 signed or otherwise authenticated request.
-    def generate_authenticated_request(options={})
-      return authorization.generate_authenticated_request(options)
+    # @return [Array] The response from the API.
+    #
+    # @example
+    #   request = client.generate_request(
+    #     :api_method => 'chili.activities.list',
+    #     :parameters =>
+    #       {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
+    #   )
+    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
+        options = {}
+      end
+      options[:api_method] = params.shift if params.size > 0
+      options[:parameters] = params.shift if params.size > 0
+      options[:merged_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,
+        options[:adapter] || self.http_adapter
+      )
+      return Google::APIClient::Result.new(reference, request, response)
+    end
+
+    ##
+    # Same as Google::APIClient#execute, but raises an exception if there was
+    # an error.
+    #
+    # @see Google::APIClient#execute
+    def execute!(*params)
+      result = self.execute(*params)
+      status, _, _ = result.response
+      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 status >= 400 && status < 500
+        raise ClientError,
+          error_message || "A client error has occurred."
+      elsif status >= 500 && status < 600
+        raise ServerError,
+          error_message || "A server error has occurred."
+      elsif status > 600
+        raise TransmissionError,
+          error_message || "A transmission error has occurred."
+      end
+      return result
     end
   end
 end