acceptable 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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