roda 2.15.0 → 2.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +8 -0
- data/doc/release_notes/2.16.0.txt +48 -0
- data/lib/roda.rb +6 -1
- data/lib/roda/plugins/per_thread_caching.rb +3 -7
- data/lib/roda/plugins/request_headers.rb +83 -0
- data/lib/roda/plugins/type_routing.rb +198 -0
- data/lib/roda/plugins/unescape_path.rb +32 -0
- data/lib/roda/version.rb +1 -1
- data/spec/plugin/request_headers_spec.rb +39 -0
- data/spec/plugin/type_routing_spec.rb +255 -0
- data/spec/plugin/unescape_path_spec.rb +22 -0
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c3051f95d0665c935a0aa391b7f5b3d4ec3905ec
|
4
|
+
data.tar.gz: c3a81d9c6384dc8708c941fcb8171295fcaff6bc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e6479fd6fffd9fdcefc3c09e3543142668b7cd15b3f52af8fda896d438a174b54c1a8350c5aea6a5745188f5eaec78b5fe3a11405365a1318ff38a0098acd307
|
7
|
+
data.tar.gz: 1a837206ad2a4a158e5c939c0a52107752f3e9c9108255e843db681c1f2a62265ee5ca54ba96071b441ca760de7ee2871cf58822ce9f12ed704fa283fcb786b2
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
= 2.16.0 (2016-07-13)
|
2
|
+
|
3
|
+
* Add type_routing plugin, for routing based on path extensions and Accept headers (Papierkorb, jeremyevans) (#75)
|
4
|
+
|
5
|
+
* Add unescape_path plugin, for decoding URL-encoded PATH_INFO before routing (jeremyevans) (#74)
|
6
|
+
|
7
|
+
* Add request_headers plugin, for simpler access to request headers (celsworth) (#72)
|
8
|
+
|
1
9
|
= 2.15.0 (2016-06-13)
|
2
10
|
|
3
11
|
* Add public plugin for r.public method for serving all files in the public directory (jeremyevans)
|
@@ -0,0 +1,48 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* A type_routing plugin has been added. This plugin allows routing
|
4
|
+
based on the requested type, which can be submitted either via a
|
5
|
+
file extension or Accept header:
|
6
|
+
|
7
|
+
plugin :type_routing
|
8
|
+
|
9
|
+
route do |r|
|
10
|
+
r.get 'a' do
|
11
|
+
r.html{ "<h1>This is the HTML response</h1>" }
|
12
|
+
r.json{ '{"json": "ok"}' }
|
13
|
+
r.xml{ "<root>This is the XML response</root>" }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# /a or /a.html => HTML response
|
18
|
+
# /a.json => JSON response
|
19
|
+
# /a.xml => XML response
|
20
|
+
|
21
|
+
The response content type is set appropriately when the r.html,
|
22
|
+
r.json, or r.xml block is yielded to. Using plugin options, you can
|
23
|
+
add support for custom types, and choose whether to use only file
|
24
|
+
extensions or only Accept headers for type matching.
|
25
|
+
|
26
|
+
* A request_headers plugin has been added. This allows easier access
|
27
|
+
to request headers. For example, to access a header called
|
28
|
+
X-My-Header, by default you would need to use the CGI mangled name:
|
29
|
+
|
30
|
+
r.env['HTTP_X_MY_HEADER']
|
31
|
+
|
32
|
+
The request_headers plugin allows the easier to use:
|
33
|
+
|
34
|
+
r.headers['X-My-Header']
|
35
|
+
|
36
|
+
* An unescape_path plugin has been added. By default, Roda does not
|
37
|
+
unescape a URL-encoded PATH_INFO before routing. This plugin allows
|
38
|
+
URL-encoded PATH_INFO to work, supporting %2f as well as / as path
|
39
|
+
separators, and having captures return unescaped values:
|
40
|
+
|
41
|
+
plugin :unescape_path
|
42
|
+
|
43
|
+
route do |r|
|
44
|
+
# Assume /b/a URL encoded at %2f%62%2f%61
|
45
|
+
r.on :x, /(.)/ do |*x|
|
46
|
+
# x => ['b', 'a']
|
47
|
+
end
|
48
|
+
end
|
data/lib/roda.rb
CHANGED
@@ -350,7 +350,7 @@ class Roda
|
|
350
350
|
def initialize(scope, env)
|
351
351
|
@scope = scope
|
352
352
|
@captures = []
|
353
|
-
@remaining_path = env
|
353
|
+
@remaining_path = _remaining_path(env)
|
354
354
|
super(env)
|
355
355
|
end
|
356
356
|
|
@@ -722,6 +722,11 @@ class Roda
|
|
722
722
|
SEGMENT
|
723
723
|
end
|
724
724
|
|
725
|
+
# The base remaining path to use.
|
726
|
+
def _remaining_path(env)
|
727
|
+
env[PATH_INFO]
|
728
|
+
end
|
729
|
+
|
725
730
|
# Backbone of the verb method support, using a terminal match if
|
726
731
|
# args is not empty, or a regular match if it is empty.
|
727
732
|
def _verb(args, &block)
|
@@ -6,13 +6,9 @@ class Roda
|
|
6
6
|
# The per_thread_caching plugin changes the default cache
|
7
7
|
# from being a shared thread safe cache to a separate cache per
|
8
8
|
# thread. This means getting or setting values no longer
|
9
|
-
# needs a mutex
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
# Note that it does not make sense to use this plugin on MRI,
|
14
|
-
# since the default cache on MRI doesn't use a mutex as it
|
15
|
-
# is already thread safe due to the GVL.
|
9
|
+
# needs a mutex, which may be faster when using a thread pool.
|
10
|
+
# However, since the caches are no longer shared, this will
|
11
|
+
# take up more memory.
|
16
12
|
#
|
17
13
|
# Using this plugin changes the matcher regexp cache to use
|
18
14
|
# per-thread caches, and changes the default for future
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
class Roda
|
6
|
+
module RodaPlugins
|
7
|
+
# The request_headers plugin provides access to headers sent in the
|
8
|
+
# request in a more natural way than directly accessing the env hash.
|
9
|
+
#
|
10
|
+
# In practise this means you don't need to uppercase, convert dashes
|
11
|
+
# to underscores, or add a HTTP_ prefix.
|
12
|
+
#
|
13
|
+
# For example, to access a header called X-My-Header you
|
14
|
+
# would previously need to do:
|
15
|
+
#
|
16
|
+
# r.env['HTTP_X_MY_HEADER']
|
17
|
+
#
|
18
|
+
# But with this plugin you can now say:
|
19
|
+
#
|
20
|
+
# r.headers['X-My-Header']
|
21
|
+
#
|
22
|
+
# The name is actually case-insensitive so x-my-header will work as well.
|
23
|
+
#
|
24
|
+
#
|
25
|
+
# Example:
|
26
|
+
#
|
27
|
+
# plugin :request_headers
|
28
|
+
#
|
29
|
+
module RequestHeaders
|
30
|
+
module RequestMethods
|
31
|
+
# Provide access to the request headers while normalising indexes.
|
32
|
+
def headers
|
33
|
+
@request_headers ||= Headers.new(@env)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Headers
|
38
|
+
# Set of environment variable names that don't need HTTP_ prepended to them.
|
39
|
+
CGI_VARIABLES = Set.new(%w'
|
40
|
+
AUTH_TYPE
|
41
|
+
CONTENT_LENGTH
|
42
|
+
CONTENT_TYPE
|
43
|
+
GATEWAY_INTERFACE
|
44
|
+
HTTPS
|
45
|
+
PATH_INFO
|
46
|
+
PATH_TRANSLATED
|
47
|
+
QUERY_STRING
|
48
|
+
REMOTE_ADDR
|
49
|
+
REMOTE_HOST
|
50
|
+
REMOTE_IDENT
|
51
|
+
REMOTE_USER
|
52
|
+
REQUEST_METHOD
|
53
|
+
SCRIPT_NAME
|
54
|
+
SERVER_NAME
|
55
|
+
SERVER_PORT
|
56
|
+
SERVER_PROTOCOL
|
57
|
+
SERVER_SOFTWARE
|
58
|
+
').freeze
|
59
|
+
|
60
|
+
def initialize(env)
|
61
|
+
@env = env
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns the value for the given key mapped to @env
|
65
|
+
def [](key)
|
66
|
+
@env[env_name(key)]
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# Convert a HTTP header name into an environment variable name
|
72
|
+
def env_name(key)
|
73
|
+
key = key.to_s.upcase
|
74
|
+
key.tr!('-', '_')
|
75
|
+
key = 'HTTP_' + key unless CGI_VARIABLES.include?(key)
|
76
|
+
key
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
register_plugin(:request_headers, RequestHeaders)
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
class Roda
|
5
|
+
module RodaPlugins
|
6
|
+
# Allows to respond to specific request data types. User agents can request
|
7
|
+
# specific data types by either supplying an appropriate +Accept+ header
|
8
|
+
# or by appending it as file extension to the path.
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
#
|
12
|
+
# plugin :type_routing
|
13
|
+
#
|
14
|
+
# route do |r|
|
15
|
+
# r.get 'a' do
|
16
|
+
# r.html{ "<h1>This is the HTML response</h1>" }
|
17
|
+
# r.json{ '{"json": "ok"}' }
|
18
|
+
# r.xml{ "<root>This is the XML response</root>" }
|
19
|
+
# "Unsupported data type"
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# This application will handle the following paths:
|
24
|
+
# /a.html :: HTML response
|
25
|
+
# /a.json :: JSON response
|
26
|
+
# /a.xml :: XML response
|
27
|
+
# /a :: HTML, JSON, or XML response, depending on the Accept header
|
28
|
+
#
|
29
|
+
# The response +Content-Type+ header will be set to a suitable value when
|
30
|
+
# the block is matched.
|
31
|
+
#
|
32
|
+
# Note that if no match is found, code will continue to execute, which can
|
33
|
+
# result in unexpected behaviour. This should only happen if you do not
|
34
|
+
# handle all supported/configured types. If you want to simplify handling,
|
35
|
+
# you can just place the html handling after the other types, without using
|
36
|
+
# a separate block:
|
37
|
+
#
|
38
|
+
# route do |r|
|
39
|
+
# r.get 'a' do
|
40
|
+
# r.json{ '{"json": "ok"}' }
|
41
|
+
# r.xml{ "<root>This is the XML response</root>" }
|
42
|
+
#
|
43
|
+
# "<h1>This is the HTML response</h1>"
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# This works correctly because Roda assumes the html type by default.
|
48
|
+
#
|
49
|
+
# To match custom extensions, use the :types option:
|
50
|
+
#
|
51
|
+
# plugin :type_routing, :types => {
|
52
|
+
# :yaml => 'application/x-yaml',
|
53
|
+
# :js => 'application/javascript; charset=utf-8',
|
54
|
+
# }
|
55
|
+
#
|
56
|
+
# route do |r|
|
57
|
+
# r.get 'a' do
|
58
|
+
# r.yaml{ YAML.dump "YAML data" }
|
59
|
+
# r.js{ "JavaScript code" }
|
60
|
+
# # or:
|
61
|
+
# r.on_type(:js){ "JavaScript code" }
|
62
|
+
# "Unsupported data type"
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# = Plugin options
|
67
|
+
#
|
68
|
+
# The following plugin options are supported:
|
69
|
+
#
|
70
|
+
# :default_type :: The default data type to assume if the client did not
|
71
|
+
# provide one. Defaults to +:html+.
|
72
|
+
# :exclude :: Exclude one or more types from the default set (default set
|
73
|
+
# is :html, :xml, :json).
|
74
|
+
# :types :: Mapping from a data type to its MIME-Type. Used both to match
|
75
|
+
# incoming requests and to provide +Content-Type+ values. If the
|
76
|
+
# value is +nil+, no +Content-Type+ will be set. The type may
|
77
|
+
# contain media type parameters, which will be sent to the client
|
78
|
+
# but ignored for request matching.
|
79
|
+
# :use_extension :: Whether to take the path extension into account.
|
80
|
+
# Default is +true+.
|
81
|
+
# :use_header :: Whether to take the +Accept+ header into account.
|
82
|
+
# Default is +true+.
|
83
|
+
module TypeRouting
|
84
|
+
ACCEPT_HEADER = 'HTTP_ACCEPT'.freeze
|
85
|
+
CONTENT_TYPE_HEADER = 'Content-Type'.freeze
|
86
|
+
|
87
|
+
CONFIGURATION = {
|
88
|
+
:mimes => {
|
89
|
+
'text/json' => :json,
|
90
|
+
'application/json' => :json,
|
91
|
+
'text/xml' => :xml,
|
92
|
+
'application/xml' => :xml,
|
93
|
+
'text/html' => :html,
|
94
|
+
}.freeze,
|
95
|
+
:types => {
|
96
|
+
:json => 'application/json'.freeze,
|
97
|
+
:xml => 'application/xml'.freeze,
|
98
|
+
:html => 'text/html'.freeze,
|
99
|
+
}.freeze,
|
100
|
+
:use_extension => true,
|
101
|
+
:use_header => true,
|
102
|
+
:default_type => :html
|
103
|
+
}.freeze
|
104
|
+
|
105
|
+
def self.configure(app, opts = {})
|
106
|
+
config = (app.opts[:type_routing] || CONFIGURATION).dup
|
107
|
+
[:use_extension, :use_header, :default_type].each do |key|
|
108
|
+
config[key] = opts[key] if opts.has_key?(key)
|
109
|
+
end
|
110
|
+
|
111
|
+
types = config[:types] = config[:types].dup
|
112
|
+
mimes = config[:mimes] = config[:mimes].dup
|
113
|
+
|
114
|
+
Array(opts[:exclude]).each do |type|
|
115
|
+
types.delete(type)
|
116
|
+
mimes.reject!{|_, v| v == type}
|
117
|
+
end
|
118
|
+
|
119
|
+
if mapping = opts[:types]
|
120
|
+
types.merge!(mapping)
|
121
|
+
|
122
|
+
mapping.each do |k, v|
|
123
|
+
if v
|
124
|
+
mimes[v.split(';', 2).first] = k
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
types.freeze
|
130
|
+
mimes.freeze
|
131
|
+
|
132
|
+
type_keys = config[:types].keys
|
133
|
+
config[:extension_regexp] = /(.+)\.(#{Regexp.union(type_keys.map(&:to_s))})\z/
|
134
|
+
|
135
|
+
type_keys.each do |type|
|
136
|
+
app::RodaRequest.send(:define_method, type) do |&block|
|
137
|
+
on_type(type, &block)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
app.opts[:type_routing] = config.freeze
|
142
|
+
end
|
143
|
+
|
144
|
+
module RequestMethods
|
145
|
+
# Yields if the given +type+ matches the requested data type and halts
|
146
|
+
# the request afterwards, returning the result of the block.
|
147
|
+
def on_type(type, &block)
|
148
|
+
return unless type == requested_type
|
149
|
+
response[CONTENT_TYPE_HEADER] ||= @scope.opts[:type_routing][:types][type]
|
150
|
+
always(&block)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Returns the data type the client requests.
|
154
|
+
def requested_type
|
155
|
+
return @requested_type if defined?(@requested_type)
|
156
|
+
|
157
|
+
opts = @scope.opts[:type_routing]
|
158
|
+
@requested_type = accept_response_type if opts[:use_header]
|
159
|
+
@requested_type ||= opts[:default_type]
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
|
164
|
+
# Removes a trailing file extension from the path, and sets
|
165
|
+
# the requested type if so.
|
166
|
+
def _remaining_path(env)
|
167
|
+
opts = scope.opts[:type_routing]
|
168
|
+
path = super
|
169
|
+
|
170
|
+
if opts[:use_extension]
|
171
|
+
if m = path.match(opts[:extension_regexp])
|
172
|
+
@requested_type = m[2].to_sym
|
173
|
+
path = m[1]
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
path
|
178
|
+
end
|
179
|
+
|
180
|
+
# The response type indicated by the Accept request header.
|
181
|
+
def accept_response_type
|
182
|
+
mimes = @scope.opts[:type_routing][:mimes]
|
183
|
+
|
184
|
+
@env[ACCEPT_HEADER].to_s.split(/\s*,\s*/).map do |part|
|
185
|
+
mime, _= part.split(/\s*;\s*/, 2)
|
186
|
+
if sym = mimes[mime]
|
187
|
+
return sym
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
nil
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
register_plugin(:type_routing, TypeRouting)
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
class Roda
|
5
|
+
module RodaPlugins
|
6
|
+
# The unescape_path plugin decodes a URL-encoded path
|
7
|
+
# before routing. This fixes routing when the slashes
|
8
|
+
# are URL-encoded as %2f and returns decoded parameters
|
9
|
+
# when matched by symbols or regexps.
|
10
|
+
#
|
11
|
+
# plugin :unescape_path
|
12
|
+
#
|
13
|
+
# route do |r|
|
14
|
+
# # Assume /b/a URL encoded at %2f%62%2f%61
|
15
|
+
# r.on :x, /(.)/ do |*x|
|
16
|
+
# # x => ['b', 'a']
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
module UnescapePath
|
20
|
+
module RequestMethods
|
21
|
+
private
|
22
|
+
|
23
|
+
# Unescape the path.
|
24
|
+
def _remaining_path(env)
|
25
|
+
Rack::Utils.unescape(super)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
register_plugin(:unescape_path, UnescapePath)
|
31
|
+
end
|
32
|
+
end
|
data/lib/roda/version.rb
CHANGED
@@ -0,0 +1,39 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
describe "request_headers plugin" do
|
4
|
+
def header_app(header_name)
|
5
|
+
app(:bare) do
|
6
|
+
plugin :request_headers
|
7
|
+
route do |r|
|
8
|
+
r.on do
|
9
|
+
# return the value of the request header in the response body,
|
10
|
+
# or the static string 'not found' if it hasn't been supplied.
|
11
|
+
r.headers[header_name] || 'not found'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it "must add HTTP_ prefix when appropriate" do
|
18
|
+
header_app('Foo')
|
19
|
+
body('/', {'HTTP_FOO' => 'a'}).must_equal 'a'
|
20
|
+
end
|
21
|
+
|
22
|
+
it "must ignore HTTP_ prefix when appropriate" do
|
23
|
+
header_app('Content-Type')
|
24
|
+
body('/', {'CONTENT_TYPE' => 'a'}).must_equal 'a'
|
25
|
+
end
|
26
|
+
|
27
|
+
it "must return nil for non-existant headers" do
|
28
|
+
header_app('X-Non-Existant')
|
29
|
+
body('/').must_equal 'not found'
|
30
|
+
end
|
31
|
+
|
32
|
+
it "must be case-insensitive" do
|
33
|
+
header_app('X-My-Header')
|
34
|
+
body('/', {'HTTP_X_MY_HEADER' => 'a'}).must_equal 'a'
|
35
|
+
|
36
|
+
header_app('x-my-header')
|
37
|
+
body('/', {'HTTP_X_MY_HEADER' => 'a'}).must_equal 'a'
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,255 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
|
4
|
+
|
5
|
+
describe "type_routing plugin" do
|
6
|
+
before do
|
7
|
+
app(:type_routing) do |r|
|
8
|
+
r.is 'a' do
|
9
|
+
r.html{ "HTML: #{r.requested_type}" }
|
10
|
+
r.json{ "JSON: #{r.requested_type}" }
|
11
|
+
r.xml{ "XML: #{r.requested_type}" }
|
12
|
+
"No match"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it "uses the file extension in the path" do
|
18
|
+
body('/a').must_equal 'HTML: html'
|
19
|
+
header('Content-Type', '/a').must_equal 'text/html'
|
20
|
+
|
21
|
+
body('/a.html').must_equal 'HTML: html'
|
22
|
+
header('Content-Type', '/a.html').must_equal 'text/html'
|
23
|
+
|
24
|
+
body('/a.json').must_equal 'JSON: json'
|
25
|
+
header('Content-Type', '/a.json').must_equal 'application/json'
|
26
|
+
|
27
|
+
body('/a.xml').must_equal 'XML: xml'
|
28
|
+
header('Content-Type', '/a.xml').must_equal 'application/xml'
|
29
|
+
|
30
|
+
status('/a.yadda').must_equal 404
|
31
|
+
end
|
32
|
+
|
33
|
+
it "uses the Accept header value" do
|
34
|
+
body('/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'HTML: html'
|
35
|
+
header('Content-Type', '/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'text/html'
|
36
|
+
|
37
|
+
body('/a', 'HTTP_ACCEPT' => 'application/json').must_equal 'JSON: json'
|
38
|
+
header('Content-Type', '/a', 'HTTP_ACCEPT' => 'application/json').must_equal 'application/json'
|
39
|
+
|
40
|
+
body('/a', 'HTTP_ACCEPT' => 'application/xml').must_equal 'XML: xml'
|
41
|
+
header('Content-Type', '/a', 'HTTP_ACCEPT' => 'application/xml').must_equal 'application/xml'
|
42
|
+
|
43
|
+
body('/a', 'HTTP_ACCEPT' => 'some/thing').must_equal 'HTML: html'
|
44
|
+
header('Content-Type', '/a', 'HTTP_ACCEPT' => 'some/thing').must_equal 'text/html'
|
45
|
+
end
|
46
|
+
|
47
|
+
it "favors the file extension over the Accept header" do
|
48
|
+
body('/a.json', 'HTTP_ACCEPT' => 'text/html').must_equal 'JSON: json'
|
49
|
+
body('/a.xml', 'HTTP_ACCEPT' => 'application/json').must_equal 'XML: xml'
|
50
|
+
body('/a.html', 'HTTP_ACCEPT' => 'application/xml').must_equal 'HTML: html'
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
it "uses the default if neither file extension nor Accept header are given" do
|
55
|
+
body('/a').must_equal 'HTML: html'
|
56
|
+
header('Content-Type', '/a').must_equal 'text/html'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "type_routing plugin" do
|
61
|
+
it "does not use the file extension if its disabled" do
|
62
|
+
app(:bare) do
|
63
|
+
plugin :type_routing, :use_extension => false
|
64
|
+
|
65
|
+
route do |r|
|
66
|
+
r.is 'a' do
|
67
|
+
r.html{ "HTML" }
|
68
|
+
r.json{ "JSON" }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
status('/a.json').must_equal 404
|
74
|
+
status('/a.html').must_equal 404
|
75
|
+
body('/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'HTML'
|
76
|
+
body('/a', 'HTTP_ACCEPT' => 'application/json').must_equal 'JSON'
|
77
|
+
end
|
78
|
+
|
79
|
+
it "does not use the Accept header if its disabled" do
|
80
|
+
app(:bare) do
|
81
|
+
plugin :type_routing, :use_header => false
|
82
|
+
|
83
|
+
route do |r|
|
84
|
+
r.is 'a' do
|
85
|
+
r.html{ "HTML" }
|
86
|
+
r.json{ "JSON" }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
body('/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'HTML'
|
92
|
+
body('/a', 'HTTP_ACCEPT' => 'application/json').must_equal 'HTML'
|
93
|
+
body('/a.html', 'HTTP_ACCEPT' => 'application/json').must_equal 'HTML'
|
94
|
+
body('/a.json', 'HTTP_ACCEPT' => 'text/html').must_equal 'JSON'
|
95
|
+
end
|
96
|
+
|
97
|
+
it "only eats known file extensions" do
|
98
|
+
app(:bare) do
|
99
|
+
plugin :type_routing
|
100
|
+
|
101
|
+
route do |r|
|
102
|
+
r.is 'a' do
|
103
|
+
r.html{ "HTML" }
|
104
|
+
r.json{ "JSON" }
|
105
|
+
r.xml{ "XML" }
|
106
|
+
raise "Mismatch!"
|
107
|
+
end
|
108
|
+
|
109
|
+
r.is 'a.jpg' do
|
110
|
+
"Okay"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
body('/a.html').must_equal 'HTML'
|
116
|
+
body('/a.json').must_equal 'JSON'
|
117
|
+
body('/a.xml').must_equal 'XML'
|
118
|
+
body('/a.jpg').must_equal 'Okay'
|
119
|
+
end
|
120
|
+
|
121
|
+
it "uses custom data types" do
|
122
|
+
app(:bare) do
|
123
|
+
plugin :type_routing, :types => { :yaml => 'application/x-yaml' }
|
124
|
+
|
125
|
+
route do |r|
|
126
|
+
r.is 'a' do
|
127
|
+
r.html{ "HTML" }
|
128
|
+
r.yaml{ "YAML" }
|
129
|
+
raise "Mismatch!"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
body('/a.html').must_equal 'HTML'
|
135
|
+
body('/a.yaml').must_equal 'YAML'
|
136
|
+
header('Content-Type', '/a.yaml').must_equal 'application/x-yaml'
|
137
|
+
end
|
138
|
+
|
139
|
+
it "handles response-specific type information when using custom types" do
|
140
|
+
app(:bare) do
|
141
|
+
plugin :type_routing, :exclude=>:html, :default_type=>:json, :types => { :html => 'text/html; charset=utf-8' }
|
142
|
+
|
143
|
+
route do |r|
|
144
|
+
r.is 'a' do
|
145
|
+
r.json{ "JSON" }
|
146
|
+
r.html{ "HTML" }
|
147
|
+
raise "Mismatch!"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
body('/a').must_equal 'JSON'
|
153
|
+
body('/a.html').must_equal 'HTML'
|
154
|
+
header('Content-Type', '/a.html').must_equal 'text/html; charset=utf-8'
|
155
|
+
header('Content-Type', '/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'text/html; charset=utf-8'
|
156
|
+
end
|
157
|
+
|
158
|
+
it "Handle nil content type when using custom types" do
|
159
|
+
app(:bare) do
|
160
|
+
plugin :type_routing, :exclude=>:html, :default_type=>:json, :types => { :html => nil}
|
161
|
+
|
162
|
+
route do |r|
|
163
|
+
r.is 'a' do
|
164
|
+
r.html{ "HTML" }
|
165
|
+
r.json{ "JSON" }
|
166
|
+
raise "Mismatch!"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
body('/a').must_equal 'JSON'
|
172
|
+
body('/a.html').must_equal 'HTML'
|
173
|
+
header('Content-Type', '/a.html').must_equal 'text/html'
|
174
|
+
header('Content-Type', '/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'application/json'
|
175
|
+
end
|
176
|
+
|
177
|
+
it "uses custom default type" do
|
178
|
+
app(:bare) do
|
179
|
+
plugin :type_routing, :default_type => :json
|
180
|
+
|
181
|
+
route do |r|
|
182
|
+
r.is 'a' do
|
183
|
+
r.html{ "HTML" }
|
184
|
+
r.json{ "JSON" }
|
185
|
+
raise "Mismatch!"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
body('/a').must_equal 'JSON'
|
191
|
+
body('/a.html').must_equal 'HTML'
|
192
|
+
body('/a.json').must_equal 'JSON'
|
193
|
+
end
|
194
|
+
|
195
|
+
it "supports nil default type" do
|
196
|
+
app(:bare) do
|
197
|
+
plugin :type_routing, :default_type => nil
|
198
|
+
|
199
|
+
route do |r|
|
200
|
+
r.is 'a' do
|
201
|
+
r.html{ "HTML" }
|
202
|
+
r.json{ "JSON" }
|
203
|
+
"None"
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
body('/a').must_equal 'None'
|
209
|
+
body('/a.html').must_equal 'HTML'
|
210
|
+
body('/a.json').must_equal 'JSON'
|
211
|
+
end
|
212
|
+
|
213
|
+
it "excludes given types" do
|
214
|
+
app(:bare) do
|
215
|
+
plugin :type_routing, :exclude => [ :xml ]
|
216
|
+
|
217
|
+
route do |r|
|
218
|
+
r.is 'a' do
|
219
|
+
r.html{ "HTML" }
|
220
|
+
r.json{ "JSON" }
|
221
|
+
r.xml{ raise "Mismatch!" }
|
222
|
+
raise "Mismatch"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
body('/a.html').must_equal 'HTML'
|
228
|
+
body('/a.json').must_equal 'JSON'
|
229
|
+
status('/a.xml').must_equal 404
|
230
|
+
|
231
|
+
body('/a', 'HTTP_ACCEPT' => 'text/xml').must_equal 'HTML'
|
232
|
+
body('/a', 'HTTP_ACCEPT' => 'application/json').must_equal 'JSON'
|
233
|
+
body('/a', 'HTTP_ACCEPT' => 'text/xml').must_equal 'HTML'
|
234
|
+
body('/a', 'HTTP_ACCEPT' => 'application/xml').must_equal 'HTML'
|
235
|
+
end
|
236
|
+
|
237
|
+
it "handles loading the plugin multiple times correctly" do
|
238
|
+
app(:bare) do
|
239
|
+
plugin :type_routing, :default_type => :json
|
240
|
+
plugin :type_routing
|
241
|
+
|
242
|
+
route do |r|
|
243
|
+
r.is 'a' do
|
244
|
+
r.html{ "HTML" }
|
245
|
+
r.json{ "JSON" }
|
246
|
+
raise "Mismatch!"
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
body('/a').must_equal 'JSON'
|
252
|
+
body('/a.html').must_equal 'HTML'
|
253
|
+
body('/a.json').must_equal 'JSON'
|
254
|
+
end
|
255
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
describe "unescape_path_path plugin" do
|
4
|
+
it "decodes URL-encoded routing path" do
|
5
|
+
app(:unescape_path) do |r|
|
6
|
+
r.on 'b' do
|
7
|
+
r.get /(.)/ do |a|
|
8
|
+
"#{a}-b"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
r.get :name do |name|
|
13
|
+
name
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
body('/a').must_equal 'a'
|
18
|
+
body('/%61').must_equal 'a'
|
19
|
+
body('%2f%61').must_equal 'a'
|
20
|
+
body('%2f%62%2f%61').must_equal 'a-b'
|
21
|
+
end
|
22
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: roda
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.16.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Evans
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-07-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -181,6 +181,7 @@ extra_rdoc_files:
|
|
181
181
|
- doc/release_notes/2.13.0.txt
|
182
182
|
- doc/release_notes/2.14.0.txt
|
183
183
|
- doc/release_notes/2.15.0.txt
|
184
|
+
- doc/release_notes/2.16.0.txt
|
184
185
|
files:
|
185
186
|
- CHANGELOG
|
186
187
|
- MIT-LICENSE
|
@@ -199,6 +200,7 @@ files:
|
|
199
200
|
- doc/release_notes/2.13.0.txt
|
200
201
|
- doc/release_notes/2.14.0.txt
|
201
202
|
- doc/release_notes/2.15.0.txt
|
203
|
+
- doc/release_notes/2.16.0.txt
|
202
204
|
- doc/release_notes/2.2.0.txt
|
203
205
|
- doc/release_notes/2.3.0.txt
|
204
206
|
- doc/release_notes/2.4.0.txt
|
@@ -264,6 +266,7 @@ files:
|
|
264
266
|
- lib/roda/plugins/public.rb
|
265
267
|
- lib/roda/plugins/render.rb
|
266
268
|
- lib/roda/plugins/render_each.rb
|
269
|
+
- lib/roda/plugins/request_headers.rb
|
267
270
|
- lib/roda/plugins/response_request.rb
|
268
271
|
- lib/roda/plugins/run_handler.rb
|
269
272
|
- lib/roda/plugins/shared_vars.rb
|
@@ -276,6 +279,8 @@ files:
|
|
276
279
|
- lib/roda/plugins/symbol_matchers.rb
|
277
280
|
- lib/roda/plugins/symbol_status.rb
|
278
281
|
- lib/roda/plugins/symbol_views.rb
|
282
|
+
- lib/roda/plugins/type_routing.rb
|
283
|
+
- lib/roda/plugins/unescape_path.rb
|
279
284
|
- lib/roda/plugins/view_options.rb
|
280
285
|
- lib/roda/plugins/view_subdirs.rb
|
281
286
|
- lib/roda/plugins/websockets.rb
|
@@ -346,6 +351,7 @@ files:
|
|
346
351
|
- spec/plugin/public_spec.rb
|
347
352
|
- spec/plugin/render_each_spec.rb
|
348
353
|
- spec/plugin/render_spec.rb
|
354
|
+
- spec/plugin/request_headers_spec.rb
|
349
355
|
- spec/plugin/response_request_spec.rb
|
350
356
|
- spec/plugin/run_handler_spec.rb
|
351
357
|
- spec/plugin/shared_vars_spec.rb
|
@@ -357,6 +363,8 @@ files:
|
|
357
363
|
- spec/plugin/symbol_matchers_spec.rb
|
358
364
|
- spec/plugin/symbol_status_spec.rb
|
359
365
|
- spec/plugin/symbol_views_spec.rb
|
366
|
+
- spec/plugin/type_routing_spec.rb
|
367
|
+
- spec/plugin/unescape_path_spec.rb
|
360
368
|
- spec/plugin/view_options_spec.rb
|
361
369
|
- spec/plugin/websockets_spec.rb
|
362
370
|
- spec/plugin_spec.rb
|