roda 1.1.0 → 1.2.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 +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
|