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.
@@ -0,0 +1,37 @@
1
+ require "accept_headers/language"
2
+ require "accept_headers/negotiatable"
3
+
4
+ module AcceptHeaders
5
+ class Language
6
+ class Negotiator
7
+ include Negotiatable
8
+
9
+ LANGUAGE_PATTERN = /^\s*(?<primary_tag>[\w]{1,8}|\*)(?:\s*\-\s*(?<subtag>[\w]{1,8}|\*))?\s*$/
10
+
11
+ private
12
+ def parse(original_header)
13
+ header = original_header.dup
14
+ header.sub!(/\AAccept-Language:\s*/, '')
15
+ header.strip!
16
+ return [Language.new] if header.empty?
17
+ languages = []
18
+ header.split(',').each do |entry|
19
+ language_arr = entry.split(';', 2)
20
+ next if language_arr[0].nil?
21
+ language_range = LANGUAGE_PATTERN.match(language_arr[0])
22
+ next if language_range.nil?
23
+ begin
24
+ languages << Language.new(
25
+ language_range[:primary_tag],
26
+ language_range[:subtag],
27
+ q: parse_q(language_arr[1])
28
+ )
29
+ rescue Error
30
+ next
31
+ end
32
+ end
33
+ languages.sort! { |x,y| y <=> x }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -70,45 +70,16 @@ module AcceptHeaders
70
70
  string
71
71
  end
72
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
73
+ def match(other)
74
+ if type == other.type && subtype == other.subtype
75
+ true
76
+ elsif type == other.type && subtype == '*'
77
+ true
78
+ elsif other.type == '*' && other.subtype == '*'
79
+ true
80
+ else
81
+ false
109
82
  end
110
- params.delete('q')
111
- params
112
83
  end
113
84
  end
114
85
  end
@@ -0,0 +1,50 @@
1
+ require "accept_headers/media_type"
2
+ require "accept_headers/negotiatable"
3
+
4
+ module AcceptHeaders
5
+ class MediaType
6
+ class Negotiator
7
+ include Negotiatable
8
+
9
+ private
10
+ MEDIA_TYPE_PATTERN = /^\s*(?<type>[\w!#$%^&*\-\+{}\\|'.`~]+)(?:\s*\/\s*(?<subtype>[\w!#$%^&*\-\+{}\\|'.`~]+))?\s*$/
11
+ PARAM_PATTERN = /(?<attribute>[\w!#$%^&*\-\+{}\\|'.`~]+)\s*\=\s*(?:\"(?<value>[^"]*)\"|\'(?<value>[^']*)\'|(?<value>[\w!#$%^&*\-\+{}\\|\'.`~]*))/
12
+
13
+ def parse(original_header)
14
+ header = original_header.dup
15
+ header.sub!(/\AAccept:\s*/, '')
16
+ header.strip!
17
+ return [MediaType.new] if header.empty?
18
+ media_types = []
19
+ header.split(',').each do |entry|
20
+ accept_media_range, accept_params = entry.split(';', 2)
21
+ next if accept_media_range.nil?
22
+ media_range = MEDIA_TYPE_PATTERN.match(accept_media_range)
23
+ next if media_range.nil?
24
+ begin
25
+ media_types << MediaType.new(
26
+ media_range[:type],
27
+ media_range[:subtype],
28
+ q: parse_q(accept_params),
29
+ params: parse_params(accept_params)
30
+ )
31
+ rescue Error
32
+ next
33
+ end
34
+ end
35
+ media_types.sort! { |x,y| y <=> x }
36
+ end
37
+
38
+ def parse_params(params_string)
39
+ params = {}
40
+ return params if !params_string || params_string.empty?
41
+ params_string.split(';').each do |part|
42
+ param = PARAM_PATTERN.match(part)
43
+ params[param[:attribute]] = param[:value] if param
44
+ end
45
+ params.delete('q')
46
+ params
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,54 @@
1
+ module AcceptHeaders
2
+ module Negotiatable
3
+ class Error < StandardError; end
4
+ class ParseError < Error; end
5
+
6
+ TOKEN_PATTERN = /^\s*(?<token>[\w!#$%^&*\-\+{}\\|'.`~]+)\s*$/
7
+ Q_PATTERN = /(?:\A|;)\s*(?<exists>qs*\=)\s*(?:(?<q>0\.\d{1,3}|[01])|(?:[^;]*))\s*(?:\z|;)/
8
+
9
+ attr_reader :list
10
+
11
+ def initialize(header)
12
+ @list = parse(header)
13
+ end
14
+
15
+ def negotiate(supported_string)
16
+ supported = parse(supported_string)
17
+ return nil if @list.empty?
18
+ rejects, acceptable = @list.partition { |m| m.q == 0.0 }
19
+ rejects.each do |reject|
20
+ supported.each do |support|
21
+ if support.match(reject)
22
+ return nil
23
+ end
24
+ end
25
+ end
26
+ acceptable.sort { |x,y| y <=> x }.each do |accepted|
27
+ supported.each do |support|
28
+ if support.match(accepted)
29
+ return accepted
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+ def parse(header)
37
+ raise NotImplementedError.new("#parse(header) is not implemented")
38
+ end
39
+
40
+ def parse_q(header)
41
+ q = 1
42
+ return q unless header
43
+ q_match = Q_PATTERN.match(header)
44
+ if q_match && q_match[:exists]
45
+ if q_match[:q]
46
+ q = q_match[:q]
47
+ else
48
+ q = 0.001
49
+ end
50
+ end
51
+ q
52
+ end
53
+ end
54
+ end
@@ -1,3 +1,3 @@
1
1
  module AcceptHeaders
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -0,0 +1,73 @@
1
+ require_relative "../spec_helper"
2
+
3
+ module AcceptHeaders
4
+ class Charset
5
+ describe Negotiator do
6
+ subject do
7
+ AcceptHeaders::Charset::Negotiator
8
+ end
9
+
10
+ describe "parsing an accept header" do
11
+ it "returns a sorted array of charsets" do
12
+ subject.new("*; q=0.2, unicode-1-1").list.must_equal [
13
+ Charset.new('unicode-1-1'),
14
+ Charset.new('*', q: 0.2)
15
+ ]
16
+
17
+ subject.new("us-ascii; q=0.5, iso-8859-1, utf-8; q=0.8, macintosh").list.must_equal [
18
+ Charset.new('iso-8859-1'),
19
+ Charset.new('macintosh'),
20
+ Charset.new('utf-8', q: 0.8),
21
+ Charset.new('us-ascii', q: 0.5)
22
+ ]
23
+ end
24
+
25
+ it "sets charset to * when the accept-charset header is empty" do
26
+ subject.new('').list.must_equal [
27
+ Charset.new('*')
28
+ ]
29
+ end
30
+
31
+ it "defaults q to 1 if it's not explicitly specified" do
32
+ subject.new("iso-8859-1").list.must_equal [
33
+ Charset.new('iso-8859-1', q: 1.0)
34
+ ]
35
+ end
36
+
37
+ it "strips whitespace from between charsets" do
38
+ subject.new("\tunicode-1-1\r,\niso-8859-1\s").list.must_equal [
39
+ Charset.new('unicode-1-1'),
40
+ Charset.new('iso-8859-1')
41
+ ]
42
+ end
43
+
44
+ it "strips whitespace around q" do
45
+ subject.new("iso-8859-1;\tq\r=\n1, unicode-1-1;q=0.8\n").list.must_equal [
46
+ Charset.new('iso-8859-1'),
47
+ Charset.new('unicode-1-1', q: 0.8)
48
+ ]
49
+ end
50
+
51
+ it "has a q value of 0.001 when parsed q is invalid" do
52
+ subject.new("iso-8859-1;q=x").list.must_equal [
53
+ Charset.new('iso-8859-1', q: 0.001)
54
+ ]
55
+ end
56
+
57
+ it "skips invalid character sets" do
58
+ subject.new("iso-8859-1, @unicode-1-1").list.must_equal [
59
+ Charset.new('iso-8859-1', q: 1)
60
+ ]
61
+ end
62
+ end
63
+
64
+ describe "negotiate supported charsets" do
65
+ it "returns a best matching charset" do
66
+ match = Charset.new('iso-8859-1')
67
+ n = subject.new('us-ascii; q=0.5, iso-8859-1, utf-8; q=0.8, macintosh')
68
+ n.negotiate('iso-8859-1').must_equal match
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
data/spec/charset_spec.rb CHANGED
@@ -34,11 +34,11 @@ module AcceptHeaders
34
34
  subject.new('iso-8859-1', q: '1')
35
35
  end
36
36
 
37
- it "raises an OutOfRangeError unless q value is between 0 and 1" do
37
+ it "raises an InvalidQError unless q value is between 0 and 1" do
38
38
  [-1.0, -0.1, 1.1].each do |q|
39
39
  e = -> do
40
40
  subject.new('iso-8859-1', q: q)
41
- end.must_raise Charset::OutOfRangeError
41
+ end.must_raise Charset::InvalidQError
42
42
 
43
43
  e.message.must_equal "q must be between 0 and 1"
44
44
  end
@@ -47,10 +47,10 @@ module AcceptHeaders
47
47
  subject.new('unicode-1-1', q: 0)
48
48
  end
49
49
 
50
- it "raises an InvalidPrecisionError if q has more than a precision of 3" do
50
+ it "raises an InvalidQError if q has more than a precision of 3" do
51
51
  e = -> do
52
52
  subject.new('iso-8859-1', q: 0.1234)
53
- end.must_raise Charset::InvalidPrecisionError
53
+ end.must_raise Charset::InvalidQError
54
54
 
55
55
  e.message.must_equal "q must be at most 3 decimal places"
56
56
 
@@ -68,59 +68,5 @@ module AcceptHeaders
68
68
  s = subject.new('iso-8859-1', q: 0.9).to_s
69
69
  s.must_equal "iso-8859-1;q=0.9"
70
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
71
  end
126
72
  end
@@ -0,0 +1,73 @@
1
+ require_relative "../spec_helper"
2
+
3
+ module AcceptHeaders
4
+ class Encoding
5
+ describe Negotiator do
6
+ subject do
7
+ AcceptHeaders::Encoding::Negotiator
8
+ end
9
+
10
+ describe "parsing an accept header" do
11
+ it "returns a sorted array of encodings" do
12
+ subject.new("*; q=0.2, compress").list.must_equal [
13
+ Encoding.new('compress'),
14
+ Encoding.new('*', q: 0.2)
15
+ ]
16
+
17
+ subject.new("deflate; q=0.5, gzip, compress; q=0.8, identity").list.must_equal [
18
+ Encoding.new('gzip'),
19
+ Encoding.new('identity'),
20
+ Encoding.new('compress', q: 0.8),
21
+ Encoding.new('deflate', q: 0.5)
22
+ ]
23
+ end
24
+
25
+ it "sets encoding to * when the accept-encoding header is empty" do
26
+ subject.new('').list.must_equal [
27
+ Encoding.new('*')
28
+ ]
29
+ end
30
+
31
+ it "defaults q to 1 if it's not explicitly specified" do
32
+ subject.new("gzip").list.must_equal [
33
+ Encoding.new('gzip', q: 1.0)
34
+ ]
35
+ end
36
+
37
+ it "strips whitespace from between encodings" do
38
+ subject.new("\tcompress\r,\ngzip\s").list.must_equal [
39
+ Encoding.new('compress'),
40
+ Encoding.new('gzip')
41
+ ]
42
+ end
43
+
44
+ it "strips whitespace around q" do
45
+ subject.new("gzip;\tq\r=\n1, compress;q=0.8\n").list.must_equal [
46
+ Encoding.new('gzip'),
47
+ Encoding.new('compress', q: 0.8)
48
+ ]
49
+ end
50
+
51
+ it "has a q value of 0.001 when parsed q is invalid" do
52
+ subject.new("gzip;q=x").list.must_equal [
53
+ Encoding.new('gzip', q: 0.001)
54
+ ]
55
+ end
56
+
57
+ it "skips invalid encodings" do
58
+ subject.new("gzip, @blah").list.must_equal [
59
+ Encoding.new('gzip', q: 1.0)
60
+ ]
61
+ end
62
+ end
63
+
64
+ describe "negotiate supported encodings" do
65
+ it "returns a best matching encoding" do
66
+ match =
67
+ n = subject.new("deflate; q=0.5, gzip, compress; q=0.8, identity")
68
+ n.negotiate("identity").must_equal Encoding.new('identity')
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -34,11 +34,11 @@ module AcceptHeaders
34
34
  subject.new('gzip', q: '1')
35
35
  end
36
36
 
37
- it "raises an OutOfRangeError unless q value is between 0 and 1" do
37
+ it "raises an InvalidQError unless q value is between 0 and 1" do
38
38
  [-1.0, -0.1, 1.1].each do |q|
39
39
  e = -> do
40
40
  subject.new('gzip', q: q)
41
- end.must_raise Encoding::OutOfRangeError
41
+ end.must_raise Encoding::InvalidQError
42
42
 
43
43
  e.message.must_equal "q must be between 0 and 1"
44
44
  end
@@ -47,10 +47,10 @@ module AcceptHeaders
47
47
  subject.new('compress', q: 0)
48
48
  end
49
49
 
50
- it "raises an InvalidPrecisionError if q has more than a precision of 3" do
50
+ it "raises an InvalidQError if q has more than a precision of 3" do
51
51
  e = -> do
52
52
  subject.new('gzip', q: 0.1234)
53
- end.must_raise Encoding::InvalidPrecisionError
53
+ end.must_raise Encoding::InvalidQError
54
54
 
55
55
  e.message.must_equal "q must be at most 3 decimal places"
56
56
 
@@ -68,59 +68,5 @@ module AcceptHeaders
68
68
  s = subject.new('gzip', q: 0.9).to_s
69
69
  s.must_equal "gzip;q=0.9"
70
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
71
  end
126
72
  end