Skip to content

Commit 3b14d08

Browse files
committed
Add support for accept header.
1 parent 8190cf4 commit 3b14d08

File tree

4 files changed

+193
-4
lines changed

4 files changed

+193
-4
lines changed

lib/protocol/http/header/accept.rb

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2020-2023, by Samuel Williams.
5+
# Copyright, 2023, by Thomas Morgan.
6+
7+
require_relative "split"
8+
require_relative "quoted_string"
9+
require_relative "../error"
10+
11+
module Protocol
12+
module HTTP
13+
module Header
14+
# The `accept-content-type` header represents a list of content-types that the client can accept.
15+
class Accept < Array
16+
# Regular expression used to split values on commas, with optional surrounding whitespace, taking into account quoted strings.
17+
SPLIT = /
18+
(?: # Start non-capturing group
19+
"[^"\\]*" # Match quoted strings (no escaping of quotes within)
20+
| # OR
21+
[^,"]+ # Match non-quoted strings until a comma or quote
22+
)+
23+
(?=,|\z) # Match until a comma or end of string
24+
/x
25+
26+
ParseError = Class.new(Error)
27+
28+
MEDIA_RANGE = /\A(?<type>#{TOKEN})\/(?<subtype>#{TOKEN})(?<parameters>.*)\z/
29+
30+
PARAMETER = /\s*;\s*(?<key>#{TOKEN})=((?<value>#{TOKEN})|(?<quoted_value>#{QUOTED_STRING}))/
31+
32+
# A single entry in the Accept: header, which includes a mime type and associated parameters.
33+
MediaRange = Struct.new(:type, :subtype, :parameters) do
34+
def initialize(type, subtype = '*', parameters = {})
35+
super(type, subtype, parameters)
36+
end
37+
38+
def <=> other
39+
other.quality_factor <=> self.quality_factor
40+
end
41+
42+
def parameters_string
43+
return '' if parameters == nil or parameters.empty?
44+
45+
parameters.collect do |key, value|
46+
"; #{key.to_s}=#{QuotedString.quote(value.to_s)}"
47+
end.join
48+
end
49+
50+
def === other
51+
if other.is_a? self.class
52+
super
53+
else
54+
return self.mime_type === other
55+
end
56+
end
57+
58+
def mime_type
59+
"#{type}/#{subtype}"
60+
end
61+
62+
def to_s
63+
"#{type}/#{subtype}#{parameters_string}"
64+
end
65+
66+
alias to_str to_s
67+
68+
def quality_factor
69+
parameters.fetch('q', 1.0).to_f
70+
end
71+
72+
def split(*args)
73+
return [type, subtype]
74+
end
75+
end
76+
77+
def initialize(value = nil)
78+
if value
79+
super(value.scan(SPLIT).map(&:strip))
80+
else
81+
end
82+
end
83+
84+
# Adds one or more comma-separated values to the header.
85+
#
86+
# The input string is split into distinct entries and appended to the array.
87+
#
88+
# @parameter value [String] the value or values to add, separated by commas.
89+
def << (value)
90+
self.concat(value.scan(SPLIT).map(&:strip))
91+
end
92+
93+
# Serializes the stored values into a comma-separated string.
94+
#
95+
# @returns [String] the serialized representation of the header values.
96+
def to_s
97+
join(",")
98+
end
99+
100+
# Parse the `accept` header.
101+
#
102+
# @returns [Array(Charset)] the list of content types and their associated parameters.
103+
def media_ranges
104+
self.map do |value|
105+
self.parse_media_range(value)
106+
end
107+
end
108+
109+
private
110+
111+
def parse_media_range(value)
112+
if match = value.match(MEDIA_RANGE)
113+
type = match[:type]
114+
subtype = match[:subtype]
115+
parameters = {}
116+
117+
match[:parameters].scan(PARAMETER) do |key, value, quoted_value|
118+
parameters[key] = quoted_value || value
119+
end
120+
121+
return MediaRange.new(type, subtype, parameters)
122+
else
123+
raise ArgumentError, "Invalid media type: #{value.inspect}"
124+
end
125+
end
126+
end
127+
end
128+
end
129+
end

lib/protocol/http/header/split.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def initialize(value = nil)
3030
#
3131
# @parameter value [String] the value or values to add, separated by commas.
3232
def << value
33-
self.push(*value.split(COMMA))
33+
self.concat(value.split(COMMA))
3434
end
3535

3636
# Serializes the stored values into a comma-separated string.

lib/protocol/http/headers.rb

+8-3
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33
# Released under the MIT License.
44
# Copyright, 2018-2024, by Samuel Williams.
55

6-
require_relative "header/accept_charset"
7-
require_relative "header/accept_encoding"
8-
require_relative "header/accept_language"
96
require_relative "header/split"
107
require_relative "header/multiple"
8+
119
require_relative "header/cookie"
1210
require_relative "header/connection"
1311
require_relative "header/cache_control"
@@ -18,6 +16,11 @@
1816
require_relative "header/date"
1917
require_relative "header/priority"
2018

19+
require_relative "header/accept"
20+
require_relative "header/accept_charset"
21+
require_relative "header/accept_encoding"
22+
require_relative "header/accept_language"
23+
2124
module Protocol
2225
module HTTP
2326
# @namespace
@@ -281,6 +284,8 @@ def []= key, value
281284
"if-modified-since" => Header::Date,
282285
"if-unmodified-since" => Header::Date,
283286

287+
# Accept headers:
288+
"accept" => Header::Accept,
284289
"accept-charset" => Header::AcceptCharset,
285290
"accept-encoding" => Header::AcceptEncoding,
286291
"accept-language" => Header::AcceptLanguage,

test/protocol/http/header/accept.rb

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2016, by Matthew Kerwin.
5+
# Copyright, 2017-2024, by Samuel Williams.
6+
7+
require 'protocol/http/header/accept'
8+
9+
describe Protocol::HTTP::Header::Accept::MediaRange do
10+
it "should have default quality_factor of 1.0" do
11+
language = subject.new('text/plain', nil)
12+
expect(language.quality_factor).to be == 1.0
13+
end
14+
end
15+
16+
describe Protocol::HTTP::Header::Accept do
17+
let(:header) {subject.new(description)}
18+
let(:media_ranges) {header.media_ranges.sort}
19+
20+
with "text/plain, text/html;q=0.5, text/xml;q=0.25" do
21+
it "can parse media ranges" do
22+
expect(header.length).to be == 3
23+
24+
expect(media_ranges[0].mime_type).to be == "text/plain"
25+
expect(media_ranges[0].quality_factor).to be == 1.0
26+
27+
expect(media_ranges[1].mime_type).to be == "text/html"
28+
expect(media_ranges[1].quality_factor).to be == 0.5
29+
30+
expect(media_ranges[2].mime_type).to be == "text/xml"
31+
expect(media_ranges[2].quality_factor).to be == 0.25
32+
end
33+
end
34+
35+
with "text/html;q=0.25, text/xml;q=0.5, text/plain" do
36+
it "should order based on quality factor" do
37+
expect(media_ranges.collect(&:mime_type)).to be == %w{text/plain text/xml text/html}
38+
end
39+
end
40+
41+
with "text/html, text/plain;q=0.8, text/xml;q=0.6, application/json" do
42+
it "should order based on quality factor" do
43+
expect(media_ranges.collect(&:mime_type)).to be == %w{text/html application/json text/plain text/xml}
44+
end
45+
end
46+
47+
with "*/*;q=0" do
48+
it "should accept wildcard media range" do
49+
expect(media_ranges[0].mime_type).to be == "*/*"
50+
expect(media_ranges[0].quality_factor).to be == 0
51+
end
52+
end
53+
54+
55+
end

0 commit comments

Comments
 (0)