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