wants 1.0.0
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.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
|