accept_headers 0.0.6 → 0.0.7
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/.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
|