http-form_data 1.0.3 → 2.0.0.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7a6ca603a4fcbd166b6144c239780cfc3b5d536c
4
- data.tar.gz: a5ba2c8cd037579a827425a29d25aa6ffe5c385d
3
+ metadata.gz: 89b84a117e41ca60f5962948d9ce6b4932497080
4
+ data.tar.gz: 93238b392e0c1817768d6f24d9d44cb9368b2c30
5
5
  SHA512:
6
- metadata.gz: f448941c9efd96ba33381419853940a1eb4059160d516f302d02656fb7b3109402c8e6511bbe28ea885e586d8086c011f26b538b33ea5152cbeff6a141a7e0bd
7
- data.tar.gz: d4a2cd9d878a62f6b25e38f6656056c414576944587553096c6f3a4d6ce68f6e5b8d7f3179081069e40f29aef614befe3f4ec2fe007cd49e4bad06ebaba57074
6
+ metadata.gz: 70d163f00ea75806e0268fed63956f554f706ce869bf22bffb13e1aa8f728f7b69b73e8a1ede97bf05efbbd85a62609d1ee4df7eddb81120b95e3f8f88731d36
7
+ data.tar.gz: be03a767b75746a513e78a56ac7dace8da976da47b35b5a3ba15c149c2807e4d2c8a3a06d107e09cd566703d34471cde4b47316a636f6ffd2fe8ce8a18357936
data/.travis.yml CHANGED
@@ -15,9 +15,7 @@ env: JRUBY_OPTS="$JRUBY_OPTS --debug"
15
15
 
16
16
  rvm:
17
17
  # Include JRuby first because it takes the longest
18
- - jruby-1.7.27
19
18
  - jruby-9.1.8.0
20
- - 1.9.3
21
19
  - 2.0.0
22
20
  - 2.1
23
21
  - 2.2
@@ -26,6 +24,9 @@ rvm:
26
24
 
27
25
  matrix:
28
26
  fast_finish: true
27
+ include:
28
+ - rvm: 2.4.1
29
+ env: SUITE="rubocop"
29
30
 
30
31
  branches:
31
32
  only:
data/CHANGES.md CHANGED
@@ -1,18 +1,18 @@
1
- ## 1.0.2 (2017-05-18)
1
+ ## 2.0.0-pre1 (2017-05-10)
2
2
 
3
- * [#16](https://github.com/httprb/form_data/issues/16)
4
- Fix ruby < 2.0.0 support.
5
- [@ixti][]
3
+ * [#12](https://github.com/httprb/form_data.rb/pull/12)
4
+ Enable form data streaming.
5
+ [@janko-m][]
6
6
 
7
7
 
8
8
  ## 1.0.2 (2017-05-08)
9
9
 
10
10
  * [#5](https://github.com/httprb/form_data.rb/issues/5)
11
- Allow setting Content-Type non-file parts.
11
+ Allow setting Content-Type non-file parts
12
12
  [@abotalov][]
13
13
 
14
14
  * [#6](https://github.com/httprb/form_data.rb/issues/6)
15
- Creation of file parts without filename.
15
+ Creation of file parts without filename
16
16
  [@abotalov][]
17
17
 
18
18
  * [#11](https://github.com/httprb/form_data.rb/pull/11)
@@ -43,3 +43,4 @@
43
43
 
44
44
  [@ixti]: https://github.com/ixti
45
45
  [@abotalov]: https://github.com/abotalov
46
+ [@janko-m]: https://github.com/janko-m
data/Gemfile CHANGED
@@ -4,8 +4,22 @@ source "https://rubygems.org"
4
4
 
5
5
  gem "rake"
6
6
 
7
+ group :development do
8
+ gem "guard"
9
+ gem "guard-rspec", :require => false
10
+ gem "pry"
11
+ end
12
+
7
13
  group :test do
8
- gem "rspec", "~> 3.1"
14
+ gem "coveralls"
15
+ gem "rspec", "~> 3.1"
16
+ gem "rubocop", "= 0.48.1"
17
+ gem "simplecov", ">= 0.9"
18
+ end
19
+
20
+ group :doc do
21
+ gem "redcarpet"
22
+ gem "yard"
9
23
  end
10
24
 
11
25
  # Specify your gem's dependencies in form_data.gemspec
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # FormData
1
+ # HTTP::FormData
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/http-form_data.png)](http://rubygems.org/gems/http-form_data)
4
4
  [![Build Status](https://secure.travis-ci.org/httprb/form_data.rb.png?branch=master)](http://travis-ci.org/httprb/form_data.rb)
@@ -58,13 +58,10 @@ form = HTTP::FormData.create({
58
58
  This library aims to support and is [tested against][ci] the following Ruby
59
59
  versions:
60
60
 
61
- * Ruby 1.9.x
62
- * Ruby 2.0.x
63
61
  * Ruby 2.1.x
64
62
  * Ruby 2.2.x
65
63
  * Ruby 2.3.x
66
64
  * Ruby 2.4.x
67
- * JRuby 1.7.x
68
65
  * JRuby 9.1.x.x
69
66
 
70
67
  If something doesn't work on one of these versions, it's a bug.
data/Rakefile CHANGED
@@ -5,4 +5,20 @@ require "bundler/gem_tasks"
5
5
  require "rspec/core/rake_task"
6
6
  RSpec::Core::RakeTask.new
7
7
 
8
- task :default => :spec
8
+ begin
9
+ require "rubocop/rake_task"
10
+ RuboCop::RakeTask.new
11
+ rescue LoadError
12
+ task :rubocop do
13
+ $stderr.puts "RuboCop is disabled"
14
+ end
15
+ end
16
+
17
+ if ENV["CI"].nil?
18
+ task :default => %i[spec rubocop]
19
+ else
20
+ case ENV["SUITE"]
21
+ when "rubocop" then task :default => :rubocop
22
+ else task :default => :spec
23
+ end
24
+ end
@@ -18,8 +18,6 @@ Gem::Specification.new do |spec|
18
18
  > `multipart/form-data` types.
19
19
  DESC
20
20
 
21
- spec.required_ruby_version = ">= 1.9"
22
-
23
21
  spec.files = `git ls-files -z`.split("\x0")
24
22
  spec.executables = spec.files.grep(%r{^bin\/}).map { |f| File.basename(f) }
25
23
  spec.test_files = spec.files.grep(%r{^(test|spec|features)\/})
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module HTTP
6
+ module FormData
7
+ # Provides IO interface across multiple IO objects.
8
+ class CompositeIO
9
+ # @param [Array<IO>] ios Array of IO objects
10
+ def initialize(ios)
11
+ @index = 0
12
+ @buffer = String.new
13
+ @ios = ios.map do |io|
14
+ if io.is_a?(String)
15
+ StringIO.new(io)
16
+ elsif io.respond_to?(:read)
17
+ io
18
+ else
19
+ raise ArgumentError,
20
+ "#{io.inspect} is neither a String nor an IO object"
21
+ end
22
+ end
23
+ end
24
+
25
+ # Reads and returns partial content acrosss multiple IO objects.
26
+ #
27
+ # @param [Integer] length Number of bytes to retrieve
28
+ # @param [String] outbuf String to be replaced with retrieved data
29
+ #
30
+ # @return [String, nil]
31
+ def read(length = nil, outbuf = nil)
32
+ outbuf = outbuf.to_s.replace("")
33
+
34
+ while current_io
35
+ current_io.read(length, @buffer)
36
+ outbuf << @buffer
37
+
38
+ if length
39
+ length -= @buffer.length
40
+ break if length.zero?
41
+ end
42
+
43
+ advance_io
44
+ end
45
+
46
+ outbuf unless length && outbuf.empty?
47
+ end
48
+
49
+ # Returns sum of all IO sizes.
50
+ def size
51
+ @size ||= @ios.map(&:size).inject(0, :+)
52
+ end
53
+
54
+ # Rewinds all IO objects and set cursor to the first IO object.
55
+ def rewind
56
+ @ios.each(&:rewind)
57
+ @index = 0
58
+ end
59
+
60
+ private
61
+
62
+ # Returns IO object under the cursor.
63
+ def current_io
64
+ @ios[@index]
65
+ end
66
+
67
+ # Advances cursor to the next IO object.
68
+ def advance_io
69
+ @index += 1
70
+ end
71
+ end
72
+ end
73
+ end
@@ -26,15 +26,14 @@ module HTTP
26
26
  alias mime_type content_type
27
27
 
28
28
  # @see DEFAULT_MIME
29
- # @param [String, StringIO, File] file_or_io Filename or IO instance.
29
+ # @param [String, Pathname, IO] path_or_io Filename or IO instance.
30
30
  # @param [#to_h] opts
31
31
  # @option opts [#to_s] :content_type (DEFAULT_MIME)
32
32
  # Value of Content-Type header
33
33
  # @option opts [#to_s] :filename
34
- # When `file` is a String, defaults to basename of `file`.
35
- # When `file` is a File, defaults to basename of `file`.
36
- # When `file` is a StringIO, defaults to `"stream-{object_id}"`
37
- def initialize(file_or_io, opts = {})
34
+ # When `path_or_io` is a String, Pathname or File, defaults to basename.
35
+ # When `path_or_io` is a IO, defaults to `"stream-{object_id}"`.
36
+ def initialize(path_or_io, opts = {})
38
37
  opts = FormData.ensure_hash(opts)
39
38
 
40
39
  if opts.key? :mime_type
@@ -42,40 +41,28 @@ module HTTP
42
41
  opts[:content_type] = opts[:mime_type]
43
42
  end
44
43
 
45
- @file_or_io = file_or_io
44
+ @io = make_io(path_or_io)
46
45
  @content_type = opts.fetch(:content_type, DEFAULT_MIME).to_s
47
- @filename = opts.fetch :filename do
48
- case file_or_io
49
- when String then ::File.basename file_or_io
50
- when ::File then ::File.basename file_or_io.path
51
- else "stream-#{file_or_io.object_id}"
52
- end
53
- end
46
+ @filename = opts.fetch(:filename, filename_for(@io))
54
47
  end
55
48
 
56
- # Returns content size.
57
- #
58
- # @return [Integer]
59
- def size
60
- with_io(&:size)
61
- end
49
+ private
62
50
 
63
- # Returns content of a file of IO.
64
- #
65
- # @return [String]
66
- def to_s
67
- with_io(&:read)
51
+ def make_io(path_or_io)
52
+ if path_or_io.is_a?(String)
53
+ ::File.open(path_or_io, :binmode => true)
54
+ elsif defined?(Pathname) && path_or_io.is_a?(Pathname)
55
+ path_or_io.open(:binmode => true)
56
+ else
57
+ path_or_io
58
+ end
68
59
  end
69
60
 
70
- private
71
-
72
- # @yield [io] Gives IO instance to the block
73
- # @return result of yielded block
74
- def with_io
75
- if @file_or_io.is_a?(::File) || @file_or_io.is_a?(StringIO)
76
- yield @file_or_io
61
+ def filename_for(io)
62
+ if io.respond_to?(:path)
63
+ ::File.basename io.path
77
64
  else
78
- ::File.open(@file_or_io, "rb") { |io| yield io }
65
+ "stream-#{io.object_id}"
79
66
  end
80
67
  end
81
68
  end
@@ -3,23 +3,31 @@
3
3
  require "securerandom"
4
4
 
5
5
  require "http/form_data/multipart/param"
6
+ require "http/form_data/readable"
7
+ require "http/form_data/composite_io"
6
8
 
7
9
  module HTTP
8
10
  module FormData
9
11
  # `multipart/form-data` form data.
10
12
  class Multipart
13
+ include Readable
14
+
15
+ attr_reader :boundary
16
+
11
17
  # @param [#to_h, Hash] data form data key-value Hash
12
- def initialize(data)
13
- @parts = Param.coerce FormData.ensure_hash data
14
- @boundary = (Array.new(21, "-") << SecureRandom.hex(21)).join("")
15
- @content_length = nil
18
+ def initialize(data, boundary: self.class.generate_boundary)
19
+ parts = Param.coerce FormData.ensure_hash data
20
+
21
+ @boundary = boundary.to_s.freeze
22
+ @io = CompositeIO.new [*parts.flat_map { |part| [glue, part] }, tail]
16
23
  end
17
24
 
18
- # Returns content to be used for HTTP request body.
25
+ # Generates a string suitable for using as a boundary in multipart form
26
+ # data.
19
27
  #
20
28
  # @return [String]
21
- def to_s
22
- head + @parts.map(&:to_s).join(glue) + tail
29
+ def self.generate_boundary
30
+ ("-" * 21) << SecureRandom.hex(21)
23
31
  end
24
32
 
25
33
  # Returns MIME type to be used for HTTP request `Content-Type` header.
@@ -33,31 +41,18 @@ module HTTP
33
41
  # `Content-Length` header.
34
42
  #
35
43
  # @return [Integer]
36
- def content_length
37
- unless @content_length
38
- @content_length = head.bytesize + tail.bytesize
39
- @content_length += @parts.map(&:size).reduce(:+)
40
- @content_length += (glue.bytesize * (@parts.count - 1))
41
- end
42
-
43
- @content_length
44
- end
44
+ alias content_length size
45
45
 
46
46
  private
47
47
 
48
- # @return [String]
49
- def head
50
- @head ||= "--#{@boundary}#{CRLF}"
51
- end
52
-
53
48
  # @return [String]
54
49
  def glue
55
- @glue ||= "#{CRLF}--#{@boundary}#{CRLF}"
50
+ @glue ||= "--#{@boundary}#{CRLF}"
56
51
  end
57
52
 
58
53
  # @return [String]
59
54
  def tail
60
- @tail ||= "#{CRLF}--#{@boundary}--"
55
+ @tail ||= "--#{@boundary}--"
61
56
  end
62
57
  end
63
58
  end
@@ -1,34 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "http/form_data/readable"
4
+ require "http/form_data/composite_io"
5
+
3
6
  module HTTP
4
7
  module FormData
5
8
  class Multipart
6
9
  # Utility class to represent multi-part chunks
7
10
  class Param
8
- # @param [#to_s] name
9
- # @param [FormData::File, FormData::Part, #to_s] value
10
- def initialize(name, value)
11
- @name = name.to_s
12
-
13
- @part =
14
- if value.is_a?(FormData::Part)
15
- value
16
- else
17
- FormData::Part.new(value)
18
- end
19
-
20
- parameters = { :name => @name }
21
- parameters[:filename] = @part.filename if @part.filename
22
- parameters = parameters.map { |k, v| "#{k}=#{v.inspect}" }.join("; ")
11
+ include Readable
23
12
 
24
- @header = "Content-Disposition: form-data; #{parameters}"
25
-
26
- return unless @part.content_type
27
-
28
- @header += "#{CRLF}Content-Type: #{@part.content_type}"
29
- end
30
-
31
- # Returns body part with headers and data.
13
+ # Initializes body part with headers and data.
32
14
  #
33
15
  # @example With {FormData::File} value
34
16
  #
@@ -44,15 +26,19 @@ module HTTP
44
26
  # ixti
45
27
  #
46
28
  # @return [String]
47
- def to_s
48
- "#{@header}#{CRLF * 2}#{@part}"
49
- end
29
+ # @param [#to_s] name
30
+ # @param [FormData::File, FormData::Part, #to_s] value
31
+ def initialize(name, value)
32
+ @name = name.to_s
50
33
 
51
- # Calculates size of a part (headers + body).
52
- #
53
- # @return [Integer]
54
- def size
55
- @header.bytesize + (CRLF.bytesize * 2) + @part.size
34
+ @part =
35
+ if value.is_a?(FormData::Part)
36
+ value
37
+ else
38
+ FormData::Part.new(value)
39
+ end
40
+
41
+ @io = CompositeIO.new [header, @part, footer]
56
42
  end
57
43
 
58
44
  # Flattens given `data` Hash into an array of `Param`'s.
@@ -72,6 +58,34 @@ module HTTP
72
58
 
73
59
  params
74
60
  end
61
+
62
+ private
63
+
64
+ def header
65
+ header = String.new
66
+ header << "Content-Disposition: form-data; #{parameters}#{CRLF}"
67
+ header << "Content-Type: #{content_type}#{CRLF}" if content_type
68
+ header << CRLF
69
+ header
70
+ end
71
+
72
+ def parameters
73
+ parameters = { :name => @name }
74
+ parameters[:filename] = filename if filename
75
+ parameters.map { |k, v| "#{k}=#{v.inspect}" }.join("; ")
76
+ end
77
+
78
+ def content_type
79
+ @part.content_type
80
+ end
81
+
82
+ def filename
83
+ @part.filename
84
+ end
85
+
86
+ def footer
87
+ CRLF.dup
88
+ end
75
89
  end
76
90
  end
77
91
  end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "stringio"
4
+
5
+ require "http/form_data/readable"
6
+
3
7
  module HTTP
4
8
  module FormData
5
9
  # Represents a body part of multipart/form-data request.
@@ -9,30 +13,17 @@ module HTTP
9
13
  # body = "Message"
10
14
  # FormData::Part.new body, :content_type => 'foobar.txt; charset="UTF-8"'
11
15
  class Part
12
- attr_reader :content_type, :filename
16
+ include Readable
13
17
 
14
- # @param body [#to_s]
15
- # @param opts [Hash]
16
- # @option opts [String] :content_type Value of Content-Type header
17
- # @option opts [String] :filename Value of filename parameter
18
- def initialize(body, opts = {})
19
- @body = body.to_s
20
- @content_type = opts[:content_type]
21
- @filename = opts[:filename]
22
- end
23
-
24
- # Returns content size.
25
- #
26
- # @return [Integer]
27
- def size
28
- @body.bytesize
29
- end
18
+ attr_reader :content_type, :filename
30
19
 
31
- # Returns content of a file of IO.
32
- #
33
- # @return [String]
34
- def to_s
35
- @body
20
+ # @param [#to_s] body
21
+ # @param [String] content_type Value of Content-Type header
22
+ # @param [String] filename Value of filename parameter
23
+ def initialize(body, content_type: nil, filename: nil)
24
+ @io = StringIO.new(body.to_s)
25
+ @content_type = content_type
26
+ @filename = filename
36
27
  end
37
28
  end
38
29
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ module FormData
5
+ # Common behaviour for objects defined by an IO object.
6
+ module Readable
7
+ # Returns IO content.
8
+ #
9
+ # @return [String]
10
+ def to_s
11
+ rewind
12
+ read
13
+ end
14
+
15
+ # Reads and returns part of IO content.
16
+ #
17
+ # @param [Integer] length Number of bytes to retrieve
18
+ # @param [String] outbuf String to be replaced with retrieved data
19
+ #
20
+ # @return [String, nil]
21
+ def read(length = nil, outbuf = nil)
22
+ @io.read(length, outbuf)
23
+ end
24
+
25
+ # Returns IO size.
26
+ #
27
+ # @return [Integer]
28
+ def size
29
+ @io.size
30
+ end
31
+
32
+ # Rewinds the IO.
33
+ def rewind
34
+ @io.rewind
35
+ end
36
+ end
37
+ end
38
+ end
@@ -3,6 +3,6 @@
3
3
  module HTTP
4
4
  module FormData
5
5
  # Gem version.
6
- VERSION = "1.0.3"
6
+ VERSION = "2.0.0.pre1"
7
7
  end
8
8
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe HTTP::FormData::CompositeIO do
4
+ subject(:composite_io) { HTTP::FormData::CompositeIO.new(ios) }
5
+
6
+ let(:ios) { ["Hello", " ", "", "world", "!"].map { |s| StringIO.new(s) } }
7
+
8
+ describe "#initialize" do
9
+ it "accepts IOs and strings" do
10
+ io = HTTP::FormData::CompositeIO.new(["Hello ", StringIO.new("world!")])
11
+ expect(io.read).to eq "Hello world!"
12
+ end
13
+
14
+ it "fails if an IO is neither a String nor an IO" do
15
+ expect { HTTP::FormData::CompositeIO.new %i[hello world] }
16
+ .to raise_error(ArgumentError)
17
+ end
18
+ end
19
+
20
+ describe "#read" do
21
+ it "reads all data" do
22
+ expect(composite_io.read).to eq "Hello world!"
23
+ end
24
+
25
+ it "reads partial data" do
26
+ expect(composite_io.read(3)).to eq "Hel"
27
+ expect(composite_io.read(2)).to eq "lo"
28
+ expect(composite_io.read(1)).to eq " "
29
+ expect(composite_io.read(6)).to eq "world!"
30
+ end
31
+
32
+ it "returns empty string when no data was retrieved" do
33
+ composite_io.read
34
+ expect(composite_io.read).to eq ""
35
+ end
36
+
37
+ it "returns nil when no partial data was retrieved" do
38
+ composite_io.read
39
+ expect(composite_io.read(3)).to eq nil
40
+ end
41
+
42
+ it "reads partial data with a buffer" do
43
+ outbuf = String.new
44
+ expect(composite_io.read(3, outbuf)).to eq "Hel"
45
+ expect(composite_io.read(2, outbuf)).to eq "lo"
46
+ expect(composite_io.read(1, outbuf)).to eq " "
47
+ expect(composite_io.read(6, outbuf)).to eq "world!"
48
+ end
49
+
50
+ it "fills the buffer with retrieved content" do
51
+ outbuf = String.new
52
+ composite_io.read(3, outbuf)
53
+ expect(outbuf).to eq "Hel"
54
+ composite_io.read(2, outbuf)
55
+ expect(outbuf).to eq "lo"
56
+ composite_io.read(1, outbuf)
57
+ expect(outbuf).to eq " "
58
+ composite_io.read(6, outbuf)
59
+ expect(outbuf).to eq "world!"
60
+ end
61
+
62
+ it "returns nil when no partial data was retrieved with a buffer" do
63
+ outbuf = String.new("content")
64
+ composite_io.read
65
+ expect(composite_io.read(3, outbuf)).to eq nil
66
+ expect(outbuf).to eq ""
67
+ end
68
+ end
69
+
70
+ describe "#rewind" do
71
+ it "rewinds all IOs" do
72
+ composite_io.read
73
+ composite_io.rewind
74
+ expect(composite_io.read).to eq "Hello world!"
75
+ end
76
+ end
77
+
78
+ describe "#size" do
79
+ it "returns sum of all IO sizes" do
80
+ expect(composite_io.size).to eq 12
81
+ end
82
+
83
+ it "returns 0 when there are no IOs" do
84
+ empty_composite_io = HTTP::FormData::CompositeIO.new []
85
+ expect(empty_composite_io.size).to eq 0
86
+ end
87
+ end
88
+ end
@@ -1,5 +1,5 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
2
+ # coding: utf-8
3
3
 
4
4
  RSpec.describe HTTP::FormData::File do
5
5
  let(:opts) { nil }
@@ -12,9 +12,9 @@ RSpec.describe HTTP::FormData::File do
12
12
  it { is_expected.to eq fixture("the-http-gem.info").size }
13
13
  end
14
14
 
15
- context "when file given as StringIO" do
16
- let(:file) { StringIO.new "привет мир!" }
17
- it { is_expected.to eq 20 }
15
+ context "when file given as a Pathname" do
16
+ let(:file) { fixture("the-http-gem.info") }
17
+ it { is_expected.to eq fixture("the-http-gem.info").size }
18
18
  end
19
19
 
20
20
  context "when file given as File" do
@@ -22,6 +22,11 @@ RSpec.describe HTTP::FormData::File do
22
22
  after { file.close }
23
23
  it { is_expected.to eq fixture("the-http-gem.info").size }
24
24
  end
25
+
26
+ context "when file given as IO" do
27
+ let(:file) { StringIO.new "привет мир!" }
28
+ it { is_expected.to eq 20 }
29
+ end
25
30
  end
26
31
 
27
32
  describe "#to_s" do
@@ -32,16 +37,91 @@ RSpec.describe HTTP::FormData::File do
32
37
  it { is_expected.to eq fixture("the-http-gem.info").read(:mode => "rb") }
33
38
  end
34
39
 
35
- context "when file given as StringIO" do
40
+ context "when file given as a Pathname" do
41
+ let(:file) { fixture("the-http-gem.info") }
42
+ it { is_expected.to eq fixture("the-http-gem.info").read(:mode => "rb") }
43
+ end
44
+
45
+ context "when file given as File" do
46
+ let(:file) { fixture("the-http-gem.info").open("rb") }
47
+ after { file.close }
48
+ it { is_expected.to eq fixture("the-http-gem.info").read(:mode => "rb") }
49
+ end
50
+
51
+ context "when file given as IO" do
36
52
  let(:file) { StringIO.new "привет мир!" }
37
53
  it { is_expected.to eq "привет мир!" }
38
54
  end
55
+ end
56
+
57
+ describe "#read" do
58
+ subject { described_class.new(file, opts).read }
59
+
60
+ context "when file given as a String" do
61
+ let(:file) { fixture("the-http-gem.info").to_s }
62
+ it { is_expected.to eq fixture("the-http-gem.info").read(:mode => "rb") }
63
+ end
64
+
65
+ context "when file given as a Pathname" do
66
+ let(:file) { fixture("the-http-gem.info") }
67
+ it { is_expected.to eq fixture("the-http-gem.info").read(:mode => "rb") }
68
+ end
39
69
 
40
70
  context "when file given as File" do
41
71
  let(:file) { fixture("the-http-gem.info").open("rb") }
42
72
  after { file.close }
43
73
  it { is_expected.to eq fixture("the-http-gem.info").read(:mode => "rb") }
44
74
  end
75
+
76
+ context "when file given as IO" do
77
+ let(:file) { StringIO.new "привет мир!" }
78
+ it { is_expected.to eq "привет мир!" }
79
+ end
80
+ end
81
+
82
+ describe "#rewind" do
83
+ subject { described_class.new(file, opts) }
84
+
85
+ context "when file given as a String" do
86
+ let(:file) { fixture("the-http-gem.info").to_s }
87
+
88
+ it "rewinds the underlying IO object" do
89
+ content = subject.read
90
+ subject.rewind
91
+ expect(subject.read).to eq content
92
+ end
93
+ end
94
+
95
+ context "when file given as a Pathname" do
96
+ let(:file) { fixture("the-http-gem.info") }
97
+
98
+ it "rewinds the underlying IO object" do
99
+ content = subject.read
100
+ subject.rewind
101
+ expect(subject.read).to eq content
102
+ end
103
+ end
104
+
105
+ context "when file given as File" do
106
+ let(:file) { fixture("the-http-gem.info").open("rb") }
107
+ after { file.close }
108
+
109
+ it "rewinds the underlying IO object" do
110
+ content = subject.read
111
+ subject.rewind
112
+ expect(subject.read).to eq content
113
+ end
114
+ end
115
+
116
+ context "when file given as IO" do
117
+ let(:file) { StringIO.new "привет мир!" }
118
+
119
+ it "rewinds the underlying IO object" do
120
+ content = subject.read
121
+ subject.rewind
122
+ expect(subject.read).to eq content
123
+ end
124
+ end
45
125
  end
46
126
 
47
127
  describe "#filename" do
@@ -58,10 +138,10 @@ RSpec.describe HTTP::FormData::File do
58
138
  end
59
139
  end
60
140
 
61
- context "when file given as StringIO" do
62
- let(:file) { StringIO.new }
141
+ context "when file given as a Pathname" do
142
+ let(:file) { fixture("the-http-gem.info") }
63
143
 
64
- it { is_expected.to eq "stream-#{file.object_id}" }
144
+ it { is_expected.to eq ::File.basename file }
65
145
 
66
146
  context "and filename given with options" do
67
147
  let(:opts) { { :filename => "foobar.txt" } }
@@ -80,6 +160,17 @@ RSpec.describe HTTP::FormData::File do
80
160
  it { is_expected.to eq "foobar.txt" }
81
161
  end
82
162
  end
163
+
164
+ context "when file given as IO" do
165
+ let(:file) { StringIO.new }
166
+
167
+ it { is_expected.to eq "stream-#{file.object_id}" }
168
+
169
+ context "and filename given with options" do
170
+ let(:opts) { { :filename => "foobar.txt" } }
171
+ it { is_expected.to eq "foobar.txt" }
172
+ end
173
+ end
83
174
  end
84
175
 
85
176
  describe "#content_type" do
@@ -6,19 +6,6 @@ RSpec.describe HTTP::FormData::Multipart do
6
6
  let(:boundary) { /-{21}[a-f0-9]{42}/ }
7
7
  subject(:form_data) { HTTP::FormData::Multipart.new params }
8
8
 
9
- describe "#content_type" do
10
- subject { form_data.content_type }
11
-
12
- let(:content_type) { %r{^multipart\/form-data; boundary=#{boundary}$} }
13
-
14
- it { is_expected.to match(content_type) }
15
- end
16
-
17
- describe "#content_length" do
18
- subject { form_data.content_length }
19
- it { is_expected.to eq form_data.to_s.bytesize }
20
- end
21
-
22
9
  describe "#to_s" do
23
10
  def disposition(params)
24
11
  params = params.map { |k, v| "#{k}=#{v.inspect}" }.join("; ")
@@ -36,14 +23,31 @@ RSpec.describe HTTP::FormData::Multipart do
36
23
  "#{crlf}bar#{crlf}",
37
24
  "--#{boundary_value}#{crlf}",
38
25
  "#{disposition 'name' => 'baz', 'filename' => file.filename}#{crlf}",
39
- "Content-Type: #{file.mime_type}#{crlf}",
26
+ "Content-Type: #{file.content_type}#{crlf}",
40
27
  "#{crlf}#{file}#{crlf}",
41
28
  "--#{boundary_value}--"
42
29
  ].join("")
43
30
  end
44
31
 
32
+ context "with user-defined boundary" do
33
+ let(:form_data) { HTTP::FormData::Multipart.new params, boundary: "my-boundary" }
34
+
35
+ it "uses the given boundary" do
36
+ expect(form_data.to_s).to eq [
37
+ "--my-boundary#{crlf}",
38
+ "#{disposition 'name' => 'foo'}#{crlf}",
39
+ "#{crlf}bar#{crlf}",
40
+ "--my-boundary#{crlf}",
41
+ "#{disposition 'name' => 'baz', 'filename' => file.filename}#{crlf}",
42
+ "Content-Type: #{file.content_type}#{crlf}",
43
+ "#{crlf}#{file}#{crlf}",
44
+ "--my-boundary--"
45
+ ].join("")
46
+ end
47
+ end
48
+
45
49
  context "with filename set to nil" do
46
- let(:part) { HTTP::FormData::Part.new("s", :filename => nil) }
50
+ let(:part) { HTTP::FormData::Part.new("s", :content_type => "mime/type") }
47
51
  let(:form_data) { HTTP::FormData::Multipart.new(:foo => part) }
48
52
 
49
53
  it "doesn't include a filename" do
@@ -52,10 +56,88 @@ RSpec.describe HTTP::FormData::Multipart do
52
56
  expect(form_data.to_s).to eq [
53
57
  "--#{boundary_value}#{crlf}",
54
58
  "#{disposition 'name' => 'foo'}#{crlf}",
59
+ "Content-Type: #{part.content_type}#{crlf}",
55
60
  "#{crlf}s#{crlf}",
56
61
  "--#{boundary_value}--"
57
62
  ].join("")
58
63
  end
59
64
  end
65
+
66
+ context "with content type set to nil" do
67
+ let(:part) { HTTP::FormData::Part.new("s") }
68
+ let(:form_data) { HTTP::FormData::Multipart.new(:foo => part) }
69
+
70
+ it "doesn't include a filename" do
71
+ boundary_value = form_data.content_type[/(#{boundary})$/, 1]
72
+
73
+ expect(form_data.to_s).to eq [
74
+ "--#{boundary_value}#{crlf}",
75
+ "#{disposition 'name' => 'foo'}#{crlf}",
76
+ "#{crlf}s#{crlf}",
77
+ "--#{boundary_value}--"
78
+ ].join("")
79
+ end
80
+ end
81
+ end
82
+
83
+ describe "#size" do
84
+ it "returns bytesize of multipart data" do
85
+ expect(form_data.size).to eq form_data.to_s.bytesize
86
+ end
87
+ end
88
+
89
+ describe "#read" do
90
+ it "returns multipart data" do
91
+ expect(form_data.read).to eq form_data.to_s
92
+ end
93
+ end
94
+
95
+ describe "#rewind" do
96
+ it "rewinds the multipart data IO" do
97
+ form_data.read
98
+ form_data.rewind
99
+ expect(form_data.read).to eq form_data.to_s
100
+ end
101
+ end
102
+
103
+ describe "#content_type" do
104
+ subject { form_data.content_type }
105
+
106
+ let(:content_type) { %r{^multipart\/form-data; boundary=#{boundary}$} }
107
+
108
+ it { is_expected.to match(content_type) }
109
+
110
+ context "with user-defined boundary" do
111
+ let(:form_data) { HTTP::FormData::Multipart.new params, boundary: "my-boundary" }
112
+
113
+ it "includes the given boundary" do
114
+ expect(form_data.content_type).to eq "multipart/form-data; boundary=my-boundary"
115
+ end
116
+ end
117
+ end
118
+
119
+ describe "#content_length" do
120
+ subject { form_data.content_length }
121
+ it { is_expected.to eq form_data.to_s.bytesize }
122
+ end
123
+
124
+ describe "#boundary" do
125
+ it "returns a new boundary" do
126
+ expect(form_data.boundary).to match(boundary)
127
+ end
128
+
129
+ context "with user-defined boundary" do
130
+ let(:form_data) { HTTP::FormData::Multipart.new params, boundary: "my-boundary" }
131
+
132
+ it "returns the given boundary" do
133
+ expect(form_data.boundary).to eq "my-boundary"
134
+ end
135
+ end
136
+ end
137
+
138
+ describe ".generate_boundary" do
139
+ it "returns a string suitable as a multipart boundary" do
140
+ expect(form_data.class.generate_boundary).to match(boundary)
141
+ end
60
142
  end
61
143
  end
@@ -1,12 +1,12 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  RSpec.describe HTTP::FormData::Part do
5
- let(:body) { "" }
6
- let(:opts) { {} }
4
+ let(:body) { "" }
5
+ let(:opts) { {} }
6
+ subject(:part) { HTTP::FormData::Part.new(body, opts) }
7
7
 
8
8
  describe "#size" do
9
- subject { described_class.new(body, opts).size }
9
+ subject { part.size }
10
10
 
11
11
  context "when body given as a String" do
12
12
  let(:body) { "привет мир!" }
@@ -15,7 +15,7 @@ RSpec.describe HTTP::FormData::Part do
15
15
  end
16
16
 
17
17
  describe "#to_s" do
18
- subject { described_class.new(body, opts).to_s }
18
+ subject! { part.to_s }
19
19
 
20
20
  context "when body given as String" do
21
21
  let(:body) { "привет мир!" }
@@ -23,8 +23,29 @@ RSpec.describe HTTP::FormData::Part do
23
23
  end
24
24
  end
25
25
 
26
+ describe "#read" do
27
+ subject { part.read }
28
+
29
+ context "when body given as String" do
30
+ let(:body) { "привет мир!" }
31
+ it { is_expected.to eq "привет мир!" }
32
+ end
33
+ end
34
+
35
+ describe "#rewind" do
36
+ context "when body given as String" do
37
+ let(:body) { "привет мир!" }
38
+
39
+ it "rewinds the underlying IO object" do
40
+ part.read
41
+ part.rewind
42
+ expect(part.read).to eq "привет мир!"
43
+ end
44
+ end
45
+ end
46
+
26
47
  describe "#filename" do
27
- subject { described_class.new(body, opts).filename }
48
+ subject { part.filename }
28
49
 
29
50
  it { is_expected.to eq nil }
30
51
 
@@ -35,7 +56,7 @@ RSpec.describe HTTP::FormData::Part do
35
56
  end
36
57
 
37
58
  describe "#content_type" do
38
- subject { described_class.new(body, opts).content_type }
59
+ subject { part.content_type }
39
60
 
40
61
  it { is_expected.to eq nil }
41
62
 
@@ -1,5 +1,5 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
2
+ # coding: utf-8
3
3
 
4
4
  RSpec.describe HTTP::FormData::Urlencoded do
5
5
  let(:data) { { "foo[bar]" => "test" } }
@@ -10,14 +10,14 @@ RSpec.describe HTTP::FormData do
10
10
  end
11
11
 
12
12
  context "when form has at least one file param" do
13
- let(:gemspec) { HTTP::FormData::File.new "gemspec" }
14
- let(:params) { { :foo => :bar, :baz => gemspec } }
13
+ let(:file) { HTTP::FormData::File.new(fixture("the-http-gem.info").to_s) }
14
+ let(:params) { { :foo => :bar, :baz => file } }
15
15
  it { is_expected.to be_a HTTP::FormData::Multipart }
16
16
  end
17
17
 
18
18
  context "when form has file in an array param" do
19
- let(:gemspec) { HTTP::FormData::File.new "gemspec" }
20
- let(:params) { { :foo => :bar, :baz => [gemspec] } }
19
+ let(:file) { HTTP::FormData::File.new(fixture("the-http-gem.info").to_s) }
20
+ let(:params) { { :foo => :bar, :baz => [file] } }
21
21
  it { is_expected.to be_a HTTP::FormData::Multipart }
22
22
  end
23
23
  end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "simplecov"
4
+ require "coveralls"
5
+
6
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
7
+ SimpleCov::Formatter::HTMLFormatter,
8
+ Coveralls::SimpleCov::Formatter
9
+ ]
10
+
11
+ SimpleCov.start { add_filter "/spec/" }
12
+
3
13
  require "http/form_data"
4
14
  require "support/fixtures_helper"
5
15
 
metadata CHANGED
@@ -1,24 +1,24 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: http-form_data
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 2.0.0.pre1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aleksey V Zapparov
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-05-18 00:00:00.000000000 Z
11
+ date: 2017-05-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
+ name: bundler
14
15
  requirement: !ruby/object:Gem::Requirement
15
16
  requirements:
16
17
  - - "~>"
17
18
  - !ruby/object:Gem::Version
18
19
  version: '1.7'
19
- name: bundler
20
- prerelease: false
21
20
  type: :development
21
+ prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
@@ -46,14 +46,17 @@ files:
46
46
  - appveyor.yml
47
47
  - http-form_data.gemspec
48
48
  - lib/http/form_data.rb
49
+ - lib/http/form_data/composite_io.rb
49
50
  - lib/http/form_data/file.rb
50
51
  - lib/http/form_data/multipart.rb
51
52
  - lib/http/form_data/multipart/param.rb
52
53
  - lib/http/form_data/part.rb
54
+ - lib/http/form_data/readable.rb
53
55
  - lib/http/form_data/urlencoded.rb
54
56
  - lib/http/form_data/version.rb
55
57
  - spec/fixtures/expected-multipart-body.tpl
56
58
  - spec/fixtures/the-http-gem.info
59
+ - spec/lib/http/form_data/composite_io_spec.rb
57
60
  - spec/lib/http/form_data/file_spec.rb
58
61
  - spec/lib/http/form_data/multipart_spec.rb
59
62
  - spec/lib/http/form_data/part_spec.rb
@@ -65,7 +68,7 @@ homepage: https://github.com/httprb/form_data.rb
65
68
  licenses:
66
69
  - MIT
67
70
  metadata: {}
68
- post_install_message:
71
+ post_install_message:
69
72
  rdoc_options: []
70
73
  require_paths:
71
74
  - lib
@@ -73,21 +76,22 @@ required_ruby_version: !ruby/object:Gem::Requirement
73
76
  requirements:
74
77
  - - ">="
75
78
  - !ruby/object:Gem::Version
76
- version: '1.9'
79
+ version: '0'
77
80
  required_rubygems_version: !ruby/object:Gem::Requirement
78
81
  requirements:
79
- - - ">="
82
+ - - ">"
80
83
  - !ruby/object:Gem::Version
81
- version: '0'
84
+ version: 1.3.1
82
85
  requirements: []
83
- rubyforge_project:
84
- rubygems_version: 2.6.8
85
- signing_key:
86
+ rubyforge_project:
87
+ rubygems_version: 2.6.11
88
+ signing_key:
86
89
  specification_version: 4
87
- summary: http-form_data-1.0.3
90
+ summary: http-form_data-2.0.0.pre1
88
91
  test_files:
89
92
  - spec/fixtures/expected-multipart-body.tpl
90
93
  - spec/fixtures/the-http-gem.info
94
+ - spec/lib/http/form_data/composite_io_spec.rb
91
95
  - spec/lib/http/form_data/file_spec.rb
92
96
  - spec/lib/http/form_data/multipart_spec.rb
93
97
  - spec/lib/http/form_data/part_spec.rb