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 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
@@ -0,0 +1,18 @@
1
+ .DS_Store
2
+ *.gem
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ /doc
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,15 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.0
6
+ - jruby
7
+ - rbx
8
+ - ruby-head
9
+ - jruby-head
10
+
11
+ matrix:
12
+ allow_failures:
13
+ - rvm: ruby-head
14
+ - rvm: jruby-head
15
+ - rvm: rbx
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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rack-accept_headers.gemspec
4
+ gemspec
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,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs.push 'lib'
6
+ t.test_files = FileList['spec/**/*_spec.rb']
7
+ t.verbose = true
8
+ end
9
+
10
+ task default: :test
@@ -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