diff --git a/README.md b/README.md index a2a266f..feb0356 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,14 @@ socket << "\r\n" socket << form.to_s ``` +It's also possible to create a non-file part with Content-Type: + +``` ruby +form = HTTP::FormData.create({ + :username => HTTP::FormData::Part.new('{"a": 1}', content_type: 'application/json'), + :avatar_file => HTTP::FormData::File.new("/home/ixti/avatar.png") +}) +``` ## Supported Ruby Versions diff --git a/lib/http/form_data.rb b/lib/http/form_data.rb index 73749af..1a26906 100644 --- a/lib/http/form_data.rb +++ b/lib/http/form_data.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "http/form_data/part" require "http/form_data/file" require "http/form_data/multipart" require "http/form_data/urlencoded" diff --git a/lib/http/form_data/file.rb b/lib/http/form_data/file.rb index adcf5de..3d4da02 100644 --- a/lib/http/form_data/file.rb +++ b/lib/http/form_data/file.rb @@ -18,12 +18,10 @@ module FormData # @example Usage with pathname # # FormData::File.new "/home/ixti/avatar.png" - class File + class File < Part # Default MIME type DEFAULT_MIME = "application/octet-stream" - attr_reader :mime_type, :filename - # @see DEFAULT_MIME # @param [String, StringIO, File] file_or_io Filename or IO instance. # @param [#to_h] opts diff --git a/lib/http/form_data/multipart/param.rb b/lib/http/form_data/multipart/param.rb index 32b9c90..fc63e06 100644 --- a/lib/http/form_data/multipart/param.rb +++ b/lib/http/form_data/multipart/param.rb @@ -5,19 +5,27 @@ module FormData class Multipart # Utility class to represent multi-part chunks class Param - CONTENT_DISPOSITION = "" - # @param [#to_s] name - # @param [FormData::File, #to_s] value + # @param [FormData::File, FormData::Part, #to_s] value def initialize(name, value) - @name = name.to_s - @value = value - @header = "Content-Disposition: form-data; name=#{@name.inspect}" + @name = name.to_s + + @part = + if value.is_a?(FormData::Part) + value + else + FormData::Part.new(value) + end - return unless file? + parameters = { :name => @name } + parameters[:filename] = @part.filename if @part.filename + parameters = parameters.map { |k, v| "#{k}=#{v.inspect}" }.join("; ") - @header = "#{@header}; filename=#{value.filename.inspect}#{CRLF}" \ - "Content-Type: #{value.mime_type}" + @header = "Content-Disposition: form-data; #{parameters}" + + return unless @part.mime_type + + @header += "#{CRLF}Content-Type: #{@part.mime_type}" end # Returns body part with headers and data. @@ -37,20 +45,14 @@ def initialize(name, value) # # @return [String] def to_s - "#{@header}#{CRLF * 2}#{@value}" + "#{@header}#{CRLF * 2}#{@part}" end # Calculates size of a part (headers + body). # # @return [Fixnum] def size - size = @header.bytesize + (CRLF.bytesize * 2) - - if file? - size + @value.size - else - size + @value.to_s.bytesize - end + @header.bytesize + (CRLF.bytesize * 2) + @part.size end # Flattens given `data` Hash into an array of `Param`'s. @@ -70,15 +72,6 @@ def self.coerce(data) params end - - private - - # Tells whenever value is a {FormData::File} or not. - # - # @return [Boolean] - def file? - @value.is_a? FormData::File - end end end end diff --git a/lib/http/form_data/part.rb b/lib/http/form_data/part.rb new file mode 100644 index 0000000..89442aa --- /dev/null +++ b/lib/http/form_data/part.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module HTTP + module FormData + class Part + attr_reader :mime_type, :filename + + # @param [#to_s] body + # @param [String] :mime_type + # @param [String] :filename + def initialize(body, mime_type: nil, filename: nil) + @body = body.to_s + @mime_type = mime_type + @filename = filename + end + + # Returns content size. + # + # @return [Integer] + def size + @body.bytesize + end + + # Returns content of a file of IO. + # + # @return [String] + def to_s + @body + end + end + end +end diff --git a/spec/lib/http/form_data/multipart_spec.rb b/spec/lib/http/form_data/multipart_spec.rb index 8571fab..bd95614 100644 --- a/spec/lib/http/form_data/multipart_spec.rb +++ b/spec/lib/http/form_data/multipart_spec.rb @@ -41,5 +41,21 @@ def disposition(params) "--#{boundary_value}--" ].join("") end + + context "with filename set to nil" do + let(:part) { HTTP::FormData::Part.new("s", :filename => nil) } + let(:form_data) { HTTP::FormData::Multipart.new(:foo => part) } + + it "doesn't include a filename" do + boundary_value = form_data.content_type[/(#{boundary})$/, 1] + + expect(form_data.to_s).to eq [ + "--#{boundary_value}#{crlf}", + "#{disposition 'name' => 'foo'}#{crlf}", + "#{crlf}s#{crlf}", + "--#{boundary_value}--" + ].join("") + end + end end end diff --git a/spec/lib/http/form_data/part_spec.rb b/spec/lib/http/form_data/part_spec.rb new file mode 100644 index 0000000..b2e619b --- /dev/null +++ b/spec/lib/http/form_data/part_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +RSpec.describe HTTP::FormData::Part do + let(:body) { "" } + let(:opts) { {} } + + describe "#size" do + subject { described_class.new(body, opts).size } + + context "when body given as a String" do + let(:body) { "привет мир!" } + it { is_expected.to eq 20 } + end + end + + describe "#to_s" do + subject { described_class.new(body, opts).to_s } + + context "when body given as String" do + let(:body) { "привет мир!" } + it { is_expected.to eq "привет мир!" } + end + end + + describe "#filename" do + subject { described_class.new(body, opts).filename } + + it { is_expected.to eq nil } + + context "when it was given with options" do + let(:opts) { { :filename => "foobar.txt" } } + it { is_expected.to eq "foobar.txt" } + end + end + + describe "#mime_type" do + subject { described_class.new(body, opts).mime_type } + + it { is_expected.to eq nil } + + context "when it was given with options" do + let(:opts) { { :mime_type => "application/json" } } + it { is_expected.to eq "application/json" } + end + end +end