1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
|
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2025, by Samuel Williams.
# Copyright, 2025, by William T. Nelson.
require_relative "split"
require_relative "../quoted_string"
require_relative "../error"
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
# Regular expression used to split values on commas, with optional surrounding whitespace, taking into account quoted strings.
SEPARATOR = /
(?: # Start non-capturing group
"[^"\\]*" # Match quoted strings (no escaping of quotes within)
| # OR
[^,"]+ # Match non-quoted strings until a comma or quote
)+
(?=,|\z) # Match until a comma or end of string
/x
ParseError = Class.new(Error)
MEDIA_RANGE = /\A(?<type>#{TOKEN})\/(?<subtype>#{TOKEN})(?<parameters>.*)\z/
PARAMETER = /\s*;\s*(?<key>#{TOKEN})=((?<value>#{TOKEN})|(?<quoted_value>#{QUOTED_STRING}))/
# A single entry in the Accept: header, which includes a mime type and associated parameters. A media range can include wild cards, but a media type is a specific type and subtype.
MediaRange = Struct.new(:type, :subtype, :parameters) do
# Create a new media range.
#
# @parameter type [String] the type of the media range.
# @parameter subtype [String] the subtype of the media range.
# @parameter parameters [Hash] the parameters associated with the media range.
def initialize(type, subtype = "*", parameters = {})
super(type, subtype, parameters)
end
# Compare the media range with another media range or a string, based on the quality factor.
def <=> other
other.quality_factor <=> self.quality_factor
end
private def parameters_string
return "" if parameters == nil or parameters.empty?
parameters.collect do |key, value|
";#{key.to_s}=#{QuotedString.quote(value.to_s)}"
end.join
end
# The string representation of the media range, including the type, subtype, and any parameters.
def to_s
"#{type}/#{subtype}#{parameters_string}"
end
alias to_str to_s
# The quality factor associated with the media range, which is used to determine the order of preference.
#
# @returns [Float] the quality factor, which defaults to 1.0 if not specified.
def quality_factor
parameters.fetch("q", 1.0).to_f
end
end
# Parse the `accept` header value into a list of content types.
#
# @parameter value [String] the value of the header.
def initialize(value = nil)
if value
super(value.scan(SEPARATOR).map(&:strip))
end
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.
def << value
self.concat(value.scan(SEPARATOR).map(&:strip))
end
# Serializes the stored values into a comma-separated string.
#
# @returns [String] the serialized representation of the header values.
def to_s
join(",")
end
# Whether this header is acceptable in HTTP trailers.
# @returns [Boolean] `false`, as Accept headers are used for response content negotiation.
def self.trailer?
false
end
# Parse the `accept` header.
#
# @returns [Array(Charset)] the list of content types and their associated parameters.
def media_ranges
self.map do |value|
self.parse_media_range(value)
end
end
private
def parse_media_range(value)
if match = value.match(MEDIA_RANGE)
type = match[:type]
subtype = match[:subtype]
parameters = {}
match[:parameters].scan(PARAMETER) do |key, value, quoted_value|
if quoted_value
value = QuotedString.unquote(quoted_value)
end
parameters[key] = value
end
return MediaRange.new(type, subtype, parameters)
else
raise ParseError, "Invalid media type: #{value.inspect}"
end
end
end
end
end
end
|