Skip to content
12 changes: 9 additions & 3 deletions lib/http/form_data/multipart.rb
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
2 changes: 2 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
# ...rather than:
# # => "be bigger than 2"
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
# This option will remove the default 200 character limit for RSpec diffs
expectations.max_formatted_output_length = nil
end

config.mock_with :rspec do |mocks|
Expand Down