Skip to content

Commit

Permalink
Add support for accept header.
Browse files Browse the repository at this point in the history
  • Loading branch information
ioquatix committed Jan 7, 2025
1 parent 8190cf4 commit 3b14d08
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 4 deletions.
129 changes: 129 additions & 0 deletions lib/protocol/http/header/accept.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2020-2023, by Samuel Williams.
# Copyright, 2023, by Thomas Morgan.

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.
SPLIT = /
(?: # 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.
MediaRange = Struct.new(:type, :subtype, :parameters) do
def initialize(type, subtype = '*', parameters = {})
super(type, subtype, parameters)
end

def <=> other
other.quality_factor <=> self.quality_factor
end

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

def === other
if other.is_a? self.class
super
else
return self.mime_type === other
end
end

def mime_type
"#{type}/#{subtype}"
end

def to_s
"#{type}/#{subtype}#{parameters_string}"
end

alias to_str to_s

def quality_factor
parameters.fetch('q', 1.0).to_f
end

def split(*args)
return [type, subtype]
end
end

def initialize(value = nil)
if value
super(value.scan(SPLIT).map(&:strip))
else
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(SPLIT).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

# 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|
parameters[key] = quoted_value || value
end

return MediaRange.new(type, subtype, parameters)
else
raise ArgumentError, "Invalid media type: #{value.inspect}"
end
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/protocol/http/header/split.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def initialize(value = nil)
#
# @parameter value [String] the value or values to add, separated by commas.
def << value
self.push(*value.split(COMMA))
self.concat(value.split(COMMA))
end

# Serializes the stored values into a comma-separated string.
Expand Down
11 changes: 8 additions & 3 deletions lib/protocol/http/headers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
# Released under the MIT License.
# Copyright, 2018-2024, by Samuel Williams.

require_relative "header/accept_charset"
require_relative "header/accept_encoding"
require_relative "header/accept_language"
require_relative "header/split"
require_relative "header/multiple"

require_relative "header/cookie"
require_relative "header/connection"
require_relative "header/cache_control"
Expand All @@ -18,6 +16,11 @@
require_relative "header/date"
require_relative "header/priority"

require_relative "header/accept"
require_relative "header/accept_charset"
require_relative "header/accept_encoding"
require_relative "header/accept_language"

module Protocol
module HTTP
# @namespace
Expand Down Expand Up @@ -281,6 +284,8 @@ def []= key, value
"if-modified-since" => Header::Date,
"if-unmodified-since" => Header::Date,

# Accept headers:
"accept" => Header::Accept,
"accept-charset" => Header::AcceptCharset,
"accept-encoding" => Header::AcceptEncoding,
"accept-language" => Header::AcceptLanguage,
Expand Down
55 changes: 55 additions & 0 deletions test/protocol/http/header/accept.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2016, by Matthew Kerwin.
# Copyright, 2017-2024, by Samuel Williams.

require 'protocol/http/header/accept'

describe Protocol::HTTP::Header::Accept::MediaRange do
it "should have default quality_factor of 1.0" do
language = subject.new('text/plain', nil)
expect(language.quality_factor).to be == 1.0
end
end

describe Protocol::HTTP::Header::Accept do
let(:header) {subject.new(description)}
let(:media_ranges) {header.media_ranges.sort}

with "text/plain, text/html;q=0.5, text/xml;q=0.25" do
it "can parse media ranges" do
expect(header.length).to be == 3

expect(media_ranges[0].mime_type).to be == "text/plain"
expect(media_ranges[0].quality_factor).to be == 1.0

expect(media_ranges[1].mime_type).to be == "text/html"
expect(media_ranges[1].quality_factor).to be == 0.5

expect(media_ranges[2].mime_type).to be == "text/xml"
expect(media_ranges[2].quality_factor).to be == 0.25
end
end

with "text/html;q=0.25, text/xml;q=0.5, text/plain" do
it "should order based on quality factor" do
expect(media_ranges.collect(&:mime_type)).to be == %w{text/plain text/xml text/html}
end
end

with "text/html, text/plain;q=0.8, text/xml;q=0.6, application/json" do
it "should order based on quality factor" do
expect(media_ranges.collect(&:mime_type)).to be == %w{text/html application/json text/plain text/xml}
end
end

with "*/*;q=0" do
it "should accept wildcard media range" do
expect(media_ranges[0].mime_type).to be == "*/*"
expect(media_ranges[0].quality_factor).to be == 0
end
end


end

0 comments on commit 3b14d08

Please sign in to comment.