accept_headers 0.1.0 → 0.2.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.
@@ -0,0 +1,121 @@
1
+ require_relative "spec_helper"
2
+
3
+ class MalformedHeaderSpec < Minitest::Test
4
+ def test_absent_accept_header_accepts_any_media_type
5
+ result = AcceptHeaders::MediaType::Negotiator.new(nil).negotiate('text/html')
6
+
7
+ assert_equal 'text/html', result.media_range
8
+ end
9
+
10
+ def test_present_empty_accept_header_accepts_no_media_types
11
+ ['', " \t", 'Accept:'].each do |header|
12
+ negotiator = AcceptHeaders::MediaType::Negotiator.new(header)
13
+
14
+ assert_empty negotiator.list
15
+ assert_nil negotiator.negotiate('text/html')
16
+ end
17
+ end
18
+
19
+ def test_unqualified_media_types_are_skipped_without_raising
20
+ negotiator = AcceptHeaders::MediaType::Negotiator.new('nonsense, text/html')
21
+
22
+ assert_equal [{ type: 'text', subtype: 'html', q: 1.0, extensions: {} }], negotiator.list.map(&:to_h)
23
+ end
24
+
25
+ def test_invalidly_tagged_token_bytes_are_skipped_by_each_negotiator
26
+ quality_negotiators.each do |negotiator, valid_token|
27
+ invalid_token = "\xFF".freeze
28
+ assert_empty negotiator.new(invalid_token).list
29
+
30
+ header = "#{invalid_token},#{valid_token}"
31
+ assert_equal negotiator.new(valid_token).list.map(&:to_h), negotiator.new(header).list.map(&:to_h)
32
+ assert_equal Encoding::UTF_8, invalid_token.encoding
33
+ refute invalid_token.valid_encoding?
34
+ end
35
+ end
36
+
37
+ def test_quoted_obs_text_bytes_are_preserved_and_round_trip
38
+ value = (0x80..0xFF).to_a.pack('C*')
39
+ header = ('text/html;note="'.b + value + '"').force_encoding('UTF-8')
40
+ item = AcceptHeaders::MediaType::Negotiator.new(header).list.first
41
+
42
+ assert_equal value, item.extensions['note']
43
+ assert_equal Encoding::ASCII_8BIT, item.extensions['note'].encoding
44
+ assert_equal item.to_h, AcceptHeaders::MediaType::Negotiator.new(item.to_s).list.first.to_h
45
+ end
46
+
47
+ def test_valid_utf8_quoted_values_retain_their_encoding_and_round_trip
48
+ value = "日本語; café"
49
+ item = AcceptHeaders::MediaType::Negotiator.new("text/html;note=\"#{value}\"").list.first
50
+
51
+ assert_equal value, item.extensions['note']
52
+ assert_equal Encoding::UTF_8, item.extensions['note'].encoding
53
+ assert_equal item.to_h, AcceptHeaders::MediaType::Negotiator.new(item.to_s).list.first.to_h
54
+ end
55
+
56
+ def test_invalid_media_ranges_do_not_match_partial_lines_or_non_token_characters
57
+ ['*/json', 'text/html/extra', 'text/ht{ml}', 'text/ht\\ml', "text/html\nnonsense", 'text /html'].each do |range|
58
+ assert_empty AcceptHeaders::MediaType::Negotiator.new(range).list, range
59
+ end
60
+ end
61
+
62
+ def test_media_ranges_accept_http_token_characters_and_supported_wildcards
63
+ ['application/x_example+json', 'application/problem+json', 'image/avif', 'text/*', '*/*', '*'].each do |range|
64
+ assert_equal 1, AcceptHeaders::MediaType::Negotiator.new(range).list.size, range
65
+ end
66
+ end
67
+
68
+ def test_quality_uses_the_complete_rfc_9110_qvalue_grammar
69
+ values = {
70
+ '0' => 0.0, '0.' => 0.0, '0.000' => 0.0,
71
+ '0.123' => 0.123, '1' => 1.0, '1.' => 1.0, '1.000' => 1.0
72
+ }
73
+
74
+ quality_negotiators.each do |negotiator, value|
75
+ values.each do |qvalue, expected|
76
+ assert_equal expected, negotiator.new("#{value};q=#{qvalue}").list.first.q, "#{value};q=#{qvalue}"
77
+ end
78
+ end
79
+ end
80
+
81
+ def test_invalid_quality_preserves_the_documented_recovery_value
82
+ ['', '.5', '+0.5', '01', '0.1234', '1.0000', '0.1e0', '"0.1"', '1.001', 'NaN', "0\nx"].each do |qvalue|
83
+ quality_negotiators.each do |negotiator, value|
84
+ assert_equal 0.001, negotiator.new("#{value};q=#{qvalue}").list.first.q, "#{value};q=#{qvalue}"
85
+ end
86
+ end
87
+ end
88
+
89
+ def test_quality_parameter_name_is_case_insensitive_and_exact
90
+ quality_negotiators.each do |negotiator, value|
91
+ assert_equal 0.4, negotiator.new("#{value};Q = 0.4").list.first.q
92
+ assert_equal 1.0, negotiator.new("#{value};qs=0").list.first.q
93
+ end
94
+ end
95
+
96
+ def test_quoted_parameter_contents_are_not_quality_parameters
97
+ header = 'text/html;note="a;q=0;b"'
98
+
99
+ assert_equal 1.0, AcceptHeaders::MediaType::Negotiator.new(header).list.first.q
100
+ end
101
+
102
+ def test_quality_after_escaped_quotes_is_still_parsed
103
+ header = 'text/html;note="a\";q=0";q=0.8'
104
+
105
+ assert_equal 0.8, AcceptHeaders::MediaType::Negotiator.new(header).list.first.q
106
+ end
107
+
108
+ def test_unterminated_quoted_parameters_are_skipped
109
+ assert_empty AcceptHeaders::MediaType::Negotiator.new('text/html;note="a;q=0').list
110
+ end
111
+
112
+ private
113
+
114
+ def quality_negotiators
115
+ {
116
+ AcceptHeaders::MediaType::Negotiator => 'text/html',
117
+ AcceptHeaders::Encoding::Negotiator => 'gzip',
118
+ AcceptHeaders::Language::Negotiator => 'en'
119
+ }
120
+ end
121
+ end
@@ -34,12 +34,12 @@ describe AcceptHeaders::MediaType::Negotiator do
34
34
  end
35
35
 
36
36
  it "ignores the 'Accept:' prefix" do
37
- subject.new('Accept: text/html').list.must_equal [
37
+ subject.new('aCcEpT: text/html').list.must_equal [
38
38
  media_type.new('text', 'html')
39
39
  ]
40
40
  end
41
41
 
42
- it "supports all registered IANA media types" do
42
+ it "parses media types in the bundled IANA snapshot" do
43
43
  require 'csv'
44
44
  # https://www.iana.org/assignments/media-types/media-types.xhtml
45
45
  %w[application audio image message model multipart text video].each do |filename|
@@ -54,10 +54,8 @@ describe AcceptHeaders::MediaType::Negotiator do
54
54
  end
55
55
  end
56
56
 
57
- it "sets media type to */* when the accept header is empty" do
58
- subject.new('').list.must_equal [
59
- media_type.new('*', '*')
60
- ]
57
+ it "accepts no media types when the accept header is present but empty" do
58
+ assert_empty subject.new('').list
61
59
  end
62
60
 
63
61
  it "sets media type to */* when the type is only *" do
@@ -92,11 +90,47 @@ describe AcceptHeaders::MediaType::Negotiator do
92
90
  ]
93
91
  end
94
92
 
95
- it "parses extensions with quoted values" do
96
- subject.new('text/html;q=1;version="2";level="a;b;cc\'cd", text/html;version=\'1\';level=\'\blah;x;1;;\'').list.must_equal [
97
- media_type.new('text', 'html', extensions: { 'version' => '2', 'level' => 'a;b;cc\'cd'}),
98
- media_type.new('text', 'html', extensions: { 'version' => '1', 'level' => '\'\blah;x;1;;\''})
99
- ]
93
+ it "preserves delimiters inside quoted parameter values" do
94
+ parsed = subject.new('text/html;version="2";level="a,b;c", application/json;q=0.8').list
95
+ parsed.map(&:media_range).must_equal ['text/html', 'application/json']
96
+ parsed.first.extensions.must_equal('version' => '2', 'level' => 'a,b;c')
97
+ end
98
+
99
+ it "unescapes quoted pairs without splitting escaped quotes" do
100
+ parsed = subject.new('text/html;note="a\\"b,c;d\\\\e"').list
101
+ parsed.size.must_equal 1
102
+ parsed.first.extensions.must_equal('note' => 'a"b,c;d\\e')
103
+ end
104
+
105
+ it "treats apostrophes as token characters rather than quotes" do
106
+ parsed = subject.new("text/html;version='1', application/json;q=0.8").list
107
+ parsed.map(&:media_range).must_equal ['text/html', 'application/json']
108
+ parsed.first.extensions.must_equal('version' => "'1'")
109
+ end
110
+
111
+ it "does not read quality values inside quoted parameters" do
112
+ parsed = subject.new('text/html;note="example;q=0.2";q=0.8').list.first
113
+ parsed.q.must_equal 0.8
114
+ parsed.extensions.must_equal('note' => 'example;q=0.2')
115
+ end
116
+
117
+ it "does not invent parameters from malformed values" do
118
+ ['text/html;note="unterminated', 'text/html;note="trailing' + "\\",
119
+ 'text/html;note="closed"junk', "text/html;note=\"line\nbreak\"",
120
+ "text/html;note=\"escaped\\\nbreak\"", 'text/html;note=two words'].each do |header|
121
+ subject.new(header).list.must_equal []
122
+ end
123
+ parsed = subject.new('text/html;note="closed"junk, application/json').list
124
+ parsed.map(&:media_range).must_equal ['application/json']
125
+ end
126
+
127
+ it "round trips quoted parameter values and quality" do
128
+ original = media_type.new('text', 'html', q: 0.8, extensions: {
129
+ 'note' => 'a,b;c"d\\e', 'empty' => '', 'version' => "'1'"
130
+ })
131
+ parsed = subject.new(original.to_s).list
132
+ parsed.size.must_equal 1
133
+ parsed.first.to_h.must_equal original.to_h
100
134
  end
101
135
 
102
136
  it "skips invalid media types" do
@@ -108,7 +142,7 @@ describe AcceptHeaders::MediaType::Negotiator do
108
142
 
109
143
  describe "#to_s" do
110
144
  it "returns a string of each media type #to_s joined by a comma" do
111
- subject.new("text/html;level=1, text/plain;q=0.9").to_s.must_equal "text/html;q=1;level=1,text/plain;q=0.9"
145
+ subject.new("text/html;level=1, text/plain;q=0.9").to_s.must_equal "text/html;level=1;q=1,text/plain;q=0.9"
112
146
  end
113
147
  end
114
148
 
@@ -150,13 +184,14 @@ describe AcceptHeaders::MediaType::Negotiator do
150
184
  api = subject.new('application/json;q=1;version=2,application/json;q=0.9;version=1')
151
185
  api.negotiate('text/html').must_be_nil
152
186
  api.negotiate('application/xml').must_be_nil
153
- api.negotiate(['application/xml', 'application/json']).must_equal media_type.new(
187
+ api.negotiate(['application/xml', 'application/json']).must_be_nil
188
+ api.negotiate(['application/json;version=1', 'application/json;version=2']).to_h.must_equal media_type.new(
154
189
  'application',
155
190
  'json',
156
191
  extensions: {
157
- 'version' => '1'
192
+ 'version' => '2'
158
193
  }
159
- )
194
+ ).to_h
160
195
 
161
196
  q0 = subject.new('application/json,application/xml;q=0')
162
197
  q0.negotiate('application/json').must_equal media_type.new(
@@ -170,11 +205,54 @@ describe AcceptHeaders::MediaType::Negotiator do
170
205
  )
171
206
  end
172
207
 
208
+ it "uses the most specific matching range to assign quality before choosing a representation" do
209
+ n = subject.new('text/html;q=0.1,text/*;q=0.9,application/json;q=0.5')
210
+ n.negotiate(['text/html', 'application/json']).media_range.must_equal 'application/json'
211
+ n.negotiate(['text/html', 'text/plain']).media_range.must_equal 'text/plain'
212
+ end
213
+
214
+ it "gives matching parameters precedence over a parameterless range" do
215
+ n = subject.new('text/plain;q=0.7,text/plain;format=fixed;q=0.4,image/jpeg;q=0.5')
216
+ n.negotiate(['text/plain;format=fixed', 'image/jpeg']).media_range.must_equal 'image/jpeg'
217
+ n.negotiate(['text/plain', 'image/jpeg']).media_range.must_equal 'text/plain'
218
+ end
219
+
220
+ it "treats parameters after q as media parameters" do
221
+ n = subject.new('application/json;q=0.9;version=2')
222
+ n.negotiate('application/json').must_be_nil
223
+ n.negotiate('application/json;version=2').extensions.must_equal('version' => '2')
224
+ end
225
+
226
+ it "preserves the parameters of the supported representation" do
227
+ n = subject.new('application/json')
228
+ n.negotiate('application/json;version=2').to_h.must_equal media_type.new(
229
+ 'application', 'json', extensions: { 'version' => '2' }
230
+ ).to_h
231
+ end
232
+
233
+ it "uses supported order to break equally preferred matching representations" do
234
+ n = subject.new('text/*')
235
+ n.negotiate(['text/plain', 'text/html']).media_range.must_equal 'text/plain'
236
+ n.negotiate(['text/html', 'text/plain']).media_range.must_equal 'text/html'
237
+ end
238
+
239
+ it "allows a specific positive quality to override a wildcard exclusion" do
240
+ n = subject.new('text/*;q=0,text/html;q=0.5')
241
+ n.negotiate('text/html').media_range.must_equal 'text/html'
242
+ n.negotiate('text/plain').must_be_nil
243
+ end
244
+
245
+ it "excludes only representations matching the rejected parameters" do
246
+ n = subject.new('text/plain;format=fixed;q=0,text/plain;q=1')
247
+ n.negotiate('text/plain;format=fixed').must_be_nil
248
+ n.negotiate('text/plain;format=flowed').extensions.must_equal('format' => 'flowed')
249
+ end
250
+
173
251
  it "rejects matching q=0 even if it matches media ranges where q > 0" do
174
- n = subject.new('application/xml;q=0;*/*')
252
+ n = subject.new('application/xml;q=0,*/*')
175
253
  n.negotiate('application/xml').must_be_nil
176
254
 
177
- n2 = subject.new('application/xml;q=0;application/xml;q=1')
255
+ n2 = subject.new('application/xml;q=0,application/xml;q=1')
178
256
  n2.negotiate('application/xml').must_be_nil
179
257
  end
180
258
  end
@@ -196,10 +274,10 @@ describe AcceptHeaders::MediaType::Negotiator do
196
274
  end
197
275
 
198
276
  it "returns false when q=0 even if it matches media ranges where q > 0" do
199
- n = subject.new('application/xml;q=0;*/*')
277
+ n = subject.new('application/xml;q=0,*/*')
200
278
  n.accept?('application/xml').must_equal false
201
279
 
202
- n2 = subject.new('application/xml;q=0;application/xml;q=1')
280
+ n2 = subject.new('application/xml;q=0,application/xml;q=1')
203
281
  n2.accept?('application/xml').must_equal false
204
282
  end
205
283
  end
@@ -25,8 +25,8 @@ describe AcceptHeaders::MediaType do
25
25
  subject.new('*', nil).subtype.must_equal '*'
26
26
  end
27
27
 
28
- it "strips only the keys in the extensions hash" do
29
- subject.new('*', '*', extensions: { "\s\nLEVEL\r\t" => "\t\nX\s\n"}).extensions['LEVEL'].must_equal "\t\nX\s\n"
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,7 +92,12 @@ 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;level=1"
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
@@ -100,6 +105,37 @@ describe AcceptHeaders::MediaType do
100
105
  end
101
106
 
102
107
  describe "#accept?" do
108
+ it "matches type and subtype without case sensitivity" do
109
+ subject.new('text', 'html').accept?('TEXT/HTML').must_equal true
110
+ end
111
+
112
+ it "requires every media range parameter to match the supported representation" do
113
+ range = subject.new('application', 'json', extensions: { 'version' => '2' })
114
+ range.accept?('application/json').must_equal false
115
+ range.accept?('application/json;version=1').must_equal false
116
+ range.accept?('application/json;VERSION=2').must_equal true
117
+ range.accept?('application/json;version=2;profile=public').must_equal true
118
+ end
119
+
120
+ it "matches charset values without case sensitivity" do
121
+ range = subject.new('text', 'plain', extensions: { 'charset' => 'UTF-8' })
122
+ range.accept?('text/plain;charset=utf-8').must_equal true
123
+ range.accept?('text/plain;charset=iso-8859-1').must_equal false
124
+ range.accept?('text/plain').must_equal false
125
+ end
126
+
127
+ it "preserves case sensitivity for other parameter values" do
128
+ range = subject.new('application', 'json', extensions: { 'profile' => 'Public' })
129
+ range.accept?('application/json;profile=Public').must_equal true
130
+ range.accept?('application/json;profile=public').must_equal false
131
+ end
132
+
133
+ it "does not match an empty candidate or a list of candidates" do
134
+ range = subject.new('*')
135
+ range.accept?('').must_equal false
136
+ range.accept?('text/html,application/json').must_equal false
137
+ end
138
+
103
139
  it "accepted if the type and subtype are the same" do
104
140
  a = subject.new('text', 'html')
105
141
  a.accept?('text/html').must_equal true
@@ -1,6 +1,7 @@
1
1
  require_relative "spec_helper"
2
2
  require "accept_headers/middleware"
3
- require "rack/test"
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
- 'Content-Type' => matched_media_type ? matched_media_type.media_range : '',
23
- 'Content-Encoding' => matched_encoding ? matched_encoding.encoding : '',
24
- 'Content-Language' => matched_language ? matched_language.language_tag : '',
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, [ '' ] ]
@@ -0,0 +1,66 @@
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', 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_ENCODING' => 'identity;q=0' },
48
+ { 'HTTP_ACCEPT_LANGUAGE' => 'fr' }
49
+ ].each do |headers|
50
+ rejected = request.get('/', headers)
51
+ assert_equal 406, rejected.status, headers.inspect
52
+ assert_equal 'Not acceptable', rejected.body
53
+ assert_equal 'Accept, Accept-Encoding, Accept-Language', rejected['vary']
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def readme_path
60
+ File.expand_path('../README.md', __dir__)
61
+ end
62
+
63
+ def examples
64
+ File.read(readme_path).scan(/```ruby\n(.*?)```/m).flatten
65
+ end
66
+ 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
@@ -1,9 +1,7 @@
1
1
  require "simplecov"
2
2
 
3
3
  SimpleCov.start do
4
- formatter SimpleCov::Formatter::MultiFormatter[
5
- SimpleCov::Formatter::HTMLFormatter,
6
- ]
4
+ formatter SimpleCov::Formatter::HTMLFormatter
7
5
  add_filter 'spec/'
8
6
  end
9
7
 
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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jack Chu
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2019-04-19 00:00:00.000000000 Z
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: '12.0'
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: '12.0'
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 does content negotiation and parses and sorts http
70
- accept headers. Adheres to RFC 2616.
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
@@ -96,11 +96,15 @@ files:
96
96
  - lib/accept_headers/version.rb
97
97
  - spec/encoding/negotiator_spec.rb
98
98
  - spec/encoding_spec.rb
99
+ - spec/equality_spec.rb
99
100
  - spec/language/negotiator_spec.rb
100
101
  - spec/language_spec.rb
102
+ - spec/malformed_spec.rb
101
103
  - spec/media_type/negotiator_spec.rb
102
104
  - spec/media_type_spec.rb
103
105
  - spec/middleware_spec.rb
106
+ - spec/readme_spec.rb
107
+ - spec/rejection_spec.rb
104
108
  - spec/spec_helper.rb
105
109
  - spec/support/encodings/content-coding.csv
106
110
  - spec/support/media_types/application.csv
@@ -115,7 +119,6 @@ homepage: ''
115
119
  licenses:
116
120
  - MIT
117
121
  metadata: {}
118
- post_install_message:
119
122
  rdoc_options: []
120
123
  require_paths:
121
124
  - lib
@@ -130,19 +133,22 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
133
  - !ruby/object:Gem::Version
131
134
  version: '0'
132
135
  requirements: []
133
- rubygems_version: 3.0.3
134
- signing_key:
136
+ rubygems_version: 4.0.16
135
137
  specification_version: 4
136
138
  summary: A ruby library that does content negotiation and parses and sorts http accept
137
139
  headers.
138
140
  test_files:
139
141
  - spec/encoding/negotiator_spec.rb
140
142
  - spec/encoding_spec.rb
143
+ - spec/equality_spec.rb
141
144
  - spec/language/negotiator_spec.rb
142
145
  - spec/language_spec.rb
146
+ - spec/malformed_spec.rb
143
147
  - spec/media_type/negotiator_spec.rb
144
148
  - spec/media_type_spec.rb
145
149
  - spec/middleware_spec.rb
150
+ - spec/readme_spec.rb
151
+ - spec/rejection_spec.rb
146
152
  - spec/spec_helper.rb
147
153
  - spec/support/encodings/content-coding.csv
148
154
  - spec/support/media_types/application.csv
data/.travis.yml DELETED
@@ -1,14 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.4.6
4
- - 2.5.5
5
- - 2.6.3
6
- - ruby-head
7
- - jruby-9.2.7.0
8
- - jruby-head
9
- - rbx-3.107
10
- matrix:
11
- allow_failures:
12
- - rvm: ruby-head
13
- - rvm: jruby-head
14
- - rvm: rbx-3.107