X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/82d480e924e6a4975b885d3a6c5100b2d4e9bd58..fea9f00597a76e5c91f3f06ffa3ddf89032a986e:/lib/google/api_client.rb diff --git a/lib/google/api_client.rb b/lib/google/api_client.rb index 6ea179b0be..511ebf5a5c 100644 --- a/lib/google/api_client.rb +++ b/lib/google/api_client.rb @@ -12,52 +12,97 @@ # 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/environment' require 'google/api_client/discovery' +require 'google/api_client/reference' +require 'google/api_client/result' module Google # TODO(bobaman): Document all this stuff. + ## - # This class manages communication with a single API. + # This class manages APIs communication. class APIClient ## - # An error which is raised when there is an unexpected response or other - # transport error that prevents an operation from succeeding. - class TransmissionError < StandardError - end - + # Creates a new Google API client. + # + # @param [Hash] options The configuration parameters for the client. + # @option options [Symbol, #generate_authenticated_request] :authorization + # (:oauth_1) + # The authorization mechanism used by the client. The following + # mechanisms are supported out-of-the-box: + #
:two_legged_oauth_1
:oauth_1
:oauth_2
Google::APIClient::Service
objects.
- def discovered_services
- return @discovered_services ||= (begin
- service_names = self.discovery_document['data'].keys()
- services = []
- for service_name in service_names
- versions = self.discovery_document['data'][service_name]
- for service_version in versions.keys()
- service_description =
- self.discovery_document['data'][service_name][service_version]
- services << ::Google::APIClient::Service.new(
- service_name,
- service_version,
- service_description
+ # Returns all APIs published in the directory document.
+ #
+ # @return [Array] The list of available APIs.
+ def discovered_apis
+ @directory_apis ||= (begin
+ document_base = self.directory_uri
+ if self.directory_document && self.directory_document['items']
+ self.directory_document['items'].map do |discovery_document|
+ Google::APIClient::API.new(
+ document_base,
+ discovery_document
)
end
+ else
+ []
end
- services
end)
end
##
# Returns the service object for a given service name and service version.
#
- # @param [String, Symbol] service_name The service name.
- # @param [String] service_version The desired version of the service.
+ # @param [String, Symbol] api The service name.
+ # @param [String] version The desired version of the service.
#
- # @return [Google::APIClient::Service] The service object.
- def discovered_service(service_name, service_version='v1')
- if !service_name.kind_of?(String) && !service_name.kind_of?(Symbol)
+ # @return [Google::APIClient::API] The service object.
+ def discovered_api(api, version=nil)
+ if !api.kind_of?(String) && !api.kind_of?(Symbol)
raise TypeError,
- "Expected String or Symbol, got #{service_name.class}."
+ "Expected String or Symbol, got #{api.class}."
end
- service_name = service_name.to_s
- for service in self.discovered_services
- if service.name == service_name &&
- service.version.to_s == service_version.to_s
- return service
+ api = api.to_s
+ version = version || 'v1'
+ return @discovered_apis["#{api}:#{version}"] ||= begin
+ document_base = self.discovery_uri(api, version)
+ discovery_document = self.discovery_document(api, version)
+ if document_base && discovery_document
+ Google::APIClient::API.new(
+ document_base,
+ discovery_document
+ )
+ else
+ nil
end
end
- return nil
end
##
# Returns the method object for a given RPC name and service version.
#
# @param [String, Symbol] rpc_name The RPC name of the desired method.
- # @param [String] service_version The desired version of the service.
+ # @param [String] version The desired version of the service.
#
# @return [Google::APIClient::Method] The method object.
- def discovered_method(rpc_name, service_version='v1')
+ def discovered_method(rpc_name, api, version=nil)
if !rpc_name.kind_of?(String) && !rpc_name.kind_of?(Symbol)
raise TypeError,
"Expected String or Symbol, got #{rpc_name.class}."
end
rpc_name = rpc_name.to_s
- for service in self.discovered_services
- # This looks kinda weird, but is not a real problem because there's
- # almost always only one service, and this is memoized anyhow.
- if service.version.to_s == service_version.to_s
- return service.to_h[rpc_name] if service.to_h[rpc_name]
- end
+ api = api.to_s
+ version = version || 'v1'
+ service = self.discovered_api(api, version)
+ if service.to_h[rpc_name]
+ return service.to_h[rpc_name]
+ else
+ return nil
end
- return nil
end
##
# Returns the service object with the highest version number.
#
- # Warning: This method should be used with great care. As APIs
- # are updated, minor differences between versions may cause
+ # @note Warning: This method should be used with great care.
+ # As APIs are updated, minor differences between versions may cause
# incompatibilities. Requesting a specific version will avoid this issue.
#
- # @param [String, Symbol] service_name The name of the service.
+ # @param [String, Symbol] api The name of the service.
#
- # @return [Google::APIClient::Service] The service object.
- def latest_service(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
- versions = {}
- for service in self.discovered_services
- next if service.name != service_name
- sortable_version = service.version.gsub(/^v/, '').split('.').map do |v|
- v.to_i
- end
- versions[sortable_version] = service
+ 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
- return versions[versions.keys.sort.last]
end
##
@@ -232,48 +414,93 @@ module Google
# @param [Hash, Array] headers The HTTP headers for the request.
# @param [Hash] options
# The configuration parameters for the request.
- # - :service_version
â
+ # - :version
â
# The service version. Only used if api_method
is a
# String
. Defaults to 'v1'
.
- # - :parser
â
- # The parser for the response.
# - :authorization
â
# The authorization mechanism for the response. Used only if
- # :signed
is true
.
- # - :signed
â
- # true
if the request must be signed, false
- # otherwise. Defaults to true
.
+ # :authenticated
is true
.
+ # - :authenticated
â
+ # true
if the request must be signed or otherwise
+ # authenticated, false
+ # otherwise. Defaults to true
if an authorization
+ # mechanism has been set, false
otherwise.
#
# @return [Array] The generated request.
- def generate_request(
- api_method, parameters={}, body='', headers=[], options={})
+ #
+ # @example
+ # request = client.generate_request(
+ # :api_method => 'chili.activities.list',
+ # :parameters =>
+ # {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
+ # )
+ # method, uri, headers, body = request
+ 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={
- :signed => true,
- :parser => self.parser,
- :service_version => 'v1',
+ :version => 'v1',
:authorization => self.authorization
}.merge(options)
- if api_method.kind_of?(String) || api_method.kind_of?(Symbol)
- api_method = self.discovered_method(
- api_method.to_s, options[:service_version]
- )
- elsif !api_method.kind_of?(::Google::APIClient::Service)
- raise TypeError,
- "Expected String, Symbol, or Google::APIClient::Service, " +
- "got #{api_method.class}."
- end
- unless api_method
- raise ArgumentError, "API method does not exist."
+ # 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
- request = api_method.generate_request(parameters, body, headers)
- if options[:signed]
- request = self.sign_request(request, options[:authorization])
+ reference = Google::APIClient::Reference.new(options)
+ request = reference.to_request
+ if options[:authenticated]
+ request = self.generate_authenticated_request(:request => request)
end
return request
end
##
- # Generates a request and transmits it.
+ # Signs a request using the current authorization mechanism.
+ #
+ # @param [Hash] options The options to pass through.
+ #
+ # @return [Array] 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.
+ #
+ # @param [Array] request The request to transmit.
+ # @param [#transmit] adapter The HTTP adapter.
+ #
+ # @return [Array] The response from the server.
+ 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
+ unless headers.kind_of?(Enumerable)
+ # We need to use some Enumerable methods, relying on the presence of
+ # the #each method.
+ class <:version
â
# The service version. Only used if api_method
is a
# String
. Defaults to 'v1'
.
# - :adapter
â
# The HTTP adapter.
- # - :parser
â
- # The parser for the response.
# - :authorization
â
# The authorization mechanism for the response. Used only if
- # :signed
is true
.
- # - :signed
â
- # true
if the request must be signed, false
+ # :authenticated
is true
.
+ # - :authenticated
â
+ # true
if the request must be signed or otherwise
+ # authenticated, false
# otherwise. Defaults to true
.
#
# @return [Array] The response from the API.
- def execute(api_method, parameters={}, body='', headers=[], options={})
- request = self.generate_request(
- api_method, parameters, body, headers, options
- )
- return self.transmit_request(
+ #
+ # @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
##
- # Transmits the request using the current HTTP adapter.
- #
- # @param [Array] request The request to transmit.
- # @param [#transmit] adapter The HTTP adapter.
- #
- # @return [Array] The response from the server.
- def transmit_request(request, adapter=self.http_adapter)
- ::HTTPAdapter.transmit(request, adapter)
- end
-
- ##
- # Signs a request using the current authorization mechanism.
- #
- # @param [Array] request The request to sign.
- # @param [#generate_authenticated_request] authorization
- # The authorization mechanism.
+ # Same as Google::APIClient#execute, but raises an exception if there was
+ # an error.
#
- # @return [Array] The signed request.
- def sign_request(request, authorization=self.authorization)
- return authorization.generate_authenticated_request(
- :request => request
- )
+ # @see Google::APIClient#execute
+ def execute!(*params)
+ result = self.execute(*params)
+ status, _, _ = result.response
+ if result.data.respond_to?(:error)
+ # You're going to get a terrible error message if the response isn't
+ # parsed successfully as an error.
+ error_message = result.data.error
+ 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