# 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. require 'httpadapter' require 'json' require 'google/api_client/discovery' module Google # TODO(bobaman): Document all this stuff. ## # This class manages communication with a single API. 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 def initialize(options={}) @options = { # TODO: What configuration options need to go here? }.merge(options) # Force immediate type-checking and short-cut resolution self.parser self.authorization self.http_adapter 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 end return @options[:parser] end ## # Returns the authorization mechanism used by the client. # # @return [#generate_authenticated_request] The authorization mechanism. def authorization case @options[: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( :temporary_credential_uri => 'https://www.google.com/accounts/OAuthGetRequestToken', :authorization_uri => 'https://www.google.com/accounts/OAuthAuthorizeToken', :token_credential_uri => 'https://www.google.com/accounts/OAuthGetAccessToken', :client_credential_key => 'anonymous', :client_credential_secret => 'anonymous' ) when nil # No authorization mechanism else if !@options[:authorization].respond_to?( :generate_authenticated_request) raise TypeError, 'Expected authorization mechanism to respond to ' + '#generate_authenticated_request.' end end return @options[:authorization] end ## # Sets the authorization mechanism used by the client. # # @param [#generate_authenticated_request] new_authorization # The new authorization mechanism. def authorization=(new_authorization) @options[:authorization] = new_authorization return self.authorization end ## # Returns the HTTP adapter used by the client. def http_adapter return @options[:http_adapter] ||= (begin require 'httpadapter/adapters/net_http' @options[:http_adapter] = HTTPAdapter::NetHTTPRequestAdapter end) end ## # Returns the URI for the discovery document. # # @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 end) end ## # Returns the parsed discovery document. # # @return [Hash] The parsed JSON from the discovery document. def discovery_document return @discovery_document ||= (begin request = ['GET', self.discovery_uri.to_s, [], []] 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: #{self.discovery_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 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 ) end 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. # # @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) raise TypeError, "Expected String or Symbol, got #{service_name.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 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. # # @return [Google::APIClient::Method] The method object. def discovered_method(rpc_name, service_version='v1') 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 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 # incompatibilities. Requesting a specific version will avoid this issue. # # @param [String, Symbol] service_name 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) raise TypeError, "Expected String or Symbol, got #{service_name.class}." end service_name = service_name.to_s return (self.discovered_services.select do |service| service.name == service_name end).sort.last end ## # Generates a request. # # @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. # - :service_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 if an authorization # mechanism has been set, false otherwise. # # @return [Array] The generated request. # # @example # request = client.generate_request( # 'chili.activities.list', # {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'} # ) # method, uri, headers, body = request def generate_request( api_method, parameters={}, body='', headers=[], options={}) options={ :parser => self.parser, :service_version => 'v1', :authorization => self.authorization }.merge(options) # The default value for the :signed option depends on whether an # authorization mechanism has been set. if options[:authorization] options = {:signed => true}.merge(options) else options = {:signed => false}.merge(options) end 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::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) if options[:signed] request = self.sign_request(request, options[:authorization]) end return request 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. # - :service_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 # otherwise. Defaults to true. # # @return [Array] The response from the API. # # @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 ) 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. # # @return [Array] The signed request. def sign_request(request, authorization=self.authorization) return authorization.generate_authenticated_request( :request => request ) end end end require 'google/api_client/version'