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
|
@@ -14,17 +14,7 @@ module AcceptHeaders
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def <=>(other)
|
|
17
|
-
|
|
18
|
-
-1
|
|
19
|
-
elsif q > other.q
|
|
20
|
-
1
|
|
21
|
-
elsif (primary_tag == '*' && other.primary_tag != '*') || (subtag == '*' && other.subtag != '*')
|
|
22
|
-
-1
|
|
23
|
-
elsif (primary_tag != '*' && other.primary_tag == '*') || (subtag != '*' && other.subtag == '*')
|
|
24
|
-
1
|
|
25
|
-
else
|
|
26
|
-
0
|
|
27
|
-
end
|
|
17
|
+
[q, specificity] <=> [other.q, other.specificity]
|
|
28
18
|
end
|
|
29
19
|
|
|
30
20
|
def primary_tag=(value)
|
|
@@ -53,26 +43,19 @@ module AcceptHeaders
|
|
|
53
43
|
end
|
|
54
44
|
|
|
55
45
|
def language_tag
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
subtag == '*' ? primary_tag : "#{primary_tag}-#{subtag}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def specificity
|
|
50
|
+
[primary_tag == '*' ? 0 : language_tag.split('-').length]
|
|
61
51
|
end
|
|
62
52
|
|
|
63
53
|
def match(language_tag_string)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
elsif primary_tag == match_data[:primary_tag] && subtag == '*'
|
|
70
|
-
true
|
|
71
|
-
elsif primary_tag == '*'
|
|
72
|
-
true
|
|
73
|
-
else
|
|
74
|
-
false
|
|
75
|
-
end
|
|
54
|
+
return false unless language_tag_string.is_a?(String)
|
|
55
|
+
return false unless Negotiator::SUPPORTED_LANGUAGE_TAG_PATTERN.match?(language_tag_string)
|
|
56
|
+
candidate = language_tag_string.downcase
|
|
57
|
+
range = language_tag
|
|
58
|
+
range == '*' || candidate == range || candidate.start_with?("#{range}-")
|
|
76
59
|
end
|
|
77
60
|
end
|
|
78
61
|
end
|
|
@@ -6,23 +6,31 @@ module AcceptHeaders
|
|
|
6
6
|
class Negotiator
|
|
7
7
|
include Negotiatable
|
|
8
8
|
|
|
9
|
-
MEDIA_TYPE_PATTERN =
|
|
10
|
-
PARAMS_PATTERN =
|
|
9
|
+
MEDIA_TYPE_PATTERN = /\A[ \t]*(?<type>[!#$%&'*+\-.^_`|~0-9A-Za-z]+)(?:\/(?<subtype>[!#$%&'*+\-.^_`|~0-9A-Za-z]+))?[ \t]*\z/
|
|
10
|
+
PARAMS_PATTERN = /\A\s*(?<attribute>[!#$%&'*+\-.^_`|~0-9A-Za-z]+)\s*=\s*(?<value>[!#$%&'*+\-.^_`|~0-9A-Za-z]+|"(?:[\t !#-\[\]-~\x80-\xFF]|\\[\t -~\x80-\xFF])*")\s*\z/n
|
|
11
11
|
HEADER_PREFIX = 'Accept:'
|
|
12
12
|
|
|
13
13
|
def negotiate(supported)
|
|
14
|
-
support,
|
|
15
|
-
return nil if support.nil?
|
|
16
|
-
|
|
17
|
-
media_type = parse(support).first
|
|
18
|
-
media_type.extensions = match.extensions
|
|
19
|
-
return media_type
|
|
20
|
-
rescue MediaType::Error
|
|
21
|
-
return nil
|
|
22
|
-
end
|
|
14
|
+
support, = super(supported)
|
|
15
|
+
return nil if support.nil?
|
|
16
|
+
parse(support).first
|
|
23
17
|
end
|
|
24
18
|
|
|
25
19
|
private
|
|
20
|
+
def valid_supported_value?(value)
|
|
21
|
+
media_range_string, *parameters = split_quoted(value.b, ';')
|
|
22
|
+
media_range = MEDIA_TYPE_PATTERN.match(media_range_string)
|
|
23
|
+
return false unless media_range && media_range[:subtype]
|
|
24
|
+
return false if media_range[:type] == '*' || media_range[:subtype] == '*'
|
|
25
|
+
parameters.all? do |parameter|
|
|
26
|
+
next true if parameter.strip.empty?
|
|
27
|
+
match = PARAMS_PATTERN.match(parameter)
|
|
28
|
+
match && match[:attribute].downcase != 'q'
|
|
29
|
+
end
|
|
30
|
+
rescue ParseError
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
|
|
26
34
|
def no_header
|
|
27
35
|
[MediaType.new]
|
|
28
36
|
end
|
|
@@ -34,6 +42,8 @@ module AcceptHeaders
|
|
|
34
42
|
raise Error if accept_media_range.nil?
|
|
35
43
|
media_range = MEDIA_TYPE_PATTERN.match(accept_media_range)
|
|
36
44
|
raise Error if media_range.nil?
|
|
45
|
+
raise Error if media_range[:subtype].nil? && media_range[:type] != '*'
|
|
46
|
+
raise Error if media_range[:type] == '*' && ![nil, '*'].include?(media_range[:subtype])
|
|
37
47
|
MediaType.new(
|
|
38
48
|
media_range[:type],
|
|
39
49
|
media_range[:subtype],
|
|
@@ -44,16 +54,19 @@ module AcceptHeaders
|
|
|
44
54
|
|
|
45
55
|
def parse_extensions(extensions_string)
|
|
46
56
|
return {} if !extensions_string || extensions_string.empty?
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
extensions = {}
|
|
58
|
+
split_quoted(extensions_string, ';').each do |part|
|
|
59
|
+
next if part.strip.empty?
|
|
60
|
+
name = part.split('=', 2).first.strip.downcase
|
|
61
|
+
next if name == 'q'
|
|
62
|
+
param = PARAMS_PATTERN.match(part.b)
|
|
63
|
+
raise ParseError unless param
|
|
64
|
+
value = param[:value].force_encoding(part.encoding)
|
|
65
|
+
if value.start_with?('"')
|
|
66
|
+
value = value.b[1...-1].gsub(/\\(.)/m, '\\1').force_encoding(part.encoding)
|
|
54
67
|
end
|
|
68
|
+
extensions[param[:attribute].downcase] = value
|
|
55
69
|
end
|
|
56
|
-
extensions.delete('q')
|
|
57
70
|
extensions
|
|
58
71
|
end
|
|
59
72
|
end
|
|
@@ -47,7 +47,7 @@ module AcceptHeaders
|
|
|
47
47
|
def extensions=(hash)
|
|
48
48
|
@extensions = {}
|
|
49
49
|
hash.each do |k,v|
|
|
50
|
-
@extensions[k.strip] = v
|
|
50
|
+
@extensions[k.strip.downcase] = v
|
|
51
51
|
end
|
|
52
52
|
@extensions
|
|
53
53
|
end
|
|
@@ -63,9 +63,19 @@ module AcceptHeaders
|
|
|
63
63
|
|
|
64
64
|
def to_s
|
|
65
65
|
qvalue = (q == 0 || q == 1) ? q.to_i : q
|
|
66
|
-
|
|
66
|
+
content_type.concat(";q=#{qvalue}")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def content_type
|
|
70
|
+
string = media_range
|
|
67
71
|
if extensions.size > 0
|
|
68
|
-
extensions.each
|
|
72
|
+
extensions.each do |key, value|
|
|
73
|
+
value = value.to_s
|
|
74
|
+
unless /\A[!#$%&'*+\-.^_`|~0-9A-Za-z]+\z/.match(value.b)
|
|
75
|
+
value = '"' + value.b.gsub(/["\\]/) { |character| "\\#{character}" }.force_encoding(value.encoding) + '"'
|
|
76
|
+
end
|
|
77
|
+
string.concat(";#{key}=#{value}")
|
|
78
|
+
end
|
|
69
79
|
end
|
|
70
80
|
string
|
|
71
81
|
end
|
|
@@ -74,18 +84,29 @@ module AcceptHeaders
|
|
|
74
84
|
"#{type}/#{subtype}"
|
|
75
85
|
end
|
|
76
86
|
|
|
87
|
+
def specificity
|
|
88
|
+
[type == '*' ? 0 : 1, subtype == '*' ? 0 : 1, extensions.size]
|
|
89
|
+
end
|
|
90
|
+
|
|
77
91
|
def match(media_range_string)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
return false if media_range_string.nil? || media_range_string.strip.empty?
|
|
93
|
+
candidates = Negotiator.new(media_range_string).list
|
|
94
|
+
return false unless candidates.size == 1
|
|
95
|
+
candidate = candidates.first
|
|
96
|
+
return false unless (type == '*' || type == candidate.type) &&
|
|
97
|
+
(subtype == '*' || subtype == candidate.subtype)
|
|
98
|
+
|
|
99
|
+
extensions.all? do |name, value|
|
|
100
|
+
supported_value = candidate.extensions[name]
|
|
101
|
+
if value.is_a?(String) && supported_value.is_a?(String)
|
|
102
|
+
value = value.b
|
|
103
|
+
supported_value = supported_value.b
|
|
104
|
+
end
|
|
105
|
+
if name == 'charset'
|
|
106
|
+
supported_value && value.casecmp?(supported_value)
|
|
107
|
+
else
|
|
108
|
+
value == supported_value
|
|
109
|
+
end
|
|
89
110
|
end
|
|
90
111
|
end
|
|
91
112
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "accept_headers/invalid_supported_value"
|
|
2
|
+
|
|
1
3
|
module AcceptHeaders
|
|
2
4
|
module Negotiatable
|
|
3
5
|
class Error < StandardError; end
|
|
@@ -5,29 +7,29 @@ module AcceptHeaders
|
|
|
5
7
|
|
|
6
8
|
attr_reader :list
|
|
7
9
|
|
|
8
|
-
Q_PATTERN =
|
|
10
|
+
Q_PATTERN = /\A[ \t]*q[ \t]*=[ \t]*(?<q>.*?)[ \t]*\z/im
|
|
11
|
+
QVALUE_PATTERN = /\A(?:0(?:\.[0-9]{0,3})?|1(?:\.0{0,3})?)\z/
|
|
12
|
+
SUPPORTED_CONTROL_PATTERN = /[\x00-\x08\x0A-\x1F\x7F]/n
|
|
9
13
|
|
|
10
14
|
def initialize(header)
|
|
15
|
+
header = header.b if header && !header.valid_encoding?
|
|
11
16
|
@list = parse(header)
|
|
12
17
|
end
|
|
13
18
|
|
|
14
19
|
def negotiate(supported)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
supported.each do |support|
|
|
21
|
-
if part.match(support)
|
|
22
|
-
if part.q == 0.0
|
|
23
|
-
next
|
|
24
|
-
else
|
|
25
|
-
return [support, part]
|
|
26
|
-
end
|
|
27
|
-
end
|
|
20
|
+
supported = validate_supported_values(supported)
|
|
21
|
+
candidates = supported.each_with_index.map do |support, support_index|
|
|
22
|
+
matches = list.each_with_index.select { |part, _| part.match(support.strip) }
|
|
23
|
+
part, header_index = matches.max_by do |match, _|
|
|
24
|
+
[match.specificity, match.q.zero? ? 1 : 0, match.q]
|
|
28
25
|
end
|
|
26
|
+
next if part.nil? || part.q.zero?
|
|
27
|
+
[support, part, header_index, support_index]
|
|
28
|
+
end.compact
|
|
29
|
+
best = candidates.max_by do |_, part, header_index, support_index|
|
|
30
|
+
[part.q, part.specificity, -header_index, -support_index]
|
|
29
31
|
end
|
|
30
|
-
|
|
32
|
+
best && best.first(2)
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def accept?(other)
|
|
@@ -39,6 +41,23 @@ module AcceptHeaders
|
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
private
|
|
44
|
+
def validate_supported_values(supported)
|
|
45
|
+
values = if supported.nil?
|
|
46
|
+
[]
|
|
47
|
+
elsif supported.is_a?(Array)
|
|
48
|
+
supported
|
|
49
|
+
else
|
|
50
|
+
[supported]
|
|
51
|
+
end
|
|
52
|
+
values.each do |value|
|
|
53
|
+
unless value.is_a?(String) && value.valid_encoding? && value.encoding.ascii_compatible? &&
|
|
54
|
+
!SUPPORTED_CONTROL_PATTERN.match(value.b) && valid_supported_value?(value)
|
|
55
|
+
raise InvalidSupportedValue, "Invalid supported value: #{value.inspect}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
values
|
|
59
|
+
end
|
|
60
|
+
|
|
42
61
|
def no_header
|
|
43
62
|
raise NotImplementedError.new("#no_header is not implemented")
|
|
44
63
|
end
|
|
@@ -50,11 +69,11 @@ module AcceptHeaders
|
|
|
50
69
|
def parse(header, &block)
|
|
51
70
|
return no_header unless header
|
|
52
71
|
header = header.dup
|
|
53
|
-
header.sub!(/\A#{self.class::HEADER_PREFIX}\s
|
|
72
|
+
header.sub!(/\A#{self.class::HEADER_PREFIX}\s*/i, '')
|
|
54
73
|
header.strip!
|
|
55
|
-
return
|
|
74
|
+
return [] if header.empty?
|
|
56
75
|
list = []
|
|
57
|
-
header
|
|
76
|
+
split_quoted(header, ',').each do |entry|
|
|
58
77
|
begin
|
|
59
78
|
item = parse_item(entry)
|
|
60
79
|
next if item.nil?
|
|
@@ -64,20 +83,43 @@ module AcceptHeaders
|
|
|
64
83
|
end
|
|
65
84
|
end
|
|
66
85
|
list.sort! { |x,y| y <=> x }
|
|
86
|
+
rescue ParseError
|
|
87
|
+
[]
|
|
67
88
|
end
|
|
68
89
|
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
def split_quoted(value, delimiter)
|
|
91
|
+
parts = []
|
|
92
|
+
part = +"".b
|
|
93
|
+
quoted = false
|
|
94
|
+
escaped = false
|
|
95
|
+
value.b.each_char do |character|
|
|
96
|
+
if escaped
|
|
97
|
+
escaped = false
|
|
98
|
+
elsif quoted && character == "\\"
|
|
99
|
+
escaped = true
|
|
100
|
+
elsif character == '"'
|
|
101
|
+
quoted = !quoted
|
|
102
|
+
elsif !quoted && character == delimiter
|
|
103
|
+
parts << part
|
|
104
|
+
part = +"".b
|
|
105
|
+
next
|
|
78
106
|
end
|
|
107
|
+
part << character
|
|
108
|
+
end
|
|
109
|
+
raise ParseError if quoted || escaped
|
|
110
|
+
parts << part
|
|
111
|
+
parts.map { |segment| segment.force_encoding(value.encoding) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def parse_q(header)
|
|
115
|
+
return 1 unless header
|
|
116
|
+
split_quoted(header, ';').each do |parameter|
|
|
117
|
+
q_match = Q_PATTERN.match(parameter)
|
|
118
|
+
next unless q_match
|
|
119
|
+
value = q_match[:q]
|
|
120
|
+
return QVALUE_PATTERN.match(value) ? value.to_f : 0.001
|
|
79
121
|
end
|
|
80
|
-
|
|
122
|
+
1
|
|
81
123
|
end
|
|
82
124
|
end
|
|
83
125
|
end
|
|
@@ -25,7 +25,7 @@ describe AcceptHeaders::Encoding::Negotiator do
|
|
|
25
25
|
]
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
it "
|
|
28
|
+
it "parses content codings in the bundled IANA snapshot" do
|
|
29
29
|
require 'csv'
|
|
30
30
|
# https://www.iana.org/assignments/http-parameters/http-parameters.xml#content-coding
|
|
31
31
|
CSV.foreach("spec/support/encodings/content-coding.csv", headers: true) do |row|
|
|
@@ -38,10 +38,14 @@ describe AcceptHeaders::Encoding::Negotiator do
|
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
it "
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
it "allows only identity when the accept-encoding header is empty" do
|
|
42
|
+
['', " \t", 'Accept-Encoding:', 'accept-encoding: '].each do |header|
|
|
43
|
+
subject.new(header).list.map(&:encoding).must_equal ['identity']
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "allows any content coding when the accept-encoding header is absent" do
|
|
48
|
+
subject.new(nil).list.map(&:encoding).must_equal ['*']
|
|
45
49
|
end
|
|
46
50
|
|
|
47
51
|
it "defaults q to 1 if it's not explicitly specified" do
|
|
@@ -75,6 +79,10 @@ describe AcceptHeaders::Encoding::Negotiator do
|
|
|
75
79
|
encoding.new('gzip', q: 1.0)
|
|
76
80
|
]
|
|
77
81
|
end
|
|
82
|
+
|
|
83
|
+
it "parses HTTP token characters and skips delimiter-containing coding names" do
|
|
84
|
+
subject.new('example.coding, bad{coding}, bad\\coding').list.map(&:encoding).must_equal ['example.coding']
|
|
85
|
+
end
|
|
78
86
|
end
|
|
79
87
|
|
|
80
88
|
describe "#to_s" do
|
|
@@ -86,7 +94,53 @@ describe AcceptHeaders::Encoding::Negotiator do
|
|
|
86
94
|
describe "negotiate supported encodings" do
|
|
87
95
|
it "returns a best matching encoding" do
|
|
88
96
|
n = subject.new('deflate; q=0.5, gzip, compress; q=0.8, identity')
|
|
89
|
-
n.negotiate(['identity', 'deflate']).must_equal
|
|
97
|
+
n.negotiate(['identity', 'deflate']).encoding.must_equal 'identity'
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "distinguishes an absent header from an empty field value" do
|
|
101
|
+
subject.new(nil).negotiate('gzip').encoding.must_equal 'gzip'
|
|
102
|
+
['', " \t", 'Accept-Encoding:'].each do |header|
|
|
103
|
+
negotiator = subject.new(header)
|
|
104
|
+
negotiator.negotiate('gzip').must_be_nil
|
|
105
|
+
negotiator.negotiate(['gzip', 'identity']).encoding.must_equal 'identity'
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "prefers an acceptable coding over implicit identity" do
|
|
110
|
+
subject.new('gzip;q=0.5').negotiate(['identity', 'gzip']).encoding.must_equal 'gzip'
|
|
111
|
+
subject.new('*;q=0.5').negotiate(['identity', 'br']).encoding.must_equal 'br'
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "falls back to identity without accepting unlisted content codings" do
|
|
115
|
+
negotiator = subject.new('gzip')
|
|
116
|
+
negotiator.negotiate(['br', 'identity']).encoding.must_equal 'identity'
|
|
117
|
+
negotiator.accept?('identity').must_equal true
|
|
118
|
+
negotiator.negotiate('br').must_be_nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "does not turn another coding's rejection into an identity rejection" do
|
|
122
|
+
negotiator = subject.new('gzip;q=0')
|
|
123
|
+
negotiator.negotiate(['gzip', 'identity']).encoding.must_equal 'identity'
|
|
124
|
+
negotiator.accept?('gzip').must_equal false
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "honors explicit identity preferences and exclusions" do
|
|
128
|
+
subject.new('gzip;q=0.5, identity;q=0.8').negotiate(['gzip', 'identity']).encoding.must_equal 'identity'
|
|
129
|
+
subject.new('identity;q=0').negotiate('identity').must_be_nil
|
|
130
|
+
subject.new('*;q=0').negotiate('identity').must_be_nil
|
|
131
|
+
subject.new('*;q=0, identity;q=0.5').negotiate('identity').encoding.must_equal 'identity'
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "matches supported coding names without case sensitivity" do
|
|
135
|
+
subject.new('GZIP').negotiate('GZip').encoding.must_equal 'gzip'
|
|
136
|
+
subject.new('br').negotiate('IDENTITY').encoding.must_equal 'identity'
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "requires supported names to be complete HTTP tokens" do
|
|
140
|
+
negotiator = subject.new('*')
|
|
141
|
+
['gzip\\bad', 'gzip{bad}', "gzip\nidentity", 'gzip,br'].each do |coding|
|
|
142
|
+
assert_raises(AcceptHeaders::InvalidSupportedValue) { negotiator.negotiate(coding) }
|
|
143
|
+
end
|
|
90
144
|
end
|
|
91
145
|
end
|
|
92
146
|
end
|
data/spec/encoding_spec.rb
CHANGED
|
@@ -74,6 +74,14 @@ describe AcceptHeaders::Encoding do
|
|
|
74
74
|
a.accept?('gzip').must_equal true
|
|
75
75
|
b = subject.new('gzip', q: 0.001)
|
|
76
76
|
b.accept?('gzip').must_equal true
|
|
77
|
+
a.accept?('GZIP').must_equal true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "matches identity only as its own coding or a wildcard" do
|
|
81
|
+
subject.new('gzip').accept?('identity').must_equal false
|
|
82
|
+
subject.new('gzip', q: 0).reject?('identity').must_equal false
|
|
83
|
+
subject.new('identity').accept?('IDENTITY').must_equal true
|
|
84
|
+
subject.new('*').accept?('identity').must_equal true
|
|
77
85
|
end
|
|
78
86
|
|
|
79
87
|
it "accepted if the encoding is *" do
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require_relative "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe "acceptable value equality" do
|
|
4
|
+
let(:values) do
|
|
5
|
+
[
|
|
6
|
+
AcceptHeaders::MediaType.new('text', 'html'),
|
|
7
|
+
AcceptHeaders::Encoding.new('gzip'),
|
|
8
|
+
AcceptHeaders::Language.new('en', 'us')
|
|
9
|
+
]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "compares all normalized fields for values of the same class" do
|
|
13
|
+
assert_equal AcceptHeaders::MediaType.new('text', 'html', extensions: { 'level' => '1' }),
|
|
14
|
+
AcceptHeaders::MediaType.new('TEXT', 'HTML', extensions: { 'level' => '1' })
|
|
15
|
+
assert_equal AcceptHeaders::Encoding.new('gzip'), AcceptHeaders::Encoding.new('GZIP')
|
|
16
|
+
assert_equal AcceptHeaders::Language.new('en', 'us'), AcceptHeaders::Language.new('EN', 'US')
|
|
17
|
+
values.each { |value| assert_equal value, value }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "distinguishes different media types and parameters at the same preference rank" do
|
|
21
|
+
refute_equal AcceptHeaders::MediaType.new('text', 'html'), AcceptHeaders::MediaType.new('text', 'plain')
|
|
22
|
+
refute_equal AcceptHeaders::MediaType.new('text', 'plain'), AcceptHeaders::MediaType.new('application', 'plain')
|
|
23
|
+
refute_equal AcceptHeaders::MediaType.new('text', 'html', extensions: { 'level' => '1' }),
|
|
24
|
+
AcceptHeaders::MediaType.new('text', 'html', extensions: { 'level' => '2' })
|
|
25
|
+
refute_equal AcceptHeaders::MediaType.new('text', 'html', extensions: { 'level' => '1' }),
|
|
26
|
+
AcceptHeaders::MediaType.new('text', 'html', extensions: { 'version' => '1' })
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "distinguishes encodings and languages at the same preference rank" do
|
|
30
|
+
refute_equal AcceptHeaders::Encoding.new('gzip'), AcceptHeaders::Encoding.new('compress')
|
|
31
|
+
refute_equal AcceptHeaders::Language.new('en', 'us'), AcceptHeaders::Language.new('en', 'gb')
|
|
32
|
+
refute_equal AcceptHeaders::Language.new('en', 'us'), AcceptHeaders::Language.new('fr', 'us')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "includes quality in equality" do
|
|
36
|
+
values.each do |value|
|
|
37
|
+
different_quality = value.dup
|
|
38
|
+
different_quality.q = 0.5
|
|
39
|
+
refute_equal value, different_quality
|
|
40
|
+
refute_equal different_quality, value
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "returns false symmetrically for different classes and unrelated objects" do
|
|
45
|
+
values.each do |value|
|
|
46
|
+
(values.reject { |other| other.class == value.class } + [nil, Object.new, value.to_h]).each do |other|
|
|
47
|
+
refute_equal value, other
|
|
48
|
+
refute_equal other, value
|
|
49
|
+
end
|
|
50
|
+
subclass_value = Class.new(value.class).new
|
|
51
|
+
value.to_h.each { |field, field_value| subclass_value.public_send("#{field}=", field_value) }
|
|
52
|
+
refute_equal value, subclass_value
|
|
53
|
+
refute_equal subclass_value, value
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "keeps equal preference ranks sortable without making their values equal" do
|
|
58
|
+
[
|
|
59
|
+
[AcceptHeaders::MediaType.new('text', 'html'), AcceptHeaders::MediaType.new('text', 'plain')],
|
|
60
|
+
[AcceptHeaders::Encoding.new('gzip'), AcceptHeaders::Encoding.new('compress')],
|
|
61
|
+
[AcceptHeaders::Language.new('en', 'us'), AcceptHeaders::Language.new('en', 'gb')]
|
|
62
|
+
].each do |left, right|
|
|
63
|
+
assert_equal 0, left <=> right
|
|
64
|
+
assert_equal 0, right <=> left
|
|
65
|
+
refute_equal left, right
|
|
66
|
+
refute_equal right, left
|
|
67
|
+
assert_equal [left.to_h, right.to_h], [left, right].sort.map(&:to_h)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|