diff --git a/CHANGELOG.md b/CHANGELOG.md index 93887fc37..ec69dfa27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 0.5.0 +* Beta candidate, potential incompatible changes with how requests are processed. All requests + should be made using execute() or execute!() +* Reduce memory utilization when uploading large files +* Simplify internal request processing. + # 0.4.7 * Added the ability to convert client secrets to an authorization object diff --git a/lib/google/api_client.rb b/lib/google/api_client.rb index d7bead685..4a9678d10 100644 --- a/lib/google/api_client.rb +++ b/lib/google/api_client.rb @@ -30,9 +30,6 @@ require 'google/api_client/service_account' require 'google/api_client/batch' module Google - # TODO(bobaman): Document all this stuff. - - ## # This class manages APIs communication. class APIClient @@ -66,23 +63,23 @@ module Google def initialize(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'}" + options[:application_name] ? ( + "#{options[:application_name]}/" + + "#{options[:application_version] || '0.0.0'}" ) : "" ) - self.user_agent = options["user_agent"] || ( + self.user_agent = options[:user_agent] || ( "#{application_string} " + "google-api-ruby-client/#{VERSION::STRING} " + ENV::OS_VERSION @@ -90,9 +87,9 @@ module Google # 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"] + options.key?(:authorization) ? options[:authorization] : :oauth_2 + self.key = options[:key] + self.user_ip = options[:user_ip] @discovery_uris = {} @discovery_documents = {} @discovered_apis = {} @@ -195,31 +192,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. # @@ -292,7 +264,7 @@ module Google response = self.execute!( :http_method => :get, :uri => self.directory_uri, - :authorization => :none + :authenticated => false ) response.data end) @@ -311,7 +283,7 @@ module Google response = self.execute!( :http_method => :get, :uri => self.discovery_uri(api, version), - :authorization => :none + :authenticated => false ) response.data end) @@ -447,7 +419,7 @@ module Google response = self.execute!( :http_method => :get, :uri => 'https://www.googleapis.com/oauth2/v1/certs', - :authorization => :none + :authenticated => false ) @certificates.merge!( Hash[MultiJson.load(response.body).map do |key, cert| @@ -464,31 +436,6 @@ module Google return nil end - def normalize_api_method(options) - method = options[:api_method] - version = options[:version] - if method.kind_of?(Google::APIClient::Method) || method == nil - return method - elsif method.respond_to?(:to_str) || method.kind_of?(Symbol) - # 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. - method = method.to_s - api = method[/^([^.]+)\./, 1] - api_method = self.discovered_method(method, api, version) - if api_method.nil? - raise ArgumentError, "API method could not be found." - end - return api_method - else - raise TypeError, - "Expected Google::APIClient::Method, got #{new_api_method.class}." - end - end - ## # Generates a request. # @@ -516,46 +463,19 @@ 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) - - options[:api_method] = self.normalize_api_method(options) unless options[:api_method].nil? - return Google::APIClient::Reference.new(options) - end - - ## - # Transmits the request using the current HTTP adapter. - # - # @option options [Array, Faraday::Request] :request - # The HTTP request to transmit. - # @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 - request = options[:request] - request['User-Agent'] ||= '' + self.user_agent unless self.user_agent.nil? - request_env = request.to_env(options[:connection]) - response = options[:connection].app.call(request_env) - return response + return Google::APIClient::Request.new(options) end ## # Executes a request, wrapping it in a Result object. # - # @param [Google::APIClient::BatchRequest, Hash, Array] params - # Either a Google::APIClient::BatchRequest, a Hash, or an Array. + # @param [Google::APIClient::Request, Hash, Array] params + # Either a Google::APIClient::Request, a Hash, or an Array. # - # If a Google::APIClient::BatchRequest, no other parameters are expected. + # If a Google::APIClient::Request, no other parameters are expected. # # If a Hash, the below parameters are handled. If an Array, the # parameters are assumed to be in the below order: @@ -592,6 +512,7 @@ module Google 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. @@ -610,13 +531,16 @@ module Google options[:client] = self request = self.generate_request(options) end - response = self.transmit(:request => request.to_http_request, :connection => Faraday.default_connection) - result = request.process_response(response) + + connection = options[:connection] || Faraday.default_connection + request.authorization = options[:authorization] || self.authorization unless options[:authenticated] == false + + result = request.send(connection) + if request.upload_type == 'resumable' upload = result.resumable_upload unless upload.complete? - response = self.transmit(:request => upload.to_http_request, :connection => Faraday.default_connection) - result = upload.process_response(response) + result = upload.send(connection) end end return result @@ -646,6 +570,61 @@ module Google end return result end + + ## + # Ensures API method names specified as strings resolve to + # discovered method instances + def resolve_method(method, version) + version ||= 'v1' + if method.kind_of?(Google::APIClient::Method) || method == nil + return method + elsif method.respond_to?(:to_str) || method.kind_of?(Symbol) + # 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. + method = method.to_s + api = method[/^([^.]+)\./, 1] + api_method = self.discovered_method(method, api, version) + if api_method.nil? + raise ArgumentError, "API method could not be found." + end + return api_method + else + raise TypeError, + "Expected Google::APIClient::Method, got #{method.class}." + end + end + + protected + + ## + # 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 + end end diff --git a/lib/google/api_client/batch.rb b/lib/google/api_client/batch.rb index 94c651efe..789285e51 100644 --- a/lib/google/api_client/batch.rb +++ b/lib/google/api_client/batch.rb @@ -40,8 +40,7 @@ module Google # Creates a new batch request. # # @param [Hash] options - # Set of options for this request, the only important one being - # :connection, which specifies an HTTP connection to use. + # Set of options for this request # @param [Proc] block # Callback for every call's response. Won't be called if a call defined # a callback of its own. @@ -67,7 +66,7 @@ module Google # automatically be generated, avoiding collisions. If duplicate call IDs # are provided, an error will be thrown. # - # @param [Hash, Google::APIClient::Reference] call: the call to be added. + # @param [Hash, Google::APIClient::Request] call: the call to be added. # @param [String] call_id: the ID to be used for this call. Must be unique # @param [Proc] block: callback for this call's response. # @@ -90,7 +89,7 @@ module Google # Processes the HTTP response to the batch request, issuing callbacks. # # @param [Faraday::Response] response: the HTTP response. - def process_response(response) + def process_http_response(response) content_type = find_header('Content-Type', response.headers) boundary = /.*boundary=(.+)/.match(content_type)[1] parts = response.body.split(/--#{Regexp.escape(boundary)}/) @@ -212,21 +211,22 @@ module Google # # @return [StringIO] The request as a string in application/http format. def serialize_call(call_id, call) - http_request = call.to_http_request - body = "#{http_request.method.to_s.upcase} #{http_request.path} HTTP/1.1" - http_request.headers.each do |header, value| - body << "\r\n%s: %s" % [header, value] + call.api_client = self.api_client + method, uri, headers, body = call.to_http_request + request = "#{method.to_s.upcase} #{Addressable::URI.parse(uri).path} HTTP/1.1" + headers.each do |header, value| + request << "\r\n%s: %s" % [header, value] end - if http_request.body + if body # TODO - CompositeIO if body is a stream - body << "\r\n\r\n" - if http_request.body.respond_to?(:read) - body << http_request.body.read + request << "\r\n\r\n" + if body.respond_to?(:read) + request << body.read else - body << http_request.body.to_s + request << body.to_s end end - Faraday::UploadIO.new(StringIO.new(body), 'application/http', 'ruby-api-request', 'Content-ID' => id_to_header(call_id)) + Faraday::UploadIO.new(StringIO.new(request), 'application/http', 'ruby-api-request', 'Content-ID' => id_to_header(call_id)) end ## diff --git a/lib/google/api_client/discovery/method.rb b/lib/google/api_client/discovery/method.rb index 4af5ae7a4..af6013d0b 100644 --- a/lib/google/api_client/discovery/method.rb +++ b/lib/google/api_client/discovery/method.rb @@ -222,21 +222,14 @@ module Google # The HTTP connection to use. # # @return [Array] The generated HTTP request. - def generate_request(parameters={}, body='', headers=[], options={}) - options[:connection] ||= Faraday.default_connection + def generate_request(parameters={}, body='', headers={}, options={}) if !headers.kind_of?(Array) && !headers.kind_of?(Hash) raise TypeError, "Expected Hash or Array, got #{headers.class}." end - method = self.http_method + method = self.http_method.to_s.downcase.to_sym uri = self.generate_uri(parameters) - headers = headers.to_a if headers.kind_of?(Hash) - return options[:connection].build_request( - method.to_s.downcase.to_sym - ) do |req| - req.url(Addressable::URI.parse(uri).to_s) - req.headers = Faraday::Utils::Headers.new(headers) - req.body = body - end + headers = Faraday::Utils::Headers.new(headers) + return [method, uri, headers, body] end diff --git a/lib/google/api_client/media.rb b/lib/google/api_client/media.rb index 9237d5108..826637948 100644 --- a/lib/google/api_client/media.rb +++ b/lib/google/api_client/media.rb @@ -131,7 +131,7 @@ module Google # # @param [Faraday::Response] r # Result of a chunk upload or range query - def process_response(response) + def process_http_response(response) case response.status when 200...299 @complete = true diff --git a/lib/google/api_client/reference.rb b/lib/google/api_client/reference.rb index 77ad20866..a30a7db04 100644 --- a/lib/google/api_client/reference.rb +++ b/lib/google/api_client/reference.rb @@ -26,23 +26,23 @@ module Google class Request MULTIPART_BOUNDARY = "-----------RubyApiMultipartPost".freeze - attr_reader :connection, :parameters, :api_method, :headers - attr_accessor :media, :authorization, :body + attr_reader :parameters, :headers + attr_accessor :api_client, :connection, :api_method, :version ,:media, :authorization, :authenticated, :body def initialize(options={}) - - self.connection = options[:connection] || Faraday.default_connection - self.authorization = options[:authorization] - self.api_method = options[:api_method] - @parameters = Hash[options[:parameters] || {}] + @headers = Faraday::Utils::Headers.new + self.api_client = options[:api_client] + self.headers.merge!(options[:headers]) unless options[:headers].nil? + self.api_method = options[:api_method] + self.version = options[:version] + self.authenticated = options[:authenticated] + self.authorization = options[:authorization] + # These parameters are handled differently because they're not # parameters to the API method, but rather to the API system. self.parameters['key'] ||= options[:key] if options[:key] self.parameters['userIp'] ||= options[:user_ip] if options[:user_ip] - - @headers = Faraday::Utils::Headers.new - self.headers.merge!(options[:headers]) if options[:headers] if options[:media] self.initialize_media_upload(options) @@ -58,12 +58,102 @@ module Google unless self.api_method self.http_method = options[:http_method] || 'GET' self.uri = options[:uri] - unless self.parameters.empty? - self.uri.query = Addressable::URI.form_encode(self.parameters) - end end end + def upload_type + return self.parameters['uploadType'] || self.parameters['upload_type'] + end + + def http_method + return @http_method ||= self.api_method.http_method.to_s.downcase.to_sym + end + + def http_method=(new_http_method) + if new_http_method.kind_of?(Symbol) + @http_method = new_http_method.to_s.downcase.to_sym + elsif new_http_method.respond_to?(:to_str) + @http_method = new_http_method.to_s.downcase.to_sym + else + raise TypeError, + "Expected String or Symbol, got #{new_http_method.class}." + end + end + + def uri + return @uri ||= self.api_method.generate_uri(self.parameters) + end + + def uri=(new_uri) + @uri = Addressable::URI.parse(new_uri) + @parameters.update(@uri.query_values) unless @uri.query_values.nil? + end + + def send(connection) + response = connection.app.call(self.to_env(connection)) + self.process_http_response(response) + end + + def to_http_request + if self.api_client + self.headers['User-Agent'] ||= '' + self.api_client.user_agent unless self.api_client.user_agent.nil? + self.parameters['key'] ||= self.api_client.key unless self.api_client.key.nil? + self.parameters['userIp'] ||= self.api_client.user_ip unless self.api_client.user_ip.nil? + self.api_method = self.api_client.resolve_method(self.api_method, self.version) unless self.api_method.nil? + end + request = ( + if self.uri + unless self.parameters.empty? + self.uri.query = Addressable::URI.form_encode(self.parameters) + end + [self.http_method, self.uri.to_s, self.headers, self.body] + else + self.api_method.generate_request(self.parameters, self.body, self.headers) + end) + end + + def to_hash + options = {} + if self.api_method + options[:api_method] = self.api_method + options[:parameters] = self.parameters + else + options[:http_method] = self.http_method + options[:uri] = self.uri + end + options[:headers] = self.headers + options[:body] = self.body + options[:media] = self.media + unless self.authorization.nil? + options[:authorization] = self.authorization + end + return options + end + + def to_env(connection) + method, uri, headers, body = self.to_http_request + http_request = connection.build_request(method) do |req| + req.url(uri) + req.headers.update(headers) + req.body = body + end + + if self.authorization.respond_to?(:generate_authenticated_request) + http_request = self.authorization.generate_authenticated_request( + :request => http_request, + :connection => connection + ) + end + + request_env = http_request.to_env(connection) + end + + def process_http_response(response) + Result.new(self, response) + end + + protected + def initialize_media_upload(options) self.media = options[:media] case self.upload_type @@ -109,97 +199,6 @@ module Google 'Must respond to :to_json or :to_hash.' end - def upload_type - return self.parameters['uploadType'] || self.parameters['upload_type'] - end - - def connection=(new_connection) - if new_connection.kind_of?(Faraday::Connection) - @connection = new_connection - else - raise TypeError, - "Expected Faraday::Connection, got #{new_connection.class}." - end - end - - def api_method=(new_api_method) - if new_api_method.kind_of?(Google::APIClient::Method) || - new_api_method == nil - @api_method = new_api_method - else - raise TypeError, - "Expected Google::APIClient::Method, got #{new_api_method.class}." - end - end - - def http_method - return @http_method ||= self.api_method.http_method.to_s.downcase.to_sym - end - - def http_method=(new_http_method) - if new_http_method.kind_of?(Symbol) - @http_method = new_http_method.to_s.downcase.to_sym - elsif new_http_method.respond_to?(:to_str) - @http_method = new_http_method.to_s.downcase.to_sym - else - raise TypeError, - "Expected String or Symbol, got #{new_http_method.class}." - end - end - - def uri - return @uri ||= self.api_method.generate_uri(self.parameters) - end - - def uri=(new_uri) - @uri = Addressable::URI.parse(new_uri) - end - - def to_http_request - request = ( - if self.uri - self.connection.build_request(self.http_method) do |req| - req.url(self.uri.to_str) - req.headers.update(self.headers) - req.body = self.body - end - else - self.api_method.generate_request( - self.parameters, self.body, self.headers, :connection => self.connection - ) - end) - - if self.authorization.respond_to?(:generate_authenticated_request) - request = self.authorization.generate_authenticated_request( - :request => request, - :connection => self.connection - ) - end - return request - end - - def to_hash - options = {} - if self.api_method - options[:api_method] = self.api_method - options[:parameters] = self.parameters - else - options[:http_method] = self.http_method - options[:uri] = self.uri - end - options[:headers] = self.headers - options[:body] = self.body - options[:connection] = self.connection - options[:media] = self.media - unless self.authorization.nil? - options[:authorization] = self.authorization - end - return options - end - - def process_response(response) - Result.new(self, response) - end end class Reference < Request diff --git a/spec/google/api_client/batch_spec.rb b/spec/google/api_client/batch_spec.rb index db7f0ac2e..762e3b291 100644 --- a/spec/google/api_client/batch_spec.rb +++ b/spec/google/api_client/batch_spec.rb @@ -226,7 +226,7 @@ describe Google::APIClient::BatchRequest do it 'should convert to a correct HTTP request' do batch = Google::APIClient::BatchRequest.new { |result| } batch.add(@call1, '1').add(@call2, '2') - request = batch.to_http_request.to_env(Faraday.default_connection) + request = batch.to_env(Faraday.default_connection) boundary = Google::APIClient::BatchRequest::BATCH_BOUNDARY request[:method].to_s.downcase.should == 'post' request[:url].to_s.should == 'https://www.googleapis.com/batch' diff --git a/spec/google/api_client/discovery_spec.rb b/spec/google/api_client/discovery_spec.rb index 9f14b69c9..c37e06b86 100644 --- a/spec/google/api_client/discovery_spec.rb +++ b/spec/google/api_client/discovery_spec.rb @@ -25,7 +25,24 @@ require 'signet/oauth_1/client' require 'google/api_client' require 'google/api_client/version' +def TestHandler + def initialize(&block) + @block = block + end + + def call(env) + @block.call(env) + end +end + +def mock_connection(&block) + connection = Faraday.new do |builder| + use TestHandler block + end +end + describe Google::APIClient do + include ConnectionHelpers CLIENT = Google::APIClient.new unless defined?(CLIENT) after do @@ -63,7 +80,7 @@ describe Google::APIClient do it 'should raise an error for bogus methods' do (lambda do - CLIENT.generate_request(42) + CLIENT.execute(42) end).should raise_error(TypeError) end @@ -86,44 +103,49 @@ describe Google::APIClient do it 'should correctly determine the discovery URI if :user_ip is set' do CLIENT.user_ip = '127.0.0.1' - request = CLIENT.generate_request( + + conn = stub_connection do |stub| + stub.get('/discovery/v1/apis/prediction/v1.2/rest?userIp=127.0.0.1') do |env| + end + end + CLIENT.execute( :http_method => 'GET', :uri => CLIENT.discovery_uri('prediction', 'v1.2'), - :authenticated => false - ) - request.to_http_request.to_env(Faraday.default_connection)[:url].to_s.should === ( - 'https://www.googleapis.com/discovery/v1/apis/prediction/v1.2/rest' + - '?userIp=127.0.0.1' + :authenticated => false, + :connection => conn ) + conn.verify end it 'should correctly determine the discovery URI if :key is set' do CLIENT.key = 'qwerty' - request = CLIENT.generate_request( + conn = stub_connection do |stub| + stub.get('/discovery/v1/apis/prediction/v1.2/rest?key=qwerty') do |env| + end + end + request = CLIENT.execute( :http_method => 'GET', :uri => CLIENT.discovery_uri('prediction', 'v1.2'), - :authenticated => false - ) - request.to_http_request.to_env(Faraday.default_connection)[:url].to_s.should === ( - 'https://www.googleapis.com/discovery/v1/apis/prediction/v1.2/rest' + - '?key=qwerty' - ) + :authenticated => false, + :connection => conn + ) + conn.verify end it 'should correctly determine the discovery URI if both are set' do CLIENT.key = 'qwerty' CLIENT.user_ip = '127.0.0.1' - request = CLIENT.generate_request( + conn = stub_connection do |stub| + stub.get('/discovery/v1/apis/prediction/v1.2/rest?key=qwerty&userIp=127.0.0.1') do |env| + end + end + request = CLIENT.execute( :http_method => 'GET', :uri => CLIENT.discovery_uri('prediction', 'v1.2'), - :authenticated => false - ) - Addressable::URI.parse( - request.to_http_request.to_env(Faraday.default_connection)[:url] - ).query_values.should == { - 'key' => 'qwerty', - 'userIp' => '127.0.0.1' - } + :authenticated => false, + :connection => conn + ) + conn.verify end it 'should correctly generate API objects' do @@ -165,7 +187,7 @@ describe Google::APIClient do it 'should raise an error for bogus methods' do (lambda do - CLIENT.generate_request(CLIENT.discovered_api('prediction', 'v1.2')) + CLIENT.execute(:api_method => CLIENT.discovered_api('prediction', 'v1.2')) end).should raise_error(TypeError) end @@ -179,44 +201,58 @@ describe Google::APIClient do end it 'should generate valid requests' do - request = CLIENT.generate_request( + conn = stub_connection do |stub| + stub.post('/prediction/v1.2/training?data=12345') do |env| + env[:body].should == '' + end + end + request = CLIENT.execute( :api_method => @prediction.training.insert, - :parameters => {'data' => '12345'} + :parameters => {'data' => '12345'}, + :connection => conn ) - request.to_http_request.method.should == :post - request.to_http_request.to_env(Faraday.default_connection)[:url].to_s.should === - 'https://www.googleapis.com/prediction/v1.2/training?data=12345' - request.headers.should be_empty - request.body.should == '' + conn.verify end it 'should generate valid requests when repeated parameters are passed' do pending("This is caused by Faraday's encoding of query parameters.") - request = CLIENT.generate_request( + conn = stub_connection do |stub| + stub.post('/prediction/v1.2/training?data=1&data=2') do |env| + env[:params]['data'].should include('1', '2') + end + end + request = CLIENT.execute( :api_method => @prediction.training.insert, - :parameters => [['data', '1'], ['data','2']] + :parameters => [['data', '1'], ['data','2']], + :connection => conn ) - request.to_http_request.method.should == :post - request.to_http_request.to_env(Faraday.default_connection)[:url].to_s.should === - 'https://www.googleapis.com/prediction/v1.2/training?data=1&data=2' + conn.verify end it 'should generate requests against the correct URIs' do - request = CLIENT.generate_request( + conn = stub_connection do |stub| + stub.post('/prediction/v1.2/training?data=12345') do |env| + end + end + request = CLIENT.execute( :api_method => @prediction.training.insert, - :parameters => {'data' => '12345'} + :parameters => {'data' => '12345'}, + :connection => conn ) - request.to_http_request.to_env(Faraday.default_connection)[:url].to_s.should === - 'https://www.googleapis.com/prediction/v1.2/training?data=12345' + conn.verify end it 'should generate requests against the correct URIs' do - request = CLIENT.generate_request( + conn = stub_connection do |stub| + stub.post('/prediction/v1.2/training?data=12345') do |env| + end + end + request = CLIENT.execute( :api_method => @prediction.training.insert, - :parameters => {'data' => '12345'} + :parameters => {'data' => '12345'}, + :connection => conn ) - request.to_http_request.to_env(Faraday.default_connection)[:url].to_s.should === - 'https://www.googleapis.com/prediction/v1.2/training?data=12345' + conn.verify end it 'should allow modification to the base URIs for testing purposes' do @@ -225,39 +261,58 @@ describe Google::APIClient do Google::APIClient.new.discovered_api('prediction', 'v1.2') prediction_rebase.method_base = 'https://testing-domain.example.com/prediction/v1.2/' - request = CLIENT.generate_request( + + conn = stub_connection do |stub| + stub.post('/prediction/v1.2/training') do |env| + env[:url].host.should == 'testing-domain.example.com' + end + end + + request = CLIENT.execute( :api_method => prediction_rebase.training.insert, - :parameters => {'data' => '123'} - ) - request.to_http_request.to_env(Faraday.default_connection)[:url].to_s.should === ( - 'https://testing-domain.example.com/' + - 'prediction/v1.2/training?data=123' + :parameters => {'data' => '123'}, + :connection => conn ) + conn.verify 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( + + conn = stub_connection do |stub| + stub.post('/prediction/v1.2/training?data=12345') do |env| + env[:request_headers].should have_key('Authorization') + env[:request_headers]['Authorization'].should =~ /^OAuth/ + end + end + + request = CLIENT.execute( :api_method => @prediction.training.insert, - :parameters => {'data' => '12345'} + :parameters => {'data' => '12345'}, + :connection => conn ) - http_request = request.to_http_request - http_request.headers.should have_key('Authorization') - http_request.headers['Authorization'].should =~ /^OAuth/ + conn.verify end it 'should generate OAuth 2 requests' do CLIENT.authorization = :oauth_2 CLIENT.authorization.access_token = '12345' - request = CLIENT.generate_request( + + conn = stub_connection do |stub| + stub.post('/prediction/v1.2/training?data=12345') do |env| + env[:request_headers].should have_key('Authorization') + env[:request_headers]['Authorization'].should =~ /^Bearer/ + end + end + + request = CLIENT.execute( :api_method => @prediction.training.insert, - :parameters => {'data' => '12345'} + :parameters => {'data' => '12345'}, + :connection => conn ) - http_request = request.to_http_request - http_request.headers.should have_key('Authorization') - http_request.headers['Authorization'].should =~ /^Bearer/ + conn.verify end it 'should not be able to execute improperly authorized requests' do @@ -305,15 +360,21 @@ describe Google::APIClient do end it 'should correctly handle unnamed parameters' do + conn = stub_connection do |stub| + stub.post('/prediction/v1.2/training') do |env| + env[:request_headers].should have_key('Content-Type') + env[:request_headers]['Content-Type'].should == 'application/json' + end + end CLIENT.authorization = :oauth_2 CLIENT.authorization.access_token = '12345' - result = CLIENT.execute( - @prediction.training.insert, - {}, - MultiJson.dump({"id" => "bucket/object"}), - {'Content-Type' => 'application/json'} + CLIENT.execute( + :api_method => @prediction.training.insert, + :body => MultiJson.dump({"id" => "bucket/object"}), + :headers => {'Content-Type' => 'application/json'}, + :connection => conn ) - result.reference.to_http_request.headers['Content-Type'].should == 'application/json' + conn.verify end end @@ -353,38 +414,42 @@ describe Google::APIClient do end it 'should generate requests against the correct URIs' do - request = CLIENT.generate_request( + conn = stub_connection do |stub| + stub.get('/plus/v1/people/107807692475771887386/activities/public' + + '?collection=public&userId=107807692475771887386') do |env| + end + end + + request = CLIENT.execute( :api_method => @plus.activities.list, :parameters => { 'userId' => '107807692475771887386', 'collection' => 'public' }, - :authenticated => false - ) - request.to_http_request.to_env(Faraday.default_connection)[:url].to_s.should === ( - 'https://www.googleapis.com/plus/v1/' + - 'people/107807692475771887386/activities/public' + :authenticated => false, + :connection => conn ) + conn.verify end it 'should correctly validate parameters' do (lambda do - CLIENT.generate_request( + CLIENT.execute( :api_method => @plus.activities.list, :parameters => {'alt' => 'json'}, :authenticated => false - ).to_http_request + ) end).should raise_error(ArgumentError) end it 'should correctly validate parameters' do (lambda do - CLIENT.generate_request( + CLIENT.execute( :api_method => @plus.activities.list, :parameters => { 'userId' => '107807692475771887386', 'collection' => 'bogus' }, :authenticated => false - ).to_http_request + ).to_env(Faraday.default_connection) end).should raise_error(ArgumentError) end end @@ -431,7 +496,7 @@ describe Google::APIClient do :api_method => 'latitude.currentLocation.get', :authenticated => false ) - request.to_http_request.to_env(Faraday.default_connection)[:url].to_s.should === + request.to_env(Faraday.default_connection)[:url].to_s.should === 'https://www.googleapis.com/latitude/v1/currentLocation' end @@ -440,7 +505,7 @@ describe Google::APIClient do :api_method => @latitude.current_location.get, :authenticated => false ) - request.to_http_request.to_env(Faraday.default_connection)[:url].to_s.should === + request.to_env(Faraday.default_connection)[:url].to_s.should === 'https://www.googleapis.com/latitude/v1/currentLocation' end @@ -491,21 +556,29 @@ describe Google::APIClient do end it 'should generate requests against the correct URIs' do - request = CLIENT.generate_request( + conn = stub_connection do |stub| + stub.get('/moderator/v1/profiles/@me') do |env| + end + end + request = CLIENT.execute( :api_method => 'moderator.profiles.get', - :authenticated => false + :authenticated => false, + :connection => conn ) - request.to_http_request.to_env(Faraday.default_connection)[:url].to_s.should === - 'https://www.googleapis.com/moderator/v1/profiles/@me' + conn.verify end it 'should generate requests against the correct URIs' do - request = CLIENT.generate_request( + conn = stub_connection do |stub| + stub.get('/moderator/v1/profiles/@me') do |env| + end + end + request = CLIENT.execute( :api_method => @moderator.profiles.get, - :authenticated => false + :authenticated => false, + :connection => conn ) - request.to_http_request.to_env(Faraday.default_connection)[:url].to_s.should === - 'https://www.googleapis.com/moderator/v1/profiles/@me' + conn.verify end it 'should not be able to execute requests without authorization' do @@ -551,21 +624,29 @@ describe Google::APIClient do end it 'should generate requests against the correct URIs' do - request = CLIENT.generate_request( + conn = stub_connection do |stub| + stub.get('/adsense/v1/adclients') do |env| + end + end + request = CLIENT.execute( :api_method => 'adsense.adclients.list', - :authenticated => false + :authenticated => false, + :connection => conn ) - request.to_http_request.to_env(Faraday.default_connection)[:url].to_s.should === - 'https://www.googleapis.com/adsense/v1/adclients' + conn.verify end it 'should generate requests against the correct URIs' do - request = CLIENT.generate_request( + conn = stub_connection do |stub| + stub.get('/adsense/v1/adclients') do |env| + end + end + request = CLIENT.execute( :api_method => @adsense.adclients.list, - :authenticated => false + :authenticated => false, + :connection => conn ) - request.to_http_request.to_env(Faraday.default_connection)[:url].to_s.should === - 'https://www.googleapis.com/adsense/v1/adclients' + conn.verify end it 'should not be able to execute requests without authorization' do @@ -578,16 +659,20 @@ describe Google::APIClient do it 'should fail when validating missing required parameters' do (lambda do - CLIENT.generate_request( + CLIENT.execute( :api_method => @adsense.reports.generate, :authenticated => false - ).to_http_request + ) end).should raise_error(ArgumentError) end it 'should succeed when validating parameters in a correct call' do + conn = stub_connection do |stub| + stub.get('/adsense/v1/reports?dimension=DATE&endDate=2010-01-01&metric=PAGE_VIEWS&startDate=2000-01-01') do |env| + end + end (lambda do - CLIENT.generate_request( + CLIENT.execute( :api_method => @adsense.reports.generate, :parameters => { 'startDate' => '2000-01-01', @@ -595,14 +680,16 @@ describe Google::APIClient do 'dimension' => 'DATE', 'metric' => 'PAGE_VIEWS' }, - :authenticated => false - ).to_http_request + :authenticated => false, + :connection => conn + ) end).should_not raise_error + conn.verify end it 'should fail when validating parameters with invalid values' do (lambda do - CLIENT.generate_request( + CLIENT.execute( :api_method => @adsense.reports.generate, :parameters => { 'startDate' => '2000-01-01', @@ -611,13 +698,19 @@ describe Google::APIClient do 'metric' => 'PAGE_VIEWS' }, :authenticated => false - ).to_http_request + ) end).should raise_error(ArgumentError) end it 'should succeed when validating repeated parameters in a correct call' do + conn = stub_connection do |stub| + stub.get('/adsense/v1/reports?dimension%5B%5D=DATE&dimension%5B%5D=PRODUCT_CODE'+ + '&endDate=2010-01-01&metric%5B%5D=CLICKS&metric%5B%5D=PAGE_VIEWS&'+ + 'startDate=2000-01-01') do |env| + end + end (lambda do - CLIENT.generate_request( + CLIENT.execute( :api_method => @adsense.reports.generate, :parameters => { 'startDate' => '2000-01-01', @@ -625,14 +718,16 @@ describe Google::APIClient do 'dimension' => ['DATE', 'PRODUCT_CODE'], 'metric' => ['PAGE_VIEWS', 'CLICKS'] }, - :authenticated => false + :authenticated => false, + :connection => conn ) end).should_not raise_error + conn.verify end it 'should fail when validating incorrect repeated parameters' do (lambda do - CLIENT.generate_request( + CLIENT.execute( :api_method => @adsense.reports.generate, :parameters => { 'startDate' => '2000-01-01', @@ -641,7 +736,7 @@ describe Google::APIClient do 'metric' => ['PAGE_VIEWS', 'CLICKS'] }, :authenticated => false - ).to_http_request + ) end).should raise_error(ArgumentError) end end diff --git a/spec/google/api_client/media_spec.rb b/spec/google/api_client/media_spec.rb index 8fc7fe50b..af1e01d0a 100644 --- a/spec/google/api_client/media_spec.rb +++ b/spec/google/api_client/media_spec.rb @@ -78,49 +78,49 @@ describe Google::APIClient::ResumableUpload do it 'should consider 20x status as complete' do request = @uploader.to_http_request - @uploader.process_response(mock_result(200)) + @uploader.process_http_response(mock_result(200)) @uploader.complete?.should == true end it 'should consider 30x status as incomplete' do request = @uploader.to_http_request - @uploader.process_response(mock_result(308)) + @uploader.process_http_response(mock_result(308)) @uploader.complete?.should == false @uploader.expired?.should == false end it 'should consider 40x status as fatal' do request = @uploader.to_http_request - @uploader.process_response(mock_result(404)) + @uploader.process_http_response(mock_result(404)) @uploader.expired?.should == true end it 'should detect changes to location' do request = @uploader.to_http_request - @uploader.process_response(mock_result(308, 'location' => 'https://www.googleapis.com/upload/drive/v1/files/abcdef')) + @uploader.process_http_response(mock_result(308, 'location' => 'https://www.googleapis.com/upload/drive/v1/files/abcdef')) @uploader.uri.to_s.should == 'https://www.googleapis.com/upload/drive/v1/files/abcdef' end it 'should resume from the saved range reported by the server' do @uploader.chunk_size = 200 - request = @uploader.to_http_request # Send bytes 0-199, only 0-99 saved - @uploader.process_response(mock_result(308, 'range' => '0-99')) - request = @uploader.to_http_request # Send bytes 100-299 - request.headers['Content-Range'].should == "bytes 100-299/#{@media.length}" - request.headers['Content-length'].should == "200" + @uploader.to_http_request # Send bytes 0-199, only 0-99 saved + @uploader.process_http_response(mock_result(308, 'range' => '0-99')) + method, url, headers, body = @uploader.to_http_request # Send bytes 100-299 + headers['Content-Range'].should == "bytes 100-299/#{@media.length}" + headers['Content-length'].should == "200" end it 'should resync the offset after 5xx errors' do @uploader.chunk_size = 200 - request = @uploader.to_http_request - @uploader.process_response(mock_result(500)) # Invalidates range - request = @uploader.to_http_request # Resync - request.headers['Content-Range'].should == "bytes */#{@media.length}" - request.headers['Content-length'].should == "0" - @uploader.process_response(mock_result(308, 'range' => '0-99')) - request = @uploader.to_http_request # Send next chunk at correct range - request.headers['Content-Range'].should == "bytes 100-299/#{@media.length}" - request.headers['Content-length'].should == "200" + @uploader.to_http_request + @uploader.process_http_response(mock_result(500)) # Invalidates range + method, url, headers, body = @uploader.to_http_request # Resync + headers['Content-Range'].should == "bytes */#{@media.length}" + headers['Content-length'].should == "0" + @uploader.process_http_response(mock_result(308, 'range' => '0-99')) + method, url, headers, body = @uploader.to_http_request # Send next chunk at correct range + headers['Content-Range'].should == "bytes 100-299/#{@media.length}" + headers['Content-length'].should == "200" end def mock_result(status, headers = {}) diff --git a/spec/google/api_client/result_spec.rb b/spec/google/api_client/result_spec.rb index 2dc4599e2..eba302348 100644 --- a/spec/google/api_client/result_spec.rb +++ b/spec/google/api_client/result_spec.rb @@ -82,7 +82,7 @@ describe Google::APIClient::Result do reference = @result.next_page Hash[reference.parameters].should include('pageToken') Hash[reference.parameters]['pageToken'].should == 'NEXT+PAGE+TOKEN' - url = reference.to_http_request.to_env(Faraday.default_connection)[:url] + url = reference.to_env(Faraday.default_connection)[:url] url.to_s.should include('pageToken=NEXT%2BPAGE%2BTOKEN') end diff --git a/spec/google/api_client/service_account_spec.rb b/spec/google/api_client/service_account_spec.rb index d106a1a47..f303d6466 100644 --- a/spec/google/api_client/service_account_spec.rb +++ b/spec/google/api_client/service_account_spec.rb @@ -17,6 +17,7 @@ require 'spec_helper' require 'google/api_client' describe Google::APIClient::JWTAsserter do + include ConnectionHelpers before do @key = OpenSSL::PKey::RSA.new 2048 @@ -33,7 +34,7 @@ describe Google::APIClient::JWTAsserter do end it 'should send valid access token request' do - stubs = Faraday::Adapter::Test::Stubs.new do |stub| + conn = stub_connection do |stub| stub.post('/o/oauth2/token') do |env| params = Addressable::URI.form_unencode(env[:body]) JWT.decode(params.assoc("assertion").last, @key.public_key) @@ -45,14 +46,11 @@ describe Google::APIClient::JWTAsserter do }'] end end - connection = Faraday.new(:url => 'https://accounts.google.com') do |builder| - builder.adapter(:test, stubs) - end - asserter = Google::APIClient::JWTAsserter.new('client1', 'scope1 scope2', @key) - auth = asserter.authorize(nil, { :connection => connection}) + auth = asserter.authorize(nil, { :connection => conn }) auth.should_not == nil? auth.access_token.should == "1/abcdef1234567890" + conn.verify end end diff --git a/spec/google/api_client_spec.rb b/spec/google/api_client_spec.rb index ae25518fb..b248b9673 100644 --- a/spec/google/api_client_spec.rb +++ b/spec/google/api_client_spec.rb @@ -21,6 +21,8 @@ require 'google/api_client' require 'google/api_client/version' shared_examples_for 'configurable user agent' do + include ConnectionHelpers + it 'should allow the user agent to be modified' do client.user_agent = 'Custom User Agent/1.2.3' client.user_agent.should == 'Custom User Agent/1.2.3' @@ -34,16 +36,14 @@ shared_examples_for 'configurable user agent' do it 'should not allow the user agent to be used with bogus values' do (lambda do client.user_agent = 42 - client.transmit( - ['GET', 'http://www.google.com/', [], []] - ) + client.execute(:uri=>'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' - stubs = Faraday::Adapter::Test::Stubs.new do |stub| + conn = stub_connection do |stub| stub.get('/') do |env| headers = env[:request_headers] headers.should have_key('User-Agent') @@ -51,18 +51,14 @@ shared_examples_for 'configurable user agent' do [200, {}, ['']] end end - connection = Faraday.new(:url => 'https://www.google.com') do |builder| - builder.adapter(:test, stubs) - end - request = connection.build_request(:get) do |req| - req.url('http://www.google.com/') - end - client.transmit(:request => request, :connection => connection) - stubs.verify_stubbed_calls + client.execute(:uri=>'http://www.google.com/', :connection => conn) + conn.verify end end describe Google::APIClient do + include ConnectionHelpers + let(:client) { Google::APIClient.new } it 'should make its version number available' do @@ -73,11 +69,18 @@ describe Google::APIClient do Signet::OAuth2::Client.should === client.authorization end - it_should_behave_like 'configurable user agent' - + describe 'configure for no authentication' do + before do + client.authorization = nil + end + it_should_behave_like 'configurable user agent' + end + describe 'configured for OAuth 1' do before do client.authorization = :oauth_1 + client.authorization.token_credential_key = 'abc' + client.authorization.token_credential_secret = '123' end it 'should use the default OAuth1 client configuration' do @@ -98,6 +101,7 @@ describe Google::APIClient do describe 'configured for OAuth 2' do before do client.authorization = :oauth_2 + client.authorization.access_token = '12345' end # TODO @@ -107,13 +111,10 @@ describe Google::APIClient do describe 'when executing requests' do before do client.authorization = :oauth_2 - @connection = Faraday.new(:url => 'https://www.googleapis.com') do |builder| - stubs = Faraday::Adapter::Test::Stubs.new do |stub| - stub.get('/test') do |env| - env[:request_headers]['Authorization'].should == 'Bearer 12345' - end + @connection = stub_connection do |stub| + stub.get('/test') do |env| + env[:request_headers]['Authorization'].should == 'Bearer 12345' end - builder.adapter(:test, stubs) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 900caa382..57612f477 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,6 +2,53 @@ $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__)) $LOAD_PATH.uniq! require 'rspec' +require 'faraday' +require 'faraday/adapter/test' + +module Faraday + class Connection + def verify + if app.kind_of?(Faraday::Adapter::Test) + app.stubs.verify_stubbed_calls + else + raise TypeError, "Expected test adapter" + end + end + end +end + +module ConnectionHelpers + def stub_connection(&block) + stubs = Faraday::Adapter::Test::Stubs.new do |stub| + block.call(stub) + end + connection = Faraday.new do |builder| + builder.adapter(:test, stubs) + end + end +end + +module JSONMatchers + class EqualsJson + def initialize(expected) + @expected = JSON.parse(expected) + end + def matches?(target) + @target = JSON.parse(target) + @target.eql?(@expected) + end + def failure_message + "expected #{@target.inspect} to be #{@expected}" + end + def negative_failure_message + "expected #{@target.inspect} not to be #{@expected}" + end + end + + def be_json(expected) + EqualsJson.new(expected) + end +end RSpec.configure do |config| end