lib/tap/http/dispatch.rb in tap-http-0.0.1 vs lib/tap/http/dispatch.rb in tap-http-0.1.0
- old
+ new
@@ -1,200 +1,260 @@
require 'tap/http/helpers'
require 'net/http'
-#module Net
-# class HTTP
-# attr_reader :socket
-# end
-
-# class BufferedIO
-#include Prosperity::Acts::Monitorable
-
-#private
-
-#def rbuf_fill
-# tick_monitor
-# timeout(@read_timeout) {
-# @rbuf << @io.sysread(1024)
-# }
-#end
-#end
-#end
-
module Tap
module Http
# Dispatch provides methods for constructing and submitting get and post
- # HTTP requests.
+ # HTTP requests from a configuration hash.
+ #
+ # res = Tap::Http::Dispatch.submit_request(
+ # :url => "http://tap.rubyforge.org",
+ # :version => '1.1',
+ # :request_method => 'GET',
+ # :headers => {},
+ # :params => {}
+ # )
+ # res.inspect # => "#<Net::HTTPOK 200 OK readbody=true>"
+ # res.body =~ /Tap/ # => true
+ #
+ # Headers and parameters take the form:
+ #
+ # { 'single' => 'value',
+ # 'multiple' => ['value one', 'value two']}
+ #
module Dispatch
- REQUEST_KEYS = [:url, :request_method, :headers, :params, :redirection_limit]
-
module_function
- # Constructs and submits a request to the url using the headers and parameters.
- # Returns the response from the submission.
+ DEFAULT_CONFIG = {
+ :request_method => 'GET',
+ :version => '1.1',
+ :params => {},
+ :headers => {},
+ :redirection_limit => nil
+ }
+
+ # Constructs and submits a request to the url using the request configuration.
+ # A url must be specified in the configuration, but other configurations are
+ # optional; if unspecified, the values in DEFAULT_CONFIG will be used. A
+ # block may be given to receive the Net::HTTP and request just prior to
+ # submission.
#
- # res = submit_request("http://www.google.com/search", {:request_method => 'get'}, {:q => 'tap rubyforge'})
- # # => <Net::HTTPOK 200 OK readbody=true>
+ # Returns the response from the submission.
#
- # Notes:
- # - A request method must be specified in the headers; currently only get and
- # post are supported. See construct_post for supported post content types.
- # - A url or a url (as would result from URI.parse(url)) can be provided to
- # submit request. The Net::HTTP object performing the submission is passed
- # to the block, if given, before the request is made.
- def submit_request(config) # :yields: http
+ def submit_request(config)
+ symbolized = DEFAULT_CONFIG.dup
+ config.each_pair do |key, value|
+ symbolized[key.to_sym] = value
+ end
+ config = symbolized
+
+ request_method = (config[:request_method]).to_s
url_or_uri = config[:url]
- params = config[:params] || {}
- headers = headerize_keys( config[:headers] || {})
- request_method = (config[:request_method] || 'GET').to_s
+ version = config[:version]
+ params = config[:params]
+ headers = headerize_keys(config[:headers])
+ raise ArgumentError, "no url specified" unless url_or_uri
uri = url_or_uri.kind_of?(URI) ? url_or_uri : URI.parse(url_or_uri)
uri.path = "/" if uri.path.empty?
# construct the request based on the method
request = case request_method
when /^get$/i then construct_get(uri, headers, params)
when /^post$/i then construct_post(uri, headers, params)
else
- raise ArgumentError.new("Missing or unsupported request_method: #{request_method}")
+ raise ArgumentError, "unsupported request method: #{request_method}"
end
# set the http version
- # if version = config[:http_version]
- # version_method = "version_#{version.to_s.gsub(".", "_")}".to_sym
- # if Object::Net::HTTP.respond_to?(version_method)
- # Object::Net::HTTP.send(version_method)
- # else
- # raise ArgumentError.new("unsupported http_version: #{version}")
- # end
- # end
+ version_method = "version_#{version.to_s.gsub(".", "_")}".to_sym
+ if ::Net::HTTP.respond_to?(version_method)
+ ::Net::HTTP.send(version_method)
+ else
+ raise ArgumentError, "unsupported HTTP version: #{version}"
+ end
# submit the request
- res = Object::Net::HTTP.new(uri.host, uri.port).start do |http|
- yield(http) if block_given?
+ res = ::Net::HTTP.new(uri.host, uri.port).start do |http|
+ yield(http, request) if block_given?
http.request(request)
end
# fetch redirections
redirection_limit = config[:redirection_limit]
redirection_limit ? fetch_redirection(res, redirection_limit) : res
end
- # Constructs a post query. The 'Content-Type' header determines the format of the body
- # content. If the content type is 'multipart/form-data', then the parameters will be
- # formatted using the boundary in the content-type header, if provided, or a randomly
- # generated boundary.
+ # Constructs a Net::HTTP::Post query, setting headers and parameters.
#
- # Headers for the request are set in this method. If uri contains query parameters,
- # they will be included in the request URI.
+ # ==== Supported Content Types:
#
- # Supported content-types:
- # - application/x-www-form-urlencoded (the default)
- # - multipart/form-data
+ # - application/x-www-form-urlencoded (the default)
+ # - multipart/form-data
#
+ # The multipart/form-data content type may specify a boundary. If no
+ # boundary is specified, a randomly generated boundary will be used
+ # to delimit the parameters.
+ #
+ # post = construct_post(
+ # URI.parse('http://some.url/'),
+ # {:content_type => 'multipart/form-data; boundary=1234'},
+ # {:key => 'value'})
+ #
+ # post.body
+ # # => %Q{--1234\r
+ # # Content-Disposition: form-data; name="key"\r
+ # # \r
+ # # value\r
+ # # --1234--\r
+ # # }
+ #
+ # (Note the carriage returns are required in multipart content)
+ #
+ # The content-length header is determined automatically from the
+ # formatted request body; manually specified content-length headers
+ # will be overridden.
+ #
def construct_post(uri, headers, params)
- req = Object::Net::HTTP::Post.new( URI.encode("#{uri.path}#{construct_query(uri)}") )
+ req = ::Net::HTTP::Post.new( URI.encode("#{uri.path}#{format_query(uri)}") )
headers = headerize_keys(headers)
content_type = headers['Content-Type']
case content_type
- when /multipart\/form-data/i
+ when nil, /^application\/x-www-form-urlencoded$/i
+ req.body = format_www_form_urlencoded(params)
+ headers['Content-Type'] ||= "application/x-www-form-urlencoded"
+ headers['Content-Length'] = req.body.length
+
+ when /^multipart\/form-data(;\s*boundary=(.*))?$/i
# extract the boundary if it exists
- content_type =~ /boundary=(.*)/i
- boundary = $1 || rand.to_s[2..20]
+ boundary = $2 || rand.to_s[2..20]
- req.body = format_multipart_form_data(boundary, params)
+ req.body = format_multipart_form_data(params, boundary)
headers['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
headers['Content-Length'] = req.body.length
+
else
- req.body = format_www_form_urlencoded(params)
- headers['Content-Type'] = "application/x-www-form-urlencoded"
- headers['Content-Length'] = req.body.length
+ raise ArgumentError, "unsupported Content-Type for POST: #{content_type}"
end
-
+
headers.each_pair { |key, value| req[key] = value }
req
end
- # Constructs a get query. All parameters in uri and params are added to the
- # request URI. Headers for the request are set in this method.
+ # Constructs a Net::HTTP::Get query. All parameters in uri and params are
+ # encoded and added to the request URI.
+ #
+ # get = construct_get(URI.parse('http://some.url/path'), {}, {:key => 'value'})
+ # get.path # => "/path?key=value"
+ #
def construct_get(uri, headers, params)
- req = Object::Net::HTTP::Get.new( URI.encode("#{uri.path}#{construct_query(uri, params)}") )
+ req = ::Net::HTTP::Get.new( URI.encode("#{uri.path}#{format_query(uri, params)}") )
headerize_keys(headers).each_pair { |key, value| req[key] = value }
req
end
- # Checks the type of the response; if it is a redirection, get the redirection,
- # otherwise return the response.
+ # Checks the type of the response; if it is a redirection, fetches the
+ # redirection. Otherwise return the response.
#
# Notes:
- # - The redirections will only recurse for the input redirection limit (default 10)
- # - Responses that are not Net::HTTPRedirection or Net::HTTPSuccess raise an error.
+ # - Fetch will recurse up to the input redirection limit (default 10)
+ # - Responses that are not Net::HTTPRedirection or Net::HTTPSuccess
+ # raise an error.
def fetch_redirection(res, limit=10)
- raise ArgumentError, 'Could not follow all the redirections.' if limit == 0
+ raise 'exceeded the redirection limit' if limit < 1
case res
- when Object::Net::HTTPRedirection
- redirect = Object::Net::HTTP.get_response( URI.parse(res['location']) )
+ when ::Net::HTTPRedirection
+ redirect = ::Net::HTTP.get_response( URI.parse(res['location']) )
fetch_redirection(redirect, limit - 1)
- when Object::Net::HTTPSuccess then res
- else raise StandardError, res.error!
+ when ::Net::HTTPSuccess
+ res
+ else
+ raise StandardError, res.error!
end
end
- # Normalizes the header keys to a titleized, dasherized string.
- # 'some_header' => 'Some-Header'
- # :some_header => 'Some-Header'
- # 'some header' => 'Some-Header'
- def headerize_keys(headers)
+ # Converts the keys of a hash to headers. See Helpers#headerize.
+ #
+ # headerize_keys('some_header' => 'value') # => {'Some-Header' => 'value'}
+ #
+ def headerize_keys(hash)
result = {}
- headers.each_pair do |key, value|
+ hash.each_pair do |key, value|
result[Helpers.headerize(key)] = value
end
result
end
- # Returns a URI query constructed from the query in uri and the input parameters.
+ # Constructs a URI query string from the uri and the input parameters.
+ # Multiple values for a parameter may be specified using an array.
# The query is not encoded, so you may need to URI.encode it later.
- def construct_query(uri, params={})
+ #
+ # format_query(URI.parse('http://some.url/path'), {:key => 'value'})
+ # # => "?key=value"
+ #
+ # format_query(URI.parse('http://some.url/path?one=1'), {:two => '2'})
+ # # => "?one=1&two=2"
+ #
+ def format_query(uri, params={})
query = []
+ query << uri.query if uri.query
params.each_pair do |key, values|
- values = values.kind_of?(Array) ? values : [values]
+ values = [values] unless values.kind_of?(Array)
values.each { |value| query << "#{key}=#{value}" }
end
- query << uri.query if uri.query
"#{query.empty? ? '' : '?'}#{query.join('&')}"
end
-
+
+ # Formats params as 'application/x-www-form-urlencoded' for use as the
+ # body of a post request. Multiple values for a parameter may be
+ # specified using an array. The result is obviously URI encoded.
+ #
+ # format_www_form_urlencoded(:key => 'value with spaces')
+ # # => "key=value%20with%20spaces"
+ #
def format_www_form_urlencoded(params={})
query = []
params.each_pair do |key, values|
- values = values.kind_of?(Array) ? values : [values]
+ values = [values] unless values.kind_of?(Array)
values.each { |value| query << "#{key}=#{value}" }
end
URI.encode( query.join('&') )
end
- # Returns a post body formatted as 'multipart/form-data'. Special formatting occures
- # if value in one of the key-value parameter pairs is an Array or Hash.
+ # Formats params as 'multipart/form-data' using the specified boundary,
+ # for use as the body of a post request. Multiple values for a parameter
+ # may be specified using an array. All newlines include a carriage
+ # return for proper formatting.
#
- # Array values are treated as a multiple inputs for a single key; each array value
- # is assigned to the key. Hash values are treated as files, with all file-related
- # headers specified in the hash.
+ # format_multipart_form_data(:key => 'value')
+ # # => %Q{--1234567890\r
+ # # Content-Disposition: form-data; name="key"\r
+ # # \r
+ # # value\r
+ # # --1234567890--\r
+ # # }
#
- #--
- # Example:
- # "--1234", {:key => 'value'} =>
- # --1234
- # Content-Disposition: form-data; name="key"
+ # To specify a file, use a hash of file-related headers.
#
- # value
- # --1234--
- def format_multipart_form_data(boundary, params)
+ # format_multipart_form_data(:key => {
+ # 'Content-Type' => 'text/plain',
+ # 'Filename' => "path/to/file.txt"}
+ # )
+ # # => %Q{--1234567890\r
+ # # Content-Disposition: form-data; name="key"; filename="path/to/file.txt"\r
+ # # Content-Type: text/plain\r
+ # # \r
+ # # \r
+ # # --1234567890--\r
+ # # }
+ #
+ def format_multipart_form_data(params, boundary="1234567890")
body = []
params.each_pair do |key, values|
- values = values.kind_of?(Array) ? values : [values]
+ values = [values] unless values.kind_of?(Array)
values.each do |value|
body << case value
when Hash
hash = headerize_keys(value)