# Copyright (C) 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/version'
require 'httpadapter/connection'
##
# A module which provides methods to aid in conversion of HTTP request and
# response objects. It uses tuples as a generic intermediary format.
#
# @example
# class StubAdapter
# include HTTPAdapter
#
# def convert_request_to_a(request_obj)
# return ['GET', '/', [], [""]] # Stubbed request tuple
# end
#
# def convert_request_from_a(request_ary)
# return Object.new # Stubbed request object
# end
#
# def convert_response_to_a(response_obj)
# return [200, [], ['']] # Stubbed response tuple
# end
#
# def convert_response_from_a(response_ary)
# return Object.new # Stubbed response object
# end
#
# def fetch_resource(request_ary, connection=nil)
# return [200, [], ['']] # Stubbed response tuple from server
# end
# end
module HTTPAdapter
##
# Converts an HTTP request object to a simple tuple.
#
# @param [Object] request
# The request object to be converted. The adapter must implement
# the #convert_request_to_a
method, which takes the request
# object as a parameter.
#
# @return [Array] The tuple that the request was converted to.
def adapt_request(request_obj)
if self.respond_to?(:convert_request_to_a)
converted_request = self.convert_request_to_a(request_obj)
else
raise TypeError,
'Expected adapter to implement #convert_request_to_a.'
end
return HTTPAdapter.verified_request(converted_request)
end
##
# Converts a tuple to a specific HTTP implementation's request format.
#
# @param [Array] request_ary
# The request array to be converted. The request array must be a tuple
# with a length of 4. The first element must be the request method.
# The second element must be the URI. The URI may be relative. The third
# element contains the headers. It must respond to #each
and
# iterate over the header names and values. The fourth element must be
# the body. It must respond to #each
but may not be a
# String
. It should emit String
objects.
#
# @return [Array] The implementation-specific request object.
def specialize_request(request_ary)
request = HTTPAdapter.verified_request(request_ary)
if self.respond_to?(:convert_request_from_a)
return self.convert_request_from_a(request)
else
raise TypeError,
'Expected adapter to implement #convert_request_from_a.'
end
end
##
# Converts an HTTP response object to a simple tuple.
#
# @param [Object] response
# The response object to be converted. The adapter must implement
# the #convert_response_to_a
method, which takes the response
# object as a parameter.
#
# @return [Array] The tuple that the reponse was converted to.
def adapt_response(response_obj)
if self.respond_to?(:convert_response_to_a)
converted_response = self.convert_response_to_a(response_obj)
else
raise TypeError,
'Expected adapter to implement #convert_response_to_a.'
end
return HTTPAdapter.verified_response(converted_response)
end
##
# Converts a tuple to a specific HTTP implementation's response format.
#
# @param [Array] response_ary
# The response object to be converted. The response object must be a
# tuple with a length of 3. The first element must be the HTTP status
# code. The second element contains the headers. It must respond to
# #each
and iterate over the header names and values. The
# third element must be the body. It must respond to #each
# but may not be a String
. It should emit
# String
objects. This is essentially the same format that
# Rack uses.
#
# @return [Array] The implementation-specific response object.
def specialize_response(response_ary)
response_ary = HTTPAdapter.verified_response(response_ary)
if self.respond_to?(:convert_response_from_a)
return self.convert_response_from_a(response_ary)
else
raise TypeError,
'Expected adapter to implement #convert_response_from_a.'
end
end
##
# Transmits a request.
#
# @param [Array] request_ary
# The request tuple that will be sent.
# @param [HTTPAdapter::Connection] connection
# An object representing a connection. This object represents an open
# HTTP connection that is used to make multiple HTTP requests.
# @return [Array]
# The response given by the server.
#
# @return [Array] A tuple representing the response from the server.
def transmit(request_ary, connection=nil)
request_ary = HTTPAdapter.verified_request(request_ary)
if connection && !connection.kind_of?(HTTPAdapter::Connection)
raise TypeError,
"Expected HTTPAdapter::Connection, got #{connection.class}."
end
if self.respond_to?(:fetch_resource)
response_ary = self.fetch_resource(request_ary, connection)
return HTTPAdapter.verified_response(response_ary)
else
raise TypeError, 'Expected adapter to implement .fetch_resource.'
end
end
##
# Verifies a request tuple matches the specification.
#
# @param [Array] request
# The request object to be verified.
#
# @return [Array] The tuple, after normalization.
def self.verified_request(request)
if !request.kind_of?(Array)
raise TypeError, "Expected Array, got #{request.class}."
end
if request.size == 4
# Verify that the request object matches the specification
method, uri, headers, body = request
method = method.to_str if method.respond_to?(:to_str)
# Special-casing symbols here
method = method.to_s if method.kind_of?(Symbol)
if !method.kind_of?(String)
raise TypeError,
"Expected String, got #{method.class}."
end
method = method.upcase
if uri.respond_to?(:to_str)
uri = uri.to_str
else
raise TypeError, "Expected String, got #{uri.class}."
end
original_headers, headers = headers, []
if original_headers.respond_to?(:each)
original_headers.each do |header, value|
if header.respond_to?(:to_str)
header = header.to_str
else
raise TypeError, "Expected String, got #{header.class}."
end
if value.respond_to?(:to_str)
value = value.to_str
else
raise TypeError, "Expected String, got #{value.class}."
end
headers << [header, value]
end
else
raise TypeError, 'Expected headers to respond to #each.'
end
if body.kind_of?(String)
raise TypeError,
'Body must not be a String; it must respond to #each and ' +
'emit String values.'
end
# Can't verify that all chunks are Strings because #each may be
# effectively destructive.
if !body.respond_to?(:each)
raise TypeError, 'Expected body to respond to #each.'
end
else
raise TypeError,
"Expected tuple of [method, uri, headers, body], " +
"got #{request.inspect}."
end
return [method, uri, headers, body]
end
##
# Verifies a response tuple matches the specification.
#
# @param [Array] response
# The response object to be verified.
#
# @return [Array] The tuple, after normalization.
def self.verified_response(response)
if !response.kind_of?(Array)
raise TypeError, "Expected Array, got #{response.class}."
end
if response.size == 3
# Verify that the response object matches the specification
status, headers, body = response
status = status.to_i if status.respond_to?(:to_i)
if !status.kind_of?(Integer)
raise TypeError, "Expected Integer, got #{status.class}."
end
original_headers, headers = headers, []
if original_headers.respond_to?(:each)
original_headers.each do |header, value|
if header.respond_to?(:to_str)
header = header.to_str
else
raise TypeError, "Expected String, got #{header.class}."
end
if value.respond_to?(:to_str)
value = value.to_str
else
raise TypeError, "Expected String, got #{value.class}."
end
headers << [header, value]
end
else
raise TypeError, 'Expected headers to respond to #each.'
end
if body.kind_of?(String)
raise TypeError,
'Body must not be a String; it must respond to #each and ' +
'emit String values.'
end
# Can't verify that all chunks are Strings because #each may be
# effectively destructive.
if !body.respond_to?(:each)
raise TypeError, 'Expected body to respond to #each.'
end
else
raise TypeError,
"Expected tuple of [status, headers, body], got #{response.inspect}."
end
return [status, headers, body]
end
end