http-form_data 1.0.3 → 2.0.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
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