roda 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +70 -0
- data/README.rdoc +261 -302
- data/Rakefile +1 -1
- data/doc/release_notes/1.2.0.txt +406 -0
- data/lib/roda.rb +206 -124
- data/lib/roda/plugins/all_verbs.rb +11 -10
- data/lib/roda/plugins/assets.rb +5 -5
- data/lib/roda/plugins/backtracking_array.rb +12 -5
- data/lib/roda/plugins/caching.rb +10 -8
- data/lib/roda/plugins/class_level_routing.rb +94 -0
- data/lib/roda/plugins/content_for.rb +6 -0
- data/lib/roda/plugins/default_headers.rb +4 -11
- data/lib/roda/plugins/delay_build.rb +42 -0
- data/lib/roda/plugins/delegate.rb +64 -0
- data/lib/roda/plugins/drop_body.rb +33 -0
- data/lib/roda/plugins/empty_root.rb +48 -0
- data/lib/roda/plugins/environments.rb +68 -0
- data/lib/roda/plugins/error_email.rb +1 -2
- data/lib/roda/plugins/error_handler.rb +1 -1
- data/lib/roda/plugins/halt.rb +7 -5
- data/lib/roda/plugins/head.rb +4 -2
- data/lib/roda/plugins/header_matchers.rb +17 -9
- data/lib/roda/plugins/hooks.rb +16 -32
- data/lib/roda/plugins/json.rb +4 -10
- data/lib/roda/plugins/mailer.rb +233 -0
- data/lib/roda/plugins/match_affix.rb +48 -0
- data/lib/roda/plugins/multi_route.rb +9 -11
- data/lib/roda/plugins/multi_run.rb +81 -0
- data/lib/roda/plugins/named_templates.rb +93 -0
- data/lib/roda/plugins/not_allowed.rb +43 -48
- data/lib/roda/plugins/path.rb +63 -2
- data/lib/roda/plugins/render.rb +79 -48
- data/lib/roda/plugins/render_each.rb +6 -0
- data/lib/roda/plugins/sinatra_helpers.rb +523 -0
- data/lib/roda/plugins/slash_path_empty.rb +25 -0
- data/lib/roda/plugins/static_path_info.rb +64 -0
- data/lib/roda/plugins/streaming.rb +1 -1
- data/lib/roda/plugins/view_subdirs.rb +12 -8
- data/lib/roda/version.rb +1 -1
- data/spec/integration_spec.rb +33 -0
- data/spec/plugin/backtracking_array_spec.rb +24 -18
- data/spec/plugin/class_level_routing_spec.rb +138 -0
- data/spec/plugin/delay_build_spec.rb +23 -0
- data/spec/plugin/delegate_spec.rb +20 -0
- data/spec/plugin/drop_body_spec.rb +20 -0
- data/spec/plugin/empty_root_spec.rb +14 -0
- data/spec/plugin/environments_spec.rb +31 -0
- data/spec/plugin/h_spec.rb +1 -3
- data/spec/plugin/header_matchers_spec.rb +14 -0
- data/spec/plugin/hooks_spec.rb +3 -5
- data/spec/plugin/mailer_spec.rb +191 -0
- data/spec/plugin/match_affix_spec.rb +22 -0
- data/spec/plugin/multi_run_spec.rb +31 -0
- data/spec/plugin/named_templates_spec.rb +65 -0
- data/spec/plugin/path_spec.rb +66 -2
- data/spec/plugin/render_spec.rb +46 -1
- data/spec/plugin/sinatra_helpers_spec.rb +534 -0
- data/spec/plugin/slash_path_empty_spec.rb +22 -0
- data/spec/plugin/static_path_info_spec.rb +50 -0
- data/spec/request_spec.rb +23 -0
- data/spec/response_spec.rb +12 -1
- metadata +48 -6
@@ -6,10 +6,9 @@ class Roda
|
|
6
6
|
# trace, unlink.
|
7
7
|
#
|
8
8
|
# These methods operate just like Roda's default get and post
|
9
|
-
# methods
|
10
|
-
#
|
11
|
-
#
|
12
|
-
# match the full path.
|
9
|
+
# methods, so using them without any arguments just checks for
|
10
|
+
# the request method, while using them with any arguments also
|
11
|
+
# checks that the arguments match the full path.
|
13
12
|
#
|
14
13
|
# Example:
|
15
14
|
#
|
@@ -30,12 +29,14 @@ class Roda
|
|
30
29
|
# The verb methods are defined via metaprogramming, so there
|
31
30
|
# isn't documentation for the individual methods created.
|
32
31
|
module AllVerbs
|
33
|
-
|
34
|
-
%w'delete head options link patch put trace unlink'.each do |
|
35
|
-
if ::Rack::Request.method_defined?("#{
|
36
|
-
|
37
|
-
|
38
|
-
|
32
|
+
module RequestMethods
|
33
|
+
%w'delete head options link patch put trace unlink'.each do |verb|
|
34
|
+
if ::Rack::Request.method_defined?("#{verb}?")
|
35
|
+
class_eval(<<-END, __FILE__, __LINE__+1)
|
36
|
+
def #{verb}(*args, &block)
|
37
|
+
_verb(args, &block) if #{verb}?
|
38
|
+
end
|
39
|
+
END
|
39
40
|
end
|
40
41
|
end
|
41
42
|
end
|
data/lib/roda/plugins/assets.rb
CHANGED
@@ -197,7 +197,7 @@ class Roda
|
|
197
197
|
# and compressing files (default: false)
|
198
198
|
# :css_dir :: Directory name containing your css source, inside :path (default: 'css')
|
199
199
|
# :css_headers :: A hash of additional headers for your rendered css files
|
200
|
-
# :css_opts ::
|
200
|
+
# :css_opts :: Template options to pass to the render plugin (via :opts) when rendering css assets
|
201
201
|
# :css_route :: Route under :prefix for css assets (default: :css_dir)
|
202
202
|
# :dependencies :: A hash of dependencies for your asset files. Keys should be paths to asset files,
|
203
203
|
# values should be arrays of paths your asset files depends on. This is used to
|
@@ -207,7 +207,7 @@ class Roda
|
|
207
207
|
# :headers :: A hash of additional headers for both js and css rendered files
|
208
208
|
# :js_dir :: Directory name containing your javascript source, inside :path (default: 'js')
|
209
209
|
# :js_headers :: A hash of additional headers for your rendered javascript files
|
210
|
-
# :js_opts ::
|
210
|
+
# :js_opts :: Template options to pass to the render plugin (via :opts) when rendering javascript assets
|
211
211
|
# :js_route :: Route under :prefix for javascript assets (default: :js_dir)
|
212
212
|
# :path :: Path to your asset source directory (default: 'assets')
|
213
213
|
# :prefix :: Prefix for assets path in your URL/routes (default: 'assets')
|
@@ -520,7 +520,7 @@ class Roda
|
|
520
520
|
if file.end_with?(".#{type}")
|
521
521
|
::File.read(file)
|
522
522
|
else
|
523
|
-
render_asset_file(file, self.class.assets_opts[:"#{type}_opts"])
|
523
|
+
render_asset_file(file, :opts=>self.class.assets_opts[:"#{type}_opts"])
|
524
524
|
end
|
525
525
|
end
|
526
526
|
|
@@ -541,8 +541,8 @@ class Roda
|
|
541
541
|
# a 304 response immediately. Otherwise, add the appropriate
|
542
542
|
# type-specific headers.
|
543
543
|
def check_asset_request(file, type, mtime)
|
544
|
-
|
545
|
-
|
544
|
+
@_request.last_modified(mtime)
|
545
|
+
@_response.headers.merge!(self.class.assets_opts[:"#{type}_headers"])
|
546
546
|
end
|
547
547
|
|
548
548
|
# Render the given asset file using the render plugin, with the given options.
|
@@ -35,10 +35,13 @@ class Roda
|
|
35
35
|
# entry in the array.
|
36
36
|
def _match_array(arg, rest=nil)
|
37
37
|
return super unless rest
|
38
|
-
env = @env
|
39
38
|
|
40
|
-
|
41
|
-
|
39
|
+
unless path = @remaining_path
|
40
|
+
e = @env
|
41
|
+
script = e[SCRIPT_NAME]
|
42
|
+
path = e[PATH_INFO]
|
43
|
+
end
|
44
|
+
captures = @captures
|
42
45
|
caps = captures.dup
|
43
46
|
arg.each do |v|
|
44
47
|
if match(v, rest)
|
@@ -52,8 +55,12 @@ class Roda
|
|
52
55
|
|
53
56
|
# Matching all remaining elements failed, reset state
|
54
57
|
captures.replace(caps)
|
55
|
-
|
56
|
-
|
58
|
+
if @remaining_path
|
59
|
+
@remaining_path = path
|
60
|
+
else
|
61
|
+
e[SCRIPT_NAME] = script
|
62
|
+
e[PATH_INFO] = path
|
63
|
+
end
|
57
64
|
end
|
58
65
|
end
|
59
66
|
false
|
data/lib/roda/plugins/caching.rb
CHANGED
@@ -87,18 +87,19 @@ class Roda
|
|
87
87
|
# with a 412 status.
|
88
88
|
def last_modified(time)
|
89
89
|
return unless time
|
90
|
-
|
90
|
+
res = response
|
91
91
|
e = env
|
92
|
+
res[LAST_MODIFIED] = time.httpdate
|
92
93
|
return if e[HTTP_IF_NONE_MATCH]
|
93
|
-
status =
|
94
|
+
status = res.status
|
94
95
|
|
95
96
|
if (!status || status == 200) && (ims = time_from_header(e[HTTP_IF_MODIFIED_SINCE])) && ims >= time.to_i
|
96
|
-
|
97
|
+
res.status = 304
|
97
98
|
halt
|
98
99
|
end
|
99
100
|
|
100
101
|
if (!status || (status >= 200 && status < 300) || status == 412) && (ius = time_from_header(e[HTTP_IF_UNMODIFIED_SINCE])) && ius < time.to_i
|
101
|
-
|
102
|
+
res.status = 412
|
102
103
|
halt
|
103
104
|
end
|
104
105
|
end
|
@@ -122,19 +123,20 @@ class Roda
|
|
122
123
|
weak = opts[:weak]
|
123
124
|
new_resource = opts.fetch(:new_resource){post?}
|
124
125
|
|
125
|
-
|
126
|
-
status = response.status
|
126
|
+
res = response
|
127
127
|
e = env
|
128
|
+
res[ETAG] = etag = "#{'W/' if weak}\"#{value}\""
|
129
|
+
status = res.status
|
128
130
|
|
129
131
|
if (!status || (status >= 200 && status < 300) || status == 304)
|
130
132
|
if etag_matches?(e[HTTP_IF_NONE_MATCH], etag, new_resource)
|
131
|
-
|
133
|
+
res.status = (request_method =~ /\AGET|HEAD|OPTIONS|TRACE\z/i ? 304 : 412)
|
132
134
|
halt
|
133
135
|
end
|
134
136
|
|
135
137
|
if ifm = e[HTTP_IF_MATCH]
|
136
138
|
unless etag_matches?(ifm, etag, new_resource)
|
137
|
-
|
139
|
+
res.status = 412
|
138
140
|
halt
|
139
141
|
end
|
140
142
|
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The class_level_routing plugin adds routing methods at the class level, which can
|
4
|
+
# be used instead of or in addition to using the normal +route+ method to start the
|
5
|
+
# routing tree. If a request is not matched by the normal routing tree, the class
|
6
|
+
# level routes will be tried. This offers a more Sinatra-like API, while
|
7
|
+
# still allowing you to use a routing tree inside individual actions.
|
8
|
+
#
|
9
|
+
# Here's the first example from the README, modified to use the class_level_routing
|
10
|
+
# plugin:
|
11
|
+
#
|
12
|
+
# class App < Roda
|
13
|
+
# plugin :class_level_routing
|
14
|
+
#
|
15
|
+
# # GET / request
|
16
|
+
# root do
|
17
|
+
# request.redirect "/hello"
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# # GET /hello/world request
|
21
|
+
# get "hello/world" do
|
22
|
+
# "Hello world!"
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# # /hello request
|
26
|
+
# is "hello" do
|
27
|
+
# # GET /hello request
|
28
|
+
# request.get do
|
29
|
+
# "Hello!"
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# # POST /hello request
|
33
|
+
# request.post do
|
34
|
+
# puts "Someone said hello!"
|
35
|
+
# request.redirect
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# When using the the class_level_routing plugin with nested routes, you may also want to use the
|
41
|
+
# delegate plugin to delegate certain instance methods to the request object, so you don't have
|
42
|
+
# to continually use +request.+ in your routing blocks.
|
43
|
+
#
|
44
|
+
# Note that class level routing is implemented via a simple array of routes, so routing performance
|
45
|
+
# will degrade linearly as the number of routes increases. For best performance, you should use
|
46
|
+
# the normal +route+ class method to define your routing tree. This plugin does make it simpler to
|
47
|
+
# add additional routes after the routing tree has already been defined, though.
|
48
|
+
module ClassLevelRouting
|
49
|
+
# Initialize the class_routes array when the plugin is loaded. Also, if the application doesn't
|
50
|
+
# currently have a routing block, setup an empty routing block so that things will still work if
|
51
|
+
# a routing block isn't added.
|
52
|
+
def self.configure(app)
|
53
|
+
app.route{} unless app.route_block
|
54
|
+
app.opts[:class_level_routes] ||= []
|
55
|
+
end
|
56
|
+
|
57
|
+
module ClassMethods
|
58
|
+
# Define routing methods that will store class level routes.
|
59
|
+
[:root, :on, :is, :get, :post, :delete, :head, :options, :link, :patch, :put, :trace, :unlink].each do |meth|
|
60
|
+
define_method(meth) do |*args, &block|
|
61
|
+
opts[:class_level_routes] << [meth, args, block]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
module InstanceMethods
|
67
|
+
private
|
68
|
+
|
69
|
+
# If the normal routing tree doesn't handle an action, try each class level route
|
70
|
+
# to see if it matches.
|
71
|
+
def _route(&block)
|
72
|
+
result = super
|
73
|
+
|
74
|
+
if result[0] == 404 && (v = result[2]).is_a?(Array) && v.empty?
|
75
|
+
# Reset the response so it doesn't inherit the status or any headers from
|
76
|
+
# the original response.
|
77
|
+
@_response = self.class::RodaResponse.new
|
78
|
+
super do |r|
|
79
|
+
opts[:class_level_routes].each do |meth, args, blk|
|
80
|
+
r.send(meth, *args) do |*a|
|
81
|
+
instance_exec(*a, &blk)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
else
|
86
|
+
result
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
register_plugin(:class_level_routing, ClassLevelRouting)
|
93
|
+
end
|
94
|
+
end
|
@@ -21,6 +21,12 @@ class Roda
|
|
21
21
|
#
|
22
22
|
# <%= content_for :foo %>
|
23
23
|
module ContentFor
|
24
|
+
# Depend on the render plugin, since this plugin only makes
|
25
|
+
# sense when the render plugin is used.
|
26
|
+
def self.load_dependencies(app)
|
27
|
+
app.plugin :render
|
28
|
+
end
|
29
|
+
|
24
30
|
module InstanceMethods
|
25
31
|
# If called with a block, store content enclosed by block
|
26
32
|
# under the given key. If called without a block, retrieve
|
@@ -20,27 +20,20 @@ class Roda
|
|
20
20
|
module DefaultHeaders
|
21
21
|
# Merge the given headers into the existing default headers, if any.
|
22
22
|
def self.configure(app, headers={})
|
23
|
-
app.
|
24
|
-
@default_headers ||= {}
|
25
|
-
@default_headers.merge!(headers)
|
26
|
-
end
|
23
|
+
(app.opts[:default_headers] ||= {}).merge!(headers)
|
27
24
|
end
|
28
25
|
|
29
26
|
module ClassMethods
|
30
27
|
# The default response headers to use for the current class.
|
31
|
-
|
32
|
-
|
33
|
-
# Copy the default headers into the subclass when inheriting.
|
34
|
-
def inherited(subclass)
|
35
|
-
super
|
36
|
-
subclass.instance_variable_set(:@default_headers, default_headers.dup)
|
28
|
+
def default_headers
|
29
|
+
opts[:default_headers]
|
37
30
|
end
|
38
31
|
end
|
39
32
|
|
40
33
|
module ResponseMethods
|
41
34
|
# Get the default headers from the related roda class.
|
42
35
|
def default_headers
|
43
|
-
|
36
|
+
roda_class.default_headers.dup
|
44
37
|
end
|
45
38
|
end
|
46
39
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The delay_build plugin does not build the rack app until
|
4
|
+
# Roda.app is called, and only rebuilds the rack app if Roda.build!
|
5
|
+
# is called. This differs from Roda's default behavior, which
|
6
|
+
# rebuilds the rack app every time the route block changes and
|
7
|
+
# every time middleware is added if a route block has already
|
8
|
+
# been defined.
|
9
|
+
#
|
10
|
+
# If you are loading hundreds of middleware after a
|
11
|
+
# route block has already been defined, this can fix a possible
|
12
|
+
# performance issue, turning an O(n^2) calculation into an
|
13
|
+
# O(n) calculation, where n is the number of middleware used.
|
14
|
+
module DelayBuild
|
15
|
+
module ClassMethods
|
16
|
+
# If the app is not been defined yet, build the app.
|
17
|
+
def app
|
18
|
+
@app || build!
|
19
|
+
end
|
20
|
+
|
21
|
+
# Rebuild the application.
|
22
|
+
def build!
|
23
|
+
@build_app = true
|
24
|
+
build_rack_app
|
25
|
+
@app
|
26
|
+
ensure
|
27
|
+
@build_app = false
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Do not build the rack app automatically, wait for an
|
33
|
+
# explicit call to build!.
|
34
|
+
def build_rack_app
|
35
|
+
super if @build_app
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
register_plugin(:delay_build, DelayBuild)
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The delegate plugin allows you to easily setup instance methods in
|
4
|
+
# the scope of the route block that call methods on the related
|
5
|
+
# request or response, which may offer a simpler API in some cases.
|
6
|
+
# Roda doesn't automatically setup such delegate methods because
|
7
|
+
# it pollutes the application's method namespace, but this plugin
|
8
|
+
# allows the user to do so.
|
9
|
+
#
|
10
|
+
# Here's an example based on the README's initial example, using the
|
11
|
+
# request_delegate method to simplify the DSL:
|
12
|
+
#
|
13
|
+
# plugin :delegate
|
14
|
+
# request_delegate :root, :on, :is, :get, :post, :redirect
|
15
|
+
#
|
16
|
+
# route do |r|
|
17
|
+
# # GET / request
|
18
|
+
# root do
|
19
|
+
# redirect "/hello"
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# # /hello branch
|
23
|
+
# on "hello" do
|
24
|
+
# # GET /hello/world request
|
25
|
+
# get "world" do
|
26
|
+
# "Hello world!"
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# # /hello request
|
30
|
+
# is do
|
31
|
+
# # GET /hello request
|
32
|
+
# get do
|
33
|
+
# "Hello!"
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# # POST /hello request
|
37
|
+
# post do
|
38
|
+
# puts "Someone said hello!"
|
39
|
+
# redirect
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
module Delegate
|
45
|
+
module ClassMethods
|
46
|
+
# Delegate the given methods to the request
|
47
|
+
def request_delegate(*meths)
|
48
|
+
meths.each do |meth|
|
49
|
+
define_method(meth){|*a, &block| @_request.send(meth, *a, &block)}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Delegate the given methods to the response
|
54
|
+
def response_delegate(*meths)
|
55
|
+
meths.each do |meth|
|
56
|
+
define_method(meth){|*a, &block| @_response.send(meth, *a, &block)}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
register_plugin(:delegate, Delegate)
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The drop_body plugin automatically drops the body and
|
4
|
+
# Content-Type/Content-Length headers from the response if
|
5
|
+
# the response status indicates that the response should
|
6
|
+
# not include a body (response statuses 100, 101, 102, 204, 205,
|
7
|
+
# and 304).
|
8
|
+
module DropBody
|
9
|
+
module ResponseMethods
|
10
|
+
DROP_BODY_STATUSES = [100, 101, 102, 204, 205, 304].freeze
|
11
|
+
EMPTY_BODY = [].freeze
|
12
|
+
CONTENT_LENGTH = "Content-Length".freeze
|
13
|
+
CONTENT_TYPE = "Content-Type".freeze
|
14
|
+
|
15
|
+
# If the response status indicates a body should not be
|
16
|
+
# returned, use an empty body and remove the Content-Length
|
17
|
+
# and Content-Type headers.
|
18
|
+
def finish
|
19
|
+
r = super
|
20
|
+
if DROP_BODY_STATUSES.include?(r[0])
|
21
|
+
r[2] = EMPTY_BODY
|
22
|
+
h = r[1]
|
23
|
+
h.delete(CONTENT_LENGTH)
|
24
|
+
h.delete(CONTENT_TYPE)
|
25
|
+
end
|
26
|
+
r
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
register_plugin(:drop_body, DropBody)
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The empty_root plugin makes +r.root+ match both on +/+ and
|
4
|
+
# on the empty string. This is mostly useful when using multiple
|
5
|
+
# rack applications, where the initial PATH_INFO has been moved
|
6
|
+
# to SCRIPT_NAME. For example, if you have the following
|
7
|
+
# applications:
|
8
|
+
#
|
9
|
+
# class App1 < Roda
|
10
|
+
# on "albums" do
|
11
|
+
# run App2
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# class App2 < Roda
|
16
|
+
# plugin :empty_root
|
17
|
+
#
|
18
|
+
# route do |r|
|
19
|
+
# r.root do
|
20
|
+
# "root"
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# Then requests for both +/albums/+ and +/albums+ will return
|
26
|
+
# "root". Without this plugin loaded into App2, only requests
|
27
|
+
# for +/albums/+ will return "root", since by default, +r.root+
|
28
|
+
# matches only when the current PATH_INFO is +/+ and not when
|
29
|
+
# it is empty.
|
30
|
+
module EmptyRoot
|
31
|
+
EMPTY_STRING = ''.freeze
|
32
|
+
|
33
|
+
module RequestMethods
|
34
|
+
# Match when the remaining path is the empty string,
|
35
|
+
# in addition to the default behavior of matching when
|
36
|
+
# the remaining path is +/+.
|
37
|
+
def root(&block)
|
38
|
+
super
|
39
|
+
if remaining_path == EMPTY_STRING && is_get?
|
40
|
+
always(&block)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
register_plugin(:empty_root, EmptyRoot)
|
47
|
+
end
|
48
|
+
end
|