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.
@@ -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,3 @@
1
+ module AcceptHeaders
2
+ VERSION = "0.0.1"
3
+ 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