roda 2.15.0 → 2.16.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.
- 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
|