accept_headers 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +8 -0
- data/.github/workflows/ci.yml +23 -0
- data/CHANGELOG.md +23 -1
- data/Gemfile +2 -1
- data/README.md +134 -151
- data/accept_headers.gemspec +2 -2
- data/lib/accept_headers/acceptable.rb +8 -0
- data/lib/accept_headers/encoding/negotiator.rb +23 -8
- data/lib/accept_headers/encoding.rb +5 -3
- data/lib/accept_headers/invalid_supported_value.rb +3 -0
- data/lib/accept_headers/language/negotiator.rb +7 -2
- data/lib/accept_headers/language.rb +11 -28
- data/lib/accept_headers/media_type/negotiator.rb +32 -19
- data/lib/accept_headers/media_type.rb +35 -14
- data/lib/accept_headers/negotiatable.rb +70 -28
- data/lib/accept_headers/version.rb +1 -1
- data/spec/encoding/negotiator_spec.rb +60 -6
- data/spec/encoding_spec.rb +8 -0
- data/spec/equality_spec.rb +70 -0
- data/spec/language/negotiator_spec.rb +73 -50
- data/spec/language_spec.rb +14 -4
- data/spec/malformed_spec.rb +178 -0
- data/spec/media_type/negotiator_spec.rb +97 -19
- data/spec/media_type_spec.rb +117 -3
- data/spec/middleware_spec.rb +5 -4
- data/spec/readme_spec.rb +67 -0
- data/spec/rejection_spec.rb +62 -0
- data/spec/spec_helper.rb +1 -3
- data/spec/supported_values_spec.rb +158 -0
- metadata +20 -11
- data/.travis.yml +0 -14
data/spec/media_type_spec.rb
CHANGED
|
@@ -25,8 +25,8 @@ describe AcceptHeaders::MediaType do
|
|
|
25
25
|
subject.new('*', nil).subtype.must_equal '*'
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
it "strips
|
|
29
|
-
subject.new('*', '*', extensions: { "\s\nLEVEL\r\t" => "\t\nX\s\n"}).extensions['
|
|
28
|
+
it "strips and downcases parameter names while preserving values" do
|
|
29
|
+
subject.new('*', '*', extensions: { "\s\nLEVEL\r\t" => "\t\nX\s\n"}).extensions['level'].must_equal "\t\nX\s\n"
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
it "optionally supports a q argument" do
|
|
@@ -92,14 +92,128 @@ describe AcceptHeaders::MediaType do
|
|
|
92
92
|
|
|
93
93
|
it "converts to string" do
|
|
94
94
|
s = subject.new('text', 'html', q: 0.9, extensions: { 'level' => '1' }).to_s
|
|
95
|
-
s.must_equal "text/html;q=0.9
|
|
95
|
+
s.must_equal "text/html;level=1;q=0.9"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "quotes parameter delimiters and escapes quotes and backslashes" do
|
|
99
|
+
value = subject.new('text', 'html', extensions: { 'note' => 'a,b;c"d\\e', 'empty' => '' })
|
|
100
|
+
value.to_s.must_equal 'text/html;note="a,b;c\\"d\\\\e";empty="";q=1'
|
|
96
101
|
end
|
|
97
102
|
|
|
98
103
|
it "outputs the media range" do
|
|
99
104
|
subject.new('text', 'html', extensions: { 'level' => '1' }).media_range.must_equal "text/html"
|
|
100
105
|
end
|
|
101
106
|
|
|
107
|
+
describe "#content_type" do
|
|
108
|
+
it "outputs type, subtype, and parameters without the quality weight" do
|
|
109
|
+
media_type = subject.new('text', 'html', q: 0.9, extensions: { 'level' => '1' })
|
|
110
|
+
|
|
111
|
+
media_type.content_type.must_equal 'text/html;level=1'
|
|
112
|
+
media_type.to_s.must_equal 'text/html;level=1;q=0.9'
|
|
113
|
+
media_type.media_range.must_equal 'text/html'
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "does not add a quality parameter when there are no media parameters" do
|
|
117
|
+
subject.new('application', 'json').content_type.must_equal 'application/json'
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "uses the same parameter quoting as the Accept representation" do
|
|
121
|
+
{
|
|
122
|
+
'a,b' => '"a,b"',
|
|
123
|
+
'a;b' => '"a;b"',
|
|
124
|
+
'a"b' => '"a\\"b"',
|
|
125
|
+
'a\\b' => '"a\\\\b"',
|
|
126
|
+
'' => '""',
|
|
127
|
+
"caf\u00e9" => "\"caf\u00e9\"",
|
|
128
|
+
"\x80\xFF".b => "\"\x80\xFF\"".b
|
|
129
|
+
}.each do |parameter, encoded|
|
|
130
|
+
media_type = subject.new('text', 'plain', extensions: { 'note' => parameter })
|
|
131
|
+
|
|
132
|
+
media_type.content_type.must_equal "text/plain;note=#{encoded}"
|
|
133
|
+
media_type.to_s.must_equal "text/plain;note=#{encoded};q=1"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "does not mutate the media type or parameter values" do
|
|
138
|
+
parameter = 'a,b;c"d\\e'.freeze
|
|
139
|
+
extensions = { 'note' => parameter }.freeze
|
|
140
|
+
media_type = subject.new('text', 'plain', q: 0.8, extensions: extensions)
|
|
141
|
+
original = media_type.to_h
|
|
142
|
+
result = media_type.content_type
|
|
143
|
+
result.replace('application/json')
|
|
144
|
+
|
|
145
|
+
media_type.to_h.must_equal original
|
|
146
|
+
media_type.extensions.must_equal extensions
|
|
147
|
+
media_type.content_type.must_equal 'text/plain;note="a,b;c\\"d\\\\e"'
|
|
148
|
+
media_type.to_s.must_equal 'text/plain;note="a,b;c\\"d\\\\e";q=0.8'
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
describe "#match" do
|
|
153
|
+
it "preserves raw comparison for hand-built non-String parameter values" do
|
|
154
|
+
[2, :public, nil].each do |value|
|
|
155
|
+
range = subject.new('application', 'json', extensions: { 'version' => value })
|
|
156
|
+
excluded = subject.new('application', 'json', q: 0, extensions: { 'version' => value })
|
|
157
|
+
|
|
158
|
+
['application/json;version=2', 'application/json;version=public'].each do |candidate|
|
|
159
|
+
range.match(candidate).must_equal false
|
|
160
|
+
range.accept?(candidate).must_equal false
|
|
161
|
+
excluded.reject?(candidate).must_equal false
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
expected = value.nil?
|
|
165
|
+
range.match('application/json').must_equal expected
|
|
166
|
+
range.accept?('application/json').must_equal expected
|
|
167
|
+
excluded.reject?('application/json').must_equal expected
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "folds only ASCII case in charset values" do
|
|
172
|
+
{ 'UTF-8' => 'utf-8', 'É' => 'é', 'K' => 'k' }.each do |value, candidate|
|
|
173
|
+
range = subject.new('text', 'plain', extensions: { 'charset' => value })
|
|
174
|
+
excluded = subject.new('text', 'plain', q: 0, extensions: { 'charset' => value })
|
|
175
|
+
header = "text/plain;charset=\"#{candidate}\""
|
|
176
|
+
expected = value == 'UTF-8'
|
|
177
|
+
|
|
178
|
+
range.match(header).must_equal expected
|
|
179
|
+
range.accept?(header).must_equal expected
|
|
180
|
+
excluded.reject?(header).must_equal expected
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
102
185
|
describe "#accept?" do
|
|
186
|
+
it "matches type and subtype without case sensitivity" do
|
|
187
|
+
subject.new('text', 'html').accept?('TEXT/HTML').must_equal true
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it "requires every media range parameter to match the supported representation" do
|
|
191
|
+
range = subject.new('application', 'json', extensions: { 'version' => '2' })
|
|
192
|
+
range.accept?('application/json').must_equal false
|
|
193
|
+
range.accept?('application/json;version=1').must_equal false
|
|
194
|
+
range.accept?('application/json;VERSION=2').must_equal true
|
|
195
|
+
range.accept?('application/json;version=2;profile=public').must_equal true
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it "matches charset values without case sensitivity" do
|
|
199
|
+
range = subject.new('text', 'plain', extensions: { 'charset' => 'UTF-8' })
|
|
200
|
+
range.accept?('text/plain;charset=utf-8').must_equal true
|
|
201
|
+
range.accept?('text/plain;charset=iso-8859-1').must_equal false
|
|
202
|
+
range.accept?('text/plain').must_equal false
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it "preserves case sensitivity for other parameter values" do
|
|
206
|
+
range = subject.new('application', 'json', extensions: { 'profile' => 'Public' })
|
|
207
|
+
range.accept?('application/json;profile=Public').must_equal true
|
|
208
|
+
range.accept?('application/json;profile=public').must_equal false
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "does not match an empty candidate or a list of candidates" do
|
|
212
|
+
range = subject.new('*')
|
|
213
|
+
range.accept?('').must_equal false
|
|
214
|
+
range.accept?('text/html,application/json').must_equal false
|
|
215
|
+
end
|
|
216
|
+
|
|
103
217
|
it "accepted if the type and subtype are the same" do
|
|
104
218
|
a = subject.new('text', 'html')
|
|
105
219
|
a.accept?('text/html').must_equal true
|
data/spec/middleware_spec.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require_relative "spec_helper"
|
|
2
2
|
require "accept_headers/middleware"
|
|
3
|
-
require "rack/
|
|
3
|
+
require "rack/builder"
|
|
4
|
+
require "rack/mock"
|
|
4
5
|
|
|
5
6
|
describe AcceptHeaders::Middleware do
|
|
6
7
|
subject do
|
|
@@ -19,9 +20,9 @@ describe AcceptHeaders::Middleware do
|
|
|
19
20
|
matched_encoding = env["accept_headers.encodings"].negotiate(supported_encodings)
|
|
20
21
|
matched_language = env["accept_headers.languages"].negotiate(supported_languages)
|
|
21
22
|
headers = {
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
23
|
+
'content-type' => matched_media_type ? matched_media_type.media_range : '',
|
|
24
|
+
'content-encoding' => matched_encoding ? matched_encoding.encoding : '',
|
|
25
|
+
'content-language' => matched_language ? matched_language.language_tag : '',
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
[ 200, headers, [ '' ] ]
|
data/spec/readme_spec.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require_relative "spec_helper"
|
|
2
|
+
require "rack/builder"
|
|
3
|
+
require "rack/lint"
|
|
4
|
+
require "rack/mock"
|
|
5
|
+
|
|
6
|
+
class ReadmeSpec < Minitest::Test
|
|
7
|
+
def test_documented_negotiation_results
|
|
8
|
+
context = binding
|
|
9
|
+
examples.each do |example|
|
|
10
|
+
next if example.start_with?("gem ") || example.include?('use AcceptHeaders::Middleware')
|
|
11
|
+
source = +""
|
|
12
|
+
example.each_line do |line|
|
|
13
|
+
if (expectation = /\A# => (.*)/.match(line))
|
|
14
|
+
expected = eval(expectation[1], context, readme_path)
|
|
15
|
+
actual = eval(source, context, readme_path)
|
|
16
|
+
if expected.nil?
|
|
17
|
+
assert_nil actual, source
|
|
18
|
+
else
|
|
19
|
+
assert_equal expected, actual, source
|
|
20
|
+
end
|
|
21
|
+
source.clear
|
|
22
|
+
else
|
|
23
|
+
source << line
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
eval(source, context, readme_path) unless source.strip.empty?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_rack_example_selects_supported_responses_and_honors_rejections
|
|
31
|
+
source = examples.find { |example| example.include?('use AcceptHeaders::Middleware') }
|
|
32
|
+
refute_nil source
|
|
33
|
+
builder = Rack::Builder.new
|
|
34
|
+
builder.instance_eval(source, readme_path)
|
|
35
|
+
request = Rack::MockRequest.new(Rack::Lint.new(builder.to_app))
|
|
36
|
+
|
|
37
|
+
response = request.get('/', 'HTTP_ACCEPT' => 'application/json', 'HTTP_ACCEPT_LANGUAGE' => 'en-GB')
|
|
38
|
+
assert_equal 200, response.status
|
|
39
|
+
assert_equal 'application/json;version=2', response['content-type']
|
|
40
|
+
assert_equal 'en-gb', response['content-language']
|
|
41
|
+
assert_equal '{"message":"Hello"}', response.body
|
|
42
|
+
assert_nil response['content-encoding']
|
|
43
|
+
assert_equal 'Accept, Accept-Encoding, Accept-Language', response['vary']
|
|
44
|
+
|
|
45
|
+
[
|
|
46
|
+
{ 'HTTP_ACCEPT' => 'application/xml' },
|
|
47
|
+
{ 'HTTP_ACCEPT' => 'application/json;version=1' },
|
|
48
|
+
{ 'HTTP_ACCEPT_ENCODING' => 'identity;q=0' },
|
|
49
|
+
{ 'HTTP_ACCEPT_LANGUAGE' => 'fr' }
|
|
50
|
+
].each do |headers|
|
|
51
|
+
rejected = request.get('/', headers)
|
|
52
|
+
assert_equal 406, rejected.status, headers.inspect
|
|
53
|
+
assert_equal 'Not acceptable', rejected.body
|
|
54
|
+
assert_equal 'Accept, Accept-Encoding, Accept-Language', rejected['vary']
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def readme_path
|
|
61
|
+
File.expand_path('../README.md', __dir__)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def examples
|
|
65
|
+
File.read(readme_path).scan(/```ruby\n(.*?)```/m).flatten
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require_relative "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe "Negotiation exclusions" do
|
|
4
|
+
it "rejects an explicitly excluded media type despite an acceptable wildcard" do
|
|
5
|
+
negotiator = AcceptHeaders::MediaType::Negotiator.new('text/html;q=0, */*;q=1')
|
|
6
|
+
|
|
7
|
+
negotiator.negotiate('text/html').must_be_nil
|
|
8
|
+
negotiator.accept?('text/html').must_equal false
|
|
9
|
+
negotiator.negotiate(['text/html', 'application/json']).media_range.must_equal 'application/json'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "allows an explicit media type to override a wildcard exclusion" do
|
|
13
|
+
negotiator = AcceptHeaders::MediaType::Negotiator.new('*/*;q=0, text/html;q=1')
|
|
14
|
+
|
|
15
|
+
negotiator.negotiate('text/html').media_range.must_equal 'text/html'
|
|
16
|
+
negotiator.accept?('application/json').must_equal false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "rejects an explicitly excluded encoding despite an acceptable wildcard" do
|
|
20
|
+
negotiator = AcceptHeaders::Encoding::Negotiator.new('gzip;q=0, *;q=1')
|
|
21
|
+
|
|
22
|
+
negotiator.negotiate('gzip').must_be_nil
|
|
23
|
+
negotiator.accept?('gzip').must_equal false
|
|
24
|
+
negotiator.negotiate(['gzip', 'br']).encoding.must_equal 'br'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "allows an explicit encoding to override a wildcard exclusion" do
|
|
28
|
+
negotiator = AcceptHeaders::Encoding::Negotiator.new('*;q=0, gzip;q=1')
|
|
29
|
+
|
|
30
|
+
negotiator.negotiate('gzip').encoding.must_equal 'gzip'
|
|
31
|
+
negotiator.accept?('br').must_equal false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "rejects an explicitly excluded language despite an acceptable wildcard" do
|
|
35
|
+
negotiator = AcceptHeaders::Language::Negotiator.new('en-us;q=0, *;q=1')
|
|
36
|
+
|
|
37
|
+
negotiator.negotiate('en-us').must_be_nil
|
|
38
|
+
negotiator.accept?('en-us').must_equal false
|
|
39
|
+
negotiator.negotiate(['en-us', 'fr-ca']).language_tag.must_equal 'fr-ca'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "allows an explicit language to override a wildcard exclusion" do
|
|
43
|
+
negotiator = AcceptHeaders::Language::Negotiator.new('*;q=0, en-us;q=1')
|
|
44
|
+
|
|
45
|
+
negotiator.negotiate('en-us').language_tag.must_equal 'en-us'
|
|
46
|
+
negotiator.accept?('fr-ca').must_equal false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "allows a regional language to override a broader language exclusion" do
|
|
50
|
+
negotiator = AcceptHeaders::Language::Negotiator.new('en;q=0, en-us;q=1')
|
|
51
|
+
|
|
52
|
+
negotiator.negotiate('en-us').language_tag.must_equal 'en-us'
|
|
53
|
+
negotiator.accept?('en-gb').must_equal false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "rejects a regional language despite an acceptable broader language" do
|
|
57
|
+
negotiator = AcceptHeaders::Language::Negotiator.new('en-us;q=0, en;q=1')
|
|
58
|
+
|
|
59
|
+
negotiator.negotiate('en-us').must_be_nil
|
|
60
|
+
negotiator.negotiate(['en-us', 'en-gb']).language_tag.must_equal 'en-gb'
|
|
61
|
+
end
|
|
62
|
+
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
require_relative "spec_helper"
|
|
2
|
+
require "open3"
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
|
|
5
|
+
describe "supported representation validation" do
|
|
6
|
+
[
|
|
7
|
+
[AcceptHeaders::MediaType::Negotiator, 'text/plain', '*/*', 'Accept: text/plain'],
|
|
8
|
+
[AcceptHeaders::Encoding::Negotiator, 'gzip', '*', 'Accept-Encoding: gzip'],
|
|
9
|
+
[AcceptHeaders::Language::Negotiator, 'en-US', '*', 'Accept-Language: en-US']
|
|
10
|
+
].each do |negotiator_class, valid, wildcard, prefixed|
|
|
11
|
+
describe negotiator_class.name do
|
|
12
|
+
it "rejects invalid offers before considering any header or other offer" do
|
|
13
|
+
invalid = [nil, false, 42, Object.new, '', ' ', wildcard, prefixed,
|
|
14
|
+
"#{valid},#{valid}", "#{valid};q=1", "#{valid};Q=1",
|
|
15
|
+
"#{valid};q=0.5", "#{valid};q=0", "#{valid}\r\n",
|
|
16
|
+
"#{valid}\0", "#{valid}\x7f", "#{valid}\v",
|
|
17
|
+
"\xff".b.force_encoding('UTF-8'), valid.encode('UTF-16LE')]
|
|
18
|
+
[nil, '', "#{wildcard};q=0", wildcard].each do |header|
|
|
19
|
+
negotiator = negotiator_class.new(header)
|
|
20
|
+
invalid.each do |value|
|
|
21
|
+
[:negotiate, :accept?].each do |method|
|
|
22
|
+
assert_raises(AcceptHeaders::InvalidSupportedValue, "#{method}: #{value.inspect}") do
|
|
23
|
+
negotiator.public_send(method, [valid, value])
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "raises ArgumentError for an invalid standalone offer" do
|
|
31
|
+
[false, {}, Object.new].each do |offer|
|
|
32
|
+
assert_raises(AcceptHeaders::InvalidSupportedValue) do
|
|
33
|
+
negotiator_class.new(wildcard).negotiate(offer)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "retains nil and empty arrays as no offers" do
|
|
39
|
+
[nil, []].each do |supported|
|
|
40
|
+
negotiator = negotiator_class.new(nil)
|
|
41
|
+
assert_nil negotiator.negotiate(supported)
|
|
42
|
+
assert_equal false, negotiator.accept?(supported)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "rejects objects that convert themselves to offer arrays" do
|
|
47
|
+
supported = Object.new
|
|
48
|
+
supported.define_singleton_method(:to_a) { [valid] }
|
|
49
|
+
assert_raises(AcceptHeaders::InvalidSupportedValue) do
|
|
50
|
+
negotiator_class.new(nil).negotiate(supported)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "accepts case and optional whitespace without mutating frozen offers" do
|
|
55
|
+
offer = " \t#{valid.upcase}\t ".freeze
|
|
56
|
+
negotiator = negotiator_class.new(wildcard)
|
|
57
|
+
assert_equal true, negotiator.accept?(offer)
|
|
58
|
+
assert_equal " \t#{valid.upcase}\t ", offer
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe "media type syntax" do
|
|
64
|
+
let(:negotiator) { AcceptHeaders::MediaType::Negotiator.new('*/*') }
|
|
65
|
+
|
|
66
|
+
it "rejects incomplete types and malformed parameters throughout the offer" do
|
|
67
|
+
['text', 'text/*', '*/plain', 'text/plain/', 'text /plain',
|
|
68
|
+
'text/plain;note', 'text/plain;note=', 'text/plain;=value',
|
|
69
|
+
'text/plain;note=two words', 'text/plain;note="unclosed',
|
|
70
|
+
'text/plain;note="closed"junk', 'text/plain;note="trailing' + "\\",
|
|
71
|
+
'text/plain;note="valid";broken', 'text/plain; Q = "1"',
|
|
72
|
+
'text/plain;note="a,b" , application/json', 'text/plain,garbage',
|
|
73
|
+
"text/plain;note=\"line\nbreak\"", "text/plain;note=\"escaped\\\nbreak\""].each do |offer|
|
|
74
|
+
assert_raises(AcceptHeaders::InvalidSupportedValue, offer) { negotiator.negotiate(offer) }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "preserves quoted delimiters, escapes, quality text, and non-ASCII values" do
|
|
79
|
+
offer = 'Text/Plain;note="a,b;c\\"d\\\\e;q=1";label="café"'.freeze
|
|
80
|
+
result = negotiator.negotiate(offer)
|
|
81
|
+
assert_equal 'text/plain', result.media_range
|
|
82
|
+
assert_equal({'note' => 'a,b;c"d\\e;q=1', 'label' => "caf\u00e9"}, result.extensions)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "preserves quoted obs-text bytes" do
|
|
86
|
+
offer = "text/plain;note=\"\x80\xff\"".b.freeze
|
|
87
|
+
result = negotiator.negotiate(offer)
|
|
88
|
+
assert_equal "\x80\xff".b, result.extensions['note']
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "uses HTTP bytes for multibyte offers, quoted pairs, and response escaping" do
|
|
92
|
+
{
|
|
93
|
+
[0x95, 0x5c, 0x5c] => [0x95, 0x5c],
|
|
94
|
+
[0x95, 0x5c, 0x5c, 0x61] => [0x95, 0x5c, 0x61],
|
|
95
|
+
[0x95, 0x5c, 0x61] => [0x95, 0x61]
|
|
96
|
+
}.each do |bytes, expected|
|
|
97
|
+
wire = 'text/plain;note="'.b + bytes.pack('C*') + '"'
|
|
98
|
+
offer = wire.dup.force_encoding('Shift_JIS').freeze
|
|
99
|
+
original = offer.dup
|
|
100
|
+
[nil, '*/*', wire, offer].each do |header|
|
|
101
|
+
matcher = AcceptHeaders::MediaType::Negotiator.new(header)
|
|
102
|
+
assert matcher.accept?(offer)
|
|
103
|
+
result = matcher.negotiate(offer)
|
|
104
|
+
assert_equal expected, result.extensions['note'].bytes
|
|
105
|
+
binary_result = negotiator.negotiate(wire)
|
|
106
|
+
assert_equal binary_result.content_type.bytes, result.content_type.bytes
|
|
107
|
+
assert_equal binary_result.to_s.bytes, result.to_s.bytes
|
|
108
|
+
[result.content_type, result.to_s].each do |serialized|
|
|
109
|
+
reparsed = AcceptHeaders::MediaType::Negotiator.new(serialized.b).list.first
|
|
110
|
+
assert_equal expected, reparsed.extensions['note'].bytes
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
assert_equal original, offer
|
|
114
|
+
assert_equal original.encoding, offer.encoding
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "compares parameter bytes across encodings without transcoding or Unicode case folding" do
|
|
119
|
+
offer = 'text/plain;note="café";charset="K"'.freeze
|
|
120
|
+
labelled_offer = offer.dup.force_encoding('ISO-8859-1').freeze
|
|
121
|
+
[offer, offer.b, labelled_offer].each do |header|
|
|
122
|
+
matcher = AcceptHeaders::MediaType::Negotiator.new(header)
|
|
123
|
+
[offer, offer.b, labelled_offer].each do |supported|
|
|
124
|
+
result = matcher.negotiate(supported)
|
|
125
|
+
assert_equal offer.bytes, result.content_type.bytes
|
|
126
|
+
end
|
|
127
|
+
refute matcher.accept?('text/plain;note="café";charset=k')
|
|
128
|
+
refute matcher.accept?(offer.encode('Windows-1252', undef: :replace))
|
|
129
|
+
end
|
|
130
|
+
result = negotiator.negotiate(offer)
|
|
131
|
+
assert_equal Encoding::UTF_8, result.extensions['note'].encoding
|
|
132
|
+
assert_equal Encoding::UTF_8, result.content_type.encoding
|
|
133
|
+
|
|
134
|
+
utf8_offer = 'text/plain;note="café"'.freeze
|
|
135
|
+
latin1_offer = utf8_offer.encode('ISO-8859-1').freeze
|
|
136
|
+
[utf8_offer, latin1_offer].each do |header|
|
|
137
|
+
matcher = AcceptHeaders::MediaType::Negotiator.new(header)
|
|
138
|
+
other_offer = header.equal?(utf8_offer) ? latin1_offer : utf8_offer
|
|
139
|
+
assert_nil matcher.negotiate(other_offer)
|
|
140
|
+
refute matcher.accept?(other_offer)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "accepts empty parameter segments and empty quoted values" do
|
|
145
|
+
result = negotiator.negotiate('text/plain; ; note="";;')
|
|
146
|
+
assert_equal({'note' => ''}, result.extensions)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it "exposes the shared exception through each direct negotiator require" do
|
|
151
|
+
%w[media_type encoding language].each do |kind|
|
|
152
|
+
script = "require 'accept_headers/#{kind}/negotiator'; " \
|
|
153
|
+
"abort unless AcceptHeaders::InvalidSupportedValue < ArgumentError"
|
|
154
|
+
output, status = Open3.capture2e(RbConfig.ruby, '-Ilib', '-e', script)
|
|
155
|
+
assert status.success?, output
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: accept_headers
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jack Chu
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rake
|
|
@@ -16,14 +15,14 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - "~>"
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
18
|
+
version: '13.0'
|
|
20
19
|
type: :development
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - "~>"
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
25
|
+
version: '13.0'
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
27
|
name: minitest
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -66,16 +65,17 @@ dependencies:
|
|
|
66
65
|
- - ">="
|
|
67
66
|
- !ruby/object:Gem::Version
|
|
68
67
|
version: '0'
|
|
69
|
-
description: A ruby library that
|
|
70
|
-
|
|
68
|
+
description: A ruby library that parses HTTP accept headers and negotiates media types,
|
|
69
|
+
content codings, and languages using RFC 9110 and RFC 4647 Basic Filtering.
|
|
71
70
|
email:
|
|
72
71
|
- kamuigt@gmail.com
|
|
73
72
|
executables: []
|
|
74
73
|
extensions: []
|
|
75
74
|
extra_rdoc_files: []
|
|
76
75
|
files:
|
|
76
|
+
- ".github/dependabot.yml"
|
|
77
|
+
- ".github/workflows/ci.yml"
|
|
77
78
|
- ".gitignore"
|
|
78
|
-
- ".travis.yml"
|
|
79
79
|
- CHANGELOG.md
|
|
80
80
|
- Gemfile
|
|
81
81
|
- Guardfile
|
|
@@ -87,6 +87,7 @@ files:
|
|
|
87
87
|
- lib/accept_headers/acceptable.rb
|
|
88
88
|
- lib/accept_headers/encoding.rb
|
|
89
89
|
- lib/accept_headers/encoding/negotiator.rb
|
|
90
|
+
- lib/accept_headers/invalid_supported_value.rb
|
|
90
91
|
- lib/accept_headers/language.rb
|
|
91
92
|
- lib/accept_headers/language/negotiator.rb
|
|
92
93
|
- lib/accept_headers/media_type.rb
|
|
@@ -96,11 +97,15 @@ files:
|
|
|
96
97
|
- lib/accept_headers/version.rb
|
|
97
98
|
- spec/encoding/negotiator_spec.rb
|
|
98
99
|
- spec/encoding_spec.rb
|
|
100
|
+
- spec/equality_spec.rb
|
|
99
101
|
- spec/language/negotiator_spec.rb
|
|
100
102
|
- spec/language_spec.rb
|
|
103
|
+
- spec/malformed_spec.rb
|
|
101
104
|
- spec/media_type/negotiator_spec.rb
|
|
102
105
|
- spec/media_type_spec.rb
|
|
103
106
|
- spec/middleware_spec.rb
|
|
107
|
+
- spec/readme_spec.rb
|
|
108
|
+
- spec/rejection_spec.rb
|
|
104
109
|
- spec/spec_helper.rb
|
|
105
110
|
- spec/support/encodings/content-coding.csv
|
|
106
111
|
- spec/support/media_types/application.csv
|
|
@@ -111,11 +116,11 @@ files:
|
|
|
111
116
|
- spec/support/media_types/multipart.csv
|
|
112
117
|
- spec/support/media_types/text.csv
|
|
113
118
|
- spec/support/media_types/video.csv
|
|
119
|
+
- spec/supported_values_spec.rb
|
|
114
120
|
homepage: ''
|
|
115
121
|
licenses:
|
|
116
122
|
- MIT
|
|
117
123
|
metadata: {}
|
|
118
|
-
post_install_message:
|
|
119
124
|
rdoc_options: []
|
|
120
125
|
require_paths:
|
|
121
126
|
- lib
|
|
@@ -130,19 +135,22 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
130
135
|
- !ruby/object:Gem::Version
|
|
131
136
|
version: '0'
|
|
132
137
|
requirements: []
|
|
133
|
-
rubygems_version:
|
|
134
|
-
signing_key:
|
|
138
|
+
rubygems_version: 4.0.16
|
|
135
139
|
specification_version: 4
|
|
136
140
|
summary: A ruby library that does content negotiation and parses and sorts http accept
|
|
137
141
|
headers.
|
|
138
142
|
test_files:
|
|
139
143
|
- spec/encoding/negotiator_spec.rb
|
|
140
144
|
- spec/encoding_spec.rb
|
|
145
|
+
- spec/equality_spec.rb
|
|
141
146
|
- spec/language/negotiator_spec.rb
|
|
142
147
|
- spec/language_spec.rb
|
|
148
|
+
- spec/malformed_spec.rb
|
|
143
149
|
- spec/media_type/negotiator_spec.rb
|
|
144
150
|
- spec/media_type_spec.rb
|
|
145
151
|
- spec/middleware_spec.rb
|
|
152
|
+
- spec/readme_spec.rb
|
|
153
|
+
- spec/rejection_spec.rb
|
|
146
154
|
- spec/spec_helper.rb
|
|
147
155
|
- spec/support/encodings/content-coding.csv
|
|
148
156
|
- spec/support/media_types/application.csv
|
|
@@ -153,3 +161,4 @@ test_files:
|
|
|
153
161
|
- spec/support/media_types/multipart.csv
|
|
154
162
|
- spec/support/media_types/text.csv
|
|
155
163
|
- spec/support/media_types/video.csv
|
|
164
|
+
- spec/supported_values_spec.rb
|