accept_headers 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -1
- data/Gemfile +2 -2
- data/README.md +86 -31
- data/accept_headers.gemspec +2 -2
- data/lib/accept_headers.rb +4 -0
- data/lib/accept_headers/acceptable.rb +4 -28
- data/lib/accept_headers/charset.rb +7 -15
- data/lib/accept_headers/charset/negotiator.rb +27 -0
- data/lib/accept_headers/encoding.rb +7 -15
- data/lib/accept_headers/encoding/negotiator.rb +27 -0
- data/lib/accept_headers/language.rb +9 -25
- data/lib/accept_headers/language/negotiator.rb +37 -0
- data/lib/accept_headers/media_type.rb +9 -38
- data/lib/accept_headers/media_type/negotiator.rb +50 -0
- data/lib/accept_headers/negotiatable.rb +54 -0
- data/lib/accept_headers/version.rb +1 -1
- data/spec/charset/negotiator_spec.rb +73 -0
- data/spec/charset_spec.rb +4 -58
- data/spec/encoding/negotiator_spec.rb +73 -0
- data/spec/encoding_spec.rb +4 -58
- data/spec/language/negotiator_spec.rb +78 -0
- data/spec/language_spec.rb +4 -63
- data/spec/media_type/negotiator_spec.rb +92 -0
- data/spec/media_type_spec.rb +4 -78
- data/spec/spec_helper.rb +10 -12
- metadata +19 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e40f6f21dfdede27f8ec870248f903caa24a8207
|
4
|
+
data.tar.gz: 2a4ce23b83c7a92cbb80fffe73fa81b2eb425fe1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5c1c66df7d5771a7a8aaf90a411d74c4e3e8ef7c0bb09e595d7898fbc62dcc42847d17bc8e0e79e3a76b97ddea9c9c2c23d18983c2a14c7f0e8db64c30f823cd
|
7
|
+
data.tar.gz: 7b6d214b3c465b5eccb5bb441cdecec1e5e0290596ecd81684d779b896cba6fcccc786ff8dc9394f35a020226e59227598fc315ea28ef52c9d6ab6da7f2660b7
|
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
|
5
5
|
# AcceptHeaders
|
6
6
|
|
7
|
-
**AcceptHeaders** is a ruby library that parses and sorts http accept headers.
|
7
|
+
**AcceptHeaders** is a ruby library that does content negotiation and parses and sorts http accept headers.
|
8
8
|
|
9
9
|
Some features of the library are:
|
10
10
|
|
@@ -14,6 +14,8 @@ Some features of the library are:
|
|
14
14
|
request headers
|
15
15
|
* A comprehensive [spec suite][spec] that covers many edge cases
|
16
16
|
|
17
|
+
This library is optimistic when parsing headers. If a specific media type, encoding, charset, 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.
|
18
|
+
|
17
19
|
[rfc]: http://www.w3.org/Protocols/rfc2616/rfc2616.html
|
18
20
|
[rfc-sec14]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
19
21
|
[rfc-sec14-1]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
|
@@ -40,74 +42,127 @@ Or install it yourself as:
|
|
40
42
|
|
41
43
|
## Usage
|
42
44
|
|
43
|
-
|
45
|
+
### Accept
|
46
|
+
|
47
|
+
`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 `params` specificity.
|
44
48
|
|
45
49
|
```ruby
|
46
|
-
AcceptHeaders::MediaType.
|
47
|
-
```
|
50
|
+
media_types = AcceptHeaders::MediaType::Negotiator.new("text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5")
|
48
51
|
|
49
|
-
|
52
|
+
media_types.list
|
53
|
+
|
54
|
+
# Returns:
|
50
55
|
|
51
|
-
```ruby
|
52
56
|
[
|
53
|
-
MediaType.new('text', 'html', params: { 'level' => '1' }),
|
54
|
-
MediaType.new('text', 'html', q: 0.7),
|
55
|
-
MediaType.new('*', '*', q: 0.5),
|
56
|
-
MediaType.new('text', 'html', q: 0.4, params: { 'level' => '2' }),
|
57
|
-
MediaType.new('text', '*', q: 0.3)
|
57
|
+
AcceptHeaders::MediaType.new('text', 'html', params: { 'level' => '1' }),
|
58
|
+
AcceptHeaders::MediaType.new('text', 'html', q: 0.7),
|
59
|
+
AcceptHeaders::MediaType.new('*', '*', q: 0.5),
|
60
|
+
AcceptHeaders::MediaType.new('text', 'html', q: 0.4, params: { 'level' => '2' }),
|
61
|
+
AcceptHeaders::MediaType.new('text', '*', q: 0.3)
|
58
62
|
]
|
59
63
|
```
|
60
64
|
|
61
|
-
|
65
|
+
`#negotiate` takes a string of media types supported (by your API or route/controller) and returns the best match as a `MediaType`. This will first check the available list for any matching media types with a `q` of 0 and return `nil` if there is a match. Then it'll look to the highest `q` values and look for matches in descending `q` value order and return the first match account for wildcards.
|
62
66
|
|
63
67
|
```ruby
|
64
|
-
|
68
|
+
media_type.negotiate('text/html')
|
69
|
+
|
70
|
+
# Returns:
|
71
|
+
|
72
|
+
AcceptHeaders::MediaType.new('text', 'html', params: { 'level' => '1' })
|
65
73
|
```
|
66
74
|
|
67
|
-
|
75
|
+
### Accept-Charset
|
76
|
+
|
77
|
+
`AcceptHeader::Charset::Negotiator`:
|
68
78
|
|
69
79
|
```ruby
|
80
|
+
charsets = AcceptHeaders::Charset::Negotiator.new("us-ascii; q=0.5, iso-8859-1, utf-8; q=0.8, macintosh")
|
81
|
+
|
82
|
+
charsets.list
|
83
|
+
|
84
|
+
# Returns:
|
85
|
+
|
70
86
|
[
|
71
|
-
Charset.new('iso-8859-1'),
|
72
|
-
Charset.new('macintosh'),
|
73
|
-
Charset.new('utf-8', q: 0.8),
|
74
|
-
Charset.new('us-ascii', q: 0.5)
|
87
|
+
AcceptHeaders::Charset.new('iso-8859-1'),
|
88
|
+
AcceptHeaders::Charset.new('macintosh'),
|
89
|
+
AcceptHeaders::Charset.new('utf-8', q: 0.8),
|
90
|
+
AcceptHeaders::Charset.new('us-ascii', q: 0.5)
|
75
91
|
]
|
76
92
|
```
|
77
93
|
|
78
|
-
|
94
|
+
`#negotiate`:
|
79
95
|
|
80
96
|
```ruby
|
81
|
-
|
97
|
+
charsets.negotiate('iso-8859-1')
|
98
|
+
|
99
|
+
# Returns:
|
100
|
+
|
101
|
+
AcceptHeaders::Charset.new('iso-8859-1')
|
82
102
|
```
|
83
103
|
|
84
|
-
|
104
|
+
### Accept-Encoding
|
105
|
+
|
106
|
+
`AcceptHeader::Charset::Encoding`:
|
85
107
|
|
86
108
|
```ruby
|
109
|
+
encodings = AcceptHeaders::Encoding::Negotiator.new("deflate; q=0.5, gzip, compress; q=0.8, identity")
|
110
|
+
|
111
|
+
encodings.list
|
112
|
+
|
113
|
+
# Returns:
|
114
|
+
|
87
115
|
[
|
88
|
-
Encoding.new('gzip'),
|
89
|
-
Encoding.new('identity'),
|
90
|
-
Encoding.new('compress', q: 0.8),
|
91
|
-
Encoding.new('deflate', q: 0.5)
|
116
|
+
AcceptHeaders::Encoding.new('gzip'),
|
117
|
+
AcceptHeaders::Encoding.new('identity'),
|
118
|
+
AcceptHeaders::Encoding.new('compress', q: 0.8),
|
119
|
+
AcceptHeaders::Encoding.new('deflate', q: 0.5)
|
92
120
|
]
|
93
121
|
```
|
94
122
|
|
95
|
-
|
123
|
+
`#negotiate`:
|
96
124
|
|
97
125
|
```ruby
|
98
|
-
|
126
|
+
encodings.negotiate('identity')
|
127
|
+
|
128
|
+
# Returns:
|
129
|
+
|
130
|
+
AcceptHeaders::Encoding.new('identity')
|
99
131
|
```
|
100
132
|
|
101
|
-
|
133
|
+
### Accept-Language
|
134
|
+
|
135
|
+
`Accept::Language::Negotiator`:
|
102
136
|
|
103
137
|
```ruby
|
138
|
+
languages = AcceptHeaders::Language::Negotiator.new("en-*, en-us, *;q=0.8")
|
139
|
+
|
140
|
+
languages.list
|
141
|
+
|
142
|
+
# Returns:
|
143
|
+
|
104
144
|
[
|
105
|
-
Language.new('en', 'us'),
|
106
|
-
Language.new('en', '*'),
|
107
|
-
Language.new('*', '*', q: 0.8)
|
145
|
+
AcceptHeaders::Language.new('en', 'us'),
|
146
|
+
AcceptHeaders::Language.new('en', '*'),
|
147
|
+
AcceptHeaders::Language.new('*', '*', q: 0.8)
|
108
148
|
]
|
109
149
|
```
|
110
150
|
|
151
|
+
`#negotiate`:
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
languages.negotiate('en-us')
|
155
|
+
|
156
|
+
# Returns:
|
157
|
+
|
158
|
+
AcceptHeaders::Language.new('en', 'us')
|
159
|
+
```
|
160
|
+
|
161
|
+
## Todo
|
162
|
+
|
163
|
+
* Write rack middleware
|
164
|
+
* More edge case tests
|
165
|
+
|
111
166
|
## Contributing
|
112
167
|
|
113
168
|
1. Fork it ( https://github.com/[my-github-username]/accept_headers/fork )
|
data/accept_headers.gemspec
CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = AcceptHeaders::VERSION
|
9
9
|
spec.authors = ["Jack Chu"]
|
10
10
|
spec.email = ["kamuigt@gmail.com"]
|
11
|
-
spec.summary = %q{A ruby library that parses and sorts http accept headers.}
|
12
|
-
spec.description = %q{
|
11
|
+
spec.summary = %q{A ruby library that does content negotiation and parses and sorts http accept headers.}
|
12
|
+
spec.description = %q{a ruby library that does content negotiation and parses and sorts http accept headers. Adheres to RFC 2616.}
|
13
13
|
spec.homepage = ""
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
data/lib/accept_headers.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
require "accept_headers/version"
|
2
2
|
require "accept_headers/charset"
|
3
|
+
require "accept_headers/charset/negotiator"
|
3
4
|
require "accept_headers/encoding"
|
5
|
+
require "accept_headers/encoding/negotiator"
|
4
6
|
require "accept_headers/media_type"
|
7
|
+
require "accept_headers/media_type/negotiator"
|
5
8
|
require "accept_headers/language"
|
9
|
+
require "accept_headers/language/negotiator"
|
6
10
|
|
7
11
|
module AcceptHeaders
|
8
12
|
end
|
@@ -1,17 +1,12 @@
|
|
1
1
|
module AcceptHeaders
|
2
2
|
module Acceptable
|
3
3
|
class Error < StandardError; end
|
4
|
-
class OutOfRangeError < Error; end
|
5
|
-
class InvalidPrecisionError < Error; end
|
6
4
|
class InvalidQError < Error; end
|
7
|
-
class ParseError < Error; end
|
8
5
|
|
9
6
|
attr_reader :q
|
10
7
|
|
11
|
-
|
12
|
-
|
13
|
-
def self.included(base)
|
14
|
-
base.extend ClassMethods
|
8
|
+
def match(other)
|
9
|
+
raise NotImplementedError.new("#match is not implemented")
|
15
10
|
end
|
16
11
|
|
17
12
|
def q=(value)
|
@@ -21,31 +16,12 @@ module AcceptHeaders
|
|
21
16
|
raise InvalidQError.new(e.message)
|
22
17
|
end
|
23
18
|
if !q_float.between?(0.0, 1.0)
|
24
|
-
raise
|
19
|
+
raise InvalidQError.new("q must be between 0 and 1")
|
25
20
|
end
|
26
21
|
if q_float.to_s.match(/^\d\.(\d+)$/) && $1 && $1.size > 3
|
27
|
-
raise
|
22
|
+
raise InvalidQError.new("q must be at most 3 decimal places")
|
28
23
|
end
|
29
24
|
@q = q_float
|
30
25
|
end
|
31
|
-
|
32
|
-
module ClassMethods
|
33
|
-
Q_PATTERN = /(?:\A|;)\s*(?<exists>qs*\=)\s*(?:(?<q>0\.\d{1,3}|[01])|(?:[^;]*))\s*(?:\z|;)/
|
34
|
-
|
35
|
-
private
|
36
|
-
def parse_q(header)
|
37
|
-
q = 1
|
38
|
-
return q unless header
|
39
|
-
q_match = Q_PATTERN.match(header)
|
40
|
-
if q_match && q_match[:exists]
|
41
|
-
if q_match[:q]
|
42
|
-
q = q_match[:q]
|
43
|
-
else
|
44
|
-
q = 0.001
|
45
|
-
end
|
46
|
-
end
|
47
|
-
q
|
48
|
-
end
|
49
|
-
end
|
50
26
|
end
|
51
27
|
end
|
@@ -5,8 +5,6 @@ module AcceptHeaders
|
|
5
5
|
include Comparable
|
6
6
|
include Acceptable
|
7
7
|
|
8
|
-
class InvalidCharsetError < Error; end
|
9
|
-
|
10
8
|
attr_reader :charset
|
11
9
|
|
12
10
|
def initialize(charset = '*', q: 1.0)
|
@@ -34,20 +32,14 @@ module AcceptHeaders
|
|
34
32
|
"#{charset};q=#{qvalue}"
|
35
33
|
end
|
36
34
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
charset_arr = entry.split(';', 2)
|
45
|
-
next if charset_arr[0].nil?
|
46
|
-
charset = TOKEN_PATTERN.match(charset_arr[0])
|
47
|
-
next if charset.nil?
|
48
|
-
charsets << Charset.new(charset[:token], q: parse_q(charset_arr[1]))
|
35
|
+
def match(other)
|
36
|
+
if charset == other.charset
|
37
|
+
true
|
38
|
+
elsif other.charset == '*'
|
39
|
+
true
|
40
|
+
else
|
41
|
+
false
|
49
42
|
end
|
50
|
-
charsets.sort! { |x,y| y <=> x }
|
51
43
|
end
|
52
44
|
end
|
53
45
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "accept_headers/charset"
|
2
|
+
require "accept_headers/negotiatable"
|
3
|
+
|
4
|
+
module AcceptHeaders
|
5
|
+
class Charset
|
6
|
+
class Negotiator
|
7
|
+
include Negotiatable
|
8
|
+
|
9
|
+
private
|
10
|
+
def parse(original_header)
|
11
|
+
header = original_header.dup
|
12
|
+
header.sub!(/\AAccept-Charset:\s*/, '')
|
13
|
+
header.strip!
|
14
|
+
return [Charset.new('iso-8859-5', q: 1)] if header.empty?
|
15
|
+
charsets = []
|
16
|
+
header.split(',').each do |entry|
|
17
|
+
charset_arr = entry.split(';', 2)
|
18
|
+
next if charset_arr[0].nil?
|
19
|
+
charset = TOKEN_PATTERN.match(charset_arr[0])
|
20
|
+
next if charset.nil?
|
21
|
+
charsets << Charset.new(charset[:token], q: parse_q(charset_arr[1]))
|
22
|
+
end
|
23
|
+
charsets.sort! { |x,y| y <=> x }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -5,8 +5,6 @@ module AcceptHeaders
|
|
5
5
|
include Comparable
|
6
6
|
include Acceptable
|
7
7
|
|
8
|
-
class InvalidEncodingError < Error; end
|
9
|
-
|
10
8
|
attr_reader :encoding
|
11
9
|
|
12
10
|
def initialize(encoding = '*', q: 1.0)
|
@@ -34,20 +32,14 @@ module AcceptHeaders
|
|
34
32
|
"#{encoding};q=#{qvalue}"
|
35
33
|
end
|
36
34
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
encoding_arr = entry.split(';', 2)
|
45
|
-
next if encoding_arr[0].nil?
|
46
|
-
encoding = TOKEN_PATTERN.match(encoding_arr[0])
|
47
|
-
next if encoding.nil?
|
48
|
-
encodings << Encoding.new(encoding[:token], q: parse_q(encoding_arr[1]))
|
35
|
+
def match(other)
|
36
|
+
if encoding == other.encoding
|
37
|
+
true
|
38
|
+
elsif other.encoding == '*'
|
39
|
+
true
|
40
|
+
else
|
41
|
+
false
|
49
42
|
end
|
50
|
-
encodings.sort! { |x,y| y <=> x }
|
51
43
|
end
|
52
44
|
end
|
53
45
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "accept_headers/encoding"
|
2
|
+
require "accept_headers/negotiatable"
|
3
|
+
|
4
|
+
module AcceptHeaders
|
5
|
+
class Encoding
|
6
|
+
class Negotiator
|
7
|
+
include Negotiatable
|
8
|
+
|
9
|
+
private
|
10
|
+
def parse(original_header)
|
11
|
+
header = original_header.dup
|
12
|
+
header.sub!(/\AAccept-Encoding:\s*/, '')
|
13
|
+
header.strip!
|
14
|
+
return [Charset.new] if header.empty?
|
15
|
+
encodings = []
|
16
|
+
header.split(',').each do |entry|
|
17
|
+
encoding_arr = entry.split(';', 2)
|
18
|
+
next if encoding_arr[0].nil?
|
19
|
+
encoding = TOKEN_PATTERN.match(encoding_arr[0])
|
20
|
+
next if encoding.nil?
|
21
|
+
encodings << Encoding.new(encoding[:token], q: parse_q(encoding_arr[1]))
|
22
|
+
end
|
23
|
+
encodings.sort! { |x,y| y <=> x }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -5,8 +5,6 @@ module AcceptHeaders
|
|
5
5
|
include Comparable
|
6
6
|
include Acceptable
|
7
7
|
|
8
|
-
class InvalidLanguageTagError < Error; end
|
9
|
-
|
10
8
|
attr_reader :primary_tag, :subtag, :params
|
11
9
|
|
12
10
|
def initialize(primary_tag = '*', subtag = nil, q: 1.0)
|
@@ -54,30 +52,16 @@ module AcceptHeaders
|
|
54
52
|
"#{primary_tag}-#{subtag};q=#{qvalue}"
|
55
53
|
end
|
56
54
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
language_arr = entry.split(';', 2)
|
67
|
-
next if language_arr[0].nil?
|
68
|
-
language_range = LANGUAGE_PATTERN.match(language_arr[0])
|
69
|
-
next if language_range.nil?
|
70
|
-
begin
|
71
|
-
languages << Language.new(
|
72
|
-
language_range[:primary_tag],
|
73
|
-
language_range[:subtag],
|
74
|
-
q: parse_q(language_arr[1])
|
75
|
-
)
|
76
|
-
rescue Error
|
77
|
-
next
|
78
|
-
end
|
55
|
+
def match(other)
|
56
|
+
if primary_tag == other.primary_tag && subtag == other.subtag
|
57
|
+
true
|
58
|
+
elsif primary_tag == other.primary_tag && subtag == '*'
|
59
|
+
true
|
60
|
+
elsif other.primary_tag == '*'
|
61
|
+
true
|
62
|
+
else
|
63
|
+
false
|
79
64
|
end
|
80
|
-
languages.sort! { |x,y| y <=> x }
|
81
65
|
end
|
82
66
|
end
|
83
67
|
end
|