acceptable 0.2.1
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.
- data/README.rdoc +28 -0
- data/lib/rack/acceptable/const.rb +37 -0
- data/lib/rack/acceptable/data/mime.types +20 -0
- data/lib/rack/acceptable/language_tag.rb +405 -0
- data/lib/rack/acceptable/middleware/fake_accept.rb +28 -0
- data/lib/rack/acceptable/middleware/formats.rb +69 -0
- data/lib/rack/acceptable/middleware/provides.rb +97 -0
- data/lib/rack/acceptable/mimetypes.rb +293 -0
- data/lib/rack/acceptable/mixin/headers.rb +88 -0
- data/lib/rack/acceptable/mixin/media.rb +56 -0
- data/lib/rack/acceptable/request.rb +45 -0
- data/lib/rack/acceptable/utils.rb +231 -0
- data/lib/rack/acceptable/version.rb +7 -0
- data/lib/rack/acceptable.rb +27 -0
- metadata +88 -0
data/README.rdoc
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
== REQUIREMENTS:
|
2
|
+
|
3
|
+
rack (>=1.0.0)
|
4
|
+
|
5
|
+
== LICENSE:
|
6
|
+
|
7
|
+
(The MIT License)
|
8
|
+
|
9
|
+
Copyright (c) 2009
|
10
|
+
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
12
|
+
a copy of this software and associated documentation files (the
|
13
|
+
'Software'), to deal in the Software without restriction, including
|
14
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
15
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
16
|
+
permit persons to whom the Software is furnished to do so, subject to
|
17
|
+
the following conditions:
|
18
|
+
|
19
|
+
The above copyright notice and this permission notice shall be
|
20
|
+
included in all copies or substantial portions of the Software.
|
21
|
+
|
22
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
23
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
24
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
25
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
26
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
27
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
28
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Rack #:nodoc:
|
2
|
+
module Acceptable #:nodoc:
|
3
|
+
module Const
|
4
|
+
|
5
|
+
WILDCARD = '*'.freeze
|
6
|
+
MEDIA_RANGE_WILDCARD = '*/*'.freeze
|
7
|
+
IDENTITY = 'identity'.freeze
|
8
|
+
ISO_8859_1 = 'iso-8859-1'.freeze
|
9
|
+
TEXT = 'text'.freeze
|
10
|
+
|
11
|
+
COMMA = ','.freeze
|
12
|
+
EMPTY_STRING = ''.freeze
|
13
|
+
HYPHEN = '-'.freeze
|
14
|
+
SEMICOLON = ';'.freeze
|
15
|
+
SLASH = '/'.freeze
|
16
|
+
|
17
|
+
ENV_HTTP_ACCEPT = 'HTTP_ACCEPT'.freeze
|
18
|
+
ENV_HTTP_ACCEPT_ENCODING = 'HTTP_ACCEPT_ENCODING'.freeze
|
19
|
+
ENV_HTTP_ACCEPT_CHARSET = 'HTTP_ACCEPT_CHARSET'.freeze
|
20
|
+
ENV_HTTP_ACCEPT_LANGUAGE = 'HTTP_ACCEPT_LANGUAGE'.freeze
|
21
|
+
|
22
|
+
CHARSET = 'charset'.freeze
|
23
|
+
CONTENT_TYPE = 'Content-Type'.freeze
|
24
|
+
CONTENT_LENGTH = 'Content-Length'.freeze
|
25
|
+
|
26
|
+
TEXT_SLASH_PLAIN = 'text/plain'.freeze
|
27
|
+
|
28
|
+
NOT_ACCEPTABLE = "An appropriate representation of the requested resource could not be found.\n".freeze
|
29
|
+
NOT_ACCEPTABLE_RESPONSE = [406, {
|
30
|
+
CONTENT_TYPE => TEXT_SLASH_PLAIN, CONTENT_LENGTH => '76' },
|
31
|
+
[NOT_ACCEPTABLE]].freeze
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# EOF
|
@@ -0,0 +1,20 @@
|
|
1
|
+
application/atom+xml atom
|
2
|
+
application/javascript js
|
3
|
+
application/json json
|
4
|
+
application/octet-stream bin class iso dmg dist pkg dump
|
5
|
+
application/xhtml+xml xhtml
|
6
|
+
application/xml xml
|
7
|
+
application/xml-dtd dtd
|
8
|
+
application/xslt+xml xslt
|
9
|
+
application/zip zip
|
10
|
+
image/bmp bmp
|
11
|
+
image/gif gif
|
12
|
+
image/jpeg jpeg jpg jpe
|
13
|
+
image/png png
|
14
|
+
image/tiff tiff tif
|
15
|
+
image/vnd.djvu djvu djv
|
16
|
+
image/x-icon ico
|
17
|
+
text/css css
|
18
|
+
text/csv csv
|
19
|
+
text/html html htm
|
20
|
+
text/plain txt text conf log
|
@@ -0,0 +1,405 @@
|
|
1
|
+
require 'rack/acceptable/utils'
|
2
|
+
|
3
|
+
module Rack #:nodoc:
|
4
|
+
module Acceptable #:nodoc:
|
5
|
+
|
6
|
+
# inspired by the 'langtag' gem (author: Martin Dürst)
|
7
|
+
# http://rubyforge.org/projects/langtag/
|
8
|
+
# http://www.langtag.net/
|
9
|
+
#
|
10
|
+
class LanguageTag
|
11
|
+
|
12
|
+
GRANDFATHERED_TAGS = {
|
13
|
+
'art-lojban' => 'jbo' ,
|
14
|
+
'cel-gaulish' => nil ,
|
15
|
+
'en-gb-oed' => nil ,
|
16
|
+
'i-ami' => 'ami' ,
|
17
|
+
'i-bnn' => 'bnn' ,
|
18
|
+
'i-default' => nil ,
|
19
|
+
'i-enochian' => nil ,
|
20
|
+
'i-hak' => 'hak' ,
|
21
|
+
'i-klingon' => 'tlh' ,
|
22
|
+
'i-lux' => 'lb' ,
|
23
|
+
'i-mingo' => nil ,
|
24
|
+
'i-navajo' => 'nv' ,
|
25
|
+
'i-pwn' => 'pwn' ,
|
26
|
+
'i-tao' => 'tao' ,
|
27
|
+
'i-tay' => 'tay' ,
|
28
|
+
'i-tsu' => 'tsu' ,
|
29
|
+
'no-bok' => 'nb' ,
|
30
|
+
'no-nyn' => 'nn' ,
|
31
|
+
'sgn-be-fr' => 'sfb' ,
|
32
|
+
'sgn-be-nl' => 'vgt' ,
|
33
|
+
'sgn-ch-de' => 'sgg' ,
|
34
|
+
'zh-guoyu' => 'cmn' ,
|
35
|
+
'zh-hakka' => 'hak' ,
|
36
|
+
'zh-min' => nil ,
|
37
|
+
'zh-min-nan' => 'nan' ,
|
38
|
+
'zh-xiang' => 'hsn'
|
39
|
+
}.freeze
|
40
|
+
|
41
|
+
attr_accessor :primary, :extlang, :script, :region, :variants, :extensions, :privateuse
|
42
|
+
|
43
|
+
#--
|
44
|
+
# RFC 5646, sec. 2.2.2:
|
45
|
+
# Although the ABNF production 'extlang' permits up to three
|
46
|
+
# extended language tags in the language tag, extended language
|
47
|
+
# subtags MUST NOT include another extended language subtag in
|
48
|
+
# their 'Prefix'. That is, the second and third extended language
|
49
|
+
# subtag positions in a language tag are permanently reserved and
|
50
|
+
# tags that include those subtags in that position are, and will
|
51
|
+
# always remain, invalid.
|
52
|
+
#++
|
53
|
+
|
54
|
+
language = '([a-z]{2,8}|[a-z]{2,3}(?:-[a-z]{3})?)'
|
55
|
+
script = '(?:-([a-z]{4}))?'
|
56
|
+
region = '(?:-([a-z]{2}|\d{3}))?'
|
57
|
+
variants = '(?:-[a-z\d]{5,8}|-\d[a-z\d]{3})*'
|
58
|
+
extensions = '(?:-[a-wy-z\d]{1}(?:-[a-z\d]{2,8})+)*'
|
59
|
+
privateuse = '(?:-x(?:-[a-z\d]{1,8})+)?'
|
60
|
+
|
61
|
+
LANGTAG_COMPOSITION_REGEX = /^#{language}#{script}#{region}(?=#{variants}#{extensions}#{privateuse}$)/o.freeze
|
62
|
+
LANGTAG_INFO_REGEX = /^#{language}#{script}#{region}(#{variants})#{extensions}#{privateuse}$/o.freeze
|
63
|
+
PRIVATEUSE_REGEX = /^x(?:-[a-z\d]{1,8})+$/i.freeze
|
64
|
+
|
65
|
+
PRIVATEUSE = 'x'.freeze
|
66
|
+
|
67
|
+
class << self
|
68
|
+
|
69
|
+
# Checks if the +String+ passed could be treated as 'privateuse' Language-Tag.
|
70
|
+
# Works case-insensitively.
|
71
|
+
#
|
72
|
+
def privateuse?(tag)
|
73
|
+
PRIVATEUSE_REGEX === tag
|
74
|
+
end
|
75
|
+
|
76
|
+
# Checks if the +String+ passed represents a 'grandgathered' Language-Tag.
|
77
|
+
# Works case-insensitively.
|
78
|
+
#
|
79
|
+
def grandfathered?(tag)
|
80
|
+
GRANDFATHERED_TAGS.key?(tag) || GRANDFATHERED_TAGS.key?(tag.downcase)
|
81
|
+
end
|
82
|
+
|
83
|
+
# ==== Parameters
|
84
|
+
# langtag<String>:: The Language-Tag snippet.
|
85
|
+
#
|
86
|
+
# ==== Returns
|
87
|
+
# Array or nil::
|
88
|
+
# It returns +nil+, when the Language-Tag passed:
|
89
|
+
# * does not conform the Language-Tag ABNF (malformed)
|
90
|
+
# * grandfathered
|
91
|
+
# * starts with 'x' singleton ('privateuse').
|
92
|
+
#
|
93
|
+
# Otherwise you'll get an +Array+ with:
|
94
|
+
# * primary subtag (as +String+, downcased),
|
95
|
+
# * extlang (as +String+, downcased) or +nil+,
|
96
|
+
# * script (as +String+, capitalized) or +nil+,
|
97
|
+
# * region (as +String+, upcased) or +nil+
|
98
|
+
# * downcased variants (+Array+) or nil.
|
99
|
+
#
|
100
|
+
# ==== Notes
|
101
|
+
# In most cases, it's quite enough. Take a look, for example, at
|
102
|
+
# {'35-character recomendation'}[http://tools.ietf.org/html/rfc5646#section-4.6].
|
103
|
+
#
|
104
|
+
def extract_language_info(langtag)
|
105
|
+
tag = langtag.downcase
|
106
|
+
return nil if GRANDFATHERED_TAGS.key?(langtag)
|
107
|
+
return nil unless LANGTAG_INFO_REGEX === tag
|
108
|
+
|
109
|
+
primary = $1
|
110
|
+
extlang = nil
|
111
|
+
script = $2
|
112
|
+
region = $3
|
113
|
+
variants = $4.split(Utils::HYPHEN_SPLITTER)[1..-1]
|
114
|
+
|
115
|
+
primary, extlang = primary.split(Utils::HYPHEN_SPLITTER) if primary.include?(Const::HYPHEN)
|
116
|
+
script.capitalize! if script
|
117
|
+
region.upcase! if region
|
118
|
+
|
119
|
+
[primary, extlang, script, region, variants]
|
120
|
+
end
|
121
|
+
|
122
|
+
def parse(thing)
|
123
|
+
return nil unless thing
|
124
|
+
return thing if thing.kind_of?(self)
|
125
|
+
self.new.recompose(thing)
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
# Checks if self has a variant passed.
|
131
|
+
# Works case-insensitively.
|
132
|
+
#
|
133
|
+
# ==== Notes
|
134
|
+
# *Destructively* downcases current set of variants, if necessary.
|
135
|
+
# Just note, that variants are case-insensitive, and 'convenient' form
|
136
|
+
# of the Languge-Tag assumes they're in 'lowercase' notation.
|
137
|
+
#
|
138
|
+
def has_variant?(variant)
|
139
|
+
return false unless @variants
|
140
|
+
@variants.include?(variant) || begin
|
141
|
+
@variants.map { |v| v.downcase! }
|
142
|
+
@variants.include?(variant.downcase)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Checks if self has a singleton passed.
|
147
|
+
# Works case-insensitively.
|
148
|
+
def has_singleton?(key)
|
149
|
+
return false unless @extensions
|
150
|
+
@extensions.key?(key) || @extensions.key?(key.downcase)
|
151
|
+
end
|
152
|
+
|
153
|
+
alias :extension? :has_singleton?
|
154
|
+
|
155
|
+
# Builds an ordered list of singletons.
|
156
|
+
def singletons
|
157
|
+
return nil unless @extensions
|
158
|
+
keys = @extensions.keys
|
159
|
+
keys.sort!
|
160
|
+
keys
|
161
|
+
end
|
162
|
+
|
163
|
+
def initialize(*components)
|
164
|
+
@primary, @extlang, @script, @region, @variants, @extensions, @privateuse = *components
|
165
|
+
end
|
166
|
+
|
167
|
+
# Builds the +String+, which represents self.
|
168
|
+
# Does *not* perform validation or recomposition.
|
169
|
+
#
|
170
|
+
def compose
|
171
|
+
@tag = to_a.join(Const::HYPHEN)
|
172
|
+
@tag
|
173
|
+
end
|
174
|
+
|
175
|
+
attr_reader :tag # the most recent 'build' of tag
|
176
|
+
|
177
|
+
def nicecased
|
178
|
+
recompose # we could not conveniently format malformed or invalid tags
|
179
|
+
@nicecased #.dup #uuuuugh
|
180
|
+
end
|
181
|
+
|
182
|
+
def to_a
|
183
|
+
ary = [@primary]
|
184
|
+
ary << @extlang if @extlang
|
185
|
+
ary << @script if @script
|
186
|
+
ary << @region if @region
|
187
|
+
ary.concat @variants if @variants
|
188
|
+
singletons.each { |s| (ary << s).concat @extensions[s] } if @extensions
|
189
|
+
(ary << PRIVATEUSE).concat @privateuse if @privateuse
|
190
|
+
ary
|
191
|
+
rescue
|
192
|
+
raise "LanguageTag has at least one malformed attribute: #{self.inspect}"
|
193
|
+
end
|
194
|
+
|
195
|
+
#--
|
196
|
+
# RFC 4647, sec. 3.3.2 ('Extended Filtering')
|
197
|
+
#
|
198
|
+
# Much like basic filtering, extended filtering selects content with
|
199
|
+
# arbitrarily long tags that share the same initial subtags as the
|
200
|
+
# language range. In addition, extended filtering selects language
|
201
|
+
# tags that contain any intermediate subtags not specified in the
|
202
|
+
# language range. For example, the extended language range "de-*-DE"
|
203
|
+
# (or its synonym "de-DE") matches all of the following tags:
|
204
|
+
#
|
205
|
+
# de-DE (German, as used in Germany)
|
206
|
+
# de-de (German, as used in Germany)
|
207
|
+
# de-Latn-DE (Latin script)
|
208
|
+
# de-Latf-DE (Fraktur variant of Latin script)
|
209
|
+
# de-DE-x-goethe (private-use subtag)
|
210
|
+
# de-Latn-DE-1996 (orthography of 1996)
|
211
|
+
# de-Deva-DE (Devanagari script)
|
212
|
+
#
|
213
|
+
# The same range does not match any of the following tags for the
|
214
|
+
# reasons shown:
|
215
|
+
#
|
216
|
+
# de (missing 'DE')
|
217
|
+
# de-x-DE (singleton 'x' occurs before 'DE')
|
218
|
+
# de-Deva ('Deva' not equal to 'DE')
|
219
|
+
#++
|
220
|
+
|
221
|
+
# Checks if the *extended* Language-Range passed matches self.
|
222
|
+
def matched_by_extended_range?(range)
|
223
|
+
recompose
|
224
|
+
subtags = @composition.split(Const::HYPHEN)
|
225
|
+
subranges = range.downcase.split(Const::HYPHEN)
|
226
|
+
|
227
|
+
subrange = subranges.shift
|
228
|
+
subtag = subtags.shift
|
229
|
+
|
230
|
+
while subrange
|
231
|
+
if subrange == Const::WILDCARD
|
232
|
+
subrange = subranges.shift
|
233
|
+
elsif subtag == nil
|
234
|
+
return false
|
235
|
+
elsif subtag == subrange
|
236
|
+
subtag = subtags.shift
|
237
|
+
subrange = subranges.shift
|
238
|
+
elsif subtag.size == 1
|
239
|
+
return false
|
240
|
+
else
|
241
|
+
subtag = subtags.shift
|
242
|
+
end
|
243
|
+
end
|
244
|
+
true
|
245
|
+
rescue
|
246
|
+
false
|
247
|
+
end
|
248
|
+
|
249
|
+
#--
|
250
|
+
# RFC 4647, sec. 3.3.1 ('Basic Filtering')
|
251
|
+
#
|
252
|
+
# A language range matches a
|
253
|
+
# particular language tag if, in a case-insensitive comparison, it
|
254
|
+
# exactly equals the tag, or if it exactly equals a prefix of the tag
|
255
|
+
# such that the first character following the prefix is "-". For
|
256
|
+
# example, the language-range "de-de" (German as used in Germany)
|
257
|
+
# matches the language tag "de-DE-1996" (German as used in Germany,
|
258
|
+
# orthography of 1996), but not the language tags "de-Deva" (German as
|
259
|
+
# written in the Devanagari script) or "de-Latn-DE" (German, Latin
|
260
|
+
# script, as used in Germany).
|
261
|
+
#++
|
262
|
+
|
263
|
+
# Checks if the *basic* Language-Range passed matches self.
|
264
|
+
#
|
265
|
+
# ==== Example
|
266
|
+
# tag = LanguageTag.parse('de-Latn-DE')
|
267
|
+
# tag.matched_by_basic_range?('de-Latn-DE') #=> true
|
268
|
+
# tag.matched_by_basic_range?('de-Latn') #=> true
|
269
|
+
# tag.matched_by_basic_range?('*') #=> true
|
270
|
+
# tag.matched_by_basic_range?('de-La') #=> false
|
271
|
+
# tag.matched_by_basic_range?('de-de') #=> false
|
272
|
+
# tag.matched_by_basic_range?('malformedlangtag') #=> false
|
273
|
+
#
|
274
|
+
def matched_by_basic_range?(range)
|
275
|
+
if range.kind_of?(self.class)
|
276
|
+
s = range.recompose.tag
|
277
|
+
elsif range.respond_to?(:to_str)
|
278
|
+
return true if range.to_str == Const::WILDCARD
|
279
|
+
s = self.class.parse(range).tag
|
280
|
+
else
|
281
|
+
return false
|
282
|
+
end
|
283
|
+
recompose
|
284
|
+
@tag == s || @tag.index(s + Const::HYPHEN) == 0
|
285
|
+
rescue
|
286
|
+
false
|
287
|
+
end
|
288
|
+
|
289
|
+
alias :has_prefix? :matched_by_basic_range?
|
290
|
+
|
291
|
+
def ==(other)
|
292
|
+
return false unless other.kind_of?(self.class)
|
293
|
+
compose
|
294
|
+
other.compose
|
295
|
+
@tag == other.tag || @tag.downcase == other.tag.downcase
|
296
|
+
end
|
297
|
+
|
298
|
+
def ===(other)
|
299
|
+
if other.kind_of?(self.class)
|
300
|
+
s = other.compose
|
301
|
+
elsif other.respond_to?(:to_str)
|
302
|
+
s = other.to_str
|
303
|
+
else
|
304
|
+
return false
|
305
|
+
end
|
306
|
+
compose
|
307
|
+
@tag == s || @tag.downcase == s.downcase
|
308
|
+
end
|
309
|
+
|
310
|
+
# Validates self.
|
311
|
+
#
|
312
|
+
# ==== Notes
|
313
|
+
# Validation is deferred by default, because the paranoid
|
314
|
+
# check & dup of everything is not a good way (in this case).
|
315
|
+
# So, you may create some tags, make them malformed/invalid,
|
316
|
+
# and still be able to compare and modify them. Only note, that
|
317
|
+
# things like 'filtering' and 'lookup' are *not* validation-free.
|
318
|
+
#
|
319
|
+
def valid?
|
320
|
+
!!recompose rescue false
|
321
|
+
end
|
322
|
+
|
323
|
+
alias :langtag? :valid?
|
324
|
+
|
325
|
+
# ==== Parameters
|
326
|
+
# thing<String, optional>::
|
327
|
+
# The Language-Tag snippet
|
328
|
+
#
|
329
|
+
# ==== Returns
|
330
|
+
# +self+
|
331
|
+
#
|
332
|
+
# ==== Raises
|
333
|
+
# ArgumentError::
|
334
|
+
# The Language-Tag composition:
|
335
|
+
# * does not conform the Language-Tag ABNF (malformed)
|
336
|
+
# * grandfathered
|
337
|
+
# * starts with 'x' singleton ('privateuse').
|
338
|
+
# * contains duplicate variants
|
339
|
+
# * contains duplicate singletons
|
340
|
+
#
|
341
|
+
def recompose(thing = nil)
|
342
|
+
|
343
|
+
tag = nil
|
344
|
+
compose
|
345
|
+
|
346
|
+
if thing
|
347
|
+
raise TypeError, "Can't convert #{thing.class} into String" unless thing.respond_to?(:to_str)
|
348
|
+
return self if @tag == (tag = thing.to_str) || @tag.downcase == (tag = tag.downcase)
|
349
|
+
else
|
350
|
+
return self if @nicecased == (tag = @tag) || @composition == tag || @composition == (tag = tag.downcase)
|
351
|
+
end
|
352
|
+
|
353
|
+
if !GRANDFATHERED_TAGS.key?(tag) && LANGTAG_COMPOSITION_REGEX === tag
|
354
|
+
|
355
|
+
@primary = $1
|
356
|
+
@extlang = nil
|
357
|
+
@script = $2
|
358
|
+
@region = $3
|
359
|
+
components = $'.split(Utils::HYPHEN_SPLITTER)
|
360
|
+
components.shift
|
361
|
+
|
362
|
+
@primary, @extlang = @primary.split(Utils::HYPHEN_SPLITTER) if @primary.include?(Const::HYPHEN)
|
363
|
+
|
364
|
+
@script.capitalize! if @script
|
365
|
+
@region.upcase! if @region
|
366
|
+
|
367
|
+
@extensions = nil
|
368
|
+
@variants = nil
|
369
|
+
singleton = nil
|
370
|
+
|
371
|
+
while c = components.shift
|
372
|
+
if c.size == 1
|
373
|
+
break if c == PRIVATEUSE
|
374
|
+
@extensions ||= {}
|
375
|
+
if @extensions.key?(c)
|
376
|
+
raise ArgumentError, "Invalid langtag (repeated singleton: #{c.inspect}): #{thing.inspect}"
|
377
|
+
end
|
378
|
+
singleton = c
|
379
|
+
@extensions[singleton = c] = []
|
380
|
+
elsif singleton
|
381
|
+
@extensions[singleton] << c
|
382
|
+
else
|
383
|
+
@variants ||= []
|
384
|
+
if @variants.include?(c)
|
385
|
+
raise ArgumentError, "Invalid langtag (repeated variant: #{c.inspect}): #{thing.inspect}"
|
386
|
+
end
|
387
|
+
@variants << c
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
@privateuse = components.empty? ? nil : components
|
392
|
+
@nicecased = compose
|
393
|
+
@composition = @tag.downcase
|
394
|
+
|
395
|
+
else
|
396
|
+
raise ArgumentError, "Malformed, grandfathered or 'privateuse' Language-Tag: #{thing.inspect}"
|
397
|
+
end
|
398
|
+
|
399
|
+
self
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
# EOF
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rack/request'
|
2
|
+
require 'rack/acceptable/mimetypes'
|
3
|
+
|
4
|
+
module Rack #:nodoc:
|
5
|
+
module Acceptable #:nodoc:
|
6
|
+
class FakeAccept
|
7
|
+
|
8
|
+
ORIGINAL_HTTP_ACCEPT = 'rack-acceptable.fake_accept.original_HTTP_ACCEPT'
|
9
|
+
|
10
|
+
def initialize(app, default_media = 'text/html')
|
11
|
+
@default_media = default_media
|
12
|
+
@app = app
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(env)
|
16
|
+
request = ::Rack::Request.new(env)
|
17
|
+
extname = ::File.extname(request.path_info)
|
18
|
+
env[ORIGINAL_HTTP_ACCEPT] = env[Const::ENV_HTTP_ACCEPT]
|
19
|
+
env[Const::ENV_HTTP_ACCEPT] = Rack::Acceptable::MIMETypes.lookup(extname, @default_media)
|
20
|
+
@app.call(env)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# EOF
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'rack/acceptable/utils'
|
2
|
+
|
3
|
+
module Rack #:nodoc:
|
4
|
+
module Acceptable #:nodoc:
|
5
|
+
|
6
|
+
# * Fishes out all acceptable formats (in the appropriate order).
|
7
|
+
# The idea is to let the user decide about the preferred one,
|
8
|
+
# since the Accept request-header is not only thing the response
|
9
|
+
# depends on.
|
10
|
+
# * Stops processing and responds with 406 'Not Acceptable' *only*
|
11
|
+
# if there's no acceptable formats *and* wildcard has a zero quality
|
12
|
+
# or not explicitly mentioned; i.e, decreases (slightly) the the
|
13
|
+
# number of application calls in compliance with notes in
|
14
|
+
# RFC 2616, sec. 10.4.7.
|
15
|
+
#
|
16
|
+
# ==== Example
|
17
|
+
#
|
18
|
+
# @provides = {
|
19
|
+
# :json => %w(text/x-json application/json),
|
20
|
+
# :xml => %w(application/xml text/xml),
|
21
|
+
# :text => %w(text/plain text/*)
|
22
|
+
# }
|
23
|
+
#
|
24
|
+
# use Rack::Acceptable::Formats @provides
|
25
|
+
#
|
26
|
+
class Formats
|
27
|
+
|
28
|
+
CANDIDATES = 'rack-acceptable.formats.candidates'
|
29
|
+
|
30
|
+
#--
|
31
|
+
# RFC 2616, section 10.4.7:
|
32
|
+
# Note: HTTP/1.1 servers are allowed to return responses which are
|
33
|
+
# not acceptable according to the accept headers sent in the
|
34
|
+
# request. In some cases, this may even be preferable to sending a
|
35
|
+
# 406 response. User agents are encouraged to inspect the headers of
|
36
|
+
# an incoming response to determine if it is acceptable.
|
37
|
+
#++
|
38
|
+
|
39
|
+
def initialize(app, provides)
|
40
|
+
@app, @types = app, {}
|
41
|
+
provides.each { |f,types| types.each { |t| @types[t] = f } }
|
42
|
+
@types.update(Const::MEDIA_RANGE_WILDCARD => :all)
|
43
|
+
end
|
44
|
+
|
45
|
+
def call(env)
|
46
|
+
if accepts = env[Const::ENV_HTTP_ACCEPT]
|
47
|
+
accepts = Utils.extract_qvalues(accepts)
|
48
|
+
i = 0
|
49
|
+
accepts = accepts.sort_by { |_,q| [-q,i+=1] }
|
50
|
+
accepts.reject! { |t,q| q == 0 || !@types.key?(t) }
|
51
|
+
if accepts.empty?
|
52
|
+
return Const::NOT_ACCEPTABLE_RESPONSE
|
53
|
+
else
|
54
|
+
accepts.map! { |t,_| @types[t] }.uniq!
|
55
|
+
env[CANDIDATES] = accepts
|
56
|
+
end
|
57
|
+
else
|
58
|
+
env[CANDIDATES] = [:all]
|
59
|
+
end
|
60
|
+
@app.call(env)
|
61
|
+
#rescue
|
62
|
+
# @app.call(env)
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# EOF
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'rack/utils'
|
2
|
+
require 'rack/acceptable/request'
|
3
|
+
|
4
|
+
module Rack #:nodoc:
|
5
|
+
module Acceptable #:nodoc:
|
6
|
+
|
7
|
+
# Inspired (partially) by the Rack::AcceptFormat.
|
8
|
+
#
|
9
|
+
# * Fishes out the best one of available MIME-Types.
|
10
|
+
# * Adds an associated format (extension) to the path_info (optional).
|
11
|
+
# * Stops processing and responds with 406 'Not Acceptable',
|
12
|
+
# when there's nothing to provide.
|
13
|
+
# * Memoizes results, since the full negotiation is not
|
14
|
+
# the 'week' (but quick!) lookup.
|
15
|
+
#
|
16
|
+
# ==== Example
|
17
|
+
#
|
18
|
+
# use Rack::Acceptable::Provides w(text/x-json application/json text/plain),
|
19
|
+
# :force_format => true,
|
20
|
+
# :default_format => '.txt'
|
21
|
+
#
|
22
|
+
class Provides
|
23
|
+
|
24
|
+
CANDIDATE = 'rack-acceptable.provides.candidate'
|
25
|
+
CANDIDATE_INFO = 'rack-acceptable.provides.candidate_info'
|
26
|
+
|
27
|
+
# ==== Parameters
|
28
|
+
# app<#call>:: Rack application.
|
29
|
+
# provides<Array>:: List of available MIME-Types.
|
30
|
+
# options<Hash>:: Additional options, like +negotiate_by+. See also the
|
31
|
+
# Rack::Acceptable::MIMETypes#detect_best_mime_type method.
|
32
|
+
#
|
33
|
+
def initialize(app, provides, options = {})
|
34
|
+
raise "You should provide the list of available MIME-Types." if provides.empty?
|
35
|
+
@app = app
|
36
|
+
@provides = provides.map { |type| MIMETypes.parse_media_range(type) << type }
|
37
|
+
@provides << (options[:negotiate_by] == :qvalue_only ? true : false)
|
38
|
+
@lookup = {}
|
39
|
+
@force_format = !!options[:force_format]
|
40
|
+
if @force_format && options.key?(:default_format)
|
41
|
+
ext = options[:default_format].to_s.strip
|
42
|
+
@_extension = ext[0] == ?. ? ext : ".#{ext}" unless ext.empty?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def call(env)
|
47
|
+
request = Rack::Acceptable::Request.new(env)
|
48
|
+
return Const::NOT_ACCEPTABLE_RESPONSE unless preferred = _negotiate(request)
|
49
|
+
|
50
|
+
simple = preferred.last
|
51
|
+
request.env[CANDIDATE] = simple
|
52
|
+
request.env[CANDIDATE_INFO] = preferred[0..2]
|
53
|
+
|
54
|
+
if @force_format &&
|
55
|
+
(path = request.path_info) != Const::SLASH &&
|
56
|
+
ext = _extension_for(simple)
|
57
|
+
request.path_info = path.sub(/\/*$/, ext)
|
58
|
+
end
|
59
|
+
|
60
|
+
status, headers, body = @app.call(env)
|
61
|
+
unless Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status.to_i)
|
62
|
+
headers = Rack::Utils::HeaderHash.new(headers)
|
63
|
+
headers[Const::CONTENT_TYPE] ||= simple
|
64
|
+
end
|
65
|
+
[status, headers.to_hash, body]
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
# Picks out an extension for the MIME-Type given.
|
71
|
+
# Override this to force the usage of another MIME-Type registry.
|
72
|
+
# See Rack::Acceptable::MIMETypes about the manipulating with
|
73
|
+
# entries in the registry.
|
74
|
+
#
|
75
|
+
def _extension_for(thing)
|
76
|
+
MIMETypes.extension_for(thing) || @_extension
|
77
|
+
end
|
78
|
+
|
79
|
+
# Performs negotiation and memoizes result.
|
80
|
+
#
|
81
|
+
def _negotiate(request)
|
82
|
+
header = request.env[Const::ENV_HTTP_ACCEPT]
|
83
|
+
if @lookup.key?(header)
|
84
|
+
@lookup[header]
|
85
|
+
else
|
86
|
+
accepts = request.acceptable_media
|
87
|
+
@lookup[header] = accepts.empty? ? @provides.first : request.preferred_media_from(*@provides)
|
88
|
+
end
|
89
|
+
rescue
|
90
|
+
@lookup[header] = nil # The Accept request-header is malformed.
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# EOF
|