Skip to content

Commit f0d10a4

Browse files
Headers#[]= handles Array values. (#93)
- Parsing logic was moved to `Header::*.parse(value)`. - Coercion to header values is now handled by `Header::*.coerce(value)`.
1 parent ef20d4d commit f0d10a4

38 files changed

+749
-102
lines changed

benchmark/array.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
require "sus/fixtures/benchmark"
7+
8+
describe "Array initialization" do
9+
include Sus::Fixtures::Benchmark
10+
11+
let(:source_array) {["value1", "value2", "value3", "value4", "value5"]}
12+
13+
measure "Array.new(array)" do |repeats|
14+
repeats.times do
15+
Array.new(source_array)
16+
end
17+
end
18+
19+
measure "Array.new.concat(array)" do |repeats|
20+
repeats.times do
21+
array = Array.new
22+
array.concat(source_array)
23+
end
24+
end
25+
end
26+

gems.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
gem "rubocop-socketry"
3030

3131
gem "sus-fixtures-async"
32+
gem "sus-fixtures-benchmark"
3233

3334
gem "bake-test"
3435
gem "bake-test-external"

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/header/accept.rb

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module Protocol
1212
module HTTP
1313
module Header
1414
# The `accept-content-type` header represents a list of content-types that the client can accept.
15-
class Accept < Array
15+
class Accept < Split
1616
# Regular expression used to split values on commas, with optional surrounding whitespace, taking into account quoted strings.
1717
SEPARATOR = /
1818
(?: # Start non-capturing group
@@ -68,27 +68,26 @@ 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.
7272
#
73-
# @parameter value [String] the value of the header.
74-
def initialize(value = nil)
75-
if value
76-
super(value.scan(SEPARATOR).map(&:strip))
77-
end
73+
# @parameter value [String] a 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))
7877
end
7978

8079
# Adds one or more comma-separated values to the header.
8180
#
8281
# The input string is split into distinct entries and appended to the array.
8382
#
84-
# @parameter value [String] the value or values to add, separated by commas.
83+
# @parameter value [String] a raw header value containing one or more media types separated by commas.
8584
def << value
8685
self.concat(value.scan(SEPARATOR).map(&:strip))
8786
end
8887

89-
# Serializes the stored values into a comma-separated string.
88+
# Converts the parsed header value into a raw header value.
9089
#
91-
# @returns [String] the serialized representation of the header values.
90+
# @returns [String] a raw header value (comma-separated string).
9291
def to_s
9392
join(",")
9493
end

lib/protocol/http/header/authorization.rb

Lines changed: 18 additions & 2 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.
19+
#
20+
# @parameter value [String] a 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.
@@ -31,8 +47,8 @@ def self.basic(username, password)
3147
strict_base64_encoded = ["#{username}:#{password}"].pack("m0")
3248

3349
self.new(
34-
"Basic #{strict_base64_encoded}"
35-
)
50+
"Basic #{strict_base64_encoded}"
51+
)
3652
end
3753

3854
# Whether this header is acceptable in HTTP trailers.

lib/protocol/http/header/cache_control.rb

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,44 @@ 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.
47+
# Parses a raw header value.
4848
#
49-
# @parameter value [String | Nil] the raw Cache-Control header value.
49+
# @parameter value [String] a raw header value containing comma-separated directives.
50+
# @returns [CacheControl] a new instance containing the parsed and normalized directives.
51+
def self.parse(value)
52+
self.new(value.downcase.split(COMMA))
53+
end
54+
55+
# Coerces a value into a parsed header object.
56+
#
57+
# @parameter value [String | Array] the value to coerce.
58+
# @returns [CacheControl] a parsed header object with normalized values.
59+
def self.coerce(value)
60+
case value
61+
when Array
62+
self.new(value.map(&:downcase))
63+
else
64+
self.parse(value.to_s)
65+
end
66+
end
67+
68+
# Initializes the cache control header with the given values.
69+
#
70+
# @parameter value [Array | String | Nil] an array of directives, a raw header value, or `nil` for an empty header.
5071
def initialize(value = nil)
51-
super(value&.downcase)
72+
if value.is_a?(Array)
73+
super(value)
74+
elsif value.is_a?(String)
75+
super()
76+
self << value
77+
elsif value
78+
raise ArgumentError, "Invalid value: #{value.inspect}"
79+
end
5280
end
5381

5482
# Adds a directive to the Cache-Control header. The value will be normalized to lowercase before being added.
5583
#
56-
# @parameter value [String] the directive to add.
84+
# @parameter value [String] a raw header value containing directives to add.
5785
def << value
5886
super(value.downcase)
5987
end
@@ -132,3 +160,4 @@ def find_integer_value(value_name)
132160
end
133161
end
134162
end
163+

lib/protocol/http/header/connection.rb

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,44 @@ 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.
25+
# Parses a raw header value.
2626
#
27-
# @parameter value [String | Nil] the raw `connection` header value.
27+
# @parameter value [String] a raw header value containing comma-separated directives.
28+
# @returns [Connection] a new instance with normalized (lowercase) directives.
29+
def self.parse(value)
30+
self.new(value.downcase.split(COMMA))
31+
end
32+
33+
# Coerces a value into a parsed header object.
34+
#
35+
# @parameter value [String | Array] the value to coerce.
36+
# @returns [Connection] a parsed header object with normalized values.
37+
def self.coerce(value)
38+
case value
39+
when Array
40+
self.new(value.map(&:downcase))
41+
else
42+
self.parse(value.to_s)
43+
end
44+
end
45+
46+
# Initializes the connection header with the given values.
47+
#
48+
# @parameter value [Array | String | Nil] an array of directives, a raw header value, or `nil` for an empty header.
2849
def initialize(value = nil)
29-
super(value&.downcase)
50+
if value.is_a?(Array)
51+
super(value)
52+
elsif value.is_a?(String)
53+
super()
54+
self << value
55+
elsif value
56+
raise ArgumentError, "Invalid value: #{value.inspect}"
57+
end
3058
end
3159

3260
# Adds a directive to the `connection` header. The value will be normalized to lowercase before being added.
3361
#
34-
# @parameter value [String] the directive to add.
62+
# @parameter value [String] a raw header value containing directives to add.
3563
def << value
3664
super(value.downcase)
3765
end
@@ -61,3 +89,4 @@ def self.trailer?
6189
end
6290
end
6391
end
92+

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.
1616
#
17-
# @parameter value [String] the new value for the `date` header.
17+
# @parameter value [String] a 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.
32+
#
33+
# @parameter value [String] a raw header 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.
1414
#
15-
# @parameter value [String] the new value for the `etag` header.
15+
# @parameter value [String] a 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.
30+
#
31+
# @parameter value [String] a raw header 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
#

0 commit comments

Comments
 (0)