accept_headers 0.0.6 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +0 -1
- data/CHANGELOG.md +6 -0
- data/README.md +22 -8
- data/lib/accept_headers/language.rb +1 -1
- data/lib/accept_headers/media_type/negotiator.rb +15 -15
- data/lib/accept_headers/media_type.rb +12 -12
- data/lib/accept_headers/version.rb +1 -1
- data/spec/encoding_spec.rb +108 -102
- data/spec/language_spec.rb +161 -153
- data/spec/media_type/negotiator_spec.rb +9 -9
- data/spec/media_type_spec.rb +162 -156
- data/spec/spec_helper.rb +2 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1c59b8c10eea7c406d76ca08d6daf1eb25a92f41
|
4
|
+
data.tar.gz: 76b2194684f98c8812f71e32daf84af838a38faa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a3da1d44c7090dcd155f4b6f03e6bb6f40b9292d7bdbd9fee8f77cf7d440a1e8b9e3497f7ee0c42ac229b4377fb0c0b1932afa085f3dc0ca23fa452a82c98361
|
7
|
+
data.tar.gz: af9ead68fe19c681981c4a522b4c73cc166fd2e0e1709677232dd5d2f755b5c61d0bce036da997a218818b42297e2d899fdc1dc5324a30049f740ae4141b80e0
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
## HEAD
|
2
2
|
|
3
|
+
## 0.0.7 / November 19, 2014
|
4
|
+
|
5
|
+
* Rename `MediaType` `params` to `extensions`, since params technically includes the `q` value.
|
6
|
+
* Support rbx invalid `Float` exception message.
|
7
|
+
* Only strip accept param keys, values can contain white space if quoted.
|
8
|
+
|
3
9
|
## 0.0.6 / November 17, 2014
|
4
10
|
|
5
11
|
* Support parsing params with quoted values.
|
data/README.md
CHANGED
@@ -12,7 +12,7 @@ Some features of the library are:
|
|
12
12
|
* Full support for the [Accept][rfc-sec14-1], [Accept-Encoding][rfc-sec14-3],
|
13
13
|
and [Accept-Language][rfc-sec14-4] HTTP request headers
|
14
14
|
* `Accept-Charset` is not supported because it's [obsolete](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation#The_Accept-Charset.3A_header)
|
15
|
-
* Parser tested against all IANA registered [media types][iana-media-types]
|
15
|
+
* Parser tested against all IANA registered [media types][iana-media-types] and [encodings][iana-encodings]
|
16
16
|
* A comprehensive [spec suite][spec] that covers many edge cases
|
17
17
|
|
18
18
|
This library is optimistic when parsing headers. If a specific media type, encoding, or language can't be parsed, is in an invalid format, or contains invalid characters, it will skip that specific entry when constructing the sorted list. If a `q` value can't be read or is in the wrong format (more than 3 decimal places), it will default it to `0.001` so it still has a chance to match. Lack of an explicit `q` value of course defaults to 1.
|
@@ -23,6 +23,7 @@ This library is optimistic when parsing headers. If a specific media type, encod
|
|
23
23
|
[rfc-sec14-3]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3
|
24
24
|
[rfc-sec14-4]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
|
25
25
|
[iana-media-types]: https://www.iana.org/assignments/media-types/media-types.xhtml
|
26
|
+
[iana-encodings]: https://www.iana.org/assignments/http-parameters/http-parameters.xml#content-coding
|
26
27
|
[spec]: http://github.com/kamui/accept_headers/tree/master/spec/
|
27
28
|
|
28
29
|
## Installation
|
@@ -45,20 +46,21 @@ Or install it yourself as:
|
|
45
46
|
|
46
47
|
### Accept
|
47
48
|
|
48
|
-
`AcceptHeaders::MediaType::Negotiator` is a class that is initialized with an `Accept` header string and will internally store an array of `MediaType`s in descending order according to the spec, which takes into account `q` value, `type`/`subtype` and `
|
49
|
+
`AcceptHeaders::MediaType::Negotiator` is a class that is initialized with an `Accept` header string and will internally store an array of `MediaType`s in descending order according to the spec, which takes into account `q` value, `type`/`subtype` and `extensions` specificity.
|
49
50
|
|
50
51
|
```ruby
|
51
|
-
|
52
|
+
accept_header = 'Accept: text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5'
|
53
|
+
media_types = AcceptHeaders::MediaType::Negotiator.new(accept_header)
|
52
54
|
|
53
55
|
media_types.list
|
54
56
|
|
55
57
|
# Returns:
|
56
58
|
|
57
59
|
[
|
58
|
-
AcceptHeaders::MediaType.new('text', 'html',
|
60
|
+
AcceptHeaders::MediaType.new('text', 'html', extensions: { 'level' => '1' }),
|
59
61
|
AcceptHeaders::MediaType.new('text', 'html', q: 0.7),
|
60
62
|
AcceptHeaders::MediaType.new('*', '*', q: 0.5),
|
61
|
-
AcceptHeaders::MediaType.new('text', 'html', q: 0.4,
|
63
|
+
AcceptHeaders::MediaType.new('text', 'html', q: 0.4, extensions: { 'level' => '2' }),
|
62
64
|
AcceptHeaders::MediaType.new('text', '*', q: 0.3)
|
63
65
|
]
|
64
66
|
```
|
@@ -75,10 +77,20 @@ media_types.negotiate(['text/html', 'text/plain'])
|
|
75
77
|
|
76
78
|
{
|
77
79
|
supported: 'text/html',
|
78
|
-
matched: AcceptHeaders::MediaType.new('text', 'html', q: 1,
|
80
|
+
matched: AcceptHeaders::MediaType.new('text', 'html', q: 1, extensions: { 'level' => '1' })
|
79
81
|
}
|
80
82
|
```
|
81
83
|
|
84
|
+
It returns the matching `MediaType`, so you can see which one matched and also access the `extensions` params. For example, if you wanted to put your API version in the extensions, you could then retrieve the value.
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
versions_header = 'Accept: application/json;version=2,application/json;version=1;q=0.8'
|
88
|
+
media_types = AcceptHeaders::MediaType::Negotiator.new(versions_header)
|
89
|
+
|
90
|
+
m = media_types.negotiate('application/json')
|
91
|
+
puts m[:match].extensions['version'] # returns '2'
|
92
|
+
```
|
93
|
+
|
82
94
|
`#accept?`:
|
83
95
|
|
84
96
|
```ruby
|
@@ -90,7 +102,8 @@ media_types.accept?('text/html') # true
|
|
90
102
|
`AcceptHeader::Encoding::Encoding`:
|
91
103
|
|
92
104
|
```ruby
|
93
|
-
|
105
|
+
accept_encoding = 'Accept-Encoding: deflate; q=0.5, gzip, compress; q=0.8, identity'
|
106
|
+
encodings = AcceptHeaders::Encoding::Negotiator.new(accept_encoding)
|
94
107
|
|
95
108
|
encodings.list
|
96
109
|
|
@@ -131,7 +144,8 @@ encodings.accept?('identity') # true
|
|
131
144
|
`Accept::Language::Negotiator`:
|
132
145
|
|
133
146
|
```ruby
|
134
|
-
|
147
|
+
accept_language = 'Accept-Language: en-*, en-us, *;q=0.8'
|
148
|
+
languages = AcceptHeaders::Language::Negotiator.new(accept_language)
|
135
149
|
|
136
150
|
languages.list
|
137
151
|
|
@@ -6,9 +6,9 @@ module AcceptHeaders
|
|
6
6
|
class Negotiator
|
7
7
|
include Negotiatable
|
8
8
|
|
9
|
-
|
10
|
-
PARAM_PATTERN = /(?<attribute>[\w!#$%^&*\-\+{}\\|'.`~]+)\s*\=\s*(?:\"(?<value>[^"]*)\"|\'(?<value>[^']*)\'|(?<value>[\w!#$%^&*\-\+{}\\|\'.`~]*))/
|
9
|
+
PARAMS_PATTERN = /(?<attribute>[\w!#$%^&*\-\+{}\\|'.`~]+)\s*\=\s*(?:\"(?<value>[^"]*)\"|\'(?<value>[^']*)\'|(?<value>[\w!#$%^&*\-\+{}\\|\'.`~]*))/
|
11
10
|
|
11
|
+
private
|
12
12
|
def parse(original_header)
|
13
13
|
header = original_header.dup
|
14
14
|
header.sub!(/\AAccept:\s*/, '')
|
@@ -16,7 +16,7 @@ module AcceptHeaders
|
|
16
16
|
return [MediaType.new] if header.empty?
|
17
17
|
media_types = []
|
18
18
|
header.split(',').each do |entry|
|
19
|
-
accept_media_range,
|
19
|
+
accept_media_range, accept_extensions = entry.split(';', 2)
|
20
20
|
next if accept_media_range.nil?
|
21
21
|
media_range = MediaType::MEDIA_TYPE_PATTERN.match(accept_media_range)
|
22
22
|
next if media_range.nil?
|
@@ -24,8 +24,8 @@ module AcceptHeaders
|
|
24
24
|
media_types << MediaType.new(
|
25
25
|
media_range[:type],
|
26
26
|
media_range[:subtype],
|
27
|
-
q: parse_q(
|
28
|
-
|
27
|
+
q: parse_q(accept_extensions),
|
28
|
+
extensions: parse_extensions(accept_extensions)
|
29
29
|
)
|
30
30
|
rescue Error
|
31
31
|
next
|
@@ -34,19 +34,19 @@ module AcceptHeaders
|
|
34
34
|
media_types.sort! { |x,y| y <=> x }
|
35
35
|
end
|
36
36
|
|
37
|
-
def
|
38
|
-
return {} if !
|
39
|
-
if
|
40
|
-
|
37
|
+
def parse_extensions(extensions_string)
|
38
|
+
return {} if !extensions_string || extensions_string.empty?
|
39
|
+
if extensions_string.match(/['"]/)
|
40
|
+
extensions = extensions_string.scan(PARAMS_PATTERN).map(&:compact).to_h
|
41
41
|
else
|
42
|
-
|
43
|
-
|
44
|
-
param =
|
45
|
-
|
42
|
+
extensions = {}
|
43
|
+
extensions_string.split(';').each do |part|
|
44
|
+
param = PARAMS_PATTERN.match(part)
|
45
|
+
extensions[param[:attribute]] = param[:value] if param
|
46
46
|
end
|
47
47
|
end
|
48
|
-
|
49
|
-
|
48
|
+
extensions.delete('q')
|
49
|
+
extensions
|
50
50
|
end
|
51
51
|
end
|
52
52
|
end
|
@@ -5,15 +5,15 @@ module AcceptHeaders
|
|
5
5
|
include Comparable
|
6
6
|
include Acceptable
|
7
7
|
|
8
|
-
attr_reader :type, :subtype, :
|
8
|
+
attr_reader :type, :subtype, :extensions
|
9
9
|
|
10
10
|
MEDIA_TYPE_PATTERN = /^\s*(?<type>[\w!#$%^&*\-\+{}\\|'.`~]+)(?:\s*\/\s*(?<subtype>[\w!#$%^&*\-\+{}\\|'.`~]+))?\s*$/
|
11
11
|
|
12
|
-
def initialize(type = '*', subtype = '*', q: 1.0,
|
12
|
+
def initialize(type = '*', subtype = '*', q: 1.0, extensions: {})
|
13
13
|
self.type = type
|
14
14
|
self.subtype = subtype
|
15
15
|
self.q = q
|
16
|
-
self.
|
16
|
+
self.extensions = extensions
|
17
17
|
end
|
18
18
|
|
19
19
|
def <=>(other)
|
@@ -25,9 +25,9 @@ module AcceptHeaders
|
|
25
25
|
-1
|
26
26
|
elsif (type != '*' && other.type == '*') || (subtype != '*' && other.subtype == '*')
|
27
27
|
1
|
28
|
-
elsif
|
28
|
+
elsif extensions.size < other.extensions.size
|
29
29
|
-1
|
30
|
-
elsif
|
30
|
+
elsif extensions.size > other.extensions.size
|
31
31
|
1
|
32
32
|
else
|
33
33
|
0
|
@@ -46,12 +46,12 @@ module AcceptHeaders
|
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
|
-
def
|
50
|
-
@
|
49
|
+
def extensions=(hash)
|
50
|
+
@extensions = {}
|
51
51
|
hash.each do |k,v|
|
52
|
-
@
|
52
|
+
@extensions[k.strip] = v
|
53
53
|
end
|
54
|
-
@
|
54
|
+
@extensions
|
55
55
|
end
|
56
56
|
|
57
57
|
def to_h
|
@@ -59,15 +59,15 @@ module AcceptHeaders
|
|
59
59
|
type: type,
|
60
60
|
subtype: subtype,
|
61
61
|
q: q,
|
62
|
-
|
62
|
+
extensions: extensions
|
63
63
|
}
|
64
64
|
end
|
65
65
|
|
66
66
|
def to_s
|
67
67
|
qvalue = (q == 0 || q == 1) ? q.to_i : q
|
68
68
|
string = "#{media_range};q=#{qvalue}"
|
69
|
-
if
|
70
|
-
|
69
|
+
if extensions.size > 0
|
70
|
+
extensions.each { |k, v| string.concat(";#{k}=#{v}") }
|
71
71
|
end
|
72
72
|
string
|
73
73
|
end
|
data/spec/encoding_spec.rb
CHANGED
@@ -1,140 +1,146 @@
|
|
1
1
|
require_relative "spec_helper"
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
end
|
3
|
+
describe AcceptHeaders::Encoding do
|
4
|
+
subject do
|
5
|
+
AcceptHeaders::Encoding
|
6
|
+
end
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
it "defaults encoding to *" do
|
9
|
+
subject.new.encoding.must_equal '*'
|
10
|
+
end
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
it "strips and downcases the encoding" do
|
13
|
+
subject.new("\t\nGZIP\s\r").encoding.must_equal "gzip"
|
14
|
+
end
|
16
15
|
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
it "optionally supports a q argument" do
|
17
|
+
subject.new('gzip', q: 0.8).q.must_equal 0.8
|
18
|
+
end
|
20
19
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
20
|
+
it "compares on q value all other values remaining equal" do
|
21
|
+
subject.new(q: 0.514).must_be :>, subject.new(q: 0.1)
|
22
|
+
subject.new(q: 0).must_be :<, subject.new(q: 1)
|
23
|
+
subject.new(q: 0.9).must_equal subject.new(q: 0.9)
|
24
|
+
end
|
26
25
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
26
|
+
it "raises an InvalidQError if q can't be converted to a float" do
|
27
|
+
e = -> do
|
28
|
+
subject.new('gzip', q: 'a')
|
29
|
+
end.must_raise AcceptHeaders::Encoding::InvalidQError
|
30
|
+
|
31
|
+
e.message.must_match INVALID_FLOAT_PATTERN
|
32
|
+
|
33
|
+
subject.new('gzip', q: '1')
|
34
|
+
end
|
31
35
|
|
32
|
-
|
36
|
+
it "raises an InvalidQError unless q value is between 0 and 1" do
|
37
|
+
[-1.0, -0.1, 1.1].each do |q|
|
38
|
+
e = -> do
|
39
|
+
subject.new('gzip', q: q)
|
40
|
+
end.must_raise AcceptHeaders::Encoding::InvalidQError
|
33
41
|
|
34
|
-
|
42
|
+
e.message.must_equal "q must be between 0 and 1"
|
35
43
|
end
|
36
44
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
subject.new('gzip', q: q)
|
41
|
-
end.must_raise Encoding::InvalidQError
|
45
|
+
subject.new('gzip', q: 1)
|
46
|
+
subject.new('compress', q: 0)
|
47
|
+
end
|
42
48
|
|
43
|
-
|
44
|
-
|
49
|
+
it "raises an InvalidQError if q has more than a precision of 3" do
|
50
|
+
e = -> do
|
51
|
+
subject.new('gzip', q: 0.1234)
|
52
|
+
end.must_raise AcceptHeaders::Encoding::InvalidQError
|
45
53
|
|
46
|
-
|
47
|
-
subject.new('compress', q: 0)
|
48
|
-
end
|
54
|
+
e.message.must_equal "q must be at most 3 decimal places"
|
49
55
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
56
|
+
subject.new('gzip', q: 0.123)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "converts to hash" do
|
60
|
+
subject.new('gzip').to_h.must_equal({
|
61
|
+
encoding: 'gzip',
|
62
|
+
q: 1.0
|
63
|
+
})
|
64
|
+
end
|
54
65
|
|
55
|
-
|
66
|
+
it "converts to string" do
|
67
|
+
s = subject.new('gzip', q: 0.9).to_s
|
68
|
+
s.must_equal "gzip;q=0.9"
|
69
|
+
end
|
56
70
|
|
57
|
-
|
71
|
+
describe "#accept?" do
|
72
|
+
it "accepted if the encoding is the same" do
|
73
|
+
a = subject.new('gzip')
|
74
|
+
a.accept?('gzip').must_equal true
|
75
|
+
b = subject.new('gzip', q: 0.001)
|
76
|
+
b.accept?('gzip').must_equal true
|
58
77
|
end
|
59
78
|
|
60
|
-
it "
|
61
|
-
subject.new('
|
62
|
-
|
63
|
-
|
64
|
-
|
79
|
+
it "accepted if the encoding is *" do
|
80
|
+
a = subject.new('*')
|
81
|
+
a.accept?('gzip').must_equal true
|
82
|
+
b = subject.new('*', q: 0.1)
|
83
|
+
b.accept?('gzip').must_equal true
|
65
84
|
end
|
66
85
|
|
67
|
-
it "
|
68
|
-
|
69
|
-
|
86
|
+
it "not accepted if the encoding doesn't match" do
|
87
|
+
a = subject.new('gzip')
|
88
|
+
a.accept?('compress').must_equal false
|
89
|
+
b = subject.new('gzip', q: 0.4)
|
90
|
+
b.accept?('compress').must_equal false
|
70
91
|
end
|
71
92
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
end
|
93
|
+
it "not accepted if q is 0" do
|
94
|
+
a = subject.new('gzip', q: 0)
|
95
|
+
a.accept?('gzip').must_equal false
|
96
|
+
b = subject.new('*', q: 0)
|
97
|
+
b.accept?('gzip').must_equal false
|
98
|
+
end
|
79
99
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
100
|
+
it "not accepted compared against nil" do
|
101
|
+
subject.new('gzip').accept?(nil).must_equal false
|
102
|
+
end
|
103
|
+
|
104
|
+
# TODO: test *
|
105
|
+
# it "not accepted if..." do
|
106
|
+
# a = subject.new('gzip')
|
107
|
+
# a.accept?('*').must_equal true
|
108
|
+
# end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe "#reject?" do
|
112
|
+
describe "given q is 0" do
|
113
|
+
it "rejected if the encoding is the same" do
|
114
|
+
a = subject.new('gzip', q: 0)
|
115
|
+
a.reject?('gzip').must_equal true
|
85
116
|
end
|
86
117
|
|
87
|
-
it "
|
88
|
-
a = subject.new('
|
89
|
-
a.
|
90
|
-
b = subject.new('gzip', q: 0.4)
|
91
|
-
b.accept?('compress').must_equal false
|
118
|
+
it "rejected if the encoding is *" do
|
119
|
+
a = subject.new('*', q: 0)
|
120
|
+
a.reject?('gzip').must_equal true
|
92
121
|
end
|
93
122
|
|
94
|
-
it "not
|
123
|
+
it "not rejected if the encoding doesn't match" do
|
95
124
|
a = subject.new('gzip', q: 0)
|
96
|
-
a.
|
97
|
-
b = subject.new('*', q: 0)
|
98
|
-
b.accept?('gzip').must_equal false
|
125
|
+
a.reject?('compress').must_equal false
|
99
126
|
end
|
100
127
|
|
101
128
|
# TODO: test *
|
102
|
-
# it "not
|
103
|
-
# a = subject.new('gzip')
|
104
|
-
# a.
|
129
|
+
# it "not rejected if..." do
|
130
|
+
# a = subject.new('gzip', q: 0)
|
131
|
+
# a.reject?('*').must_equal true
|
105
132
|
# end
|
106
133
|
end
|
107
134
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
it "rejected if the encoding is *" do
|
116
|
-
a = subject.new('*', q: 0)
|
117
|
-
a.reject?('gzip').must_equal true
|
118
|
-
end
|
119
|
-
|
120
|
-
it "not rejected if the encoding doesn't match" do
|
121
|
-
a = subject.new('gzip', q: 0)
|
122
|
-
a.reject?('compress').must_equal false
|
123
|
-
end
|
124
|
-
|
125
|
-
# TODO: test *
|
126
|
-
# it "not rejected if..." do
|
127
|
-
# a = subject.new('gzip', q: 0)
|
128
|
-
# a.reject?('*').must_equal true
|
129
|
-
# end
|
130
|
-
end
|
135
|
+
it "not rejected if q > 0" do
|
136
|
+
a = subject.new('gzip', q: 0.001)
|
137
|
+
a.reject?('gzip').must_equal false
|
138
|
+
b = subject.new('*', q: 0.9)
|
139
|
+
b.reject?('gzip').must_equal false
|
140
|
+
end
|
131
141
|
|
132
|
-
|
133
|
-
|
134
|
-
a.reject?('gzip').must_equal false
|
135
|
-
b = subject.new('*', q: 0.9)
|
136
|
-
b.reject?('gzip').must_equal false
|
137
|
-
end
|
142
|
+
it "not rejected compared against nil" do
|
143
|
+
subject.new('gzip').reject?(nil).must_equal false
|
138
144
|
end
|
139
145
|
end
|
140
146
|
end
|