Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions benchmark/array.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2025, by Samuel Williams.

require "sus/fixtures/benchmark"

describe "Array initialization" do
include Sus::Fixtures::Benchmark

let(:source_array) {["value1", "value2", "value3", "value4", "value5"]}

measure "Array.new(array)" do |repeats|
repeats.times do
Array.new(source_array)
end
end

measure "Array.new.concat(array)" do |repeats|
repeats.times do
array = Array.new
array.concat(source_array)
end
end
end

1 change: 1 addition & 0 deletions gems.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
gem "rubocop-socketry"

gem "sus-fixtures-async"
gem "sus-fixtures-benchmark"

gem "bake-test"
gem "bake-test-external"
Expand Down
2 changes: 1 addition & 1 deletion lib/protocol/http/body/stream.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
module Protocol
module HTTP
module Body
# 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.
# 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.
class Stream
# The default line separator, used by {gets}.
NEWLINE = "\n"
Expand Down
19 changes: 9 additions & 10 deletions lib/protocol/http/header/accept.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module Protocol
module HTTP
module Header
# The `accept-content-type` header represents a list of content-types that the client can accept.
class Accept < Array
class Accept < Split
# Regular expression used to split values on commas, with optional surrounding whitespace, taking into account quoted strings.
SEPARATOR = /
(?: # Start non-capturing group
Expand Down Expand Up @@ -68,27 +68,26 @@ def quality_factor
end
end

# Parse the `accept` header value into a list of content types.
# Parses a raw header value.
#
# @parameter value [String] the value of the header.
def initialize(value = nil)
if value
super(value.scan(SEPARATOR).map(&:strip))
end
# @parameter value [String] a raw header value containing comma-separated media types.
# @returns [Accept] a new instance containing the parsed media types.
def self.parse(value)
self.new(value.scan(SEPARATOR).map(&:strip))
end

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

# Serializes the stored values into a comma-separated string.
# Converts the parsed header value into a raw header value.
#
# @returns [String] the serialized representation of the header values.
# @returns [String] a raw header value (comma-separated string).
def to_s
join(",")
end
Expand Down
20 changes: 18 additions & 2 deletions lib/protocol/http/header/authorization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@ module Header
#
# TODO Support other authorization mechanisms, e.g. bearer token.
class Authorization < String
# Parses a raw header value.
#
# @parameter value [String] a raw header value.
# @returns [Authorization] a new instance.
def self.parse(value)
self.new(value)
end

# Coerces a value into a parsed header object.
#
# @parameter value [String] the value to coerce.
# @returns [Authorization] a parsed header object.
def self.coerce(value)
self.new(value.to_s)
end

# Splits the header into the credentials.
#
# @returns [Tuple(String, String)] The username and password.
Expand All @@ -31,8 +47,8 @@ def self.basic(username, password)
strict_base64_encoded = ["#{username}:#{password}"].pack("m0")

self.new(
"Basic #{strict_base64_encoded}"
)
"Basic #{strict_base64_encoded}"
)
end

# Whether this header is acceptable in HTTP trailers.
Expand Down
37 changes: 33 additions & 4 deletions lib/protocol/http/header/cache_control.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,44 @@ class CacheControl < Split
# The `proxy-revalidate` directive is similar to `must-revalidate` but applies only to shared caches.
PROXY_REVALIDATE = "proxy-revalidate"

# Initializes the cache control header with the given value. The value is expected to be a comma-separated string of cache directives.
# Parses a raw header value.
#
# @parameter value [String | Nil] the raw Cache-Control header value.
# @parameter value [String] a raw header value containing comma-separated directives.
# @returns [CacheControl] a new instance containing the parsed and normalized directives.
def self.parse(value)
self.new(value.downcase.split(COMMA))
end

# Coerces a value into a parsed header object.
#
# @parameter value [String | Array] the value to coerce.
# @returns [CacheControl] a parsed header object with normalized values.
def self.coerce(value)
case value
when Array
self.new(value.map(&:downcase))
else
self.parse(value.to_s)
end
end

# Initializes the cache control header with the given values.
#
# @parameter value [Array | String | Nil] an array of directives, a raw header value, or `nil` for an empty header.
def initialize(value = nil)
super(value&.downcase)
if value.is_a?(Array)
super(value)
elsif value.is_a?(String)
super()
self << value
elsif value
raise ArgumentError, "Invalid value: #{value.inspect}"
end
end

# Adds a directive to the Cache-Control header. The value will be normalized to lowercase before being added.
#
# @parameter value [String] the directive to add.
# @parameter value [String] a raw header value containing directives to add.
def << value
super(value.downcase)
end
Expand Down Expand Up @@ -132,3 +160,4 @@ def find_integer_value(value_name)
end
end
end

37 changes: 33 additions & 4 deletions lib/protocol/http/header/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,44 @@ class Connection < Split
# The `upgrade` directive indicates that the connection should be upgraded to a different protocol, as specified in the `Upgrade` header.
UPGRADE = "upgrade"

# Initializes the connection header with the given value. The value is expected to be a comma-separated string of directives.
# Parses a raw header value.
#
# @parameter value [String | Nil] the raw `connection` header value.
# @parameter value [String] a raw header value containing comma-separated directives.
# @returns [Connection] a new instance with normalized (lowercase) directives.
def self.parse(value)
self.new(value.downcase.split(COMMA))
end

# Coerces a value into a parsed header object.
#
# @parameter value [String | Array] the value to coerce.
# @returns [Connection] a parsed header object with normalized values.
def self.coerce(value)
case value
when Array
self.new(value.map(&:downcase))
else
self.parse(value.to_s)
end
end

# Initializes the connection header with the given values.
#
# @parameter value [Array | String | Nil] an array of directives, a raw header value, or `nil` for an empty header.
def initialize(value = nil)
super(value&.downcase)
if value.is_a?(Array)
super(value)
elsif value.is_a?(String)
super()
self << value
elsif value
raise ArgumentError, "Invalid value: #{value.inspect}"
end
end

# Adds a directive to the `connection` header. The value will be normalized to lowercase before being added.
#
# @parameter value [String] the directive to add.
# @parameter value [String] a raw header value containing directives to add.
def << value
super(value.downcase)
end
Expand Down Expand Up @@ -61,3 +89,4 @@ def self.trailer?
end
end
end

20 changes: 18 additions & 2 deletions lib/protocol/http/header/date.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,25 @@ module Header
#
# This header is typically included in HTTP responses and follows the format defined in RFC 9110.
class Date < String
# Replaces the current value of the `date` header with the specified value.
# Parses a raw header value.
#
# @parameter value [String] the new value for the `date` header.
# @parameter value [String] a raw header value.
# @returns [Date] a new instance.
def self.parse(value)
self.new(value)
end

# Coerces a value into a parsed header object.
#
# @parameter value [String] the value to coerce.
# @returns [Date] a parsed header object.
def self.coerce(value)
self.new(value.to_s)
end

# Replaces the current value of the `date` header.
#
# @parameter value [String] a raw header value for the `date` header.
def << value
replace(value)
end
Expand Down
20 changes: 18 additions & 2 deletions lib/protocol/http/header/etag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,25 @@ module Header
#
# 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.
class ETag < String
# Replaces the current value of the `etag` header with the specified value.
# Parses a raw header value.
#
# @parameter value [String] the new value for the `etag` header.
# @parameter value [String] a raw header value.
# @returns [ETag] a new instance.
def self.parse(value)
self.new(value)
end

# Coerces a value into a parsed header object.
#
# @parameter value [String] the value to coerce.
# @returns [ETag] a parsed header object.
def self.coerce(value)
self.new(value.to_s)
end

# Replaces the current value of the `etag` header.
#
# @parameter value [String] a raw header value for the `etag` header.
def << value
replace(value)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/protocol/http/header/etags.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def weak_match?(etag)
wildcard? || self.include?(etag) || self.include?(opposite_tag(etag))
end

private
private

# Converts a weak tag to its strong counterpart or vice versa.
#
Expand Down
41 changes: 35 additions & 6 deletions lib/protocol/http/header/multiple.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,47 @@ module Header
#
# 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.
class Multiple < Array
# 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.
# Parses a raw header value.
#
# @parameter value [String] the raw header value.
def initialize(value)
# Multiple headers receive each value as a separate header entry, so this method takes a single string value and creates a new instance containing it.
#
# @parameter value [String] a single raw header value.
# @returns [Multiple] a new instance containing the parsed value.
def self.parse(value)
self.new([value])
end

# Coerces a value into a parsed header object.
#
# This method is used by the Headers class when setting values via `[]=` to convert application values into the appropriate policy type.
#
# @parameter value [String | Array] the value to coerce.
# @returns [Multiple] a parsed header object.
def self.coerce(value)
case value
when Array
self.new(value.map(&:to_s))
else
self.parse(value.to_s)
end
end

# Initializes the multiple header with the given values.
#
# @parameter value [Array | Nil] an array of header values, or `nil` for an empty header.
def initialize(value = nil)
super()

self << value
if value
self.concat(value)
end
end

# Serializes the stored values into a newline-separated string.
# Converts the parsed header value into a raw header value.
#
# Multiple headers are transmitted as separate header entries, so this serializes to a newline-separated string for storage.
#
# @returns [String] the serialized representation of the header values.
# @returns [String] a raw header value (newline-separated string).
def to_s
join("\n")
end
Expand Down
Loading
Loading