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:
parent
a9523bca32
commit
bc844db311
|
@ -12,16 +12,20 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'google/api_client/version'
|
||||
|
||||
module Google #:nodoc:
|
||||
##
|
||||
# This class manages communication with a single API.
|
||||
class APIClient
|
||||
|
||||
def initialize(options={})
|
||||
@options = {
|
||||
# TODO: What configuration options need to go here?
|
||||
}.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]
|
||||
require 'google/api_client/auth/oauth_1'
|
||||
# NOTE: Do not rely on this default value, as it may change
|
||||
|
@ -32,5 +36,13 @@ module Google #:nodoc:
|
|||
@options[:transport] = HTTPTransport
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the parser used by the client.
|
||||
def parser
|
||||
return @options[:parser]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require 'google/api_client/version'
|
||||
|
|
|
@ -12,60 +12,173 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'google/api_client/parser/json_parser'
|
||||
require 'net/http'
|
||||
require 'net/https'
|
||||
require 'addressable/uri'
|
||||
|
||||
module Google #: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
|
||||
|
||||
##
|
||||
# The default transport configuration values. These may be overridden
|
||||
# simply by passing in the same key to the constructor.
|
||||
DEFAULTS = {
|
||||
:parser => :json_parser
|
||||
ALLOWED_SCHEMES = ["http", "https"]
|
||||
METHOD_MAPPING = {
|
||||
# RFC 2616
|
||||
:options => Net::HTTP::Options,
|
||||
:get => Net::HTTP::Get,
|
||||
: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={})
|
||||
@options = DEFAULTS.clone
|
||||
@options.merge!(options)
|
||||
# A mapping from authorities to Net::HTTP objects.
|
||||
@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
|
||||
|
||||
attr_reader :connection_pool
|
||||
attr_reader :cert_store
|
||||
|
||||
# first check if user passed a parser then fallback on appropriate default
|
||||
@parser = @options[@options[:parser]] || PARSERS[@options[:parser]]
|
||||
unless @parser
|
||||
raise ArgumentError,
|
||||
'Invalid :parser configuration.'
|
||||
def build_request(method, uri, options={})
|
||||
# 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
|
||||
|
||||
##
|
||||
# Returns configuration of the transport.
|
||||
# Builds a connection to the authority given in the URI using the
|
||||
# appropriate protocol.
|
||||
#
|
||||
# @return [Hash] The configuration options.
|
||||
def options
|
||||
return @options
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the parser used by the transport.
|
||||
#
|
||||
# @return The handle to the parser.
|
||||
def parser
|
||||
return @parser
|
||||
# @param [Addressable::URI, #to_str] uri The URI to connect to.
|
||||
def connect_to(uri)
|
||||
uri = Addressable::URI.parse(uri).normalize
|
||||
if !ALLOWED_SCHEMES.include?(uri.scheme)
|
||||
raise ArgumentError, "Unsupported protocol: #{uri.scheme}"
|
||||
end
|
||||
connection = @connection_pool[uri.site]
|
||||
unless connection
|
||||
connection = Net::HTTP.new(uri.host, uri.inferred_port)
|
||||
end
|
||||
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
|
||||
protected :connect_to
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,17 +12,14 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Used to prevent the class/module from being loaded more than once
|
||||
unless defined? Google::APIClient::VERSION
|
||||
module Google #:nodoc:
|
||||
class APIClient #:nodoc:
|
||||
module VERSION #:nodoc:
|
||||
MAJOR = 0
|
||||
MINOR = 1
|
||||
TINY = 0
|
||||
module Google #:nodoc:
|
||||
class APIClient #:nodoc:
|
||||
module VERSION #:nodoc:
|
||||
MAJOR = 0
|
||||
MINOR = 1
|
||||
TINY = 0
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -14,50 +14,89 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
require 'net/http'
|
||||
require 'net/https'
|
||||
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
|
||||
@transport = Google::APIClient::HTTPTransport.new
|
||||
@http = Google::APIClient::HTTPTransport.new
|
||||
end
|
||||
|
||||
it 'should use the default json parser' do
|
||||
@transport.parser.should be_instance_of Google::APIClient::JSONParser
|
||||
it 'should build a valid GET request' do
|
||||
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
|
||||
|
||||
describe Google::APIClient::HTTPTransport, 'with custom pluggable parser' do
|
||||
describe Google::APIClient::HTTPTransport,
|
||||
'with a certificate store and connection pool' do
|
||||
before do
|
||||
class FakeJsonParser
|
||||
end
|
||||
|
||||
@transport = Google::APIClient::HTTPTransport.new(:json_parser => FakeJsonParser.new)
|
||||
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
|
||||
@http = Google::APIClient::HTTPTransport.new(
|
||||
:cert_store => OpenSSL::X509::Store.new,
|
||||
:connection_pool => {
|
||||
"http://www.example.com" => Net::HTTP.new("www.example.com", 80)
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it 'should use new parser type' do
|
||||
@transport.parser.should be_instance_of FakeNewParser
|
||||
it 'should have the correct certificate store' do
|
||||
# TODO(bobaman) Write a real test
|
||||
@http.cert_store.should_not == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe Google::APIClient::HTTPTransport, 'with illegal parser config' do
|
||||
it 'should raise ArgumentError' do
|
||||
lambda { Google::APIClient::HTTPTransport.new(:parser => :fakeclass) }.should raise_exception(ArgumentError)
|
||||
it 'should have the correct connection pool' do
|
||||
@http.connection_pool.keys.should include("http://www.example.com")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue