accept_headers 0.0.1
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 +7 -0
- data/.gitignore +14 -0
- data/.travis.yml +17 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +16 -0
- data/Guardfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +117 -0
- data/Rakefile +10 -0
- data/accept_headers.gemspec +27 -0
- data/lib/accept_headers.rb +8 -0
- data/lib/accept_headers/acceptable.rb +51 -0
- data/lib/accept_headers/charset.rb +53 -0
- data/lib/accept_headers/encoding.rb +53 -0
- data/lib/accept_headers/language.rb +83 -0
- data/lib/accept_headers/media_type.rb +114 -0
- data/lib/accept_headers/version.rb +3 -0
- data/spec/charset_spec.rb +126 -0
- data/spec/encoding_spec.rb +126 -0
- data/spec/language_spec.rb +149 -0
- data/spec/media_type_spec.rb +173 -0
- data/spec/spec_helper.rb +21 -0
- data/wercker.yml +27 -0
- metadata +142 -0
@@ -0,0 +1,114 @@
|
|
1
|
+
require "accept_headers/acceptable"
|
2
|
+
|
3
|
+
module AcceptHeaders
|
4
|
+
class MediaType
|
5
|
+
include Comparable
|
6
|
+
include Acceptable
|
7
|
+
|
8
|
+
attr_reader :type, :subtype, :params
|
9
|
+
|
10
|
+
def initialize(type = '*', subtype = '*', q: 1.0, params: {})
|
11
|
+
self.type = type
|
12
|
+
self.subtype = subtype
|
13
|
+
self.q = q
|
14
|
+
self.params = params
|
15
|
+
end
|
16
|
+
|
17
|
+
def <=>(other)
|
18
|
+
if q < other.q
|
19
|
+
-1
|
20
|
+
elsif q > other.q
|
21
|
+
1
|
22
|
+
elsif (type == '*' && other.type != '*') || (subtype == '*' && other.subtype != '*')
|
23
|
+
-1
|
24
|
+
elsif (type != '*' && other.type == '*') || (subtype != '*' && other.subtype == '*')
|
25
|
+
1
|
26
|
+
elsif params.size < other.params.size
|
27
|
+
-1
|
28
|
+
elsif params.size > other.params.size
|
29
|
+
1
|
30
|
+
else
|
31
|
+
0
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def type=(value)
|
36
|
+
@type = value.strip.downcase
|
37
|
+
end
|
38
|
+
|
39
|
+
def subtype=(value)
|
40
|
+
@subtype = if value.nil? && type == '*'
|
41
|
+
'*'
|
42
|
+
else
|
43
|
+
value.strip.downcase
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def params=(hash)
|
48
|
+
@params = {}
|
49
|
+
hash.each do |k,v|
|
50
|
+
@params[k.strip] = v.strip
|
51
|
+
end
|
52
|
+
@params
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_h
|
56
|
+
{
|
57
|
+
type: type,
|
58
|
+
subtype: subtype,
|
59
|
+
q: q,
|
60
|
+
params: params
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_s
|
65
|
+
qvalue = (q == 0 || q == 1) ? q.to_i : q
|
66
|
+
string = "#{type}/#{subtype};q=#{qvalue}"
|
67
|
+
if params.size > 0
|
68
|
+
params.each { |k, v| string.concat(";#{k}=#{v}") }
|
69
|
+
end
|
70
|
+
string
|
71
|
+
end
|
72
|
+
|
73
|
+
MEDIA_TYPE_PATTERN = /^\s*(?<type>[\w!#$%^&*\-\+{}\\|'.`~]+)(?:\s*\/\s*(?<subtype>[\w!#$%^&*\-\+{}\\|'.`~]+))?\s*$/
|
74
|
+
PARAM_PATTERN = /(?<attribute>[\w!#$%^&*\-\+{}\\|'.`~]+)\s*\=\s*(?:\"(?<value>[^"]*)\"|\'(?<value>[^']*)\'|(?<value>[\w!#$%^&*\-\+{}\\|\'.`~]*))/
|
75
|
+
|
76
|
+
def self.parse(original_header)
|
77
|
+
header = original_header.dup
|
78
|
+
header.sub!(/\AAccept:\s*/, '')
|
79
|
+
header.strip!
|
80
|
+
return [MediaType.new] if header.empty?
|
81
|
+
media_types = []
|
82
|
+
header.split(',').each do |entry|
|
83
|
+
accept_media_range, accept_params = entry.split(';', 2)
|
84
|
+
next if accept_media_range.nil?
|
85
|
+
media_range = MEDIA_TYPE_PATTERN.match(accept_media_range)
|
86
|
+
next if media_range.nil?
|
87
|
+
begin
|
88
|
+
media_types << MediaType.new(
|
89
|
+
media_range[:type],
|
90
|
+
media_range[:subtype],
|
91
|
+
q: parse_q(accept_params),
|
92
|
+
params: parse_params(accept_params)
|
93
|
+
)
|
94
|
+
rescue Error
|
95
|
+
next
|
96
|
+
end
|
97
|
+
end
|
98
|
+
media_types.sort! { |x,y| y <=> x }
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def self.parse_params(params_string)
|
104
|
+
params = {}
|
105
|
+
return params if !params_string || params_string.empty?
|
106
|
+
params_string.split(';').each do |part|
|
107
|
+
param = PARAM_PATTERN.match(part)
|
108
|
+
params[param[:attribute]] = param[:value] if param
|
109
|
+
end
|
110
|
+
params.delete('q')
|
111
|
+
params
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
module AcceptHeaders
|
4
|
+
describe Charset do
|
5
|
+
subject do
|
6
|
+
AcceptHeaders::Charset
|
7
|
+
end
|
8
|
+
|
9
|
+
it "defaults charset to *" do
|
10
|
+
subject.new.charset.must_equal '*'
|
11
|
+
end
|
12
|
+
|
13
|
+
it "strips and downcases the charset" do
|
14
|
+
subject.new("\t\nISO-8859-1\s\r").charset.must_equal "iso-8859-1"
|
15
|
+
end
|
16
|
+
|
17
|
+
it "optionally supports a q argument" do
|
18
|
+
subject.new('iso-8859-1', q: 0.8).q.must_equal 0.8
|
19
|
+
end
|
20
|
+
|
21
|
+
it "compares on q value all other values remaining equal" do
|
22
|
+
subject.new(q: 0.514).must_be :>, subject.new(q: 0.1)
|
23
|
+
subject.new(q: 0).must_be :<, subject.new(q: 1)
|
24
|
+
subject.new(q: 0.9).must_equal subject.new(q: 0.9)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "raises an InvalidQError if q can't be converted to a float" do
|
28
|
+
e = -> do
|
29
|
+
subject.new('iso-8859-1', q: 'a')
|
30
|
+
end.must_raise Charset::InvalidQError
|
31
|
+
|
32
|
+
e.message.must_equal 'invalid value for Float(): "a"'
|
33
|
+
|
34
|
+
subject.new('iso-8859-1', q: '1')
|
35
|
+
end
|
36
|
+
|
37
|
+
it "raises an OutOfRangeError unless q value is between 0 and 1" do
|
38
|
+
[-1.0, -0.1, 1.1].each do |q|
|
39
|
+
e = -> do
|
40
|
+
subject.new('iso-8859-1', q: q)
|
41
|
+
end.must_raise Charset::OutOfRangeError
|
42
|
+
|
43
|
+
e.message.must_equal "q must be between 0 and 1"
|
44
|
+
end
|
45
|
+
|
46
|
+
subject.new('iso-8859-1', q: 1)
|
47
|
+
subject.new('unicode-1-1', q: 0)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "raises an InvalidPrecisionError if q has more than a precision of 3" do
|
51
|
+
e = -> do
|
52
|
+
subject.new('iso-8859-1', q: 0.1234)
|
53
|
+
end.must_raise Charset::InvalidPrecisionError
|
54
|
+
|
55
|
+
e.message.must_equal "q must be at most 3 decimal places"
|
56
|
+
|
57
|
+
subject.new('iso-8859-1', q: 0.123)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "converts to hash" do
|
61
|
+
subject.new('iso-8859-1').to_h.must_equal({
|
62
|
+
charset: 'iso-8859-1',
|
63
|
+
q: 1.0
|
64
|
+
})
|
65
|
+
end
|
66
|
+
|
67
|
+
it "convers to string" do
|
68
|
+
s = subject.new('iso-8859-1', q: 0.9).to_s
|
69
|
+
s.must_equal "iso-8859-1;q=0.9"
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "parsing an accept header" do
|
73
|
+
it "returns a sorted array of charsets" do
|
74
|
+
subject.parse("*; q=0.2, unicode-1-1").must_equal [
|
75
|
+
Charset.new('unicode-1-1'),
|
76
|
+
Charset.new('*', q: 0.2)
|
77
|
+
]
|
78
|
+
|
79
|
+
subject.parse("us-ascii; q=0.5, iso-8859-1, utf-8; q=0.8, macintosh").must_equal [
|
80
|
+
Charset.new('iso-8859-1'),
|
81
|
+
Charset.new('macintosh'),
|
82
|
+
Charset.new('utf-8', q: 0.8),
|
83
|
+
Charset.new('us-ascii', q: 0.5)
|
84
|
+
]
|
85
|
+
end
|
86
|
+
|
87
|
+
it "sets charset to * when the accept-charset header is empty" do
|
88
|
+
subject.parse('').must_equal [
|
89
|
+
Charset.new('*')
|
90
|
+
]
|
91
|
+
end
|
92
|
+
|
93
|
+
it "defaults q to 1 if it's not explicitly specified" do
|
94
|
+
subject.parse("iso-8859-1").must_equal [
|
95
|
+
Charset.new('iso-8859-1', q: 1.0)
|
96
|
+
]
|
97
|
+
end
|
98
|
+
|
99
|
+
it "strips whitespace from between charsets" do
|
100
|
+
subject.parse("\tunicode-1-1\r,\niso-8859-1\s").must_equal [
|
101
|
+
Charset.new('unicode-1-1'),
|
102
|
+
Charset.new('iso-8859-1')
|
103
|
+
]
|
104
|
+
end
|
105
|
+
|
106
|
+
it "strips whitespace around q" do
|
107
|
+
subject.parse("iso-8859-1;\tq\r=\n1, unicode-1-1;q=0.8\n").must_equal [
|
108
|
+
Charset.new('iso-8859-1'),
|
109
|
+
Charset.new('unicode-1-1', q: 0.8)
|
110
|
+
]
|
111
|
+
end
|
112
|
+
|
113
|
+
it "has a q value of 0.001 when parsed q is invalid" do
|
114
|
+
subject.parse("iso-8859-1;q=x").must_equal [
|
115
|
+
Charset.new('iso-8859-1', q: 0.001)
|
116
|
+
]
|
117
|
+
end
|
118
|
+
|
119
|
+
it "skips invalid character sets" do
|
120
|
+
subject.parse("iso-8859-1, @unicode-1-1").must_equal [
|
121
|
+
Charset.new('iso-8859-1', q: 1)
|
122
|
+
]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
module AcceptHeaders
|
4
|
+
describe Encoding do
|
5
|
+
subject do
|
6
|
+
AcceptHeaders::Encoding
|
7
|
+
end
|
8
|
+
|
9
|
+
it "defaults encoding to *" do
|
10
|
+
subject.new.encoding.must_equal '*'
|
11
|
+
end
|
12
|
+
|
13
|
+
it "strips and downcases the encoding" do
|
14
|
+
subject.new("\t\nGZIP\s\r").encoding.must_equal "gzip"
|
15
|
+
end
|
16
|
+
|
17
|
+
it "optionally supports a q argument" do
|
18
|
+
subject.new('gzip', q: 0.8).q.must_equal 0.8
|
19
|
+
end
|
20
|
+
|
21
|
+
it "compares on q value all other values remaining equal" do
|
22
|
+
subject.new(q: 0.514).must_be :>, subject.new(q: 0.1)
|
23
|
+
subject.new(q: 0).must_be :<, subject.new(q: 1)
|
24
|
+
subject.new(q: 0.9).must_equal subject.new(q: 0.9)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "raises an InvalidQError if q can't be converted to a float" do
|
28
|
+
e = -> do
|
29
|
+
subject.new('gzip', q: 'a')
|
30
|
+
end.must_raise Encoding::InvalidQError
|
31
|
+
|
32
|
+
e.message.must_equal 'invalid value for Float(): "a"'
|
33
|
+
|
34
|
+
subject.new('gzip', q: '1')
|
35
|
+
end
|
36
|
+
|
37
|
+
it "raises an OutOfRangeError unless q value is between 0 and 1" do
|
38
|
+
[-1.0, -0.1, 1.1].each do |q|
|
39
|
+
e = -> do
|
40
|
+
subject.new('gzip', q: q)
|
41
|
+
end.must_raise Encoding::OutOfRangeError
|
42
|
+
|
43
|
+
e.message.must_equal "q must be between 0 and 1"
|
44
|
+
end
|
45
|
+
|
46
|
+
subject.new('gzip', q: 1)
|
47
|
+
subject.new('compress', q: 0)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "raises an InvalidPrecisionError if q has more than a precision of 3" do
|
51
|
+
e = -> do
|
52
|
+
subject.new('gzip', q: 0.1234)
|
53
|
+
end.must_raise Encoding::InvalidPrecisionError
|
54
|
+
|
55
|
+
e.message.must_equal "q must be at most 3 decimal places"
|
56
|
+
|
57
|
+
subject.new('gzip', q: 0.123)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "converts to hash" do
|
61
|
+
subject.new('gzip').to_h.must_equal({
|
62
|
+
encoding: 'gzip',
|
63
|
+
q: 1.0
|
64
|
+
})
|
65
|
+
end
|
66
|
+
|
67
|
+
it "convers to string" do
|
68
|
+
s = subject.new('gzip', q: 0.9).to_s
|
69
|
+
s.must_equal "gzip;q=0.9"
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "parsing an accept header" do
|
73
|
+
it "returns a sorted array of encodings" do
|
74
|
+
subject.parse("*; q=0.2, compress").must_equal [
|
75
|
+
Encoding.new('compress'),
|
76
|
+
Encoding.new('*', q: 0.2)
|
77
|
+
]
|
78
|
+
|
79
|
+
subject.parse("deflate; q=0.5, gzip, compress; q=0.8, identity").must_equal [
|
80
|
+
Encoding.new('gzip'),
|
81
|
+
Encoding.new('identity'),
|
82
|
+
Encoding.new('compress', q: 0.8),
|
83
|
+
Encoding.new('deflate', q: 0.5)
|
84
|
+
]
|
85
|
+
end
|
86
|
+
|
87
|
+
it "sets encoding to * when the accept-encoding header is empty" do
|
88
|
+
subject.parse('').must_equal [
|
89
|
+
Encoding.new('*')
|
90
|
+
]
|
91
|
+
end
|
92
|
+
|
93
|
+
it "defaults q to 1 if it's not explicitly specified" do
|
94
|
+
subject.parse("gzip").must_equal [
|
95
|
+
Encoding.new('gzip', q: 1.0)
|
96
|
+
]
|
97
|
+
end
|
98
|
+
|
99
|
+
it "strips whitespace from between encodings" do
|
100
|
+
subject.parse("\tcompress\r,\ngzip\s").must_equal [
|
101
|
+
Encoding.new('compress'),
|
102
|
+
Encoding.new('gzip')
|
103
|
+
]
|
104
|
+
end
|
105
|
+
|
106
|
+
it "strips whitespace around q" do
|
107
|
+
subject.parse("gzip;\tq\r=\n1, compress;q=0.8\n").must_equal [
|
108
|
+
Encoding.new('gzip'),
|
109
|
+
Encoding.new('compress', q: 0.8)
|
110
|
+
]
|
111
|
+
end
|
112
|
+
|
113
|
+
it "has a q value of 0.001 when parsed q is invalid" do
|
114
|
+
subject.parse("gzip;q=x").must_equal [
|
115
|
+
Encoding.new('gzip', q: 0.001)
|
116
|
+
]
|
117
|
+
end
|
118
|
+
|
119
|
+
it "skips invalid encodings" do
|
120
|
+
subject.parse("gzip, @blah").must_equal [
|
121
|
+
Encoding.new('gzip', q: 1.0)
|
122
|
+
]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
module AcceptHeaders
|
4
|
+
describe Language do
|
5
|
+
subject do
|
6
|
+
AcceptHeaders::Language
|
7
|
+
end
|
8
|
+
|
9
|
+
it "defaults primary tag to *" do
|
10
|
+
subject.new.primary_tag.must_equal '*'
|
11
|
+
end
|
12
|
+
|
13
|
+
it "defaults subtag to *" do
|
14
|
+
subject.new('en').subtag.must_equal '*'
|
15
|
+
end
|
16
|
+
|
17
|
+
it "strips and downcases the primary tag" do
|
18
|
+
subject.new("\t\nEN\s\r", '*').primary_tag.must_equal "en"
|
19
|
+
end
|
20
|
+
|
21
|
+
it "strips and downcases the subtag" do
|
22
|
+
subject.new("en", "\s\nUS\r\t").subtag.must_equal "us"
|
23
|
+
end
|
24
|
+
|
25
|
+
it "sets subtag to * if value passed in is nil and primary tag is *" do
|
26
|
+
subject.new('*', nil).subtag.must_equal '*'
|
27
|
+
end
|
28
|
+
|
29
|
+
it "optionally supports a q argument" do
|
30
|
+
subject.new('en', 'us', q: 0.8).q.must_equal 0.8
|
31
|
+
end
|
32
|
+
|
33
|
+
it "compares on q value all other values remaining equal" do
|
34
|
+
subject.new(q: 0.514).must_be :>, subject.new(q: 0.1)
|
35
|
+
subject.new(q: 0).must_be :<, subject.new(q: 1)
|
36
|
+
subject.new(q: 0.9).must_equal subject.new(q: 0.9)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "compares on subtag then primary tag all other values remaining equal" do
|
40
|
+
subject.new('en', 'us').must_be :>, subject.new('en', '*')
|
41
|
+
subject.new('*', '*').must_be :<, subject.new('en', '*')
|
42
|
+
end
|
43
|
+
|
44
|
+
it "raises an InvalidQError if q can't be converted to a float" do
|
45
|
+
e = -> do
|
46
|
+
subject.new('en', 'us', q: 'a')
|
47
|
+
end.must_raise Language::InvalidQError
|
48
|
+
|
49
|
+
e.message.must_equal 'invalid value for Float(): "a"'
|
50
|
+
|
51
|
+
subject.new('en', 'us', q: '1')
|
52
|
+
end
|
53
|
+
|
54
|
+
it "raises an OutOfRangeError unless q value is between 0 and 1" do
|
55
|
+
[-1.0, -0.1, 1.1].each do |q|
|
56
|
+
e = -> do
|
57
|
+
subject.new('en', 'us', q: q)
|
58
|
+
end.must_raise Language::OutOfRangeError
|
59
|
+
|
60
|
+
e.message.must_equal "q must be between 0 and 1"
|
61
|
+
end
|
62
|
+
|
63
|
+
subject.new('en', 'us', q: 1)
|
64
|
+
subject.new('en', 'gb', q: 0)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "raises an InvalidPrecisionError if q has more than a precision of 3" do
|
68
|
+
e = -> do
|
69
|
+
subject.new('en', 'us', q: 0.1234)
|
70
|
+
end.must_raise Language::InvalidPrecisionError
|
71
|
+
|
72
|
+
e.message.must_equal "q must be at most 3 decimal places"
|
73
|
+
|
74
|
+
subject.new('en', 'us', q: 0.123)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "converts to hash" do
|
78
|
+
subject.new('en', 'us').to_h.must_equal({
|
79
|
+
primary_tag: 'en',
|
80
|
+
subtag: 'us',
|
81
|
+
q: 1.0
|
82
|
+
})
|
83
|
+
end
|
84
|
+
|
85
|
+
it "convers to string" do
|
86
|
+
s = subject.new('en', 'us', q: 0.9).to_s
|
87
|
+
s.must_equal "en-us;q=0.9"
|
88
|
+
end
|
89
|
+
|
90
|
+
describe "parsing an accept header" do
|
91
|
+
it "returns a sorted array of media primary tags" do
|
92
|
+
subject.parse("en-*;q=0.2, en-us").must_equal [
|
93
|
+
Language.new('en', 'us'),
|
94
|
+
Language.new('en', '*', q: 0.2)
|
95
|
+
]
|
96
|
+
|
97
|
+
subject.parse("en-*, en-us, *;q=0.8").must_equal [
|
98
|
+
Language.new('en', 'us'),
|
99
|
+
Language.new('en', '*'),
|
100
|
+
Language.new('*', '*', q: 0.8)
|
101
|
+
]
|
102
|
+
end
|
103
|
+
|
104
|
+
it "sets media primary tag to */* when the accept header is empty" do
|
105
|
+
subject.parse('').must_equal [
|
106
|
+
Language.new('*', '*')
|
107
|
+
]
|
108
|
+
end
|
109
|
+
|
110
|
+
it "sets media primary tag to */* when the primary tag is only *" do
|
111
|
+
subject.parse('*').must_equal [
|
112
|
+
Language.new('*', '*')
|
113
|
+
]
|
114
|
+
end
|
115
|
+
|
116
|
+
it "defaults q to 1 if it's not explicitly specified" do
|
117
|
+
subject.parse("en-us").must_equal [
|
118
|
+
Language.new('en', 'plain', q: 1.0)
|
119
|
+
]
|
120
|
+
end
|
121
|
+
|
122
|
+
it "strips whitespace from between media primary tags" do
|
123
|
+
subject.parse("\ten-us\r,\nen-gb\s").must_equal [
|
124
|
+
Language.new('en', 'us'),
|
125
|
+
Language.new('en', 'gb')
|
126
|
+
]
|
127
|
+
end
|
128
|
+
|
129
|
+
it "strips whitespace around q" do
|
130
|
+
subject.parse("en-us;\tq\r=\n1, en-gb").must_equal [
|
131
|
+
Language.new('en', 'us'),
|
132
|
+
Language.new('en', 'gb')
|
133
|
+
]
|
134
|
+
end
|
135
|
+
|
136
|
+
it "has a q value of 0.001 when parsed q is invalid" do
|
137
|
+
subject.parse("en-us;q=x").must_equal [
|
138
|
+
Language.new('en', 'plain', q: 0.001)
|
139
|
+
]
|
140
|
+
end
|
141
|
+
|
142
|
+
it "skips invalid media primary tags" do
|
143
|
+
subject.parse("en-us, en-us-omg;q=0.9").must_equal [
|
144
|
+
Language.new('en', 'us', q: 1)
|
145
|
+
]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|