Skip to content

Commit 85f75f7

Browse files
Headers#[]= handles Array values.
- `Header::*.new(value)` method expects the correct data type and does not coerce from String. - Parsing logic was moved to `Header::*.parse(value)`. - Coercion to header values is now handled by `Header::*.coerce(value)`.
1 parent ef20d4d commit 85f75f7

36 files changed

+443
-120
lines changed

lib/protocol/http/body/stream.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
module Protocol
1111
module HTTP
1212
module Body
13-
# The input stream is an IO-like object which contains the raw HTTP POST data. When applicable, its external encoding must be ASCII-8BIT and it must be opened in binary mode, for Ruby 1.9 compatibility. The input stream must respond to gets, each, read and rewind.
13+
# The input stream is an IO-like object which contains the raw HTTP POST data. When applicable, its external encoding must be "ASCII-8BIT" and it must be opened in binary mode, for Ruby 1.9 compatibility. The input stream must respond to gets, each, read and rewind.
1414
class Stream
1515
# The default line separator, used by {gets}.
1616
NEWLINE = "\n"

lib/protocol/http/body/streamable.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,10 @@ def call(stream)
127127

128128
# Ownership of the stream is passed into the block, in other words, the block is responsible for closing the stream.
129129
block.call(stream)
130-
rescue => error
131-
# If, for some reason, the block raises an error, we assume it may not have closed the stream, so we close it here:
132-
stream.close
133-
raise
130+
rescue => error
131+
# If, for some reason, the block raises an error, we assume it may not have closed the stream, so we close it here:
132+
stream.close
133+
raise
134134
end
135135

136136
# Close the input. The streaming body will eventually read all the input.

lib/protocol/http/header/accept.rb

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,27 +68,54 @@ def quality_factor
6868
end
6969
end
7070

71-
# Parse the `accept` header value into a list of content types.
71+
# Parses a raw header value from the wire.
7272
#
73-
# @parameter value [String] the value of the header.
73+
# @parameter value [String] the raw header value containing comma-separated media types.
74+
# @returns [Accept] a new instance containing the parsed media types.
75+
def self.parse(value)
76+
self.new(value.scan(SEPARATOR).map(&:strip))
77+
end
78+
79+
# Coerces a value into a parsed header object.
80+
#
81+
# @parameter value [String | Array] the value to coerce.
82+
# @returns [Accept] a parsed header object.
83+
def self.coerce(value)
84+
case value
85+
when Array
86+
self.new(value)
87+
else
88+
self.parse(value.to_s)
89+
end
90+
end
91+
92+
# Initializes an Accept header with already-parsed values.
93+
#
94+
# @parameter value [Array | Nil] an array of parsed media type strings, or `nil` for an empty header.
7495
def initialize(value = nil)
75-
if value
76-
super(value.scan(SEPARATOR).map(&:strip))
96+
if value.is_a?(Array)
97+
super(value)
98+
elsif value.is_a?(String)
99+
# Compatibility with the old constructor, prefer to use `parse` instead:
100+
super()
101+
self << value
102+
elsif value
103+
raise ArgumentError, "Invalid value: #{value.inspect}"
77104
end
78105
end
79106

80-
# Adds one or more comma-separated values to the header.
107+
# Adds one or more comma-separated values to the header from a raw wire-format string.
81108
#
82109
# The input string is split into distinct entries and appended to the array.
83110
#
84-
# @parameter value [String] the value or values to add, separated by commas.
111+
# @parameter value [String] a raw wire-format value containing one or more media types separated by commas.
85112
def << value
86113
self.concat(value.scan(SEPARATOR).map(&:strip))
87114
end
88115

89-
# Serializes the stored values into a comma-separated string.
116+
# Converts the parsed header value into a raw wire-format string.
90117
#
91-
# @returns [String] the serialized representation of the header values.
118+
# @returns [String] a raw wire-format value (comma-separated string) suitable for transmission.
92119
def to_s
93120
join(",")
94121
end

lib/protocol/http/header/authorization.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,22 @@ module Header
1515
#
1616
# TODO Support other authorization mechanisms, e.g. bearer token.
1717
class Authorization < String
18+
# Parses a raw header value from the wire.
19+
#
20+
# @parameter value [String] the raw header value.
21+
# @returns [Authorization] a new instance.
22+
def self.parse(value)
23+
self.new(value)
24+
end
25+
26+
# Coerces a value into a parsed header object.
27+
#
28+
# @parameter value [String] the value to coerce.
29+
# @returns [Authorization] a parsed header object.
30+
def self.coerce(value)
31+
self.new(value.to_s)
32+
end
33+
1834
# Splits the header into the credentials.
1935
#
2036
# @returns [Tuple(String, String)] The username and password.

lib/protocol/http/header/cache_control.rb

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,24 @@ class CacheControl < Split
4444
# The `proxy-revalidate` directive is similar to `must-revalidate` but applies only to shared caches.
4545
PROXY_REVALIDATE = "proxy-revalidate"
4646

47-
# Initializes the cache control header with the given value. The value is expected to be a comma-separated string of cache directives.
48-
#
49-
# @parameter value [String | Nil] the raw Cache-Control header value.
50-
def initialize(value = nil)
51-
super(value&.downcase)
47+
# Initializes the cache control header with already-parsed and normalized values.
48+
#
49+
# @parameter value [Array | Nil] an array of normalized (lowercase) directives, or `nil` for an empty header.
50+
def initialize(value = nil)
51+
if value.is_a?(Array)
52+
super(value.map(&:downcase))
53+
elsif value.is_a?(String)
54+
# Compatibility with the old constructor, prefer to use `parse` instead:
55+
super()
56+
self << value
57+
elsif value
58+
raise ArgumentError, "Invalid value: #{value.inspect}"
5259
end
60+
end
5361

54-
# Adds a directive to the Cache-Control header. The value will be normalized to lowercase before being added.
62+
# Adds a directive to the Cache-Control header from a raw wire-format string. The value will be normalized to lowercase before being added.
5563
#
56-
# @parameter value [String] the directive to add.
64+
# @parameter value [String] a raw wire-format directive to add.
5765
def << value
5866
super(value.downcase)
5967
end
@@ -132,3 +140,4 @@ def find_integer_value(value_name)
132140
end
133141
end
134142
end
143+

lib/protocol/http/header/connection.rb

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,24 @@ class Connection < Split
2222
# The `upgrade` directive indicates that the connection should be upgraded to a different protocol, as specified in the `Upgrade` header.
2323
UPGRADE = "upgrade"
2424

25-
# Initializes the connection header with the given value. The value is expected to be a comma-separated string of directives.
26-
#
27-
# @parameter value [String | Nil] the raw `connection` header value.
28-
def initialize(value = nil)
29-
super(value&.downcase)
25+
# Initializes the connection header with already-parsed and normalized values.
26+
#
27+
# @parameter value [Array | Nil] an array of normalized (lowercase) directives, or `nil` for an empty header.
28+
def initialize(value = nil)
29+
if value.is_a?(Array)
30+
super(value.map(&:downcase))
31+
elsif value.is_a?(String)
32+
# Compatibility with the old constructor, prefer to use `parse` instead:
33+
super()
34+
self << value
35+
elsif value
36+
raise ArgumentError, "Invalid value: #{value.inspect}"
3037
end
38+
end
3139

32-
# Adds a directive to the `connection` header. The value will be normalized to lowercase before being added.
40+
# Adds a directive to the `connection` header from a raw wire-format string. The value will be normalized to lowercase before being added.
3341
#
34-
# @parameter value [String] the directive to add.
42+
# @parameter value [String] a raw wire-format directive to add.
3543
def << value
3644
super(value.downcase)
3745
end
@@ -61,3 +69,4 @@ def self.trailer?
6169
end
6270
end
6371
end
72+

lib/protocol/http/header/date.rb

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,25 @@ module Header
1212
#
1313
# This header is typically included in HTTP responses and follows the format defined in RFC 9110.
1414
class Date < String
15-
# Replaces the current value of the `date` header with the specified value.
15+
# Parses a raw header value from the wire.
1616
#
17-
# @parameter value [String] the new value for the `date` header.
17+
# @parameter value [String] the raw header value.
18+
# @returns [Date] a new instance.
19+
def self.parse(value)
20+
self.new(value)
21+
end
22+
23+
# Coerces a value into a parsed header object.
24+
#
25+
# @parameter value [String] the value to coerce.
26+
# @returns [Date] a parsed header object.
27+
def self.coerce(value)
28+
self.new(value.to_s)
29+
end
30+
31+
# Replaces the current value of the `date` header with a raw wire-format string.
32+
#
33+
# @parameter value [String] a raw wire-format value for the `date` header.
1834
def << value
1935
replace(value)
2036
end

lib/protocol/http/header/etag.rb

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,25 @@ module Header
1010
#
1111
# The `etag` header provides a unique identifier for a specific version of a resource, typically used for cache validation or conditional requests. It can be either a strong or weak validator as defined in RFC 9110.
1212
class ETag < String
13-
# Replaces the current value of the `etag` header with the specified value.
13+
# Parses a raw header value from the wire.
1414
#
15-
# @parameter value [String] the new value for the `etag` header.
15+
# @parameter value [String] the raw header value.
16+
# @returns [ETag] a new instance.
17+
def self.parse(value)
18+
self.new(value)
19+
end
20+
21+
# Coerces a value into a parsed header object.
22+
#
23+
# @parameter value [String] the value to coerce.
24+
# @returns [ETag] a parsed header object.
25+
def self.coerce(value)
26+
self.new(value.to_s)
27+
end
28+
29+
# Replaces the current value of the `etag` header with a raw wire-format string.
30+
#
31+
# @parameter value [String] a raw wire-format value for the `etag` header.
1632
def << value
1733
replace(value)
1834
end

lib/protocol/http/header/etags.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def weak_match?(etag)
5252
wildcard? || self.include?(etag) || self.include?(opposite_tag(etag))
5353
end
5454

55-
private
55+
private
5656

5757
# Converts a weak tag to its strong counterpart or vice versa.
5858
#

lib/protocol/http/header/multiple.rb

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,47 @@ module Header
1010
#
1111
# This isn't a specific header but is used as a base for headers that store multiple values, such as cookies. The values are split and stored as an array internally, and serialized back to a newline-separated string when needed.
1212
class Multiple < Array
13-
# Initializes the multiple header with the given value. As the header key-value pair can only contain one value, the value given here is added to the internal array, and subsequent values can be added using the `<<` operator.
13+
# Parses a raw header value from the wire.
1414
#
15-
# @parameter value [String] the raw header value.
16-
def initialize(value)
15+
# Multiple headers receive each value as a separate header entry on the wire, so this method takes a single string value and creates a new instance containing it.
16+
#
17+
# @parameter value [String] a single raw header value from the wire.
18+
# @returns [Multiple] a new instance containing the parsed value.
19+
def self.parse(value)
20+
self.new([value])
21+
end
22+
23+
# Coerces a value into a parsed header object.
24+
#
25+
# This method is used by the Headers class when setting values via `[]=` to convert application values into the appropriate policy type.
26+
#
27+
# @parameter value [String | Array] the value to coerce.
28+
# @returns [Multiple] a parsed header object.
29+
def self.coerce(value)
30+
case value
31+
when Array
32+
self.new(value)
33+
else
34+
self.parse(value.to_s)
35+
end
36+
end
37+
38+
# Initializes the multiple header with already-parsed values.
39+
#
40+
# @parameter value [Array | Nil] an array of header values, or `nil` for an empty header.
41+
def initialize(value = nil)
1742
super()
1843

19-
self << value
44+
if value
45+
self.concat(value)
46+
end
2047
end
2148

22-
# Serializes the stored values into a newline-separated string.
49+
# Converts the parsed header value into a raw wire-format string.
50+
#
51+
# Multiple headers are transmitted as separate header entries on the wire, so this serializes to a newline-separated string for storage.
2352
#
24-
# @returns [String] the serialized representation of the header values.
53+
# @returns [String] a raw wire-format value (newline-separated string).
2554
def to_s
2655
join("\n")
2756
end

0 commit comments

Comments
 (0)