rack-accept_headers 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/kamui/rack-accept_headers.png)](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
|