rack-accept_headers 0.5.0
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 +18 -0
- data/.travis.yml +15 -0
- data/CHANGELOG.md +35 -0
- data/Gemfile +4 -0
- data/Guardfile +14 -0
- data/LICENSE.txt +22 -0
- data/README.md +198 -0
- data/Rakefile +10 -0
- data/lib/rack/accept_headers/charset.rb +36 -0
- data/lib/rack/accept_headers/context.rb +67 -0
- data/lib/rack/accept_headers/encoding.rb +36 -0
- data/lib/rack/accept_headers/header.rb +149 -0
- data/lib/rack/accept_headers/language.rb +36 -0
- data/lib/rack/accept_headers/media_type.rb +73 -0
- data/lib/rack/accept_headers/request.rb +90 -0
- data/lib/rack/accept_headers/response.rb +18 -0
- data/lib/rack/accept_headers/version.rb +5 -0
- data/lib/rack/accept_headers.rb +16 -0
- data/rack-accept_headers.gemspec +27 -0
- data/test/charset_test.rb +43 -0
- data/test/context_test.rb +57 -0
- data/test/encoding_test.rb +29 -0
- data/test/header_test.rb +69 -0
- data/test/language_test.rb +47 -0
- data/test/media_type_test.rb +80 -0
- data/test/request_test.rb +60 -0
- data/test/test_helper.rb +26 -0
- metadata +166 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4fe24376b339f7d18f11034851b747619af22f17
|
4
|
+
data.tar.gz: d3b43e1effbf379bec2ef4a2256721bb3bbe2590
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: adad5ec925d71f611c8ae25d88d18a98c2dbbbdc063c8134132c4e3b82c951a51ad79245d9e88d6e093ab65ee28aabd0d177f201ba84cd8259516e8b937a337c
|
7
|
+
data.tar.gz: 99110a7283ce5e03ad5d5902608cb7a4cea499a20a0dbcae4dea528896b513f71f60d482778bd193d5c50d3ac1c5909d06a97f284df6b5792d44e6d1fa35d779
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
## 0.5.x
|
2
|
+
|
3
|
+
* Add accept-extension parameter support
|
4
|
+
* Add Guardfile
|
5
|
+
* Add Gemfile
|
6
|
+
* Switch from Test::Unit to Minitest
|
7
|
+
* Added travis-ci support
|
8
|
+
|
9
|
+
## 0.4.3 / July 29, 2010
|
10
|
+
|
11
|
+
* Added support for Ruby 1.9.2
|
12
|
+
|
13
|
+
## 0.4 / April 5, 2010
|
14
|
+
|
15
|
+
* Added support for media type queries with multiple range parameters
|
16
|
+
* Changed Rack::AcceptHeaders::Header#sort method to return only single values
|
17
|
+
and moved previous functionality to Rack::AcceptHeaders::Header#sort_with_qvalues
|
18
|
+
|
19
|
+
## 0.3 / April 3, 2010
|
20
|
+
|
21
|
+
* Enhanced Rack middleware component to be able to automatically send a 406
|
22
|
+
response when the request is not acceptable
|
23
|
+
|
24
|
+
## 0.2 / April 2, 2010
|
25
|
+
|
26
|
+
* Moved all common header methods into Rack::AcceptHeaders::Header module
|
27
|
+
* Many improvements to the documentation
|
28
|
+
|
29
|
+
## 0.1.1 / April 1, 2010
|
30
|
+
|
31
|
+
* Whoops, forgot to require Rack. :]
|
32
|
+
|
33
|
+
## 0.1 / April 1, 2010
|
34
|
+
|
35
|
+
* Initial release
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard :minitest do
|
5
|
+
# with Minitest::Unit
|
6
|
+
watch(%r{^test/(.*)\/?test_(.*)\.rb})
|
7
|
+
watch(%r{^lib/rack-accept_headers/(.*/)?([^/]+)\.rb}) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
|
8
|
+
watch(%r{^test/test_helper\.rb}) { 'test' }
|
9
|
+
|
10
|
+
# with Minitest::Spec
|
11
|
+
watch(%r{^spec/(.*)_spec\.rb})
|
12
|
+
watch(%r{^lib/rack-accept_headers/(.+)\.rb}) { |m| "spec/#{m[1]}_spec.rb" }
|
13
|
+
watch(%r{^spec/spec_helper\.rb}) { 'spec' }
|
14
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Michael Jackson
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
[](https://travis-ci.org/kamui/rack-accept_headers)
|
2
|
+
|
3
|
+
**Rack::AcceptHeaders** is a suite of tools for Ruby/Rack applications that eases the
|
4
|
+
complexity of building and interpreting the Accept* family of [HTTP request headers][rfc].
|
5
|
+
|
6
|
+
Some features of the library are:
|
7
|
+
|
8
|
+
* Strict adherence to [RFC 2616][rfc], specifically [section 14][rfc-sec14]
|
9
|
+
* Full support for the [Accept][rfc-sec14-1], [Accept-Charset][rfc-sec14-2],
|
10
|
+
[Accept-Encoding][rfc-sec14-3], and [Accept-Language][rfc-sec14-4] HTTP
|
11
|
+
request headers
|
12
|
+
* May be used as [Rack][rack] middleware or standalone
|
13
|
+
* A comprehensive [test suite][test] that covers many edge cases
|
14
|
+
|
15
|
+
[rfc]: http://www.w3.org/Protocols/rfc2616/rfc2616.html
|
16
|
+
[rfc-sec14]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
17
|
+
[rfc-sec14-1]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
|
18
|
+
[rfc-sec14-2]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.2
|
19
|
+
[rfc-sec14-3]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3
|
20
|
+
[rfc-sec14-4]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
|
21
|
+
[rack]: http://rack.rubyforge.org/
|
22
|
+
[test]: http://github.com/kamui/rack-accept_headers/tree/master/test/
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
Add this line to your application's Gemfile:
|
27
|
+
|
28
|
+
gem 'rack-accept_headers'
|
29
|
+
|
30
|
+
And then execute:
|
31
|
+
|
32
|
+
$ bundle
|
33
|
+
|
34
|
+
Or install it yourself as:
|
35
|
+
|
36
|
+
$ gem install rack-accept_headers
|
37
|
+
|
38
|
+
Or install it from a local copy:
|
39
|
+
|
40
|
+
$ git clone git://github.com/kamui/rack-accept_headers.git
|
41
|
+
$ cd rack-accept_headers
|
42
|
+
$ rake package
|
43
|
+
$ rake install
|
44
|
+
|
45
|
+
## Usage
|
46
|
+
|
47
|
+
**Rack::AcceptHeaders** implements the Rack middleware interface and may be used with any
|
48
|
+
Rack-based application. Simply insert the `Rack::AcceptHeaders` module in your Rack
|
49
|
+
middleware pipeline and access the `Rack::AcceptHeaders::Request` object in the
|
50
|
+
`rack-accept_headers.request` environment key, as in the following example.
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
require 'rack/accept_headers'
|
54
|
+
|
55
|
+
use Rack::AcceptHeaders
|
56
|
+
|
57
|
+
app = lambda do |env|
|
58
|
+
accept = env['rack-accept_headers.request']
|
59
|
+
response = Rack::Response.new
|
60
|
+
|
61
|
+
if accept.media_type?('text/html')
|
62
|
+
response['Content-Type'] = 'text/html'
|
63
|
+
response.write "<p>Hello. You accept text/html!</p>"
|
64
|
+
else
|
65
|
+
response['Content-Type'] = 'text/plain'
|
66
|
+
response.write "Apparently you don't accept text/html. Too bad."
|
67
|
+
end
|
68
|
+
|
69
|
+
response.finish
|
70
|
+
end
|
71
|
+
|
72
|
+
run app
|
73
|
+
```
|
74
|
+
|
75
|
+
**Rack::AcceptHeaders** can also construct automatic [406][406] responses if you set up
|
76
|
+
the types of media, character sets, encoding, or languages your server is able
|
77
|
+
to serve ahead of time. If you pass a configuration block to your `use`
|
78
|
+
statement it will yield the `Rack::AcceptHeaders::Context` object that is used for that
|
79
|
+
invocation.
|
80
|
+
|
81
|
+
[406]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.7
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
require 'rack/accept_headers'
|
85
|
+
|
86
|
+
use(Rack::AcceptHeaders) do |context|
|
87
|
+
# We only ever serve content in English or Japanese from this site, so if
|
88
|
+
# the user doesn't accept either of these we will respond with a 406.
|
89
|
+
context.languages = %w< en jp >
|
90
|
+
end
|
91
|
+
|
92
|
+
app = ...
|
93
|
+
|
94
|
+
run app
|
95
|
+
```
|
96
|
+
|
97
|
+
**Note:** _You should think carefully before using Rack::AcceptHeaders in this way.
|
98
|
+
Many user agents are careless about the types of Accept headers they send, and
|
99
|
+
depend on apps not being too picky. Instead of automatically sending a 406, you
|
100
|
+
should probably only send one when absolutely necessary._
|
101
|
+
|
102
|
+
**Rack::AcceptHeaders** supports accept-extension parameter support. Here's an
|
103
|
+
example:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
require 'rack/accept_headers'
|
107
|
+
|
108
|
+
use Rack::AcceptHeaders
|
109
|
+
|
110
|
+
app = lambda do |env|
|
111
|
+
SUPPORTED_MEDIA_TYPES = {
|
112
|
+
"text/html" => :html
|
113
|
+
"application/json" => :json,
|
114
|
+
"text/xml" => :xml
|
115
|
+
}
|
116
|
+
|
117
|
+
accept = env['rack-accept_headers.request']
|
118
|
+
response = Rack::Response.new
|
119
|
+
|
120
|
+
if accept
|
121
|
+
media_type = accept.media_type.best_of(SUPPORTED_MEDIA_TYPES.keys)
|
122
|
+
|
123
|
+
# Here, I would return a 415 Unsupported media type, if media_type is nil
|
124
|
+
# The unsupported_media_type call is left unimplemented here
|
125
|
+
# unsupported_media_type unless media_type
|
126
|
+
|
127
|
+
# To output the media_type symbol
|
128
|
+
# puts SUPPORTED_MEDIA_TYPES[media_type]
|
129
|
+
|
130
|
+
# To return a hash of accept-extension params for the given media type
|
131
|
+
# puts accept.media_type.params(media_type)
|
132
|
+
|
133
|
+
response['Content-Type'] = media_type
|
134
|
+
response.write %Q{{ "message" : "Hello. You accept #{media_type}" }}
|
135
|
+
else
|
136
|
+
media_type = "*/*"
|
137
|
+
response['Content-Type'] = SUPPORTED_MEDIA_TYPES.keys.first
|
138
|
+
response.write "Defaulting to #{response['Content-Type']}."
|
139
|
+
end
|
140
|
+
|
141
|
+
response.finish
|
142
|
+
end
|
143
|
+
|
144
|
+
run app
|
145
|
+
```
|
146
|
+
|
147
|
+
So, given this `Accept` header:
|
148
|
+
|
149
|
+
```
|
150
|
+
Accept: application/json;version=1.0;q=0.1
|
151
|
+
```
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
accept = env['rack-accept_headers.request']
|
155
|
+
params = accept.media_type.params['application/json')
|
156
|
+
```
|
157
|
+
|
158
|
+
The `params` hash will end up with this value:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
{
|
162
|
+
"application/json" : {
|
163
|
+
"q" : 0.1,
|
164
|
+
"version" : "1.0"
|
165
|
+
}
|
166
|
+
}
|
167
|
+
```
|
168
|
+
|
169
|
+
Additionally, **Rack::AcceptHeaders** may be used outside of a Rack context to provide
|
170
|
+
any Ruby app the ability to construct and interpret Accept headers.
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
require 'rack/accept_headers'
|
174
|
+
|
175
|
+
mtype = Rack::AcceptHeaders::MediaType.new
|
176
|
+
mtype.qvalues = { 'text/html' => 1, 'text/*' => 0.8, '*/*' => 0.5 }
|
177
|
+
mtype.to_s # => "Accept: text/html, text/*;q=0.8, */*;q=0.5"
|
178
|
+
|
179
|
+
cset = Rack::AcceptHeaders::Charset.new('unicode-1-1, iso-8859-5;q=0.8')
|
180
|
+
cset.best_of(%w< iso-8859-5 unicode-1-1 >) # => "unicode-1-1"
|
181
|
+
cset.accept?('iso-8859-1') # => true
|
182
|
+
```
|
183
|
+
|
184
|
+
The very last line in this example may look like a mistake to someone not
|
185
|
+
familiar with the intricacies of [the spec][rfc-sec14-3], but it's actually
|
186
|
+
correct. It just puts emphasis on the convenience of using this library so you
|
187
|
+
don't have to worry about these kinds of details.
|
188
|
+
|
189
|
+
## Four-letter Words
|
190
|
+
|
191
|
+
- Spec: [http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html][rfc-sec14]
|
192
|
+
- Code: [http://github.com/kamui/rack-accept_headers][code]
|
193
|
+
- Bugs: [http://github.com/kamui/rack-accept_headers/issues][bugs]
|
194
|
+
- Docs: [http://rdoc.info/github/kamui/rack-accept_headers][docs]
|
195
|
+
|
196
|
+
[code]: http://github.com/kamui/rack-accept_headers
|
197
|
+
[bugs]: http://github.com/kamui/rack-accept_headers/issues
|
198
|
+
[docs]: http://rdoc.info/github/kamui/rack-accept_headers
|
data/Rakefile
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Rack::AcceptHeaders
|
2
|
+
# Represents an HTTP Accept-Charset header according to the HTTP 1.1
|
3
|
+
# specification, and provides several convenience methods for determining
|
4
|
+
# acceptable character sets.
|
5
|
+
#
|
6
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.2
|
7
|
+
class Charset
|
8
|
+
include Header
|
9
|
+
|
10
|
+
# The name of this header.
|
11
|
+
def name
|
12
|
+
'Accept-Charset'
|
13
|
+
end
|
14
|
+
|
15
|
+
# Determines the quality factor (qvalue) of the given +charset+.
|
16
|
+
def qvalue(charset)
|
17
|
+
m = matches(charset)
|
18
|
+
if m.empty?
|
19
|
+
charset == 'iso-8859-1' ? 1 : 0
|
20
|
+
else
|
21
|
+
normalize_qvalue(@qvalues[m.first])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns an array of character sets from this header that match the given
|
26
|
+
# +charset+, ordered by precedence.
|
27
|
+
def matches(charset)
|
28
|
+
values.select {|v|
|
29
|
+
v == charset || v == '*'
|
30
|
+
}.sort {|a, b|
|
31
|
+
# "*" gets least precedence, any others should be equal.
|
32
|
+
a == '*' ? 1 : (b == '*' ? -1 : 0)
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Rack::AcceptHeaders
|
2
|
+
# Implements the Rack middleware interface.
|
3
|
+
class Context
|
4
|
+
# This error is raised when the server is not able to provide an acceptable
|
5
|
+
# response.
|
6
|
+
class AcceptError < StandardError; end
|
7
|
+
|
8
|
+
attr_reader :app
|
9
|
+
|
10
|
+
def initialize(app)
|
11
|
+
@app = app
|
12
|
+
@checks = {}
|
13
|
+
@check_headers = []
|
14
|
+
yield self if block_given?
|
15
|
+
end
|
16
|
+
|
17
|
+
# Inserts a new Rack::AcceptHeaders::Request object into the environment before
|
18
|
+
# handing the request to the app immediately downstream.
|
19
|
+
def call(env)
|
20
|
+
request = env['rack-accept_headers.request'] ||= Request.new(env)
|
21
|
+
check!(request) unless @checks.empty?
|
22
|
+
@app.call(env)
|
23
|
+
rescue AcceptError
|
24
|
+
response = Response.new
|
25
|
+
response.not_acceptable!
|
26
|
+
response.finish
|
27
|
+
end
|
28
|
+
|
29
|
+
# Defines the types of media this server is able to serve.
|
30
|
+
def media_types=(media_types)
|
31
|
+
add_check(:media_type, media_types)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Defines the character sets this server is able to serve.
|
35
|
+
def charsets=(charsets)
|
36
|
+
add_check(:charset, charsets)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Defines the types of encodings this server is able to serve.
|
40
|
+
def encodings=(encodings)
|
41
|
+
add_check(:encoding, encodings)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Defines the languages this server is able to serve.
|
45
|
+
def languages=(languages)
|
46
|
+
add_check(:language, languages)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def add_check(header_name, values)
|
52
|
+
@checks[header_name] ||= []
|
53
|
+
@checks[header_name].concat(values)
|
54
|
+
@check_headers << header_name unless @check_headers.include?(header_name)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Raises an AcceptError if this server is not able to serve an acceptable
|
58
|
+
# response.
|
59
|
+
def check!(request)
|
60
|
+
@check_headers.each do |header_name|
|
61
|
+
values = @checks[header_name]
|
62
|
+
header = request.send(header_name)
|
63
|
+
raise AcceptError unless values.any? {|v| header.accept?(v) }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Rack::AcceptHeaders
|
2
|
+
# Represents an HTTP Accept-Encoding header according to the HTTP 1.1
|
3
|
+
# specification, and provides several convenience methods for determining
|
4
|
+
# acceptable content encodings.
|
5
|
+
#
|
6
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3
|
7
|
+
class Encoding
|
8
|
+
include Header
|
9
|
+
|
10
|
+
# The name of this header.
|
11
|
+
def name
|
12
|
+
'Accept-Encoding'
|
13
|
+
end
|
14
|
+
|
15
|
+
# Determines the quality factor (qvalue) of the given +encoding+.
|
16
|
+
def qvalue(encoding)
|
17
|
+
m = matches(encoding)
|
18
|
+
if m.empty?
|
19
|
+
encoding == 'identity' ? 1 : 0
|
20
|
+
else
|
21
|
+
normalize_qvalue(@qvalues[m.first])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns an array of encodings from this header that match the given
|
26
|
+
# +encoding+, ordered by precedence.
|
27
|
+
def matches(encoding)
|
28
|
+
values.select {|v|
|
29
|
+
v == encoding || v == '*'
|
30
|
+
}.sort {|a, b|
|
31
|
+
# "*" gets least precedence, any others should be equal.
|
32
|
+
a == '*' ? 1 : (b == '*' ? -1 : 0)
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module Rack::AcceptHeaders
|
2
|
+
# Contains methods that are useful for working with Accept-style HTTP
|
3
|
+
# headers. The MediaType, Charset, Encoding, and Language classes all mixin
|
4
|
+
# this module.
|
5
|
+
module Header
|
6
|
+
class InvalidHeader < StandardError; end
|
7
|
+
|
8
|
+
# Parses the value of an Accept-style request header into a hash of
|
9
|
+
# acceptable values and their respective quality factors (qvalues). The
|
10
|
+
# +join+ method may be used on the resulting hash to obtain a header
|
11
|
+
# string that is the semantic equivalent of the one provided.
|
12
|
+
def parse(header)
|
13
|
+
qvalues = {}
|
14
|
+
|
15
|
+
header.to_s.split(',').each do |part|
|
16
|
+
m = /^\s*([^\s,]+?)(?:\s*;\s*q\s*=\s*(\d+(?:\.\d+)?))?$/.match(part)
|
17
|
+
|
18
|
+
if m
|
19
|
+
qvalues[m[1].downcase] = normalize_qvalue((m[2] || 1).to_f)
|
20
|
+
else
|
21
|
+
raise InvalidHeader, "Invalid header value: #{part.inspect}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
qvalues
|
26
|
+
end
|
27
|
+
module_function :parse
|
28
|
+
|
29
|
+
# Returns a string suitable for use as the value of an Accept-style HTTP
|
30
|
+
# header from the map of acceptable values to their respective quality
|
31
|
+
# factors (qvalues). The +parse+ method may be used on the resulting string
|
32
|
+
# to obtain a hash that is the equivalent of the one provided.
|
33
|
+
def join(qvalues)
|
34
|
+
qvalues.map {|k, v| k + (v == 1 ? '' : ";q=#{v}") }.join(', ')
|
35
|
+
end
|
36
|
+
module_function :join
|
37
|
+
|
38
|
+
# Parses a media type string into its relevant pieces. The return value
|
39
|
+
# will be an array with three values: 1) the content type, 2) the content
|
40
|
+
# subtype, and 3) the media type parameters. An empty array is returned if
|
41
|
+
# no match can be made.
|
42
|
+
def parse_media_type(media_type)
|
43
|
+
m = media_type.to_s.match(/^\s*([a-zA-Z*]+)\s*\/\s*([a-zA-Z0-9*\-.+]+)\s*(?:;(.+))?$/)
|
44
|
+
m ? [m[1].downcase, m[2].downcase, m[3] || ''] : []
|
45
|
+
end
|
46
|
+
module_function :parse_media_type
|
47
|
+
|
48
|
+
# Parses a string of media type range parameters into a hash of parameters
|
49
|
+
# to their respective values.
|
50
|
+
def parse_range_params(params)
|
51
|
+
params.split(';').inject({'q' => '1'}) do |m, p|
|
52
|
+
k, v = p.split('=', 2)
|
53
|
+
m[k.strip] = v.strip if v
|
54
|
+
m
|
55
|
+
end
|
56
|
+
end
|
57
|
+
module_function :parse_range_params
|
58
|
+
|
59
|
+
# Converts 1.0 and 0.0 qvalues to 1 and 0 respectively. Used to maintain
|
60
|
+
# consistency across qvalue methods.
|
61
|
+
def normalize_qvalue(q)
|
62
|
+
(q == 1 || q == 0) && q.is_a?(Float) ? q.to_i : q
|
63
|
+
end
|
64
|
+
module_function :normalize_qvalue
|
65
|
+
|
66
|
+
module PublicInstanceMethods
|
67
|
+
# A table of all values of this header to their respective quality
|
68
|
+
# factors (qvalues).
|
69
|
+
attr_accessor :qvalues
|
70
|
+
|
71
|
+
def initialize(header='')
|
72
|
+
@qvalues = parse(header)
|
73
|
+
end
|
74
|
+
|
75
|
+
# The name of this header. Should be overridden in classes that mixin
|
76
|
+
# this module.
|
77
|
+
def name
|
78
|
+
''
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns the quality factor (qvalue) of the given +value+. Should be
|
82
|
+
# overridden in classes that mixin this module.
|
83
|
+
def qvalue(value)
|
84
|
+
1
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns the value of this header as a string.
|
88
|
+
def value
|
89
|
+
join(@qvalues)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns an array of all values of this header, in no particular order.
|
93
|
+
def values
|
94
|
+
@qvalues.keys
|
95
|
+
end
|
96
|
+
|
97
|
+
# Determines if the given +value+ is acceptable (does not have a qvalue
|
98
|
+
# of 0) according to this header.
|
99
|
+
def accept?(value)
|
100
|
+
qvalue(value) != 0
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns a copy of the given +values+ array, sorted by quality factor
|
104
|
+
# (qvalue). Each element of the returned array is itself an array
|
105
|
+
# containing two objects: 1) the value's qvalue and 2) the original
|
106
|
+
# value.
|
107
|
+
#
|
108
|
+
# It is important to note that this sort is a "stable sort". In other
|
109
|
+
# words, the order of the original values is preserved so long as the
|
110
|
+
# qvalue for each is the same. This expectation can be useful when
|
111
|
+
# trying to determine which of a variety of options has the highest
|
112
|
+
# qvalue. If the user prefers using one option over another (for any
|
113
|
+
# number of reasons), he should put it first in +values+. He may then
|
114
|
+
# use the first result with confidence that it is both most acceptable
|
115
|
+
# to the client and most convenient for him as well.
|
116
|
+
def sort_with_qvalues(values, keep_unacceptables=true)
|
117
|
+
qvalues = {}
|
118
|
+
values.each do |v|
|
119
|
+
q = qvalue(v)
|
120
|
+
if q != 0 || keep_unacceptables
|
121
|
+
qvalues[q] ||= []
|
122
|
+
qvalues[q] << v
|
123
|
+
end
|
124
|
+
end
|
125
|
+
order = qvalues.keys.sort.reverse
|
126
|
+
order.inject([]) {|m, q| m.concat(qvalues[q].map {|v| [q, v] }) }
|
127
|
+
end
|
128
|
+
|
129
|
+
# Sorts the given +values+ according to the qvalue of each while
|
130
|
+
# preserving the original order. See #sort_with_qvalues for more
|
131
|
+
# information on exactly how the sort is performed.
|
132
|
+
def sort(values, keep_unacceptables=false)
|
133
|
+
sort_with_qvalues(values, keep_unacceptables).map {|q, v| v }
|
134
|
+
end
|
135
|
+
|
136
|
+
# A shortcut for retrieving the first result of #sort.
|
137
|
+
def best_of(values, keep_unacceptables=false)
|
138
|
+
sort(values, keep_unacceptables).first
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns a string representation of this header.
|
142
|
+
def to_s
|
143
|
+
[name, value].join(': ')
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
include PublicInstanceMethods
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Rack::AcceptHeaders
|
2
|
+
# Represents an HTTP Accept-Language header according to the HTTP 1.1
|
3
|
+
# specification, and provides several convenience methods for determining
|
4
|
+
# acceptable content languages.
|
5
|
+
#
|
6
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
|
7
|
+
class Language
|
8
|
+
include Header
|
9
|
+
attr_writer :first_level_match
|
10
|
+
|
11
|
+
# The name of this header.
|
12
|
+
def name
|
13
|
+
'Accept-Language'
|
14
|
+
end
|
15
|
+
|
16
|
+
# Determines the quality factor (qvalue) of the given +language+.
|
17
|
+
def qvalue(language)
|
18
|
+
return 1 if @qvalues.empty?
|
19
|
+
m = matches(language)
|
20
|
+
return 0 if m.empty?
|
21
|
+
normalize_qvalue(@qvalues[m.first])
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns an array of languages from this header that match the given
|
25
|
+
# +language+, ordered by precedence.
|
26
|
+
def matches(language)
|
27
|
+
values.select {|v|
|
28
|
+
v = v.match(/^(.+?)-/) ? $1 : v if @first_level_match
|
29
|
+
v == language || v == '*' || (language.match(/^(.+?)-/) && v == $1)
|
30
|
+
}.sort {|a, b|
|
31
|
+
# "*" gets least precedence, any others are compared based on length.
|
32
|
+
a == '*' ? -1 : (b == '*' ? 1 : a.length <=> b.length)
|
33
|
+
}.reverse
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|