accept_headers 0.0.1 → 0.0.2

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,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