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)