diff --git a/lib/http/form_data/multipart.rb b/lib/http/form_data/multipart.rb index a667643..2724b2b 100644 --- a/lib/http/form_data/multipart.rb +++ b/lib/http/form_data/multipart.rb @@ -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 @@ -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 diff --git a/lib/http/form_data/multipart/param.rb b/lib/http/form_data/multipart/param.rb index 5972752..327ad2b 100644 --- a/lib/http/form_data/multipart/param.rb +++ b/lib/http/form_data/multipart/param.rb @@ -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] def self.coerce(data) params = [] diff --git a/spec/lib/http/form_data/multipart_spec.rb b/spec/lib/http/form_data/multipart_spec.rb index a6d3f55..dc8ee9d 100644 --- a/spec/lib/http/form_data/multipart_spec.rb +++ b/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 } } @@ -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}", @@ -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 diff --git a/spec/lib/http/form_data/urlencoded_spec.rb b/spec/lib/http/form_data/urlencoded_spec.rb index be25a38..02d1e21 100644 --- a/spec/lib/http/form_data/urlencoded_spec.rb +++ b/spec/lib/http/form_data/urlencoded_spec.rb @@ -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" }