Initial implementation of HTTP.

git-svn-id: https://google-api-ruby-client.googlecode.com/svn/trunk@22 c1d61fac-ed7f-fcc1-18f7-ff78120a04ef
This commit is contained in:
bobaman@google.com 2010-08-19 23:21:45 +00:00
parent a9523bca32
commit bc844db311
6 changed files with 395 additions and 81 deletions

View File

@ -12,16 +12,20 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
require 'google/api_client/version'
module Google #:nodoc: module Google #:nodoc:
## ##
# This class manages communication with a single API. # This class manages communication with a single API.
class APIClient class APIClient
def initialize(options={}) def initialize(options={})
@options = { @options = {
# TODO: What configuration options need to go here? # TODO: What configuration options need to go here?
}.merge(options) }.merge(options)
unless @options[:parser]
require 'google/api_client/parser/json_parser'
# NOTE: Do not rely on this default value, as it may change
@options[:parser] = JSONParser.new
end
unless @options[:authentication] unless @options[:authentication]
require 'google/api_client/auth/oauth_1' require 'google/api_client/auth/oauth_1'
# NOTE: Do not rely on this default value, as it may change # NOTE: Do not rely on this default value, as it may change
@ -32,5 +36,13 @@ module Google #:nodoc:
@options[:transport] = HTTPTransport @options[:transport] = HTTPTransport
end end
end end
##
# Returns the parser used by the client.
def parser
return @options[:parser]
end
end end
end end
require 'google/api_client/version'

View File

@ -12,60 +12,173 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
require 'google/api_client/parser/json_parser' require 'net/http'
require 'net/https'
require 'addressable/uri'
module Google #:nodoc: module Google #:nodoc:
class APIClient #:nodoc: class APIClient #:nodoc:
##
# Factory for HTTP backed client requests. ##
# Provides a consistent interface by which to make HTTP requests using the
# Net::HTTP class.
class HTTPTransport class HTTPTransport
ALLOWED_SCHEMES = ["http", "https"]
## METHOD_MAPPING = {
# The default transport configuration values. These may be overridden # RFC 2616
# simply by passing in the same key to the constructor. :options => Net::HTTP::Options,
DEFAULTS = { :get => Net::HTTP::Get,
:parser => :json_parser :head => Net::HTTP::Head,
:post => Net::HTTP::Post,
:put => Net::HTTP::Put,
:delete => Net::HTTP::Delete,
:trace => Net::HTTP::Trace,
# Other standards supported by Net::HTTP
:copy => Net::HTTP::Copy,
:lock => Net::HTTP::Lock,
:mkcol => Net::HTTP::Mkcol,
:move => Net::HTTP::Move,
:propfind => Net::HTTP::Propfind,
:proppatch => Net::HTTP::Proppatch,
:unlock => Net::HTTP::Unlock
} }
## ##
# The default implementations of various parsers. These may be overriden
# simply by passing the same key to the constructor.
PARSERS = {
:json_parser => JSONParser.new
}
##
# Creates a new HTTP request factory.
# #
# @param [Hash] options
# @return [Google::APIClient::Discovery] The HTTP request factory.
def initialize(options={}) def initialize(options={})
@options = DEFAULTS.clone # A mapping from authorities to Net::HTTP objects.
@options.merge!(options) @connection_pool = options[:connection_pool] || {}
if options[:cert_store]
@cert_store = options[:cert_store]
else
@cert_store = OpenSSL::X509::Store.new
@cert_store.set_default_paths
end
end
# first check if user passed a parser then fallback on appropriate default attr_reader :connection_pool
@parser = @options[@options[:parser]] || PARSERS[@options[:parser]] attr_reader :cert_store
unless @parser
raise ArgumentError, def build_request(method, uri, options={})
'Invalid :parser configuration.' # No type-checking here, but OK because we check against a whitelist
method = method.to_s.downcase.to_sym
uri = Addressable::URI.parse(uri).normalize
if !METHOD_MAPPING.keys.include?(method)
raise ArgumentError, "Unsupported HTTP method: #{method}"
end
headers = {
"Accept" => "application/json;q=1.0, */*;q=0.5"
}.merge(options[:headers] || {})
# TODO(bobaman) More stuff here to handle optional parameters like
# form data.
body = options[:body] || ""
if body != ""
entity_body_defaults = {
"Content-Length" => body.size.to_s,
"Content-Type" => "application/json"
}
headers = entity_body_defaults.merge(headers)
end
return [method.to_s.upcase, uri.to_s, headers, [body]]
end
def send_request(request)
retried = false
begin
method, uri, headers, body_wrapper = request
body = ""
body_wrapper.each do |chunk|
body += chunk
end
uri = Addressable::URI.parse(uri).normalize
connection = self.connect_to(uri)
# Translate to Net::HTTP request
request_class = METHOD_MAPPING[method.to_s.downcase.to_sym]
if !request_class
raise ArgumentError,
"Unsupported HTTP method: #{method.to_s.downcase.to_sym}"
end
net_http_request = request_class.new(uri.request_uri)
for key, value in headers
net_http_request[key] = value
end
net_http_request.body = body
response = connection.request(net_http_request)
response_headers = {}
# We want the canonical header name.
# Note that Net::HTTP is lossy in that it downcases header names and
# then capitalizes them afterwards.
# This results in less-than-ideal behavior for headers like 'ETag'.
# Not much we can do about it.
response.canonical_each do |header, value|
response_headers[header] = value
end
# We use the Rack spec to trivially abstract the response format
return [response.code.to_i, response_headers, [response.body]]
rescue Errno::EPIPE, IOError, EOFError => e
# If there's a problem with the connection, finish and restart
if !retried && connection.started?
retried = true
connection.finish
connection.start
retry
else
raise e
end
end end
end end
## ##
# Returns configuration of the transport. # Builds a connection to the authority given in the URI using the
# appropriate protocol.
# #
# @return [Hash] The configuration options. # @param [Addressable::URI, #to_str] uri The URI to connect to.
def options def connect_to(uri)
return @options uri = Addressable::URI.parse(uri).normalize
end if !ALLOWED_SCHEMES.include?(uri.scheme)
raise ArgumentError, "Unsupported protocol: #{uri.scheme}"
## end
# Returns the parser used by the transport. connection = @connection_pool[uri.site]
# unless connection
# @return The handle to the parser. connection = Net::HTTP.new(uri.host, uri.inferred_port)
def parser end
return @parser retried = false
begin
if uri.scheme == 'https' && !connection.started?
connection.use_ssl = true
if connection.respond_to?(:enable_post_connection_check=)
# Deals with a security vulnerability
connection.enable_post_connection_check = true
end
connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
connection.cert_store = @cert_store
end
unless connection.started?
# Since we allow a connection pool to be passed in, we don't
# actually know this connection has been started yet.
connection.start
end
rescue Errno::EPIPE, IOError, EOFError => e
# If there's a problem with the connection, finish and restart
if !retried && connection.started?
retried = true
connection.finish
connection.start
retry
else
raise e
end
end
# Keep a reference to the connection around
@connection_pool[uri.site] = connection
return connection
end end
protected :connect_to
end end
end end
end end

View File

@ -12,17 +12,14 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# Used to prevent the class/module from being loaded more than once module Google #:nodoc:
unless defined? Google::APIClient::VERSION class APIClient #:nodoc:
module Google #:nodoc: module VERSION #:nodoc:
class APIClient #:nodoc: MAJOR = 0
module VERSION #:nodoc: MINOR = 1
MAJOR = 0 TINY = 0
MINOR = 1
TINY = 0
STRING = [MAJOR, MINOR, TINY].join('.') STRING = [MAJOR, MINOR, TINY].join('.')
end
end end
end end
end end

View File

@ -0,0 +1,104 @@
# 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 'spec_helper'
require 'net/http'
require 'net/https'
require 'google/api_client/transport/http_transport'
class AlwaysFail
def initialize(*args)
raise IOError, "This would never work."
end
end
Google::APIClient::HTTPTransport::METHOD_MAPPING[:fail] = AlwaysFail
describe Google::APIClient::HTTPTransport, 'in the default configuration' do
before do
@http = Google::APIClient::HTTPTransport.new
end
it 'should send a GET request' do
request = @http.build_request(:get, "http://www.google.com/")
response = @http.send_request(request)
status, headers, body = response
status.should >= 100
body.size.should > 0
headers.size.should > 0
end
it 'should send a GET request using SSL' do
request = @http.build_request(:get, "https://www.google.com/")
response = @http.send_request(request)
status, headers, body = response
status.should >= 100
body.size.should > 0
headers.size.should > 0
end
it 'should send a POST request' do
request = @http.build_request(
:post, "http://www.google.com/", :body => "A Body."
)
response = @http.send_request(request)
status, headers, body = response
status.should >= 100
body.size.should > 0
headers.size.should > 0
end
it 'should send a PUT request' do
request = @http.build_request(
:put, "http://www.google.com/", :body => "A Body."
)
response = @http.send_request(request)
status, headers, body = response
status.should >= 100
body.size.should > 0
headers.size.should > 0
end
it 'should send a DELETE request' do
request = @http.build_request(:delete, "http://www.google.com/")
response = @http.send_request(request)
status, headers, body = response
status.should >= 100
body.size.should > 0
headers.size.should > 0
end
it 'should fail to send a FAIL request' do
(lambda do
request = @http.build_request(:fail, "http://www.google.com/")
response = @http.send_request(request)
end).should raise_error(IOError)
end
it 'should fail to send a BOGUS request' do
(lambda do
response = @http.send_request(
["BOGUS", "http://www.google.com/", {}, [""]]
)
end).should raise_error(ArgumentError)
end
it 'should fail to connect to a non-addressable URI' do
(lambda do
request = @http.build_request(:get, "bogus://www.google.com/")
response = @http.send_request(request)
end).should raise_error(ArgumentError)
end
end

View File

@ -14,50 +14,89 @@
require 'spec_helper' require 'spec_helper'
require 'net/http'
require 'net/https'
require 'google/api_client/transport/http_transport' require 'google/api_client/transport/http_transport'
require 'google/api_client/parser/json_parser'
describe Google::APIClient::HTTPTransport, 'with default configuration' do def assemble_body_string(body)
body_string = ""
body.each do |chunk|
body_string += chunk
end
return body_string
end
describe Google::APIClient::HTTPTransport, 'in the default configuration' do
before do before do
@transport = Google::APIClient::HTTPTransport.new @http = Google::APIClient::HTTPTransport.new
end end
it 'should use the default json parser' do it 'should build a valid GET request' do
@transport.parser.should be_instance_of Google::APIClient::JSONParser method, uri, headers, body =
@http.build_request(:get, "http://www.example.com/")
body_string = assemble_body_string(body)
method.should == "GET"
uri.should === "http://www.example.com/"
headers.keys.should_not include("Content-Length")
body_string.should == ""
end
it 'should build a valid POST request' do
method, uri, headers, body = @http.build_request(
:post, "http://www.example.com/", :body => "A body."
)
body_string = assemble_body_string(body)
method.should == "POST"
uri.should === "http://www.example.com/"
headers["Content-Length"].should == "7"
body_string.should == "A body."
end
it 'should build a valid PUT request' do
method, uri, headers, body = @http.build_request(
:put, "http://www.example.com/", :body => "A body."
)
body_string = assemble_body_string(body)
method.should == "PUT"
uri.should === "http://www.example.com/"
headers["Content-Length"].should == "7"
body_string.should == "A body."
end
it 'should build a valid DELETE request' do
method, uri, headers, body =
@http.build_request(:delete, "http://www.example.com/")
body_string = assemble_body_string(body)
method.should == "DELETE"
uri.should === "http://www.example.com/"
headers.keys.should_not include("Content-Length")
body_string.should == ""
end
it 'should not build a BOGUS request' do
(lambda do
@http.build_request(:bogus, "http://www.example.com/")
end).should raise_error(ArgumentError)
end end
end end
describe Google::APIClient::HTTPTransport, 'with custom pluggable parser' do describe Google::APIClient::HTTPTransport,
'with a certificate store and connection pool' do
before do before do
class FakeJsonParser @http = Google::APIClient::HTTPTransport.new(
end :cert_store => OpenSSL::X509::Store.new,
:connection_pool => {
@transport = Google::APIClient::HTTPTransport.new(:json_parser => FakeJsonParser.new) "http://www.example.com" => Net::HTTP.new("www.example.com", 80)
end }
it 'should use the custom parser' do
@transport.parser.should be_instance_of FakeJsonParser
end
end
describe Google::APIClient::HTTPTransport, 'with new parser type' do
before do
class FakeNewParser
end
@transport = Google::APIClient::HTTPTransport.new(
:parser => :new_parser,
:new_parser => FakeNewParser.new
) )
end end
it 'should use new parser type' do it 'should have the correct certificate store' do
@transport.parser.should be_instance_of FakeNewParser # TODO(bobaman) Write a real test
@http.cert_store.should_not == nil
end end
end
describe Google::APIClient::HTTPTransport, 'with illegal parser config' do it 'should have the correct connection pool' do
it 'should raise ArgumentError' do @http.connection_pool.keys.should include("http://www.example.com")
lambda { Google::APIClient::HTTPTransport.new(:parser => :fakeclass) }.should raise_exception(ArgumentError)
end end
end end

View File

@ -0,0 +1,49 @@
# 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 'spec_helper'
require 'google/api_client'
require 'google/api_client/version'
require 'google/api_client/parser/json_parser'
require 'google/api_client/auth/oauth_1'
require 'google/api_client/transport/http_transport'
describe Google::APIClient, 'with default configuration' do
before do
@client = Google::APIClient.new
end
it 'should make its version number available' do
::Google::APIClient::VERSION::STRING.should be_instance_of(String)
end
it 'should use the default JSON parser' do
@client.parser.should be_instance_of(Google::APIClient::JSONParser)
end
end
describe Google::APIClient, 'with custom pluggable parser' do
before do
class FakeJsonParser
end
@client = Google::APIClient.new(:parser => FakeJsonParser.new)
end
it 'should use the custom parser' do
@client.parser.should be_instance_of(FakeJsonParser)
end
end