Skip to content

Commit 8190cf4

Browse files
committed
Add support for accept-language header.
1 parent 77c5f02 commit 8190cf4

File tree

4 files changed

+152
-2
lines changed

4 files changed

+152
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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-language` header represents a list of languages that the client can accept.
15+
class AcceptLanguage < Split
16+
ParseError = Class.new(Error)
17+
18+
# https://tools.ietf.org/html/rfc3066#section-2.1
19+
NAME = /\*|[A-Z]{1,8}(-[A-Z0-9]{1,8})*/i
20+
21+
# https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9
22+
QVALUE = /0(\.[0-9]{0,6})?|1(\.[0]{0,6})?/
23+
24+
# https://greenbytes.de/tech/webdav/rfc7231.html#quality.values
25+
LANGUAGE = /\A(?<name>#{NAME})(\s*;\s*q=(?<q>#{QVALUE}))?\z/
26+
27+
Language = Struct.new(:name, :q) do
28+
def quality_factor
29+
(q || 1.0).to_f
30+
end
31+
32+
def <=> other
33+
other.quality_factor <=> self.quality_factor
34+
end
35+
end
36+
37+
# Parse the `accept-language` header value into a list of languages.
38+
#
39+
# @returns [Array(Charset)] the list of character sets and their associated quality factors.
40+
def languages
41+
self.map do |value|
42+
if match = value.match(LANGUAGE)
43+
Language.new(match[:name], match[:q])
44+
else
45+
raise ParseError.new("Could not parse language: #{value.inspect}")
46+
end
47+
end
48+
end
49+
end
50+
end
51+
end
52+
end

lib/protocol/http/headers.rb

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
require_relative "header/accept_charset"
77
require_relative "header/accept_encoding"
8+
require_relative "header/accept_language"
89
require_relative "header/split"
910
require_relative "header/multiple"
1011
require_relative "header/cookie"
@@ -282,6 +283,7 @@ def []= key, value
282283

283284
"accept-charset" => Header::AcceptCharset,
284285
"accept-encoding" => Header::AcceptEncoding,
286+
"accept-language" => Header::AcceptLanguage,
285287
}.tap{|hash| hash.default = Split}
286288

287289
# Delete all header values for the given key, and return the merged value.

test/protocol/http/header/accept_encoding.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
describe Protocol::HTTP::Header::AcceptEncoding::Encoding do
1010
it "should have default quality_factor of 1.0" do
11-
charset = subject.new('utf-8', nil)
12-
expect(charset.quality_factor).to be == 1.0
11+
encoding = subject.new('utf-8', nil)
12+
expect(encoding.quality_factor).to be == 1.0
1313
end
1414
end
1515

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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_language'
8+
9+
describe Protocol::HTTP::Header::AcceptLanguage::Language do
10+
it "should have default quality_factor of 1.0" do
11+
language = subject.new('utf-8', nil)
12+
expect(language.quality_factor).to be == 1.0
13+
end
14+
end
15+
16+
describe Protocol::HTTP::Header::AcceptLanguage do
17+
let(:header) {subject.new(description)}
18+
let(:languages) {header.languages.sort}
19+
20+
with "da, en-gb;q=0.5, en;q=0.25" do
21+
it "can parse languages" do
22+
expect(header.length).to be == 3
23+
24+
expect(languages[0].name).to be == "da"
25+
expect(languages[0].quality_factor).to be == 1.0
26+
27+
expect(languages[1].name).to be == "en-gb"
28+
expect(languages[1].quality_factor).to be == 0.5
29+
30+
expect(languages[2].name).to be == "en"
31+
expect(languages[2].quality_factor).to be == 0.25
32+
end
33+
end
34+
35+
with "en-gb;q=0.25, en;q=0.5, en-us" do
36+
it "should order based on quality factor" do
37+
expect(languages.collect(&:name)).to be == %w{en-us en en-gb}
38+
end
39+
end
40+
41+
with "en-us,en-gb;q=0.8,en;q=0.6,es-419" do
42+
it "should order based on quality factor" do
43+
expect(languages.collect(&:name)).to be == %w{en-us es-419 en-gb en}
44+
end
45+
end
46+
47+
with "*;q=0" do
48+
it "should accept wildcard language" do
49+
expect(languages[0].name).to be == "*"
50+
expect(languages[0].quality_factor).to be == 0
51+
end
52+
end
53+
54+
with "en, de;q=0.5, jp;q=0.5" do
55+
it "should preserve relative order" do
56+
expect(languages[0].name).to be == "en"
57+
expect(languages[1].name).to be == "de"
58+
expect(languages[2].name).to be == "jp"
59+
end
60+
end
61+
62+
with "de, en-US; q=0.7, en ; q=0.3" do
63+
it "should parse with optional whitespace" do
64+
expect(languages[0].name).to be == "de"
65+
expect(languages[1].name).to be == "en-US"
66+
expect(languages[2].name).to be == "en"
67+
end
68+
end
69+
70+
with "en;q=0.123456" do
71+
it "accepts quality factors with up to 6 decimal places" do
72+
expect(languages[0].name).to be == "en"
73+
expect(languages[0].quality_factor).to be == 0.123456
74+
end
75+
end
76+
77+
it "should not accept invalid input" do
78+
bad_values = [
79+
# Invalid quality factor:
80+
"en;f=1",
81+
82+
# Invalid parameter:
83+
"de;fr",
84+
85+
# Invalid use of separator:
86+
";",
87+
88+
# Empty (we ignore this one):
89+
# ","
90+
]
91+
92+
bad_values.each do |value|
93+
expect{subject.new(value).languages}.to raise_exception(subject::ParseError)
94+
end
95+
end
96+
end

0 commit comments

Comments
 (0)