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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3e4d8ab6edced3cc033d6a693d51c7cbd9fee32d
4
- data.tar.gz: 2fe2cf946fe203666424c666fb9d00e1fb3d9eb0
3
+ metadata.gz: e40f6f21dfdede27f8ec870248f903caa24a8207
4
+ data.tar.gz: 2a4ce23b83c7a92cbb80fffe73fa81b2eb425fe1
5
5
  SHA512:
6
- metadata.gz: e9dc99f82ca6f752b2f6d562ad63c93aa8e32527a41910e0935d58137349d47b82a1ed2f69bb7b4c52f030281e551dba84a2dd3f49c5e93e1c890b2d77161002
7
- data.tar.gz: 92c33af011978e0dbbaa9d88d0c060368edb969dfea3e73e072754e4ae4d1c634643f6ef6a834b22c45ad46194be7cd313e073a57188b6ac74eff41d5d0a8487
6
+ metadata.gz: 5c1c66df7d5771a7a8aaf90a411d74c4e3e8ef7c0bb09e595d7898fbc62dcc42847d17bc8e0e79e3a76b97ddea9c9c2c23d18983c2a14c7f0e8db64c30f823cd
7
+ data.tar.gz: 7b6d214b3c465b5eccb5bb441cdecec1e5e0290596ecd81684d779b896cba6fcccc786ff8dc9394f35a020226e59227598fc315ea28ef52c9d6ab6da7f2660b7
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.2 / November 15, 2014
2
+
3
+ * Add `Negotiator`s which can parse and find the best match.
4
+
1
5
  ## 0.1 / November 14, 2014
2
6
 
3
- * Initial release
7
+ * Initial release.
data/Gemfile CHANGED
@@ -6,8 +6,8 @@ gemspec
6
6
  group :test do
7
7
  gem "ruby_gntp"
8
8
  gem "minitest-focus"
9
- gem "codeclimate-test-reporter"
10
- gem "simplecov"
9
+ gem "codeclimate-test-reporter", require: false
10
+ gem "simplecov", require: false
11
11
  end
12
12
 
13
13
  group :development, :test do
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
- `AcceptHeaders` can parse the Accept Header and return an array of `MediaType`s in descending order according to the spec, which takes into account `q` value, `type`/`subtype` and `params` specificity.
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.parse("text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5")
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
- Will generate this equivalent array:
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
- Parsing `Charset`:
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
- AcceptHeaders::Charset.parse("us-ascii; q=0.5, iso-8859-1, utf-8; q=0.8, macintosh")
68
+ media_type.negotiate('text/html')
69
+
70
+ # Returns:
71
+
72
+ AcceptHeaders::MediaType.new('text', 'html', params: { 'level' => '1' })
65
73
  ```
66
74
 
67
- generates:
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
- Parsing `Encoding`:
94
+ `#negotiate`:
79
95
 
80
96
  ```ruby
81
- AcceptHeaders::Encoding.parse("deflate; q=0.5, gzip, compress; q=0.8, identity")
97
+ charsets.negotiate('iso-8859-1')
98
+
99
+ # Returns:
100
+
101
+ AcceptHeaders::Charset.new('iso-8859-1')
82
102
  ```
83
103
 
84
- generates:
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
- Parsing `Language`:
123
+ `#negotiate`:
96
124
 
97
125
  ```ruby
98
- AcceptHeaders::Language.parse("en-*, en-us, *;q=0.8")
126
+ encodings.negotiate('identity')
127
+
128
+ # Returns:
129
+
130
+ AcceptHeaders::Encoding.new('identity')
99
131
  ```
100
132
 
101
- generates:
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 )
@@ -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{A ruby library that parses and sorts http accept headers. Adheres to RFC 2616.}
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
 
@@ -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
- TOKEN_PATTERN = /^\s*(?<token>[\w!#$%^&*\-\+{}\\|'.`~]+)\s*$/
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 OutOfRangeError.new("q must be between 0 and 1")
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 InvalidPrecisionError.new("q must be at most 3 decimal places")
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 self.parse(original_header)
38
- header = original_header.dup
39
- header.sub!(/\AAccept-Charset:\s*/, '')
40
- header.strip!
41
- return [Charset.new('iso-8859-5', q: 1)] if header.empty?
42
- charsets = []
43
- header.split(',').each do |entry|
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 self.parse(original_header)
38
- header = original_header.dup
39
- header.sub!(/\AAccept-Encoding:\s*/, '')
40
- header.strip!
41
- return [Charset.new] if header.empty?
42
- encodings = []
43
- header.split(',').each do |entry|
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
- LANGUAGE_PATTERN = /^\s*(?<primary_tag>[\w]{1,8}|\*)(?:\s*\-\s*(?<subtag>[\w]{1,8}|\*))?\s*$/
58
-
59
- def self.parse(original_header)
60
- header = original_header.dup
61
- header.sub!(/\AAccept-Language:\s*/, '')
62
- header.strip!
63
- return [Language.new] if header.empty?
64
- languages = []
65
- header.split(',').each do |entry|
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