Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support duplicate form names in multipart forms #32

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
12 changes: 9 additions & 3 deletions lib/http/form_data/multipart.rb
Expand Up @@ -16,10 +16,8 @@ class Multipart

# @param [#to_h, Hash] data form data key-value Hash
def initialize(data, boundary: self.class.generate_boundary)
parts = Param.coerce FormData.ensure_hash data

@boundary = boundary.to_s.freeze
@io = CompositeIO.new [*parts.flat_map { |part| [glue, part] }, tail]
@io = CompositeIO.new(parts(data).flat_map { |part| [glue, part] } << tail)
end

# Generates a string suitable for using as a boundary in multipart form
Expand Down Expand Up @@ -54,6 +52,14 @@ def glue
def tail
@tail ||= "--#{@boundary}--#{CRLF}"
end

def parts(data)
if data.is_a?(Array)
Param.coerce data
else
Param.coerce FormData.ensure_hash data
end
end
end
end
end
6 changes: 3 additions & 3 deletions lib/http/form_data/multipart/param.rb
Expand Up @@ -41,11 +41,11 @@ def initialize(name, value)
@io = CompositeIO.new [header, @part, footer]
end

# Flattens given `data` Hash into an array of `Param`'s.
# Nested array are unwinded.
# Flattens given `data` Hash or Array into an array of `Param`'s.
# Nested arrays are unwinded.
# Behavior is similar to `URL.encode_www_form`.
#
# @param [Hash] data
# @param [Array || Hash] data
# @return [Array<FormData::MultiPart::Param>]
def self.coerce(data)
params = []
Expand Down
42 changes: 40 additions & 2 deletions spec/lib/http/form_data/multipart_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true

RSpec.describe HTTP::FormData::Multipart do
subject(:form_data) { HTTP::FormData::Multipart.new params }
subject(:form_data) { described_class.new params }

let(:file) { HTTP::FormData::File.new fixture "the-http-gem.info" }
let(:params) { { :foo => :bar, :baz => file } }
Expand All @@ -17,7 +17,6 @@ def disposition(params)

it "properly generates multipart data" do
boundary_value = form_data.boundary

expect(form_data.to_s).to eq([
"--#{boundary_value}#{crlf}",
"#{disposition 'name' => 'foo'}#{crlf}",
Expand Down Expand Up @@ -87,6 +86,45 @@ def disposition(params)
].join)
end
end

# https://github.com/httprb/http/issues/663
context "when params is an Array of pairs" do
let(:params) do
[
["metadata", %(filename="first.txt")],
["file", HTTP::FormData::File.new(StringIO.new("uno"), :content_type => "plain/text", :filename => "abc")],
["metadata", %(filename="second.txt")],
["file", HTTP::FormData::File.new(StringIO.new("dos"), :content_type => "plain/text", :filename => "xyz")],
["metadata", %w[question=why question=not]]
]
end

it "allows duplicate param names and preserves given order" do
expect(form_data.to_s).to eq([
%(--#{form_data.boundary}\r\n),
%(Content-Disposition: form-data; name="metadata"\r\n),
%(\r\nfilename="first.txt"\r\n),
%(--#{form_data.boundary}\r\n),
%(Content-Disposition: form-data; name="file"; filename="abc"\r\n),
%(Content-Type: plain/text\r\n),
%(\r\nuno\r\n),
%(--#{form_data.boundary}\r\n),
%(Content-Disposition: form-data; name="metadata"\r\n),
%(\r\nfilename="second.txt"\r\n),
%(--#{form_data.boundary}\r\n),
%(Content-Disposition: form-data; name="file"; filename="xyz"\r\n),
%(Content-Type: plain/text\r\n),
%(\r\ndos\r\n),
%(--#{form_data.boundary}\r\n),
%(Content-Disposition: form-data; name="metadata"\r\n),
%(\r\nquestion=why\r\n),
%(--#{form_data.boundary}\r\n),
%(Content-Disposition: form-data; name="metadata"\r\n),
%(\r\nquestion=not\r\n),
%(--#{form_data.boundary}--\r\n)
].join)
end
end
end

describe "#size" do
Expand Down
6 changes: 6 additions & 0 deletions spec/lib/http/form_data/urlencoded_spec.rb
Expand Up @@ -4,6 +4,12 @@
let(:data) { { "foo[bar]" => "test" } }
subject(:form_data) { HTTP::FormData::Urlencoded.new data }

it "supports any Enumerables of pairs" do
form_data = described_class.new([%w[foo bar], ["foo", %w[baz moo]]])

expect(form_data.to_s).to eq("foo=bar&foo=baz&foo=moo")
end

describe "#content_type" do
subject { form_data.content_type }
it { is_expected.to eq "application/x-www-form-urlencoded" }
Expand Down