http-form_data 2.3.0 → 3.0.0

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +161 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +11 -10
  5. data/http-form_data.gemspec +28 -14
  6. data/lib/http/form_data/composite_io.rb +46 -28
  7. data/lib/http/form_data/file.rb +44 -19
  8. data/lib/http/form_data/multipart/param.rb +18 -48
  9. data/lib/http/form_data/multipart.rb +59 -12
  10. data/lib/http/form_data/part.rb +24 -2
  11. data/lib/http/form_data/readable.rb +24 -6
  12. data/lib/http/form_data/urlencoded.rb +100 -10
  13. data/lib/http/form_data/version.rb +1 -1
  14. data/lib/http/form_data.rb +38 -18
  15. data/sig/http/form_data/composite_io.rbs +32 -0
  16. data/sig/http/form_data/file.rbs +23 -0
  17. data/sig/http/form_data/multipart/param.rbs +23 -0
  18. data/sig/http/form_data/multipart.rbs +40 -0
  19. data/sig/http/form_data/part.rbs +16 -0
  20. data/sig/http/form_data/readable.rbs +19 -0
  21. data/sig/http/form_data/urlencoded.rbs +46 -0
  22. data/sig/http/form_data/version.rbs +5 -0
  23. data/sig/http/form_data.rbs +30 -0
  24. metadata +24 -43
  25. data/.editorconfig +0 -9
  26. data/.gitignore +0 -15
  27. data/.rspec +0 -2
  28. data/.rubocop.yml +0 -64
  29. data/.travis.yml +0 -34
  30. data/.yardopts +0 -2
  31. data/CHANGES.md +0 -96
  32. data/Gemfile +0 -26
  33. data/Guardfile +0 -16
  34. data/Rakefile +0 -24
  35. data/appveyor.yml +0 -8
  36. data/spec/fixtures/expected-multipart-body.tpl +0 -0
  37. data/spec/fixtures/the-http-gem.info +0 -1
  38. data/spec/lib/http/form_data/composite_io_spec.rb +0 -109
  39. data/spec/lib/http/form_data/file_spec.rb +0 -217
  40. data/spec/lib/http/form_data/multipart_spec.rb +0 -157
  41. data/spec/lib/http/form_data/part_spec.rb +0 -74
  42. data/spec/lib/http/form_data/urlencoded_spec.rb +0 -78
  43. data/spec/lib/http/form_data_spec.rb +0 -50
  44. data/spec/spec_helper.rb +0 -83
  45. data/spec/support/fixtures_helper.rb +0 -13
@@ -12,48 +12,95 @@ module HTTP
12
12
  class Multipart
13
13
  include Readable
14
14
 
15
- attr_reader :boundary
15
+ # Default MIME type for multipart form data
16
+ DEFAULT_CONTENT_TYPE = "multipart/form-data"
16
17
 
17
- # @param [#to_h, Hash] data form data key-value Hash
18
- def initialize(data, boundary: self.class.generate_boundary)
19
- parts = Param.coerce FormData.ensure_hash data
18
+ # Returns the multipart boundary string
19
+ #
20
+ # @example
21
+ # multipart.boundary # => "-----abc123"
22
+ #
23
+ # @api public
24
+ # @return [String]
25
+ attr_reader :boundary
20
26
 
21
- @boundary = boundary.to_s.freeze
22
- @io = CompositeIO.new [*parts.flat_map { |part| [glue, part] }, tail]
27
+ # Creates a new Multipart form data instance
28
+ #
29
+ # @example Basic form data
30
+ # Multipart.new({ foo: "bar" })
31
+ #
32
+ # @example With custom content type
33
+ # Multipart.new(parts, content_type: "multipart/related")
34
+ #
35
+ # @api public
36
+ # @param [Enumerable, Hash, #to_h] data form data key-value pairs
37
+ # @param [String] boundary custom boundary string
38
+ # @param [String] content_type MIME type for the Content-Type header
39
+ def initialize(data, boundary: self.class.generate_boundary, content_type: DEFAULT_CONTENT_TYPE)
40
+ @boundary = boundary.to_s.freeze
41
+ @content_type = content_type
42
+ @io = CompositeIO.new(parts(data).flat_map { |part| [glue, part] } << tail)
23
43
  end
24
44
 
25
- # Generates a string suitable for using as a boundary in multipart form
26
- # data.
45
+ # Generates a boundary string for multipart form data
27
46
  #
47
+ # @example
48
+ # Multipart.generate_boundary # => "-----abc123..."
49
+ #
50
+ # @api public
28
51
  # @return [String]
29
52
  def self.generate_boundary
30
53
  ("-" * 21) << SecureRandom.hex(21)
31
54
  end
32
55
 
33
- # Returns MIME type to be used for HTTP request `Content-Type` header.
56
+ # Returns MIME type for the Content-Type header
57
+ #
58
+ # @example
59
+ # multipart.content_type
60
+ # # => "multipart/form-data; boundary=-----abc123"
34
61
  #
62
+ # @api public
35
63
  # @return [String]
36
64
  def content_type
37
- "multipart/form-data; boundary=#{@boundary}"
65
+ "#{@content_type}; boundary=#{@boundary}"
38
66
  end
39
67
 
40
- # Returns form data content size to be used for HTTP request
41
- # `Content-Length` header.
68
+ # Returns form data content size for Content-Length
42
69
  #
70
+ # @example
71
+ # multipart.content_length # => 123
72
+ #
73
+ # @api public
43
74
  # @return [Integer]
44
75
  alias content_length size
45
76
 
46
77
  private
47
78
 
79
+ # Returns the boundary glue between parts
80
+ #
81
+ # @api private
48
82
  # @return [String]
49
83
  def glue
50
84
  @glue ||= "--#{@boundary}#{CRLF}"
51
85
  end
52
86
 
87
+ # Returns the closing boundary tail
88
+ #
89
+ # @api private
53
90
  # @return [String]
54
91
  def tail
55
92
  @tail ||= "--#{@boundary}--#{CRLF}"
56
93
  end
94
+
95
+ # Coerces data into an array of Param objects
96
+ #
97
+ # @api private
98
+ # @return [Array<Param>]
99
+ def parts(data)
100
+ FormData.ensure_data(data).flat_map do |name, values|
101
+ Array(values).map { |value| Param.new(name, value) }
102
+ end
103
+ end
57
104
  end
58
105
  end
59
106
  end
@@ -11,12 +11,34 @@ module HTTP
11
11
  # @example Usage with String
12
12
  #
13
13
  # body = "Message"
14
- # FormData::Part.new body, :content_type => 'foobar.txt; charset="UTF-8"'
14
+ # FormData::Part.new body, content_type: 'foobar.txt; charset="UTF-8"'
15
15
  class Part
16
16
  include Readable
17
17
 
18
- attr_reader :content_type, :filename
18
+ # Returns the content type of this part
19
+ #
20
+ # @example
21
+ # part.content_type # => "application/json"
22
+ #
23
+ # @api public
24
+ # @return [String, nil]
25
+ attr_reader :content_type
19
26
 
27
+ # Returns the filename of this part
28
+ #
29
+ # @example
30
+ # part.filename # => "avatar.png"
31
+ #
32
+ # @api public
33
+ # @return [String, nil]
34
+ attr_reader :filename
35
+
36
+ # Creates a new Part with the given body and options
37
+ #
38
+ # @example
39
+ # Part.new("hello", content_type: "text/plain")
40
+ #
41
+ # @api public
20
42
  # @param [#to_s] body
21
43
  # @param [String] content_type Value of Content-Type header
22
44
  # @param [String] filename Value of filename parameter
@@ -4,34 +4,52 @@ module HTTP
4
4
  module FormData
5
5
  # Common behaviour for objects defined by an IO object.
6
6
  module Readable
7
- # Returns IO content.
7
+ # Returns IO content as a String
8
8
  #
9
+ # @example
10
+ # readable.to_s # => "content"
11
+ #
12
+ # @api public
9
13
  # @return [String]
10
14
  def to_s
11
15
  rewind
12
- content = read
16
+ content = read #: String
13
17
  rewind
14
18
  content
15
19
  end
16
20
 
17
- # Reads and returns part of IO content.
21
+ # Reads and returns part of IO content
22
+ #
23
+ # @example
24
+ # readable.read # => "full content"
25
+ # readable.read(5) # => "full "
18
26
  #
27
+ # @api public
19
28
  # @param [Integer] length Number of bytes to retrieve
20
29
  # @param [String] outbuf String to be replaced with retrieved data
21
- #
22
30
  # @return [String, nil]
23
31
  def read(length = nil, outbuf = nil)
24
32
  @io.read(length, outbuf)
25
33
  end
26
34
 
27
- # Returns IO size.
35
+ # Returns IO size in bytes
28
36
  #
37
+ # @example
38
+ # readable.size # => 42
39
+ #
40
+ # @api public
29
41
  # @return [Integer]
30
42
  def size
31
43
  @io.size
32
44
  end
33
45
 
34
- # Rewinds the IO.
46
+ # Rewinds the IO to the beginning
47
+ #
48
+ # @example
49
+ # readable.rewind
50
+ #
51
+ # @api public
52
+ # @return [void]
35
53
  def rewind
36
54
  @io.rewind
37
55
  end
@@ -12,7 +12,7 @@ module HTTP
12
12
  include Readable
13
13
 
14
14
  class << self
15
- # Set custom form data encoder implementation.
15
+ # Sets custom form data encoder implementation
16
16
  #
17
17
  # @example
18
18
  #
@@ -44,40 +44,130 @@ module HTTP
44
44
  #
45
45
  # HTTP::FormData::Urlencoded.encoder = CustomFormDataEncoder
46
46
  #
47
- # @raise [ArgumentError] if implementation deos not responds to `#call`.
47
+ # @api public
48
+ # @raise [ArgumentError] if implementation does not respond to `#call`
48
49
  # @param implementation [#call]
49
50
  # @return [void]
50
51
  def encoder=(implementation)
51
52
  raise ArgumentError unless implementation.respond_to? :call
53
+
52
54
  @encoder = implementation
53
55
  end
54
56
 
55
- # Returns form data encoder implementation.
56
- # Default: `URI.encode_www_form`.
57
+ # Returns form data encoder implementation
58
+ #
59
+ # @example
60
+ # Urlencoded.encoder # => #<Method: DefaultEncoder.encode>
57
61
  #
62
+ # @api public
58
63
  # @see .encoder=
59
64
  # @return [#call]
60
65
  def encoder
61
- @encoder ||= ::URI.method(:encode_www_form)
66
+ @encoder || DefaultEncoder
62
67
  end
68
+
69
+ # Default encoder for urlencoded form data
70
+ module DefaultEncoder
71
+ class << self
72
+ # Recursively encodes form data value
73
+ #
74
+ # @example
75
+ # DefaultEncoder.encode({ foo: "bar" }) # => "foo=bar"
76
+ #
77
+ # @api public
78
+ # @param [Hash, Array, String, nil] value
79
+ # @param [String, nil] prefix
80
+ # @return [String]
81
+ def encode(value, prefix = nil)
82
+ case value
83
+ when Hash then encode_hash(value, prefix)
84
+ when Array then encode_array(value, prefix)
85
+ when nil then prefix.to_s
86
+ else
87
+ raise ArgumentError, "value must be a Hash" if prefix.nil?
88
+
89
+ "#{prefix}=#{escape(value)}"
90
+ end
91
+ end
92
+
93
+ alias call encode
94
+
95
+ private
96
+
97
+ # Encodes an Array value
98
+ #
99
+ # @api private
100
+ # @return [String]
101
+ def encode_array(value, prefix)
102
+ if prefix
103
+ value.map { |v| encode(v, "#{prefix}[]") }.join("&")
104
+ else
105
+ encode_pairs(value)
106
+ end
107
+ end
108
+
109
+ # Encodes an Array of key-value pairs
110
+ #
111
+ # @api private
112
+ # @return [String]
113
+ def encode_pairs(pairs)
114
+ pairs.map { |k, v| encode(v, escape(k)) }.reject(&:empty?).join("&")
115
+ end
116
+
117
+ # Encodes a Hash value
118
+ #
119
+ # @api private
120
+ # @return [String]
121
+ def encode_hash(hash, prefix)
122
+ hash.map do |k, v|
123
+ encode(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
124
+ end.reject(&:empty?).join("&")
125
+ end
126
+
127
+ # URL-encodes a value
128
+ #
129
+ # @api private
130
+ # @return [String]
131
+ def escape(value)
132
+ URI.encode_www_form_component(value)
133
+ end
134
+ end
135
+ end
136
+
137
+ private_constant :DefaultEncoder
63
138
  end
64
139
 
65
- # @param [#to_h, Hash] data form data key-value Hash
140
+ # Creates a new Urlencoded form data instance
141
+ #
142
+ # @example
143
+ # Urlencoded.new({ "foo" => "bar" })
144
+ #
145
+ # @api public
146
+ # @param [Enumerable, Hash, #to_h] data form data key-value pairs
147
+ # @param [#call] encoder custom encoder implementation
66
148
  def initialize(data, encoder: nil)
67
149
  encoder ||= self.class.encoder
68
- @io = StringIO.new(encoder.call(FormData.ensure_hash(data)))
150
+ @io = StringIO.new(encoder.call(FormData.ensure_data(data)))
69
151
  end
70
152
 
71
- # Returns MIME type to be used for HTTP request `Content-Type` header.
153
+ # Returns MIME type for the Content-Type header
72
154
  #
155
+ # @example
156
+ # urlencoded.content_type
157
+ # # => "application/x-www-form-urlencoded"
158
+ #
159
+ # @api public
73
160
  # @return [String]
74
161
  def content_type
75
162
  "application/x-www-form-urlencoded"
76
163
  end
77
164
 
78
- # Returns form data content size to be used for HTTP request
79
- # `Content-Length` header.
165
+ # Returns form data content size for Content-Length
166
+ #
167
+ # @example
168
+ # urlencoded.content_length # => 17
80
169
  #
170
+ # @api public
81
171
  # @return [Integer]
82
172
  alias content_length size
83
173
  end
@@ -3,6 +3,6 @@
3
3
  module HTTP
4
4
  module FormData
5
5
  # Gem version.
6
- VERSION = "2.3.0"
6
+ VERSION = "3.0.0"
7
7
  end
8
8
  end
@@ -16,8 +16,8 @@ module HTTP
16
16
  # @example Usage
17
17
  #
18
18
  # form = FormData.create({
19
- # :username => "ixti",
20
- # :avatar_file => FormData::File.new("/home/ixti/avatar.png")
19
+ # username: "ixti",
20
+ # avatar_file: FormData::File.new("/home/ixti/avatar.png")
21
21
  # })
22
22
  #
23
23
  # # Assuming socket is an open socket to some HTTP server
@@ -35,46 +35,66 @@ module HTTP
35
35
  class Error < StandardError; end
36
36
 
37
37
  class << self
38
- # FormData factory. Automatically selects best type depending on given
39
- # `data` Hash.
38
+ # Selects encoder type based on given data
40
39
  #
41
- # @param [#to_h, Hash] data
40
+ # @example
41
+ # FormData.create({ username: "ixti" })
42
+ #
43
+ # @api public
44
+ # @param [Enumerable, Hash, #to_h] data
42
45
  # @return [Multipart] if any of values is a {FormData::File}
43
46
  # @return [Urlencoded] otherwise
44
47
  def create(data, encoder: nil)
45
- data = ensure_hash data
48
+ data = ensure_data data
46
49
 
47
50
  if multipart?(data)
48
51
  Multipart.new(data)
49
52
  else
50
- Urlencoded.new(data, :encoder => encoder)
53
+ Urlencoded.new(data, encoder: encoder)
51
54
  end
52
55
  end
53
56
 
54
- # Coerce `obj` to Hash.
57
+ # Coerces obj to Hash
58
+ #
59
+ # @example
60
+ # FormData.ensure_hash({ foo: :bar }) # => { foo: :bar }
55
61
  #
56
- # @note Internal usage helper, to workaround lack of `#to_h` on Ruby < 2.1
57
- # @raise [Error] `obj` can't be coerced.
62
+ # @api public
63
+ # @raise [Error] `obj` can't be coerced
58
64
  # @return [Hash]
59
65
  def ensure_hash(obj)
60
- case
61
- when obj.nil? then {}
62
- when obj.is_a?(Hash) then obj
63
- when obj.respond_to?(:to_h) then obj.to_h
66
+ if obj.is_a?(Hash) then obj
67
+ elsif obj.respond_to?(:to_h) then obj.to_h
64
68
  else raise Error, "#{obj.inspect} is neither Hash nor responds to :to_h"
65
69
  end
66
70
  end
67
71
 
72
+ # Coerces obj to an Enumerable of key-value pairs
73
+ #
74
+ # @example
75
+ # FormData.ensure_data([[:foo, :bar]]) # => [[:foo, :bar]]
76
+ #
77
+ # @api public
78
+ # @raise [Error] `obj` can't be coerced
79
+ # @return [Enumerable]
80
+ def ensure_data(obj)
81
+ if obj.nil? then []
82
+ elsif obj.is_a?(Enumerable) then obj
83
+ elsif obj.respond_to?(:to_h) then obj.to_h
84
+ else raise Error, "#{obj.inspect} is neither Enumerable nor responds to :to_h"
85
+ end
86
+ end
87
+
68
88
  private
69
89
 
70
- # Tells whenever data contains multipart data or not.
90
+ # Checks if data contains multipart data
71
91
  #
72
- # @param [Hash] data
92
+ # @api private
93
+ # @param [Enumerable] data
73
94
  # @return [Boolean]
74
95
  def multipart?(data)
75
96
  data.any? do |_, v|
76
- next true if v.is_a? FormData::Part
77
- v.respond_to?(:to_ary) && v.to_ary.any? { |e| e.is_a? FormData::Part }
97
+ v.is_a?(Part) || (v.respond_to?(:to_ary) && v.to_ary.any?(Part))
78
98
  end
79
99
  end
80
100
  end
@@ -0,0 +1,32 @@
1
+ module HTTP
2
+ module FormData
3
+ class CompositeIO
4
+ @index: Integer
5
+ @ios: Array[untyped]
6
+ @size: Integer?
7
+
8
+ # Creates a new CompositeIO from an array of IO-like objects
9
+ def initialize: (Array[untyped] ios) -> void
10
+
11
+ # Reads and returns content across multiple IO objects
12
+ def read: (?Integer? length, ?String? outbuf) -> String?
13
+
14
+ # Returns sum of all IO sizes
15
+ def size: () -> Integer
16
+
17
+ # Rewinds all IO objects and resets cursor
18
+ def rewind: () -> void
19
+
20
+ private
21
+
22
+ # Yields chunks with total length up to `length`
23
+ def read_chunks: (Integer? length) { (String chunk) -> void } -> void
24
+
25
+ # Reads chunk from current IO with length up to `max_length`
26
+ def readpartial: (Integer? max_length) -> String?
27
+
28
+ # Advances cursor to the next IO object
29
+ def advance_io: () -> void
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ module HTTP
2
+ module FormData
3
+ class File < Part
4
+ DEFAULT_MIME: String
5
+
6
+ @autoclose: bool
7
+
8
+ # Creates a new File from a path or IO object
9
+ def initialize: (String | Pathname | untyped path_or_io, ?Hash[Symbol, untyped]? opts) -> void
10
+
11
+ # Closes the underlying IO if it was opened by this instance
12
+ def close: () -> void
13
+
14
+ private
15
+
16
+ # Wraps path_or_io into an IO object
17
+ def make_io: (String | Pathname | untyped path_or_io) -> untyped
18
+
19
+ # Determines filename for the given IO
20
+ def filename_for: (untyped io) -> String
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module HTTP
2
+ module FormData
3
+ class Multipart
4
+ class Param
5
+ include Readable
6
+
7
+ @name: String
8
+ @part: Part
9
+
10
+ # Initializes body part with headers and data
11
+ def initialize: (untyped name, untyped value) -> void
12
+
13
+ private
14
+
15
+ # Builds the MIME header for this part
16
+ def header: () -> String
17
+
18
+ # Builds Content-Disposition parameters string
19
+ def parameters: () -> String
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,40 @@
1
+ module HTTP
2
+ module FormData
3
+ class Multipart
4
+ include Readable
5
+
6
+ DEFAULT_CONTENT_TYPE: String
7
+
8
+ # Returns the multipart boundary string
9
+ attr_reader boundary: String
10
+
11
+ @content_type: _ToS
12
+
13
+ # Creates a new Multipart form data instance
14
+ def initialize: (untyped data, ?boundary: _ToS, ?content_type: _ToS) -> void
15
+
16
+ # Generates a boundary string for multipart form data
17
+ def self.generate_boundary: () -> String
18
+
19
+ # Returns MIME type for the Content-Type header
20
+ def content_type: () -> String
21
+
22
+ # Returns form data content size for Content-Length
23
+ alias content_length size
24
+
25
+ private
26
+
27
+ @glue: String?
28
+ @tail: String?
29
+
30
+ # Returns the boundary glue between parts
31
+ def glue: () -> String
32
+
33
+ # Returns the closing boundary tail
34
+ def tail: () -> String
35
+
36
+ # Coerces data into an array of Param objects
37
+ def parts: (untyped data) -> Array[Param]
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,16 @@
1
+ module HTTP
2
+ module FormData
3
+ class Part
4
+ include Readable
5
+
6
+ # Returns the content type of this part
7
+ attr_reader content_type: String?
8
+
9
+ # Returns the filename of this part
10
+ attr_reader filename: String?
11
+
12
+ # Creates a new Part with the given body and options
13
+ def initialize: (_ToS body, ?content_type: String?, ?filename: String?) -> void
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ module HTTP
2
+ module FormData
3
+ module Readable
4
+ @io: StringIO | CompositeIO
5
+
6
+ # Returns IO content as a String
7
+ def to_s: () -> String
8
+
9
+ # Reads and returns part of IO content
10
+ def read: (?Integer? length, ?String? outbuf) -> String?
11
+
12
+ # Returns IO size in bytes
13
+ def size: () -> Integer
14
+
15
+ # Rewinds the IO to the beginning
16
+ def rewind: () -> (Integer | void)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,46 @@
1
+ module HTTP
2
+ module FormData
3
+ class Urlencoded
4
+ include Readable
5
+
6
+ self.@encoder: _Encoder?
7
+
8
+ # Sets custom form data encoder implementation
9
+ def self.encoder=: (untyped implementation) -> void
10
+
11
+ # Returns form data encoder implementation
12
+ def self.encoder: () -> _Encoder
13
+
14
+ # Default encoder for urlencoded form data
15
+ module DefaultEncoder
16
+ # Recursively encodes form data value
17
+ def self.encode: (untyped value, ?String? prefix) -> String
18
+
19
+ alias self.call self.encode
20
+
21
+ private
22
+
23
+ # Encodes an Array value
24
+ def self.encode_array: (Array[untyped] value, String? prefix) -> String
25
+
26
+ # Encodes an Array of key-value pairs
27
+ def self.encode_pairs: (Array[untyped] pairs) -> String
28
+
29
+ # Encodes a Hash value
30
+ def self.encode_hash: (Hash[untyped, untyped] hash, String? prefix) -> String
31
+
32
+ # URL-encodes a value
33
+ def self.escape: (untyped value) -> String
34
+ end
35
+
36
+ # Creates a new Urlencoded form data instance
37
+ def initialize: (untyped data, ?encoder: _Encoder?) -> void
38
+
39
+ # Returns MIME type for the Content-Type header
40
+ def content_type: () -> String
41
+
42
+ # Returns form data content size for Content-Length
43
+ alias content_length size
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,5 @@
1
+ module HTTP
2
+ module FormData
3
+ VERSION: String
4
+ end
5
+ end