wants 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +135 -0
- data/lib/wants/match_result.rb +70 -0
- data/lib/wants/mimeparse.rb +217 -0
- data/lib/wants/validate_accepts_middleware.rb +35 -0
- data/lib/wants.rb +23 -0
- data/spec/match_result_spec.rb +147 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/validate_accepts_middleware_spec.rb +44 -0
- data/spec/wants_spec.rb +50 -0
- metadata +105 -0
data/README.md
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
## Wants
|
2
|
+
|
3
|
+
This library provides support for choosing the proper MIME type for a
|
4
|
+
response.
|
5
|
+
|
6
|
+
### Installation
|
7
|
+
|
8
|
+
With Rubygems:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem install wants
|
12
|
+
```
|
13
|
+
|
14
|
+
With Bundler:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
gem 'wants'
|
18
|
+
```
|
19
|
+
|
20
|
+
### Loading
|
21
|
+
|
22
|
+
Simply require the gem name:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
require 'wants'
|
26
|
+
```
|
27
|
+
|
28
|
+
### Usage
|
29
|
+
|
30
|
+
#### `Wants.new`
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
wants = Wants.new( accept, [ :html, :json, :atom ] )
|
34
|
+
```
|
35
|
+
|
36
|
+
The `Wants` constructor takes two arguments:
|
37
|
+
|
38
|
+
* The value of an HTTP Accept header. cf
|
39
|
+
[RFC 2616, §14.1](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1)
|
40
|
+
* an `Array` of all the supported MIME types, each of which can be a full
|
41
|
+
type (e.g. `"application/json"`) or, if `Rack` is loaded, a key in
|
42
|
+
[`Rack::Mime::MIME_TYPES`](https://github.com/rack/rack/blob/master/lib/rack/mime.rb).
|
43
|
+
(If you want to use a different lookup table, you can set `Wants.mime_lookup_table`.)
|
44
|
+
|
45
|
+
`Wants.new` will return to you a `Wants::MatchResult` object that represents the
|
46
|
+
single best MIME type from the available options. It supports a variety of
|
47
|
+
introspection methods:
|
48
|
+
|
49
|
+
#### `MatchResult#not_acceptable?`
|
50
|
+
|
51
|
+
This predicate tell you whether there was no acceptable match. For example,
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
wants = Wants.new( { 'application/json' }, [ :html ] )
|
55
|
+
wants.not_acceptable? # true
|
56
|
+
```
|
57
|
+
|
58
|
+
This method is aliased as `#blank?` and its inverse is available as `#present?`.
|
59
|
+
|
60
|
+
#### `MatchResult#{mime}?`
|
61
|
+
|
62
|
+
You can use a MIME abbreviation as a query method on the matcher. For example,
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
acceptable = 'text/html,application/xhtml+xml;q=0.9'
|
66
|
+
offered = [ :html, :json ]
|
67
|
+
wants = Wants.new( acceptable, offered )
|
68
|
+
wants.html? # true
|
69
|
+
wants.xhtml? # false
|
70
|
+
wants.json? # false
|
71
|
+
```
|
72
|
+
|
73
|
+
#### `MatchResult#[{mime}]`
|
74
|
+
|
75
|
+
To query a full MIME type, use `#[]`. For example,
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
acceptable = 'text/html,application/xhtml+xml;q=0.9'
|
79
|
+
offered = [ :html, :json ]
|
80
|
+
wants = Wants.new( acceptable, offered )
|
81
|
+
wants[:html] # true
|
82
|
+
wants['text/html'] # true
|
83
|
+
wants['application/xhtml_xml'] # false
|
84
|
+
wants['application/json'] # false
|
85
|
+
```
|
86
|
+
|
87
|
+
#### `MatchResult#{mime}`
|
88
|
+
|
89
|
+
Lastly, you can use the matcher as DSL. For example,
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
acceptable = 'application/json,application/javascript;q=0.8'
|
93
|
+
offered = [ :html, :json ]
|
94
|
+
wants = Wants.new( acceptable, offered )
|
95
|
+
|
96
|
+
wants.atom { build_an_atom_response }
|
97
|
+
wants.json { build_a_json_response }
|
98
|
+
wants.html { build_an_html_response }
|
99
|
+
wants.not_acceptable { build_a_406_unacceptable_response }
|
100
|
+
wants.any { build_a_generic_response }
|
101
|
+
```
|
102
|
+
|
103
|
+
In this example, only `build_a_json_response` will be evaluated. `wants.json`
|
104
|
+
and all subsequent `wants.{mime}` calls, including `wants.not_acceptable` and
|
105
|
+
`wants.any`, will return whatever `build_a_json_response` returned.
|
106
|
+
More formally, each `wants.{mime}` call behaves as follows:
|
107
|
+
|
108
|
+
1. if `@response_value` is not `nil`, return it
|
109
|
+
1. if [method name] is the abbreviation for the desired MIME type,
|
110
|
+
evaluate the block and set the result to `@response_value`
|
111
|
+
|
112
|
+
`wants.not_acceptable` will match if `wants.not_acceptable?` returns `true`.
|
113
|
+
`wants.any` will match if `wants.not_acceptable?` returns `false`. Thus,
|
114
|
+
`wants.any` should be placed after all other matchers.
|
115
|
+
|
116
|
+
#### `Wants::ValidateAcceptsMiddleware`
|
117
|
+
|
118
|
+
Usage in `config.ru`:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
require 'wants/validate_accepts_middleware'
|
122
|
+
|
123
|
+
use Wants::ValidateAcceptsMiddleware, :mime_types => [ :html, :json ]
|
124
|
+
run MyApp
|
125
|
+
```
|
126
|
+
|
127
|
+
This will pass HTML and JSON requests down to `MyApp` and return a
|
128
|
+
406 Not Acceptable response for all others. You can configure the
|
129
|
+
failure case with the `:on_not_acceptable` option:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
use Wants::ValidateAcceptsMiddleware,
|
133
|
+
:mime_types => [ ... ],
|
134
|
+
:on_not_acceptable => lambda { |env| ... }
|
135
|
+
```
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'wants/mimeparse'
|
2
|
+
|
3
|
+
module Wants
|
4
|
+
class MatchResult
|
5
|
+
|
6
|
+
def initialize(accept_header, acceptable)
|
7
|
+
@accept = accept_header || ''
|
8
|
+
@acceptable = acceptable.map { |mime| parse_mime(mime) }
|
9
|
+
@best_match = MIMEParse.best_match(@acceptable, @accept)
|
10
|
+
end
|
11
|
+
|
12
|
+
def not_acceptable?
|
13
|
+
@best_match.nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
def blank?
|
17
|
+
not_acceptable?
|
18
|
+
end
|
19
|
+
|
20
|
+
def present?
|
21
|
+
!blank?
|
22
|
+
end
|
23
|
+
|
24
|
+
def [](mime)
|
25
|
+
@best_match == parse_mime(mime)
|
26
|
+
end
|
27
|
+
|
28
|
+
def any(&block)
|
29
|
+
@response_value ||= block.call if present?
|
30
|
+
end
|
31
|
+
|
32
|
+
def not_acceptable(&block)
|
33
|
+
@response_value ||= block.call if blank?
|
34
|
+
end
|
35
|
+
|
36
|
+
def method_missing(method, *args, &block)
|
37
|
+
if mime = mime_abbreviation_from_method(method)
|
38
|
+
if args.length > 0
|
39
|
+
raise ArgumentError, "wrong number of arguments (#{args.length} for 0)"
|
40
|
+
end
|
41
|
+
if method =~ /\?$/
|
42
|
+
self[mime]
|
43
|
+
elsif self[mime]
|
44
|
+
@response_value ||= block.call
|
45
|
+
else
|
46
|
+
@response_value
|
47
|
+
end
|
48
|
+
else
|
49
|
+
super
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def respond_to?(method)
|
54
|
+
return true if mime_abbreviation_from_method(method)
|
55
|
+
super
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def parse_mime(mime)
|
61
|
+
Wants.mime_lookup_table[".#{mime}"] || mime.to_s
|
62
|
+
end
|
63
|
+
|
64
|
+
def mime_abbreviation_from_method(method)
|
65
|
+
md = /([^\?]+)\??$/.match(method)
|
66
|
+
md && Wants.mime_lookup_table[".#{md[1]}"]
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
module Wants
|
2
|
+
# From http://code.google.com/p/mimeparse
|
3
|
+
#
|
4
|
+
# This module provides basic functions for handling mime-types. It can
|
5
|
+
# handle matching mime-types against a list of media-ranges. See section
|
6
|
+
# 14.1 of the HTTP specification [RFC 2616] for a complete explanation.
|
7
|
+
#
|
8
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
|
9
|
+
#
|
10
|
+
# ---------
|
11
|
+
#
|
12
|
+
# This is a port of Joe Gregario's mimeparse.py, which can be found at
|
13
|
+
# <http://code.google.com/p/mimeparse/>.
|
14
|
+
#
|
15
|
+
# ported from version 0.1.2
|
16
|
+
#
|
17
|
+
# Comments are mostly excerpted from the original.
|
18
|
+
module MIMEParse
|
19
|
+
module_function
|
20
|
+
|
21
|
+
# Carves up a mime-type and returns an Array of the
|
22
|
+
# [type, subtype, params] where "params" is a Hash of all
|
23
|
+
# the parameters for the media range.
|
24
|
+
#
|
25
|
+
# For example, the media range "application/xhtml;q=0.5" would
|
26
|
+
# get parsed into:
|
27
|
+
#
|
28
|
+
# ["application", "xhtml", { "q" => "0.5" }]
|
29
|
+
def parse_mime_type(mime_type)
|
30
|
+
parts = mime_type.split(";")
|
31
|
+
|
32
|
+
params = {}
|
33
|
+
|
34
|
+
parts[1..-1].map do |param|
|
35
|
+
k,v = param.split("=").map { |s| s.strip }
|
36
|
+
params[k] = v
|
37
|
+
end
|
38
|
+
|
39
|
+
full_type = parts[0].strip
|
40
|
+
# Java URLConnection class sends an Accept header that includes a single "*"
|
41
|
+
# Turn it into a legal wildcard.
|
42
|
+
full_type = "*/*" if full_type == "*"
|
43
|
+
type, subtype = full_type.split("/")
|
44
|
+
raise "malformed mime type" unless subtype
|
45
|
+
|
46
|
+
[type.strip, subtype.strip, params]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Carves up a media range and returns an Array of the
|
50
|
+
# [type, subtype, params] where "params" is a Hash of all
|
51
|
+
# the parameters for the media range.
|
52
|
+
#
|
53
|
+
# For example, the media range "application/*;q=0.5" would
|
54
|
+
# get parsed into:
|
55
|
+
#
|
56
|
+
# ["application", "*", { "q", "0.5" }]
|
57
|
+
#
|
58
|
+
# In addition this function also guarantees that there
|
59
|
+
# is a value for "q" in the params dictionary, filling it
|
60
|
+
# in with a proper default if necessary.
|
61
|
+
def parse_media_range(range)
|
62
|
+
type, subtype, params = parse_mime_type(range)
|
63
|
+
unless params.has_key?("q") and params["q"] and params["q"].to_f and params["q"].to_f <= 1 and params["q"].to_f >= 0
|
64
|
+
params["q"] = "1"
|
65
|
+
end
|
66
|
+
|
67
|
+
[type, subtype, params]
|
68
|
+
end
|
69
|
+
|
70
|
+
# Find the best match for a given mime-type against a list of
|
71
|
+
# media_ranges that have already been parsed by #parse_media_range
|
72
|
+
#
|
73
|
+
# Returns the fitness and the "q" quality parameter of the best match,
|
74
|
+
# or [-1, 0] if no match was found. Just as for #quality_parsed,
|
75
|
+
# "parsed_ranges" must be an Enumerable of parsed media ranges.
|
76
|
+
def fitness_and_quality_parsed(mime_type, parsed_ranges)
|
77
|
+
best_fitness = -1
|
78
|
+
best_fit_q = 0
|
79
|
+
target_type, target_subtype, target_params = parse_media_range(mime_type)
|
80
|
+
|
81
|
+
parsed_ranges.each do |type,subtype,params|
|
82
|
+
if (type == target_type or type == "*" or target_type == "*") and
|
83
|
+
(subtype == target_subtype or subtype == "*" or target_subtype == "*")
|
84
|
+
param_matches = target_params.find_all { |k,v| k != "q" and params.has_key?(k) and v == params[k] }.length
|
85
|
+
|
86
|
+
fitness = (type == target_type) ? 100 : 0
|
87
|
+
fitness += (subtype == target_subtype) ? 10 : 0
|
88
|
+
fitness += param_matches
|
89
|
+
|
90
|
+
if fitness > best_fitness
|
91
|
+
best_fitness = fitness
|
92
|
+
best_fit_q = params["q"]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
[best_fitness, best_fit_q.to_f]
|
98
|
+
end
|
99
|
+
|
100
|
+
# Find the best match for a given mime-type against a list of
|
101
|
+
# media_ranges that have already been parsed by #parse_media_range
|
102
|
+
#
|
103
|
+
# Returns the "q" quality parameter of the best match, 0 if no match
|
104
|
+
# was found. This function behaves the same as #quality except that
|
105
|
+
# "parsed_ranges" must be an Enumerable of parsed media ranges.
|
106
|
+
def quality_parsed(mime_type, parsed_ranges)
|
107
|
+
fitness_and_quality_parsed(mime_type, parsed_ranges)[1]
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns the quality "q" of a mime_type when compared against
|
111
|
+
# the media-ranges in ranges. For example:
|
112
|
+
#
|
113
|
+
# irb> quality("text/html", "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5")
|
114
|
+
# => 0.7
|
115
|
+
def quality(mime_type, ranges)
|
116
|
+
parsed_ranges = ranges.split(",").map { |r| parse_media_range(r) }
|
117
|
+
quality_parsed(mime_type, parsed_ranges)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Takes a list of supported mime-types and finds the best match
|
121
|
+
# for all the media-ranges listed in header. The value of header
|
122
|
+
# must be a string that conforms to the format of the HTTP Accept:
|
123
|
+
# header. The value of supported is an Enumerable of mime-types
|
124
|
+
#
|
125
|
+
# irb> best_match(["application/xbel+xml", "text/xml"], "text/*;q=0.5,*/*; q=0.1")
|
126
|
+
# => "text/xml"
|
127
|
+
def best_match(supported, header)
|
128
|
+
parsed_header = header.split(",").map { |r| parse_media_range(r) }
|
129
|
+
|
130
|
+
weighted_matches = supported.map do |mime_type|
|
131
|
+
[fitness_and_quality_parsed(mime_type, parsed_header), mime_type]
|
132
|
+
end
|
133
|
+
|
134
|
+
weighted_matches.sort!
|
135
|
+
|
136
|
+
weighted_matches.last[0][1].zero? ? nil : weighted_matches.last[1]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
if __FILE__ == $0
|
141
|
+
require "test/unit"
|
142
|
+
|
143
|
+
class TestMimeParsing < Test::Unit::TestCase
|
144
|
+
include MIMEParse
|
145
|
+
|
146
|
+
def test_parse_media_range
|
147
|
+
assert_equal [ "application", "xml", { "q" => "1" } ],
|
148
|
+
parse_media_range("application/xml;q=1")
|
149
|
+
|
150
|
+
assert_equal [ "application", "xml", { "q" => "1" } ],
|
151
|
+
parse_media_range("application/xml")
|
152
|
+
|
153
|
+
assert_equal [ "application", "xml", { "q" => "1" } ],
|
154
|
+
parse_media_range("application/xml;q=")
|
155
|
+
|
156
|
+
assert_equal [ "application", "xml", { "q" => "1", "b" => "other" } ],
|
157
|
+
parse_media_range("application/xml ; q=1;b=other")
|
158
|
+
|
159
|
+
assert_equal [ "application", "xml", { "q" => "1", "b" => "other" } ],
|
160
|
+
parse_media_range("application/xml ; q=2;b=other")
|
161
|
+
|
162
|
+
# Java URLConnection class sends an Accept header that includes a single "*"
|
163
|
+
assert_equal [ "*", "*", { "q" => ".2" } ],
|
164
|
+
parse_media_range(" *; q=.2")
|
165
|
+
end
|
166
|
+
|
167
|
+
def test_rfc_2616_example
|
168
|
+
accept = "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5"
|
169
|
+
|
170
|
+
assert_equal 1, quality("text/html;level=1", accept)
|
171
|
+
assert_equal 0.7, quality("text/html", accept)
|
172
|
+
assert_equal 0.3, quality("text/plain", accept)
|
173
|
+
assert_equal 0.5, quality("image/jpeg", accept)
|
174
|
+
assert_equal 0.4, quality("text/html;level=2", accept)
|
175
|
+
assert_equal 0.7, quality("text/html;level=3", accept)
|
176
|
+
end
|
177
|
+
|
178
|
+
def test_best_match
|
179
|
+
@supported_mime_types = [ "application/xbel+xml", "application/xml" ]
|
180
|
+
|
181
|
+
# direct match
|
182
|
+
assert_best_match "application/xbel+xml", "application/xbel+xml"
|
183
|
+
# direct match with a q parameter
|
184
|
+
assert_best_match "application/xbel+xml", "application/xbel+xml; q=1"
|
185
|
+
# direct match of our second choice with a q parameter
|
186
|
+
assert_best_match "application/xml", "application/xml; q=1"
|
187
|
+
# match using a subtype wildcard
|
188
|
+
assert_best_match "application/xml", "application/*; q=1"
|
189
|
+
# match using a type wildcard
|
190
|
+
assert_best_match "application/xml", "*/*"
|
191
|
+
|
192
|
+
@supported_mime_types = [ "application/xbel+xml", "text/xml" ]
|
193
|
+
# match using a type versus a lower weighted subtype
|
194
|
+
assert_best_match "text/xml", "text/*;q=0.5,*/*;q=0.1"
|
195
|
+
# fail to match anything
|
196
|
+
assert_best_match nil, "text/html,application/atom+xml; q=0.9"
|
197
|
+
# common AJAX scenario
|
198
|
+
@supported_mime_types = [ "application/json", "text/html" ]
|
199
|
+
assert_best_match "application/json", "application/json, text/javascript, */*"
|
200
|
+
# verify fitness sorting
|
201
|
+
assert_best_match "application/json", "application/json, text/html;q=0.9"
|
202
|
+
end
|
203
|
+
|
204
|
+
def test_support_wildcards
|
205
|
+
@supported_mime_types = ['image/*', 'application/xml']
|
206
|
+
# match using a type wildcard
|
207
|
+
assert_best_match 'image/*', 'image/png'
|
208
|
+
# match using a wildcard for both requested and supported
|
209
|
+
assert_best_match 'image/*', 'image/*'
|
210
|
+
end
|
211
|
+
|
212
|
+
def assert_best_match(expected, header)
|
213
|
+
assert_equal(expected, best_match(@supported_mime_types, header))
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'wants/match_result'
|
2
|
+
|
3
|
+
module Wants
|
4
|
+
class ValidateAcceptsMiddleware
|
5
|
+
|
6
|
+
DEFAULT_ON_NOT_ACCEPTABLE = lambda do |env|
|
7
|
+
[
|
8
|
+
406,
|
9
|
+
{
|
10
|
+
'Content-Type' => 'text/plain',
|
11
|
+
'Content-Length' => '14'
|
12
|
+
},
|
13
|
+
[ 'Not Acceptable' ]
|
14
|
+
]
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(app, options)
|
18
|
+
@app = app
|
19
|
+
|
20
|
+
@mime_types = options[:mime_types]
|
21
|
+
raise ArgumentError.new("#{self.class} requires option :mime_types") unless @mime_types
|
22
|
+
|
23
|
+
@on_not_acceptable = options[:on_not_acceptable] || DEFAULT_ON_NOT_ACCEPTABLE
|
24
|
+
end
|
25
|
+
|
26
|
+
def call(env)
|
27
|
+
if MatchResult.new(env['HTTP_ACCEPT'], @mime_types).present?
|
28
|
+
@app.call(env)
|
29
|
+
else
|
30
|
+
@on_not_acceptable.call(env)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
data/lib/wants.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Wants
|
2
|
+
|
3
|
+
class <<self
|
4
|
+
|
5
|
+
def new(env, mime_types)
|
6
|
+
MatchResult.new(env, mime_types)
|
7
|
+
end
|
8
|
+
|
9
|
+
def mime_lookup_table
|
10
|
+
@mime_lookup_table ||= begin
|
11
|
+
require 'rack/mime'
|
12
|
+
Rack::Mime::MIME_TYPES
|
13
|
+
rescue LoadError
|
14
|
+
{}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_writer :mime_lookup_table
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'wants/match_result'
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'wants/match_result'
|
3
|
+
|
4
|
+
describe Wants do
|
5
|
+
|
6
|
+
let(:accept) { 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' }
|
7
|
+
let(:available) { [ :html, :xhtml, :json ] }
|
8
|
+
|
9
|
+
subject { Wants::MatchResult.new(accept, available) }
|
10
|
+
|
11
|
+
describe 'when there are no acceptable MIME types' do
|
12
|
+
let(:accept) { 'application/atom+xml' }
|
13
|
+
|
14
|
+
it 'should be not_acceptable' do
|
15
|
+
subject.should be_not_acceptable
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should be blank' do
|
19
|
+
subject.should be_blank
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should not be present' do
|
23
|
+
subject.should_not be_present
|
24
|
+
end
|
25
|
+
|
26
|
+
describe 'any' do
|
27
|
+
it 'acts like a non-best-match MIME block method' do
|
28
|
+
evaluated = 0
|
29
|
+
|
30
|
+
subject.any { evaluated += 1; 'not acceptable' }.should == nil
|
31
|
+
evaluated.should == 0
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe 'not_acceptable' do
|
36
|
+
it 'acts like a best-match MIME block method' do
|
37
|
+
evaluated = 0
|
38
|
+
|
39
|
+
subject.not_acceptable { evaluated += 1; 'not_acceptable' }.should == 'not_acceptable'
|
40
|
+
evaluated.should == 1
|
41
|
+
|
42
|
+
subject.json { evaluated += 1; 'json' }.should == 'not_acceptable'
|
43
|
+
evaluated.should == 1
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe 'when there are acceptable MIME types' do
|
49
|
+
it 'should not be not_acceptable' do
|
50
|
+
subject.should_not be_not_acceptable
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should not be blank' do
|
54
|
+
subject.should_not be_blank
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should be present' do
|
58
|
+
subject.should be_present
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '[]' do
|
62
|
+
it 'returns true for the best match' do
|
63
|
+
subject['text/html'].should be_true
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'returns true for the best match as an abbreviation' do
|
67
|
+
subject[:html].should be_true
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'returns false for non-best matches' do
|
71
|
+
subject['application/xhtml+xml'].should be_false
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'returns false for non-matches' do
|
75
|
+
subject['application/atom+xml'].should be_false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe '#respond_to?' do
|
80
|
+
it 'returns true for MIME-like query methods' do
|
81
|
+
subject.respond_to?(:html?).should be_true
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'returns true for MIME block methods' do
|
85
|
+
subject.respond_to?(:json).should be_true
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe 'MIME query methods' do
|
90
|
+
it 'returns true for the best match as an abbreviation' do
|
91
|
+
subject.should be_html
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'returns false for non-best matches' do
|
95
|
+
subject.should_not be_xhtml
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'returns false for non-matches' do
|
99
|
+
subject.should_not be_atom
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'throws an exception if passed arguments' do
|
103
|
+
expect {
|
104
|
+
subject.html? :anything
|
105
|
+
}.to raise_error(ArgumentError)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe 'MIME block methods' do
|
110
|
+
it 'evaluates the block and returns its result only if the best match' do
|
111
|
+
evaluated = 0
|
112
|
+
|
113
|
+
subject.atom { evaluated += 1; 'atom' }.should be_nil
|
114
|
+
evaluated.should == 0
|
115
|
+
|
116
|
+
subject.html { evaluated += 1; 'html' }.should == 'html'
|
117
|
+
evaluated.should == 1
|
118
|
+
|
119
|
+
subject.json { evaluated += 1; 'json' }.should == 'html'
|
120
|
+
evaluated.should == 1
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe 'any' do
|
125
|
+
it 'acts like a best-match MIME block method' do
|
126
|
+
evaluated = 0
|
127
|
+
|
128
|
+
subject.any { evaluated += 1; 'any' }.should == 'any'
|
129
|
+
evaluated.should == 1
|
130
|
+
|
131
|
+
subject.json { evaluated += 1; 'json' }.should == 'any'
|
132
|
+
evaluated.should == 1
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
describe 'not_acceptable' do
|
137
|
+
it 'acts like a non-best-match MIME block method' do
|
138
|
+
evaluated = 0
|
139
|
+
|
140
|
+
subject.not_acceptable { evaluated += 1; 'not acceptable' }.should == nil
|
141
|
+
evaluated.should == 0
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'wants/validate_accepts_middleware'
|
3
|
+
|
4
|
+
describe Wants do
|
5
|
+
|
6
|
+
let(:upstream) { double('App') }
|
7
|
+
let(:accept) { nil }
|
8
|
+
let(:env) { { 'HTTP_ACCEPT' => accept } }
|
9
|
+
|
10
|
+
subject { Wants::ValidateAcceptsMiddleware.new(upstream, :mime_types => [ :html, :json ]) }
|
11
|
+
|
12
|
+
describe 'when there is an acceptable MIME type' do
|
13
|
+
let(:accept) { 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.1' }
|
14
|
+
|
15
|
+
it 'passes the request upstream' do
|
16
|
+
upstream.should_receive(:call).with(env)
|
17
|
+
subject.call(env)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'returns the upstream response' do
|
21
|
+
result = double('Result')
|
22
|
+
upstream.stub(:call) { result }
|
23
|
+
subject.call(env).should == result
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe 'when there is no acceptable MIME type' do
|
28
|
+
let(:accept) { 'application/atom+xml' }
|
29
|
+
|
30
|
+
it "doesn't pass the request upstream" do
|
31
|
+
upstream.should_not_receive(:call)
|
32
|
+
subject.call(env)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'returns a 406' do
|
36
|
+
status, headers, body = *subject.call(env)
|
37
|
+
status.should == 406
|
38
|
+
headers['Content-Type'].should == 'text/plain'
|
39
|
+
headers['Content-Length'].should == body.first.length.to_s
|
40
|
+
body.should == [ 'Not Acceptable' ]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
data/spec/wants_spec.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'wants'
|
3
|
+
|
4
|
+
describe Wants do
|
5
|
+
|
6
|
+
describe '.new' do
|
7
|
+
it 'delegates to Wants::MatchResult.new' do
|
8
|
+
env = Object.new
|
9
|
+
mime_types = [ :html, :json ]
|
10
|
+
result = Object.new
|
11
|
+
Wants::MatchResult.should_receive(:new).with(env, mime_types) { result }
|
12
|
+
Wants.new(env, mime_types).should == result
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '.mime_lookup_table' do
|
17
|
+
subject { Wants.mime_lookup_table }
|
18
|
+
|
19
|
+
before do
|
20
|
+
require 'rack/mime'
|
21
|
+
Wants.instance_eval do
|
22
|
+
@mime_lookup_table = nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe 'when Rack::Mime::MIME_TYPES is available' do
|
27
|
+
it 'defaults to that' do
|
28
|
+
subject.should == Rack::Mime::MIME_TYPES
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'when Rack::Mime::MIME_TYPES is unavailable' do
|
33
|
+
before do
|
34
|
+
Wants.stub(:require) { raise LoadError.new('Rack unavailable') }
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'defaults to an empty Hash' do
|
38
|
+
subject.should == {}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'can be set' do
|
43
|
+
table = Object.new
|
44
|
+
Wants.mime_lookup_table = table
|
45
|
+
subject.should == table
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: wants
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- James A. Rosen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rack
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rake
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: Parse and query the HTTP Accept header
|
63
|
+
email: james.a.rosen@gmail.com
|
64
|
+
executables: []
|
65
|
+
extensions: []
|
66
|
+
extra_rdoc_files: []
|
67
|
+
files:
|
68
|
+
- lib/wants/match_result.rb
|
69
|
+
- lib/wants/mimeparse.rb
|
70
|
+
- lib/wants/validate_accepts_middleware.rb
|
71
|
+
- lib/wants.rb
|
72
|
+
- README.md
|
73
|
+
- spec/match_result_spec.rb
|
74
|
+
- spec/spec_helper.rb
|
75
|
+
- spec/validate_accepts_middleware_spec.rb
|
76
|
+
- spec/wants_spec.rb
|
77
|
+
homepage: http://github.com/jamesarosen/wants
|
78
|
+
licenses: []
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ! '>='
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
requirements: []
|
96
|
+
rubyforge_project:
|
97
|
+
rubygems_version: 1.8.24
|
98
|
+
signing_key:
|
99
|
+
specification_version: 2
|
100
|
+
summary: HTTP Accept header support
|
101
|
+
test_files:
|
102
|
+
- spec/match_result_spec.rb
|
103
|
+
- spec/spec_helper.rb
|
104
|
+
- spec/validate_accepts_middleware_spec.rb
|
105
|
+
- spec/wants_spec.rb
|