extract multipart stuff from Adapter to Request::Multipart middleware

This commit is contained in:
Mislav Marohnić 2011-03-02 05:05:32 +01:00
parent 862c98344f
commit d6d86dd043
19 changed files with 156 additions and 233 deletions

View File

@ -1,11 +1,7 @@
module Faraday
class Adapter < Middleware
FORM_TYPE = 'application/x-www-form-urlencoded'.freeze
MULTIPART_TYPE = 'multipart/form-data'.freeze
CONTENT_TYPE = 'Content-Type'.freeze
DEFAULT_BOUNDARY = "-----------RubyMultipartPost".freeze
extend AutoloadHelper
autoload_all 'faraday/adapter',
:ActionDispatch => 'action_dispatch',
:NetHttp => 'net_http',
@ -27,75 +23,7 @@ module Faraday
:logger => :Logger
def call(env)
process_body_for_request(env)
end
# Converts a body hash into encoded form params. This is done as late
# as possible in the request cycle in case some other middleware wants to
# act on the request before sending it out.
#
# env - The current request environment Hash.
# body - A Hash of keys/values. Strings and empty values will be
# ignored. Default: env[:body]
# headers - The Hash of request headers. Default: env[:request_headers]
#
# Returns nothing. If the body is processed, it is replaced in the
# environment for you.
def process_body_for_request(env, body = env[:body], headers = env[:request_headers])
return if body.nil? || body.empty? || !body.respond_to?(:each_key)
if has_multipart?(body)
env[:request] ||= {}
env[:request][:boundary] ||= DEFAULT_BOUNDARY
headers[CONTENT_TYPE] = MULTIPART_TYPE + ";boundary=#{env[:request][:boundary]}"
env[:body] = create_multipart(env, body)
else
type = headers[CONTENT_TYPE]
headers[CONTENT_TYPE] = FORM_TYPE if type.nil? || type.empty?
parts = []
process_to_params(parts, env[:body]) do |key, value|
"#{key}=#{escape(value.to_s)}"
end
env[:body] = parts * "&"
end
end
def has_multipart?(body)
body.values.each do |v|
if v.respond_to?(:content_type)
return true
elsif v.respond_to?(:values)
return true if has_multipart?(v)
end
end
false
end
def create_multipart(env, params, boundary = nil)
boundary ||= env[:request][:boundary]
parts = []
process_to_params(parts, params) do |key, value|
Faraday::Parts::Part.new(boundary, key, value)
end
parts << Faraday::Parts::EpiloguePart.new(boundary)
env[:request_headers]['Content-Length'] = parts.inject(0) {|sum,i| sum + i.length }.to_s
Faraday::CompositeReadIO.new(*parts.map{|p| p.to_io })
end
def process_to_params(pieces, params, base = nil)
params.to_a.each do |key, value|
key_str = base ? "#{base}[#{key}]" : key
block = block_given? ? Proc.new : nil
case value
when Array
values = value.inject([]) { |a,v| a << [nil, v] }
process_to_params(pieces, values, key_str, &block)
when Hash
process_to_params(pieces, value, key_str, &block)
else
pieces << block.call(key_str, value)
end
end
# do nothing
end
end
end

View File

@ -27,12 +27,6 @@ module Faraday
:body => resp.body
@app.call env
end
# TODO: build in support for multipart streaming if action dispatch supports it.
def create_multipart(env, params, boundary = nil)
stream = super
stream.read
end
end
end
end

View File

@ -23,7 +23,7 @@ module Faraday
end
def call(env)
process_body_for_request(env)
super
request = EventMachine::HttpRequest.new(URI::parse(env[:url].to_s))
options = {:head => env[:request_headers]}
options[:ssl] = env[:ssl] if env[:ssl]

View File

@ -10,6 +10,9 @@ module Faraday
def call(env)
super
# TODO: support streaming requests
env[:body] = env[:body].read if env[:body].respond_to? :read
sess = ::Patron::Session.new
args = [env[:method], env[:url].to_s, env[:request_headers]]
if Faraday::Connection::METHODS_WITH_BODIES.include?(env[:method])
@ -27,12 +30,6 @@ module Faraday
rescue Errno::ECONNREFUSED
raise Error::ConnectionFailed, $!
end
# TODO: build in support for multipart streaming if patron supports it.
def create_multipart(env, params, boundary = nil)
stream = super
stream.read
end
end
end
end

View File

@ -118,11 +118,6 @@ module Faraday
end
@app.call(env)
end
def create_multipart(env, params, boundary = nil)
stream = super
stream.read
end
end
end
end

View File

@ -16,7 +16,10 @@ module Faraday
def call(env)
super
req = ::Typhoeus::Request.new env[:url].to_s,
# TODO: support streaming requests
env[:body] = env[:body].read if env[:body].respond_to? :read
req = ::Typhoeus::Request.new env[:url].to_s,
:method => env[:method],
:body => env[:body],
:headers => env[:request_headers],
@ -62,12 +65,6 @@ module Faraday
reject { |(k, v)| k.nil? }. # Ignore blank lines
map { |(k, v)| [k.downcase, v] }.flatten]
end
# TODO: build in support for multipart streaming if typhoeus supports it.
def create_multipart(env, params, boundary = nil)
stream = super
stream.read
end
end
end
end

View File

@ -77,8 +77,11 @@ module Faraday
end
def to_app
# use at least an adapter so the stack isn't a no-op
self.adapter Faraday.default_adapter if @handlers.empty?
# default stack, if nothing else is configured
if @handlers.empty?
self.request :url_encoded
self.adapter Faraday.default_adapter
end
# last added handler should be the deepest, closest to the inner app
@handlers.reverse.inject(@inner_app) { |app, handler| handler.build(app) }
end

View File

@ -14,11 +14,13 @@ module Faraday
autoload_all 'faraday/request',
:JSON => 'json',
:UrlEncoded => 'url_encoded'
:UrlEncoded => 'url_encoded',
:Multipart => 'multipart'
register_lookup_modules \
:json => :JSON,
:url_encoded => :UrlEncoded
:url_encoded => :UrlEncoded,
:multipart => :Multipart
def self.run(connection, request_method)
req = create

View File

@ -0,0 +1,63 @@
module Faraday
class Request::Multipart < Request::UrlEncoded
self.mime_type = 'multipart/form-data'.freeze
DEFAULT_BOUNDARY = "-----------RubyMultipartPost".freeze
def call(env)
match_content_type(env) do |params|
env[:request] ||= {}
env[:request][:boundary] ||= DEFAULT_BOUNDARY
env[:request_headers][CONTENT_TYPE] += ";boundary=#{env[:request][:boundary]}"
env[:body] = create_multipart(env, params)
end
@app.call env
end
def process_request?(env)
type = request_type(env)
env[:body].respond_to?(:each_key) and !env[:body].empty? and (
(type.empty? and has_multipart?(env[:body])) or
type == self.class.mime_type
)
end
def has_multipart?(body)
body.values.each do |val|
if val.respond_to?(:content_type)
return true
elsif val.respond_to?(:values)
return true if has_multipart?(val)
end
end
false
end
def create_multipart(env, params)
boundary = env[:request][:boundary]
parts = process_params(params) do |key, value|
Faraday::Parts::Part.new(boundary, key, value)
end
parts << Faraday::Parts::EpiloguePart.new(boundary)
body = Faraday::CompositeReadIO.new(parts)
env[:request_headers]['Content-Length'] = body.length.to_s
return body
end
def process_params(params, prefix = nil, pieces = nil, &block)
params.inject(pieces || []) do |all, (key, value)|
key = "#{prefix}[#{key}]" if prefix
case value
when Array
values = value.inject([]) { |a,v| a << [nil, v] }
process_params(values, key, all, &block)
when Hash
process_params(value, key, all, &block)
else
all << block.call(key, value)
end
end
end
end
end

View File

@ -1,9 +1,10 @@
module Faraday
class Request::UrlEncoded < Faraday::Middleware
CONTENT_TYPE = 'Content-Type'.freeze
class << self
attr_accessor :mime_type
end
self.mime_type = 'application/x-www-form-urlencoded'.freeze
def call(env)
@ -16,14 +17,19 @@ module Faraday
def match_content_type(env)
type = request_type(env)
if env[:body] and (type.empty? or type == self.class.mime_type)
env[:request_headers]['Content-Type'] ||= self.class.mime_type
if process_request?(env)
env[:request_headers][CONTENT_TYPE] ||= self.class.mime_type
yield env[:body] unless env[:body].respond_to?(:to_str)
end
end
def process_request?(env)
type = request_type(env)
env[:body] and (type.empty? or type == self.class.mime_type)
end
def request_type(env)
type = env[:request_headers]['Content-Type'].to_s
type = env[:request_headers][CONTENT_TYPE].to_s
type = type.split(';', 2).first if type.index(';')
type
end

View File

@ -3,13 +3,21 @@ begin
require 'parts'
require 'stringio'
rescue LoadError
puts "Install the multipart-post gem."
$stderr.puts "Install the multipart-post gem."
raise
end
# Auto-load multipart-post gem on first request.
module Faraday
CompositeReadIO = ::CompositeReadIO
UploadIO = ::UploadIO
Parts = ::Parts
end
class CompositeReadIO < ::CompositeReadIO
attr_reader :length
def initialize(parts)
@length = parts.inject(0) { |sum, part| sum + part.length }
ios = parts.map{ |part| part.to_io }
super(*ios)
end
end
UploadIO = ::UploadIO
Parts = ::Parts
end

View File

@ -45,13 +45,12 @@ else
end
define_method "test_#{adapter}_POST_sends_files" do
name = File.join(File.dirname(__FILE__), '..', 'live_server.rb')
resp = create_connection(adapter).post do |req|
req.url 'file'
req.body = {'uploaded_file' => Faraday::UploadIO.new(name, 'text/x-ruby')}
req.body = {'uploaded_file' => Faraday::UploadIO.new(__FILE__, 'text/x-ruby')}
end
assert_equal "file live_server.rb text/x-ruby", resp.body
end
assert_equal "file live_test.rb text/x-ruby", resp.body
end unless :default == adapter # isn't configured for multipart
# http://github.com/toland/patron/issues/#issue/9
if ENV['FORCE'] || adapter != Faraday::Adapter::Patron
@ -144,11 +143,15 @@ else
def create_connection(adapter)
if adapter == :default
conn = Faraday.default_connection
conn.url_prefix = LIVE_SERVER
conn
Faraday.default_connection.tap do |conn|
conn.builder.to_app # trigger default stack
conn.url_prefix = LIVE_SERVER
conn.headers['X-Faraday-Adapter'] = adapter.to_s
end
else
Faraday::Connection.new LIVE_SERVER do |b|
Faraday::Connection.new LIVE_SERVER, :headers => {'X-Faraday-Adapter' => adapter.to_s} do |b|
b.request :multipart
b.request :url_encoded
b.use adapter
end
end

View File

@ -1,5 +1,4 @@
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'helper'))
require 'webmock/test_unit'
module Adapters
class NetHttpTest < Faraday::TestCase

View File

@ -1,58 +0,0 @@
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
class FormPostTest < Faraday::TestCase
def setup
@app = Faraday::Adapter.new nil
@env = {:request_headers => {}}
end
def test_processes_nested_body
@env[:body] = {:a => 1, :b => {:c => 2}}
@app.process_body_for_request @env
assert_match /^|\&a=1/, @env[:body]
assert_match /^|\&b\[c\]=2/, @env[:body]
assert_equal Faraday::Adapter::FORM_TYPE, @env[:request_headers]['Content-Type']
end
def test_processes_with_custom_type
@env[:body] = {:a => 1}
@env[:request_headers]['Content-Type'] = 'test/type'
@app.process_body_for_request @env
assert_equal 'a=1', @env[:body]
assert_equal 'test/type', @env[:request_headers]['Content-Type']
end
def test_processes_nil_body
@env[:body] = nil
@app.process_body_for_request @env
assert_nil @env[:body]
end
def test_processes_empty_body
@env[:body] = ''
@app.process_body_for_request @env
assert_equal '', @env[:body]
end
def test_processes_string_body
@env[:body] = 'abc'
@app.process_body_for_request @env
assert_equal 'abc', @env[:body]
end
def test_processes_array_values
@env[:body] = {:a => [:b, 1]}
@app.process_body_for_request @env
assert_equal 'a[]=b&a[]=1', @env[:body]
end
def test_processes_nested_array_values
@env[:body] = {:a => [:b, {:c => :d}, [:e]]}
@app.process_body_for_request @env
# a[]=b&a[][c]=d&a[][]=e
assert_match /a\[\]=b/, @env[:body]
assert_match /a\[\]\[c\]=d/, @env[:body]
assert_match /a\[\]\[\]=e/, @env[:body]
end
end

View File

@ -1,5 +1,8 @@
require 'rubygems'
require 'test/unit'
require 'webmock/test_unit'
WebMock.disable_net_connect!(:allow_localhost => true)
if ENV['LEFTRIGHT']
begin

View File

@ -9,9 +9,13 @@ get '/json' do
end
post '/file' do
"file %s %s" % [
params[:uploaded_file][:filename],
params[:uploaded_file][:type]]
if params[:uploaded_file].respond_to? :each_key
"file %s %s" % [
params[:uploaded_file][:filename],
params[:uploaded_file][:type]]
else
status 400
end
end
post '/hello' do

View File

@ -19,8 +19,10 @@ class MiddlewareStackTest < Faraday::TestCase
def test_sets_default_adapter_if_none_set
@conn.to_app
default_middleware = Faraday::Request.lookup_module :url_encoded
default_adapter_klass = Faraday::Adapter.lookup_module Faraday.default_adapter
assert @builder[0] == default_adapter_klass
assert @builder[0] == default_middleware
assert @builder[1] == default_adapter_klass
end
def test_allows_rebuilding

View File

@ -1,48 +0,0 @@
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
Faraday::CompositeReadIO.send :attr_reader, :ios
class MultipartTest < Faraday::TestCase
def setup
@app = Faraday::Adapter.new nil
@env = {:request_headers => {}}
end
def test_processes_nested_body
# assume params are out of order
regexes = [
/name\=\"a\"/,
/name=\"b\[c\]\"\; filename\=\"multipart_test\.rb\"/,
/name=\"b\[d\]\"/]
@env[:body] = {:a => 1, :b => {:c => Faraday::UploadIO.new(__FILE__, 'text/x-ruby'), :d => 2}}
@app.process_body_for_request @env
@env[:body].send(:ios).map(&:read).each do |io|
if re = regexes.detect { |r| io =~ r }
regexes.delete re
end
end
assert_equal [], regexes
assert_kind_of CompositeReadIO, @env[:body]
assert_equal "%s;boundary=%s" %
[Faraday::Adapter::MULTIPART_TYPE, Faraday::Adapter::DEFAULT_BOUNDARY],
@env[:request_headers]['Content-Type']
end
def test_processes_nil_body
@env[:body] = nil
@app.process_body_for_request @env
assert_nil @env[:body]
end
def test_processes_empty_body
@env[:body] = ''
@app.process_body_for_request @env
assert_equal '', @env[:body]
end
def test_processes_string_body
@env[:body] = 'abc'
@app.process_body_for_request @env
assert_equal 'abc', @env[:body]
end
end

View File

@ -1,9 +1,12 @@
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
require 'rack/utils'
Faraday::CompositeReadIO.send :attr_reader, :ios
class RequestMiddlewareTest < Faraday::TestCase
def setup
@conn = Faraday.new do |b|
b.request :multipart
b.request :url_encoded
b.request :json
b.adapter :test do |stub|
@ -57,4 +60,26 @@ class RequestMiddlewareTest < Faraday::TestCase
expected = { 'user' => {'name' => 'Mislav', 'web' => 'mislav.net'} }
assert_equal expected, Rack::Utils.parse_nested_query(response.body)
end
def test_multipart
# assume params are out of order
regexes = [
/name\=\"a\"/,
/name=\"b\[c\]\"\; filename\=\"request_middleware_test\.rb\"/,
/name=\"b\[d\]\"/]
payload = {:a => 1, :b => {:c => Faraday::UploadIO.new(__FILE__, 'text/x-ruby'), :d => 2}}
response = @conn.post('/echo', payload)
assert_kind_of Faraday::CompositeReadIO, response.body
assert_equal "multipart/form-data;boundary=%s" % Faraday::Request::Multipart::DEFAULT_BOUNDARY,
response.headers['Content-Type']
response.body.send(:ios).map(&:read).each do |io|
if re = regexes.detect { |r| io =~ r }
regexes.delete re
end
end
assert_equal [], regexes
end
end