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