roda 3.44.0 → 3.48.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 +16 -0
- data/README.rdoc +11 -14
- data/doc/release_notes/3.45.0.txt +22 -0
- data/doc/release_notes/3.46.0.txt +19 -0
- data/doc/release_notes/3.47.0.txt +13 -0
- data/doc/release_notes/3.48.0.txt +10 -0
- data/lib/roda/plugins/_optimized_matching.rb +177 -0
- data/lib/roda/plugins/assets.rb +16 -0
- data/lib/roda/plugins/content_for.rb +1 -2
- data/lib/roda/plugins/mail_processor.rb +1 -1
- data/lib/roda/plugins/multi_route.rb +21 -93
- data/lib/roda/plugins/named_routes.rb +160 -0
- data/lib/roda/plugins/type_routing.rb +1 -1
- data/lib/roda/plugins/typecast_params.rb +28 -1
- data/lib/roda/request.rb +5 -7
- data/lib/roda/version.rb +1 -1
- data/lib/roda.rb +4 -0
- metadata +13 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34691daba87a890b955df8a9c89ac11e3b5b2ce3ac4726b5d4dbd526aaa5c8ce
|
|
4
|
+
data.tar.gz: e2d8399a4a17a432d39e531ef25db62028622bd258194e930ef3d2b55a722bb8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 728e35715a74a04b373b3fa36fe8a7b81298de6d6c159de0ad95e654346e8c55d593d9ad4f067a0d8e1a569434085b333549efcc0467b28158332493b2ab8087
|
|
7
|
+
data.tar.gz: ee4d3b86e0b15dae0126b6ad032cfbe3055e9a1010f3cd0cf2d847cfb12ca4910b61f0030baa3e94fac8ac52304ca60a2b7825f2080cf28d51579dfd3d8cefa6
|
data/CHANGELOG
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
= 3.48.0 (2021-09-13)
|
|
2
|
+
|
|
3
|
+
* Extract named_routes plugin from multi_route plugin (jeremyevans)
|
|
4
|
+
|
|
5
|
+
= 3.47.0 (2021-08-13)
|
|
6
|
+
|
|
7
|
+
* Automatically optimize remaining r.on calls with a single argument (jeremyevans)
|
|
8
|
+
|
|
9
|
+
= 3.46.0 (2021-07-12)
|
|
10
|
+
|
|
11
|
+
* Automatically optimize r.on/r.is/r.get/r.post methods with a single string, String, Integer, or regexp argument (jeremyevans)
|
|
12
|
+
|
|
13
|
+
= 3.45.0 (2021-06-14)
|
|
14
|
+
|
|
15
|
+
* Make typecast_params plugin check for null bytes in strings by default, with :allow_null_bytes option for previous behavior (jeremyevans)
|
|
16
|
+
|
|
1
17
|
= 3.44.0 (2021-05-12)
|
|
2
18
|
|
|
3
19
|
* Add optimized_segment_matchers plugin for optimized matchers for a single String class argument (jeremyevans)
|
data/README.rdoc
CHANGED
|
@@ -13,7 +13,6 @@ Website :: http://roda.jeremyevans.net
|
|
|
13
13
|
Source :: http://github.com/jeremyevans/roda
|
|
14
14
|
Bugs :: http://github.com/jeremyevans/roda/issues
|
|
15
15
|
Google Group :: http://groups.google.com/group/ruby-roda
|
|
16
|
-
IRC :: irc://chat.freenode.net/#roda
|
|
17
16
|
|
|
18
17
|
== Goals
|
|
19
18
|
|
|
@@ -965,19 +964,17 @@ option. If you really want to turn path checking off, you can do so via the
|
|
|
965
964
|
Roda does not ship with integrated support for code reloading, but there are rack-based
|
|
966
965
|
reloaders that will work with Roda apps.
|
|
967
966
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
rack-unreloader
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
does not rely on autoloading then +require_dependency+ must be used to require the dependencies
|
|
980
|
-
or they won't be reloaded.
|
|
967
|
+
{Zeitwerk}[https://github.com/fxn/zeitwerk] (which Rails now uses for reloading) can be used
|
|
968
|
+
with Roda. It requires minimal setup and handles most cases. It overrides +require+ when
|
|
969
|
+
activated. If it can meet the needs of your application, it's probably the best approach.
|
|
970
|
+
|
|
971
|
+
{rack-unreloader}[https://github.com/jeremyevans/rack-unreloader] uses a fast
|
|
972
|
+
approach to reloading while still being fairly safe, as it only reloads files that have
|
|
973
|
+
been modified, and unloads constants defined in the files before reloading them. It can handle
|
|
974
|
+
advanced cases that Zeitwerk does not support, such as classes defined in multiple files
|
|
975
|
+
(common when using separate route files for different routing branches in the same application).
|
|
976
|
+
However, rack-unreloader does not modify core classes and using it requires modifying your
|
|
977
|
+
application code to use rack-unreloader specific APIs, which may not be simple.
|
|
981
978
|
|
|
982
979
|
{AutoReloader}[https://github.com/rosenfeld/auto_reloader] provides transparent reloading for
|
|
983
980
|
all files reached from one of the +reloadable_paths+ option entries, by detecting new top-level
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
= Improvements
|
|
2
|
+
|
|
3
|
+
* The typecast_params plugin checks now checks for null bytes by
|
|
4
|
+
default before typecasting. If null bytes are present, it raises
|
|
5
|
+
an error. Most applications do not require null bytes in
|
|
6
|
+
parameters, and in some cases allowing them can lead to security
|
|
7
|
+
issues, especially when parameters are passed to C extensions.
|
|
8
|
+
In general, the benefit of forbidding null bytes in parameters is
|
|
9
|
+
greater than the cost.
|
|
10
|
+
|
|
11
|
+
If you would like to continue allowing null bytes, use the
|
|
12
|
+
:allow_null_bytes option when loading the plugin.
|
|
13
|
+
|
|
14
|
+
Note that this change does not affect uploaded files, since those
|
|
15
|
+
are expected to contain null bytes.
|
|
16
|
+
|
|
17
|
+
= Backwards Compatibility
|
|
18
|
+
|
|
19
|
+
* The change to the typecast_params plugin to raise an error for
|
|
20
|
+
null bytes can break applications that are expecting null bytes
|
|
21
|
+
to be passed in parameters. Such applications should use the
|
|
22
|
+
:allow_null_bytes option when loading the plugin.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
= Improvements
|
|
2
|
+
|
|
3
|
+
* The r.on, r.is, r.get and r.post methods (and other verb methods
|
|
4
|
+
if using the all_verbs plugin) have now been optimized when using
|
|
5
|
+
a single string or regexp matcher, or the String or Integer class
|
|
6
|
+
matcher. Since those four matchers are the most common types of
|
|
7
|
+
matchers passed to the methods, this can significantly improve
|
|
8
|
+
routing performance (about 50% in the r10k benchmark).
|
|
9
|
+
|
|
10
|
+
This optimization is automatically applied when freezing
|
|
11
|
+
applications, if the related methods have not been modified by
|
|
12
|
+
plugins.
|
|
13
|
+
|
|
14
|
+
This optimization does come at the expense of a small decrease
|
|
15
|
+
in routing performance (3-4%) for unoptimized cases, but the
|
|
16
|
+
majority of applications will see a overall performance benefit
|
|
17
|
+
from this change.
|
|
18
|
+
|
|
19
|
+
* Other minor performance improvements have been made.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
= Improvements
|
|
2
|
+
|
|
3
|
+
* The r.on optimization added in 3.46.0 has been extended to optimize
|
|
4
|
+
all single argument calls. This results in the following speedups
|
|
5
|
+
based on argument type:
|
|
6
|
+
|
|
7
|
+
* Hash matching: 10%
|
|
8
|
+
* Array/Symbol/Class matching: 15%
|
|
9
|
+
* Proc matching: 25%
|
|
10
|
+
* true matching: 45%
|
|
11
|
+
* false/nil matching: 65%
|
|
12
|
+
|
|
13
|
+
* Other minor performance improvements have been made.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
= New Features
|
|
2
|
+
|
|
3
|
+
* A named_routes plugin has been added, for defining named route
|
|
4
|
+
blocks that you can dispatch to with r.route. This feature was
|
|
5
|
+
previously available as part of the multi_route plugin, but there
|
|
6
|
+
are cases where the r.route method and support for named routes is
|
|
7
|
+
helpful even when the multi_route plugin is not used (such as when
|
|
8
|
+
the hash_routes plugin is used instead of the multi_route plugin).
|
|
9
|
+
The multi_route plugin now depends on the named_routes plugin, so
|
|
10
|
+
this change should not cause any backwards compatibility issues.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
class Roda
|
|
5
|
+
module RodaPlugins
|
|
6
|
+
# The _optimized_matching plugin is automatically used internally to speed
|
|
7
|
+
# up matching when a single argument String instance, String class, Integer
|
|
8
|
+
# class, or Regexp matcher is passed to +r.on+, +r.is_+, or a verb method
|
|
9
|
+
# such as +r.get+ or +r.post+.
|
|
10
|
+
#
|
|
11
|
+
# The optimization works by avoiding the +if_match+ method if possible.
|
|
12
|
+
# Instead of clearing the captures array on every call, and having the
|
|
13
|
+
# matching append to the captures, it checks directly for the match,
|
|
14
|
+
# and on succesful match, it yields directly to the block without using
|
|
15
|
+
# the captures array.
|
|
16
|
+
module OptimizedMatching
|
|
17
|
+
TERM = Base::RequestMethods::TERM
|
|
18
|
+
|
|
19
|
+
module RequestMethods
|
|
20
|
+
# Optimize the r.is method handling of a single string, String, Integer,
|
|
21
|
+
# regexp, or true, argument.
|
|
22
|
+
def is(*args, &block)
|
|
23
|
+
case args.length
|
|
24
|
+
when 1
|
|
25
|
+
_is1(args, &block)
|
|
26
|
+
when 0
|
|
27
|
+
always(&block) if @remaining_path.empty?
|
|
28
|
+
else
|
|
29
|
+
if_match(args << TERM, &block)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Optimize the r.on method handling of a single string, String, Integer,
|
|
34
|
+
# or regexp argument. Inline the related matching code to avoid the
|
|
35
|
+
# need to modify @captures.
|
|
36
|
+
def on(*args, &block)
|
|
37
|
+
case args.length
|
|
38
|
+
when 1
|
|
39
|
+
case matcher = args[0]
|
|
40
|
+
when String
|
|
41
|
+
always{yield} if _match_string(matcher)
|
|
42
|
+
when Class
|
|
43
|
+
if matcher == String
|
|
44
|
+
rp = @remaining_path
|
|
45
|
+
if rp.getbyte(0) == 47
|
|
46
|
+
if last = rp.index('/', 1)
|
|
47
|
+
@remaining_path = rp[last, rp.length]
|
|
48
|
+
always{yield rp[1, last-1]}
|
|
49
|
+
elsif (len = rp.length) > 1
|
|
50
|
+
@remaining_path = ""
|
|
51
|
+
always{yield rp[1, len]}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
elsif matcher == Integer
|
|
55
|
+
if matchdata = /\A\/(\d+)(?=\/|\z)/.match(@remaining_path)
|
|
56
|
+
@remaining_path = matchdata.post_match
|
|
57
|
+
always{yield(matchdata[1].to_i)}
|
|
58
|
+
end
|
|
59
|
+
else
|
|
60
|
+
path = @remaining_path
|
|
61
|
+
captures = @captures.clear
|
|
62
|
+
meth = :"_match_class_#{matcher}"
|
|
63
|
+
if respond_to?(meth, true)
|
|
64
|
+
# Allow calling private methods, as match methods are generally private
|
|
65
|
+
if send(meth, &block)
|
|
66
|
+
block_result(yield(*captures))
|
|
67
|
+
throw :halt, response.finish
|
|
68
|
+
else
|
|
69
|
+
@remaining_path = path
|
|
70
|
+
false
|
|
71
|
+
end
|
|
72
|
+
else
|
|
73
|
+
unsupported_matcher(matcher)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
when Regexp
|
|
77
|
+
if matchdata = self.class.cached_matcher(matcher){matcher}.match(@remaining_path)
|
|
78
|
+
@remaining_path = matchdata.post_match
|
|
79
|
+
always{yield(*matchdata.captures)}
|
|
80
|
+
end
|
|
81
|
+
when true
|
|
82
|
+
always(&block)
|
|
83
|
+
when false, nil
|
|
84
|
+
# nothing
|
|
85
|
+
else
|
|
86
|
+
path = @remaining_path
|
|
87
|
+
captures = @captures.clear
|
|
88
|
+
|
|
89
|
+
matched = case matcher
|
|
90
|
+
when Array
|
|
91
|
+
_match_array(matcher)
|
|
92
|
+
when Hash
|
|
93
|
+
_match_hash(matcher)
|
|
94
|
+
when Symbol
|
|
95
|
+
_match_symbol(matcher)
|
|
96
|
+
when Proc
|
|
97
|
+
matcher.call
|
|
98
|
+
else
|
|
99
|
+
unsupported_matcher(matcher)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
if matched
|
|
103
|
+
block_result(yield(*captures))
|
|
104
|
+
throw :halt, response.finish
|
|
105
|
+
else
|
|
106
|
+
@remaining_path = path
|
|
107
|
+
false
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
when 0
|
|
111
|
+
always(&block)
|
|
112
|
+
else
|
|
113
|
+
if_match(args, &block)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
# Optimize the r.get/r.post method handling of a single string, String, Integer,
|
|
120
|
+
# regexp, or true, argument.
|
|
121
|
+
def _verb(args, &block)
|
|
122
|
+
case args.length
|
|
123
|
+
when 0
|
|
124
|
+
always(&block)
|
|
125
|
+
when 1
|
|
126
|
+
_is1(args, &block)
|
|
127
|
+
else
|
|
128
|
+
if_match(args << TERM, &block)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Internals of r.is/r.get/r.post optimization. Inline the related matching
|
|
133
|
+
# code to avoid the need to modify @captures.
|
|
134
|
+
def _is1(args, &block)
|
|
135
|
+
case matcher = args[0]
|
|
136
|
+
when String
|
|
137
|
+
rp = @remaining_path
|
|
138
|
+
if _match_string(matcher)
|
|
139
|
+
if @remaining_path.empty?
|
|
140
|
+
always{yield}
|
|
141
|
+
else
|
|
142
|
+
@remaining_path = rp
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
when Class
|
|
147
|
+
if matcher == String
|
|
148
|
+
rp = @remaining_path
|
|
149
|
+
if rp.getbyte(0) == 47 && !rp.index('/', 1) && (len = rp.length) > 1
|
|
150
|
+
@remaining_path = ''
|
|
151
|
+
always{yield rp[1, len]}
|
|
152
|
+
end
|
|
153
|
+
elsif matcher == Integer
|
|
154
|
+
if matchdata = /\A\/(\d+)\z/.match(@remaining_path)
|
|
155
|
+
@remaining_path = ''
|
|
156
|
+
always{yield(matchdata[1].to_i)}
|
|
157
|
+
end
|
|
158
|
+
else
|
|
159
|
+
if_match(args << TERM, &block)
|
|
160
|
+
end
|
|
161
|
+
when Regexp
|
|
162
|
+
if (matchdata = self.class.cached_matcher(matcher){matcher}.match(@remaining_path)) && matchdata.post_match.empty?
|
|
163
|
+
@remaining_path = ''
|
|
164
|
+
always{yield(*matchdata.captures)}
|
|
165
|
+
end
|
|
166
|
+
when true
|
|
167
|
+
always(&block) if @remaining_path.empty?
|
|
168
|
+
else
|
|
169
|
+
if_match(args << TERM, &block)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
register_plugin(:_optimized_matching, OptimizedMatching)
|
|
176
|
+
end
|
|
177
|
+
end
|
data/lib/roda/plugins/assets.rb
CHANGED
|
@@ -152,6 +152,22 @@ class Roda
|
|
|
152
152
|
# together and not compressed during compilation. You can use the
|
|
153
153
|
# :css_compressor and :js_compressor options to specify the compressor to use.
|
|
154
154
|
#
|
|
155
|
+
# It is also possible to use the built-in compression options in the CSS or JS
|
|
156
|
+
# compiler, assuming the compiler supports such options. For example, with
|
|
157
|
+
# sass/sassc, you can use:
|
|
158
|
+
#
|
|
159
|
+
# plugin :assets,
|
|
160
|
+
# css_opts: {style: :compressed}
|
|
161
|
+
#
|
|
162
|
+
# === Source Maps (CSS)
|
|
163
|
+
#
|
|
164
|
+
# The assets plugin does not have direct support for source maps, so it is
|
|
165
|
+
# recommended you use embedded source maps if supported by the CSS compiler.
|
|
166
|
+
# For sass/sassc, you can use:
|
|
167
|
+
#
|
|
168
|
+
# plugin :assets,
|
|
169
|
+
# css_opts: {:source_map_embed=>true, source_map_contents: true, source_map_file: "."}
|
|
170
|
+
#
|
|
155
171
|
# === With Asset Groups
|
|
156
172
|
#
|
|
157
173
|
# When using asset groups, a separate compiled file will be produced per
|
|
@@ -62,8 +62,7 @@ class Roda
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
# Configure whether to append or overwrite if content_for
|
|
65
|
-
# is called multiple times
|
|
66
|
-
# the :append option to append.
|
|
65
|
+
# is called multiple times with the same key.
|
|
67
66
|
def self.configure(app, opts = OPTS)
|
|
68
67
|
app.opts[:append_content_for] = opts.fetch(:append, true)
|
|
69
68
|
end
|
|
@@ -3,15 +3,10 @@
|
|
|
3
3
|
#
|
|
4
4
|
class Roda
|
|
5
5
|
module RodaPlugins
|
|
6
|
-
# The multi_route plugin
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# and
|
|
10
|
-
# the named route will be returned.
|
|
11
|
-
#
|
|
12
|
-
# In addition, this plugin adds the +r.multi_route+ method, which will check
|
|
13
|
-
# if the first segment in the path matches a named route, and dispatch
|
|
14
|
-
# to that named route.
|
|
6
|
+
# The multi_route plugin builds on the named_routes plugin and allows for
|
|
7
|
+
# dispatching to multiple named routes # by calling the +r.multi_route+ method,
|
|
8
|
+
# which will check # if the first segment in the path matches a named route,
|
|
9
|
+
# and dispatch to that named route.
|
|
15
10
|
#
|
|
16
11
|
# The hash_routes plugin offers a +r.hash_routes+ method that is similar to
|
|
17
12
|
# and performs better than the +r.multi_route+ method, and it is recommended
|
|
@@ -35,23 +30,14 @@ class Roda
|
|
|
35
30
|
#
|
|
36
31
|
# route do |r|
|
|
37
32
|
# r.multi_route
|
|
38
|
-
#
|
|
39
|
-
# # or
|
|
40
|
-
#
|
|
41
|
-
# r.on "foo" do
|
|
42
|
-
# r.route 'foo'
|
|
43
|
-
# end
|
|
44
|
-
#
|
|
45
|
-
# r.on "bar" do
|
|
46
|
-
# r.route 'bar'
|
|
47
|
-
# end
|
|
48
33
|
# end
|
|
49
34
|
#
|
|
50
|
-
# Note that
|
|
51
|
-
#
|
|
35
|
+
# Note that only named routes with string names will be dispatched to by the
|
|
36
|
+
# +r.multi_route+ method. Named routes with other names can be dispatched to
|
|
37
|
+
# using the named_routes plugin API, but will not be automatically dispatched
|
|
38
|
+
# to by +r.multi_route+.
|
|
52
39
|
#
|
|
53
|
-
#
|
|
54
|
-
# named routes. Also, you can provide a block to +r.multi_route+ that is
|
|
40
|
+
# You can provide a block to +r.multi_route+ that is
|
|
55
41
|
# called if the route matches but the named route did not handle the
|
|
56
42
|
# request:
|
|
57
43
|
#
|
|
@@ -62,20 +48,10 @@ class Roda
|
|
|
62
48
|
# If a block is not provided to multi_route, the return value of the named
|
|
63
49
|
# route block will be used.
|
|
64
50
|
#
|
|
65
|
-
# == Routing Files
|
|
66
|
-
#
|
|
67
|
-
# The convention when using the multi_route plugin is to have a single
|
|
68
|
-
# named route per file, and these routing files should be stored in
|
|
69
|
-
# a routes subdirectory in your application. So for the above example, you
|
|
70
|
-
# would use the following files:
|
|
71
|
-
#
|
|
72
|
-
# routes/bar.rb
|
|
73
|
-
# routes/foo.rb
|
|
74
|
-
#
|
|
75
51
|
# == Namespace Support
|
|
76
52
|
#
|
|
77
53
|
# The multi_route plugin also has support for namespaces, allowing you to
|
|
78
|
-
# use r.multi_route at multiple levels in your routing tree. Example:
|
|
54
|
+
# use +r.multi_route+ at multiple levels in your routing tree. Example:
|
|
79
55
|
#
|
|
80
56
|
# route('foo') do |r|
|
|
81
57
|
# r.multi_route('foo')
|
|
@@ -103,81 +79,38 @@ class Roda
|
|
|
103
79
|
#
|
|
104
80
|
# route do |r|
|
|
105
81
|
# r.multi_route
|
|
106
|
-
#
|
|
107
|
-
# # or
|
|
108
|
-
#
|
|
109
|
-
# r.on "foo" do
|
|
110
|
-
# r.on("baz"){r.route("baz", "foo")}
|
|
111
|
-
# r.on("quux"){r.route("quux", "foo")}
|
|
112
|
-
# end
|
|
113
|
-
#
|
|
114
|
-
# r.on "bar" do
|
|
115
|
-
# r.on("baz"){r.route("baz", "bar")}
|
|
116
|
-
# r.on("quux"){r.route("quux", "bar")}
|
|
117
|
-
# end
|
|
118
82
|
# end
|
|
119
|
-
#
|
|
120
|
-
# === Routing Files
|
|
121
|
-
#
|
|
122
|
-
# The convention when using namespaces with the multi_route plugin is to
|
|
123
|
-
# store the routing files in subdirectories per namespace. So for the
|
|
124
|
-
# above example, you would have the following routing files:
|
|
125
|
-
#
|
|
126
|
-
# routes/bar.rb
|
|
127
|
-
# routes/bar/baz.rb
|
|
128
|
-
# routes/bar/quux.rb
|
|
129
|
-
# routes/foo.rb
|
|
130
|
-
# routes/foo/baz.rb
|
|
131
|
-
# routes/foo/quux.rb
|
|
132
83
|
module MultiRoute
|
|
84
|
+
def self.load_dependencies(app)
|
|
85
|
+
app.plugin :named_routes
|
|
86
|
+
end
|
|
87
|
+
|
|
133
88
|
# Initialize storage for the named routes.
|
|
134
89
|
def self.configure(app)
|
|
135
|
-
app.opts[:namespaced_routes] ||= {}
|
|
136
90
|
app::RodaRequest.instance_variable_set(:@namespaced_route_regexps, {})
|
|
137
91
|
end
|
|
138
92
|
|
|
139
93
|
module ClassMethods
|
|
140
|
-
# Freeze the
|
|
94
|
+
# Freeze the multi_route regexp matchers so that there can be no thread safety issues at runtime.
|
|
141
95
|
def freeze
|
|
142
|
-
|
|
143
|
-
|
|
96
|
+
super
|
|
97
|
+
opts[:namespaced_routes].each_key do |k|
|
|
144
98
|
self::RodaRequest.named_route_regexp(k)
|
|
145
99
|
end
|
|
146
100
|
self::RodaRequest.instance_variable_get(:@namespaced_route_regexps).freeze
|
|
147
|
-
super
|
|
148
101
|
end
|
|
149
102
|
|
|
150
103
|
# Copy the named routes into the subclass when inheriting.
|
|
151
104
|
def inherited(subclass)
|
|
152
105
|
super
|
|
153
|
-
nsr = subclass.opts[:namespaced_routes]
|
|
154
|
-
opts[:namespaced_routes].each{|k, v| nsr[k] = v.dup}
|
|
155
106
|
subclass::RodaRequest.instance_variable_set(:@namespaced_route_regexps, {})
|
|
156
107
|
end
|
|
157
108
|
|
|
158
|
-
#
|
|
159
|
-
def named_routes(namespace=nil)
|
|
160
|
-
unless routes = opts[:namespaced_routes][namespace]
|
|
161
|
-
raise RodaError, "unsupported multi_route namespace used: #{namespace.inspect}"
|
|
162
|
-
end
|
|
163
|
-
routes.keys
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Return the named route with the given name.
|
|
167
|
-
def named_route(name, namespace=nil)
|
|
168
|
-
opts[:namespaced_routes][namespace][name]
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# If the given route has a name, treat it as a named route and
|
|
172
|
-
# store the route block. Otherwise, this is the main route, so
|
|
173
|
-
# call super.
|
|
109
|
+
# Clear the multi_route regexp matcher for the namespace.
|
|
174
110
|
def route(name=nil, namespace=nil, &block)
|
|
111
|
+
super
|
|
175
112
|
if name
|
|
176
|
-
routes = opts[:namespaced_routes][namespace] ||= {}
|
|
177
|
-
routes[name] = define_roda_method(routes[name] || "multi_route_#{namespace}_#{name}", 1, &convert_route_block(block))
|
|
178
113
|
self::RodaRequest.clear_named_route_regexp!(namespace)
|
|
179
|
-
else
|
|
180
|
-
super(&block)
|
|
181
114
|
end
|
|
182
115
|
end
|
|
183
116
|
end
|
|
@@ -206,19 +139,14 @@ class Roda
|
|
|
206
139
|
# is given, yield to the block.
|
|
207
140
|
def multi_route(namespace=nil)
|
|
208
141
|
on self.class.named_route_regexp(namespace) do |section|
|
|
209
|
-
|
|
142
|
+
res = route(section, namespace)
|
|
210
143
|
if block_given?
|
|
211
144
|
yield
|
|
212
145
|
else
|
|
213
|
-
|
|
146
|
+
res
|
|
214
147
|
end
|
|
215
148
|
end
|
|
216
149
|
end
|
|
217
|
-
|
|
218
|
-
# Dispatch to the named route with the given name.
|
|
219
|
-
def route(name, namespace=nil)
|
|
220
|
-
scope.send(roda_class.named_route(name, namespace), self)
|
|
221
|
-
end
|
|
222
150
|
end
|
|
223
151
|
end
|
|
224
152
|
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
class Roda
|
|
5
|
+
module RodaPlugins
|
|
6
|
+
# The named_routes plugin allows for multiple named routes, which the
|
|
7
|
+
# main route block can dispatch to by name at any point by calling +r.route+.
|
|
8
|
+
# If the named route doesn't handle the request, execution will continue,
|
|
9
|
+
# and if the named route does handle the request, the response returned by
|
|
10
|
+
# the named route will be returned.
|
|
11
|
+
#
|
|
12
|
+
# Example:
|
|
13
|
+
#
|
|
14
|
+
# plugin :multi_route
|
|
15
|
+
#
|
|
16
|
+
# route('foo') do |r|
|
|
17
|
+
# r.is 'bar' do
|
|
18
|
+
# '/foo/bar'
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# route('bar') do |r|
|
|
23
|
+
# r.is 'foo' do
|
|
24
|
+
# '/bar/foo'
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# route do |r|
|
|
29
|
+
# r.on "foo" do
|
|
30
|
+
# r.route 'foo'
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# r.on "bar" do
|
|
34
|
+
# r.route 'bar'
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# Note that in multi-threaded code, you should not attempt to add a
|
|
39
|
+
# named route after accepting requests.
|
|
40
|
+
#
|
|
41
|
+
# == Routing Files
|
|
42
|
+
#
|
|
43
|
+
# The convention when using the named_routes plugin is to have a single
|
|
44
|
+
# named route per file, and these routing files should be stored in
|
|
45
|
+
# a routes subdirectory in your application. So for the above example, you
|
|
46
|
+
# would use the following files:
|
|
47
|
+
#
|
|
48
|
+
# routes/bar.rb
|
|
49
|
+
# routes/foo.rb
|
|
50
|
+
#
|
|
51
|
+
# == Namespace Support
|
|
52
|
+
#
|
|
53
|
+
# The named_routes plugin also has support for namespaces, allowing you to
|
|
54
|
+
# use +r.route+ at multiple levels in your routing tree. Example:
|
|
55
|
+
#
|
|
56
|
+
# route('foo') do |r|
|
|
57
|
+
# r.on("baz"){r.route("baz", "foo")}
|
|
58
|
+
# r.on("quux"){r.route("quux", "foo")}
|
|
59
|
+
# end
|
|
60
|
+
#
|
|
61
|
+
# route('bar') do |r|
|
|
62
|
+
# r.on("baz"){r.route("baz", "bar")}
|
|
63
|
+
# r.on("quux"){r.route("quux", "bar")}
|
|
64
|
+
# end
|
|
65
|
+
#
|
|
66
|
+
# route('baz', 'foo') do |r|
|
|
67
|
+
# # handles /foo/baz prefix
|
|
68
|
+
# end
|
|
69
|
+
#
|
|
70
|
+
# route('quux', 'foo') do |r|
|
|
71
|
+
# # handles /foo/quux prefix
|
|
72
|
+
# end
|
|
73
|
+
#
|
|
74
|
+
# route('baz', 'bar') do |r|
|
|
75
|
+
# # handles /bar/baz prefix
|
|
76
|
+
# end
|
|
77
|
+
#
|
|
78
|
+
# route('quux', 'bar') do |r|
|
|
79
|
+
# # handles /bar/quux prefix
|
|
80
|
+
# end
|
|
81
|
+
#
|
|
82
|
+
# route do |r|
|
|
83
|
+
# r.on "foo" do
|
|
84
|
+
# r.route("foo")
|
|
85
|
+
# end
|
|
86
|
+
#
|
|
87
|
+
# r.on "bar" do
|
|
88
|
+
# r.route("bar")
|
|
89
|
+
# end
|
|
90
|
+
# end
|
|
91
|
+
#
|
|
92
|
+
# === Routing Files
|
|
93
|
+
#
|
|
94
|
+
# The convention when using namespaces with the multi_route plugin is to
|
|
95
|
+
# store the routing files in subdirectories per namespace. So for the
|
|
96
|
+
# above example, you would have the following routing files:
|
|
97
|
+
#
|
|
98
|
+
# routes/bar.rb
|
|
99
|
+
# routes/bar/baz.rb
|
|
100
|
+
# routes/bar/quux.rb
|
|
101
|
+
# routes/foo.rb
|
|
102
|
+
# routes/foo/baz.rb
|
|
103
|
+
# routes/foo/quux.rb
|
|
104
|
+
module NamedRoutes
|
|
105
|
+
# Initialize storage for the named routes.
|
|
106
|
+
def self.configure(app)
|
|
107
|
+
app.opts[:namespaced_routes] ||= {}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
module ClassMethods
|
|
111
|
+
# Freeze the namespaced routes so that there can be no thread safety issues at runtime.
|
|
112
|
+
def freeze
|
|
113
|
+
opts[:namespaced_routes].freeze.each_value(&:freeze)
|
|
114
|
+
super
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Copy the named routes into the subclass when inheriting.
|
|
118
|
+
def inherited(subclass)
|
|
119
|
+
super
|
|
120
|
+
nsr = subclass.opts[:namespaced_routes]
|
|
121
|
+
opts[:namespaced_routes].each{|k, v| nsr[k] = v.dup}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# The names for the currently stored named routes
|
|
125
|
+
def named_routes(namespace=nil)
|
|
126
|
+
unless routes = opts[:namespaced_routes][namespace]
|
|
127
|
+
raise RodaError, "unsupported multi_route namespace used: #{namespace.inspect}"
|
|
128
|
+
end
|
|
129
|
+
routes.keys
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Return the named route with the given name.
|
|
133
|
+
def named_route(name, namespace=nil)
|
|
134
|
+
opts[:namespaced_routes][namespace][name]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# If the given route has a name, treat it as a named route and
|
|
138
|
+
# store the route block. Otherwise, this is the main route, so
|
|
139
|
+
# call super.
|
|
140
|
+
def route(name=nil, namespace=nil, &block)
|
|
141
|
+
if name
|
|
142
|
+
routes = opts[:namespaced_routes][namespace] ||= {}
|
|
143
|
+
routes[name] = define_roda_method(routes[name] || "multi_route_#{namespace}_#{name}", 1, &convert_route_block(block))
|
|
144
|
+
else
|
|
145
|
+
super(&block)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
module RequestMethods
|
|
151
|
+
# Dispatch to the named route with the given name.
|
|
152
|
+
def route(name, namespace=nil)
|
|
153
|
+
scope.send(roda_class.named_route(name, namespace), self)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
register_plugin(:named_routes, NamedRoutes)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -260,6 +260,11 @@ class Roda
|
|
|
260
260
|
# strip leading and trailing whitespace from parameter string values before processing, which
|
|
261
261
|
# you can do by passing the <tt>strip: :all</tt> option when loading the plugin.
|
|
262
262
|
#
|
|
263
|
+
# By default, the typecast_params conversion procs check that null bytes are not allowed
|
|
264
|
+
# in param string values. This check for null bytes occurs prior to any type conversion.
|
|
265
|
+
# If you would like to skip this check and allow null bytes in param string values,
|
|
266
|
+
# you can do by passing the <tt>:allow_null_bytes</tt> option when loading the plugin.
|
|
267
|
+
#
|
|
263
268
|
# By design, typecast_params only deals with string keys, it is not possible to use
|
|
264
269
|
# symbol keys as arguments to the conversion methods and have them converted.
|
|
265
270
|
module TypecastParams
|
|
@@ -356,6 +361,14 @@ class Roda
|
|
|
356
361
|
end
|
|
357
362
|
end
|
|
358
363
|
|
|
364
|
+
module AllowNullByte
|
|
365
|
+
private
|
|
366
|
+
|
|
367
|
+
# Allow ASCII NUL bytes ("\0") in parameter string values.
|
|
368
|
+
def check_null_byte(v)
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
359
372
|
module StringStripper
|
|
360
373
|
private
|
|
361
374
|
|
|
@@ -391,7 +404,10 @@ class Roda
|
|
|
391
404
|
convert_array_meth = :"_convert_array_#{type}"
|
|
392
405
|
define_method(convert_array_meth) do |v|
|
|
393
406
|
raise Error, "expected array but received #{v.inspect}" unless v.is_a?(Array)
|
|
394
|
-
v.map!
|
|
407
|
+
v.map! do |val|
|
|
408
|
+
check_null_byte(val)
|
|
409
|
+
send(convert_meth, val)
|
|
410
|
+
end
|
|
395
411
|
end
|
|
396
412
|
|
|
397
413
|
private convert_meth, convert_array_meth
|
|
@@ -927,12 +943,20 @@ class Roda
|
|
|
927
943
|
end
|
|
928
944
|
end
|
|
929
945
|
|
|
946
|
+
# Raise an Error if the value is a string containing a null byte.
|
|
947
|
+
def check_null_byte(v)
|
|
948
|
+
if v.is_a?(String) && v.index("\0")
|
|
949
|
+
handle_error(nil, :null_byte, "string parameter contains null byte", true)
|
|
950
|
+
end
|
|
951
|
+
end
|
|
952
|
+
|
|
930
953
|
# Get the value of +key+ for the object, and convert it to the expected type using +meth+.
|
|
931
954
|
# If the value either before or after conversion is nil, return the +default+ value.
|
|
932
955
|
def process(meth, key, default)
|
|
933
956
|
v = param_value(key)
|
|
934
957
|
|
|
935
958
|
unless v.nil?
|
|
959
|
+
check_null_byte(v)
|
|
936
960
|
v = send(meth, v)
|
|
937
961
|
end
|
|
938
962
|
|
|
@@ -992,6 +1016,9 @@ class Roda
|
|
|
992
1016
|
if opts[:strip] == :all
|
|
993
1017
|
app::TypecastParams.send(:include, StringStripper)
|
|
994
1018
|
end
|
|
1019
|
+
if opts[:allow_null_bytes]
|
|
1020
|
+
app::TypecastParams.send(:include, AllowNullByte)
|
|
1021
|
+
end
|
|
995
1022
|
end
|
|
996
1023
|
|
|
997
1024
|
module ClassMethods
|
data/lib/roda/request.rb
CHANGED
|
@@ -24,7 +24,7 @@ class Roda
|
|
|
24
24
|
|
|
25
25
|
# Return the cached pattern for the given object. If the object is
|
|
26
26
|
# not already cached, yield to get the basic pattern, and convert the
|
|
27
|
-
# basic pattern to a pattern that does not partial segments.
|
|
27
|
+
# basic pattern to a pattern that does not match partial segments.
|
|
28
28
|
def cached_matcher(obj)
|
|
29
29
|
cache = @match_pattern_cache
|
|
30
30
|
|
|
@@ -234,9 +234,7 @@ class Roda
|
|
|
234
234
|
|
|
235
235
|
# An alias of remaining_path. If a plugin changes remaining_path then
|
|
236
236
|
# it should override this method to return the untouched original.
|
|
237
|
-
|
|
238
|
-
remaining_path
|
|
239
|
-
end
|
|
237
|
+
alias real_remaining_path remaining_path
|
|
240
238
|
|
|
241
239
|
# Match POST requests. If no arguments are provided, matches all POST
|
|
242
240
|
# requests, otherwise, matches only POST requests where the arguments
|
|
@@ -336,7 +334,7 @@ class Roda
|
|
|
336
334
|
# Use <tt>r.get true</tt> to handle +GET+ requests where the current
|
|
337
335
|
# path is empty.
|
|
338
336
|
def root(&block)
|
|
339
|
-
if remaining_path == "/" && is_get?
|
|
337
|
+
if @remaining_path == "/" && is_get?
|
|
340
338
|
always(&block)
|
|
341
339
|
end
|
|
342
340
|
end
|
|
@@ -519,7 +517,7 @@ class Roda
|
|
|
519
517
|
# SCRIPT_NAME to include the matched path, removes the matched
|
|
520
518
|
# path from PATH_INFO, and updates captures with any regex captures.
|
|
521
519
|
def consume(pattern)
|
|
522
|
-
if matchdata =
|
|
520
|
+
if matchdata = pattern.match(@remaining_path)
|
|
523
521
|
@remaining_path = matchdata.post_match
|
|
524
522
|
captures = matchdata.captures
|
|
525
523
|
captures = yield(*captures) if block_given?
|
|
@@ -548,7 +546,7 @@ class Roda
|
|
|
548
546
|
|
|
549
547
|
# Whether the current path is considered empty.
|
|
550
548
|
def empty_path?
|
|
551
|
-
remaining_path.empty?
|
|
549
|
+
@remaining_path.empty?
|
|
552
550
|
end
|
|
553
551
|
|
|
554
552
|
# If all of the arguments match, yields to the match block and
|
data/lib/roda/version.rb
CHANGED
data/lib/roda.rb
CHANGED
|
@@ -212,6 +212,10 @@ class Roda
|
|
|
212
212
|
if @middleware.empty? && use_new_dispatch_api?
|
|
213
213
|
plugin :direct_call
|
|
214
214
|
end
|
|
215
|
+
|
|
216
|
+
if ([:on, :is, :_verb, :_match_class_String, :_match_class_Integer, :_match_string, :_match_regexp, :empty_path?, :if_match, :match, :_match_class]).all?{|m| self::RodaRequest.instance_method(m).owner == RequestMethods}
|
|
217
|
+
plugin :_optimized_matching
|
|
218
|
+
end
|
|
215
219
|
end
|
|
216
220
|
|
|
217
221
|
build_rack_app
|
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: 3.
|
|
4
|
+
version: 3.48.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: 2021-
|
|
11
|
+
date: 2021-09-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|
|
@@ -215,6 +215,10 @@ extra_rdoc_files:
|
|
|
215
215
|
- doc/release_notes/3.42.0.txt
|
|
216
216
|
- doc/release_notes/3.43.0.txt
|
|
217
217
|
- doc/release_notes/3.44.0.txt
|
|
218
|
+
- doc/release_notes/3.45.0.txt
|
|
219
|
+
- doc/release_notes/3.46.0.txt
|
|
220
|
+
- doc/release_notes/3.47.0.txt
|
|
221
|
+
- doc/release_notes/3.48.0.txt
|
|
218
222
|
- doc/release_notes/3.5.0.txt
|
|
219
223
|
- doc/release_notes/3.6.0.txt
|
|
220
224
|
- doc/release_notes/3.7.0.txt
|
|
@@ -266,6 +270,10 @@ files:
|
|
|
266
270
|
- doc/release_notes/3.42.0.txt
|
|
267
271
|
- doc/release_notes/3.43.0.txt
|
|
268
272
|
- doc/release_notes/3.44.0.txt
|
|
273
|
+
- doc/release_notes/3.45.0.txt
|
|
274
|
+
- doc/release_notes/3.46.0.txt
|
|
275
|
+
- doc/release_notes/3.47.0.txt
|
|
276
|
+
- doc/release_notes/3.48.0.txt
|
|
269
277
|
- doc/release_notes/3.5.0.txt
|
|
270
278
|
- doc/release_notes/3.6.0.txt
|
|
271
279
|
- doc/release_notes/3.7.0.txt
|
|
@@ -276,6 +284,7 @@ files:
|
|
|
276
284
|
- lib/roda/plugins.rb
|
|
277
285
|
- lib/roda/plugins/_after_hook.rb
|
|
278
286
|
- lib/roda/plugins/_before_hook.rb
|
|
287
|
+
- lib/roda/plugins/_optimized_matching.rb
|
|
279
288
|
- lib/roda/plugins/_symbol_regexp_matchers.rb
|
|
280
289
|
- lib/roda/plugins/all_verbs.rb
|
|
281
290
|
- lib/roda/plugins/assets.rb
|
|
@@ -332,6 +341,7 @@ files:
|
|
|
332
341
|
- lib/roda/plugins/multi_run.rb
|
|
333
342
|
- lib/roda/plugins/multi_view.rb
|
|
334
343
|
- lib/roda/plugins/multibyte_string_matcher.rb
|
|
344
|
+
- lib/roda/plugins/named_routes.rb
|
|
335
345
|
- lib/roda/plugins/named_templates.rb
|
|
336
346
|
- lib/roda/plugins/not_allowed.rb
|
|
337
347
|
- lib/roda/plugins/not_found.rb
|
|
@@ -407,7 +417,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
407
417
|
- !ruby/object:Gem::Version
|
|
408
418
|
version: '0'
|
|
409
419
|
requirements: []
|
|
410
|
-
rubygems_version: 3.2.
|
|
420
|
+
rubygems_version: 3.2.22
|
|
411
421
|
signing_key:
|
|
412
422
|
specification_version: 4
|
|
413
423
|
summary: Routing tree web toolkit
|