roda 3.60.0 → 3.62.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +16 -0
- data/README.rdoc +40 -15
- data/doc/release_notes/3.61.0.txt +24 -0
- data/doc/release_notes/3.62.0.txt +41 -0
- data/lib/roda/plugins/Integer_matcher_max.rb +54 -0
- data/lib/roda/plugins/_optimized_matching.rb +4 -4
- data/lib/roda/plugins/class_matchers.rb +12 -0
- data/lib/roda/plugins/common_logger.rb +1 -1
- data/lib/roda/plugins/halt.rb +1 -1
- data/lib/roda/plugins/typecast_params.rb +105 -54
- data/lib/roda/plugins/typecast_params_sized_integers.rb +107 -0
- data/lib/roda/plugins/unescape_path.rb +7 -0
- data/lib/roda/request.rb +15 -4
- data/lib/roda/version.rb +1 -1
- metadata +12 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 912f354ffcb4440bf7955d4db8546f9eccbee3f3a3f91b77604a91228a2433c4
|
4
|
+
data.tar.gz: 8df8254014b7e8e9b6db623fa2b8269ac5485b5087ee53978080a81ba344a60c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb43bcbad6e5421935d62a11abc3da27c40559ef1f55b9791d71ab22bd8cc4a73933f85bb68d77feb26efd6bf84f840bd5a202c80938ede330ca67cf9b51ca8b
|
7
|
+
data.tar.gz: 6f3234b37c506e3037864e7a7b79280f5b190758e8dada8f213b141cd1e4826a03a98017ddccb806082fa551580af9683088c94080157811867c0a3a1ec92b04
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,19 @@
|
|
1
|
+
= 3.62.0 (2022-11-14)
|
2
|
+
|
3
|
+
* Add typecast_params_sized_integers plugin for converting parameters to sized integers (jeremyevans)
|
4
|
+
|
5
|
+
* Add Integer_matcher_max plugin for setting maximum integer value matched by the Integer matcher (jeremyevans)
|
6
|
+
|
7
|
+
* Allow class matchers in the class_matchers plugin to skip matching based on regexp match values (jeremyevans)
|
8
|
+
|
9
|
+
* Fix RodaRequest#matched_path when using unescape_path plugin (jeremyevans) (#286)
|
10
|
+
|
11
|
+
= 3.61.0 (2022-10-12)
|
12
|
+
|
13
|
+
* Make Integer matcher limit integer segments to 100 characters by default (jeremyevans)
|
14
|
+
|
15
|
+
* Limit input bytesize by default for integer, float, and date/time typecasts in typecast_params (jeremyevans)
|
16
|
+
|
1
17
|
= 3.60.0 (2022-09-13)
|
2
18
|
|
3
19
|
* Add link_to plugin with link_to method for creating HTML links (jeremyevans)
|
data/README.rdoc
CHANGED
@@ -1,9 +1,35 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
1
|
+
rdoc-image:https://roda.jeremyevans.net/images/roda-logo.svg
|
2
|
+
|
3
|
+
A routing tree web toolkit, designed for building fast and maintainable web applications in Ruby.
|
4
|
+
|
5
|
+
== Table of contents
|
6
|
+
|
7
|
+
- {Installation}[#label-Installation]
|
8
|
+
- {Resources}[#label-Resources]
|
9
|
+
- {Goals}[#label-Goals]
|
10
|
+
- {Usage}[#label-Usage]
|
11
|
+
- {Running the application}[#label-Running+the+Application]
|
12
|
+
- {The routing tree}[#label-The+Routing+Tree]
|
13
|
+
- {Matchers}[#label-Matchers]
|
14
|
+
- {Optional segments}[#label-Optional+segments]
|
15
|
+
- {Match/Route Block Return Values}[#label-Match-2FRoute+Block+Return+Values]
|
16
|
+
- {Status codes}[#label-Status+Codes]
|
17
|
+
- {Verb methods}[#label-Verb+Methods]
|
18
|
+
- {Root method}[#label-Root+Method]
|
19
|
+
- {Request and Response}[#label-Request+and+Response]
|
20
|
+
- {Pollution}[#label-Pollution]
|
21
|
+
- {Composition}[#label-Composition]
|
22
|
+
- {Testing}[#label-Testing]
|
23
|
+
- {Settings}[#label-Settings]
|
24
|
+
- {Rendering}[#label-Rendering]
|
25
|
+
- {Security}[#label-Security]
|
26
|
+
- {Code Reloading}[#label-Code+Reloading]
|
27
|
+
- {Plugins}[#label-Plugins]
|
28
|
+
- {No introspection}[#label-No+Introspection]
|
29
|
+
- {Inspiration}[#label-Inspiration]
|
30
|
+
- {Ruby Support Policy}[#label-Ruby+Support+Policy]
|
31
|
+
|
32
|
+
== Installation
|
7
33
|
|
8
34
|
$ gem install roda
|
9
35
|
|
@@ -140,13 +166,11 @@ for every request.
|
|
140
166
|
== Running the Application
|
141
167
|
|
142
168
|
Running a Roda application is similar to running any other rack-based application
|
143
|
-
that uses a +config.ru+ file. You can start a basic server using +rackup
|
169
|
+
that uses a +config.ru+ file. You can start a basic server using +rackup+, +puma+,
|
170
|
+
+unicorn+, +passenger+, or any other webserver that can handle +config.ru+ files:
|
144
171
|
|
145
172
|
$ rackup
|
146
173
|
|
147
|
-
Ruby web servers such as Unicorn and Puma also ship with their own programs
|
148
|
-
that you can use to run a Roda application.
|
149
|
-
|
150
174
|
== The Routing Tree
|
151
175
|
|
152
176
|
Roda is called a routing tree web toolkit because the way most sites are structured,
|
@@ -454,7 +478,7 @@ shared branch:
|
|
454
478
|
|
455
479
|
This works well for many cases, but there are also cases where you really want to
|
456
480
|
treat it as one route with an optional segment. One simple way to do that is to
|
457
|
-
use a parameter instead of an optional segment (e.g.
|
481
|
+
use a parameter instead of an optional segment (e.g. <tt>/items/123?opt=456</tt>).
|
458
482
|
|
459
483
|
r.is "items", Integer do |item_id|
|
460
484
|
optional_data = r.params['opt'].to_s
|
@@ -647,14 +671,14 @@ If you have a lot of rack applications that you want to dispatch to, and
|
|
647
671
|
which one to dispatch to is based on the request path prefix, look into the
|
648
672
|
+multi_run+ plugin.
|
649
673
|
|
650
|
-
===
|
674
|
+
=== hash_branches plugin
|
651
675
|
|
652
676
|
If you are just looking to split up the main route block up by branches,
|
653
|
-
you should use the +
|
677
|
+
you should use the +hash_branches+ plugin,
|
654
678
|
which keeps the current scope of the +route+ block:
|
655
679
|
|
656
680
|
class App < Roda
|
657
|
-
plugin :
|
681
|
+
plugin :hash_branches
|
658
682
|
|
659
683
|
hash_branch "api" do |r|
|
660
684
|
r.is do
|
@@ -663,7 +687,7 @@ which keeps the current scope of the +route+ block:
|
|
663
687
|
end
|
664
688
|
|
665
689
|
route do |r|
|
666
|
-
r.
|
690
|
+
r.hash_branches
|
667
691
|
end
|
668
692
|
end
|
669
693
|
|
@@ -1120,3 +1144,4 @@ MIT
|
|
1120
1144
|
== Maintainer
|
1121
1145
|
|
1122
1146
|
Jeremy Evans <code@jeremyevans.net>
|
1147
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
= Improvements
|
2
|
+
|
3
|
+
* The typecast_params plugin now limits input bytesize for integer,
|
4
|
+
float, and date/time typecasts. If the input is over the allowed
|
5
|
+
bytesize, typecasting will fail. This prevents issues with trying
|
6
|
+
to typecast arbitrarily large input.
|
7
|
+
|
8
|
+
* The default Integer class matcher now limits integer segments to
|
9
|
+
100 characters by default, also to prevent issues with typecasting
|
10
|
+
arbitrarily large input. Segments larger than 100 characters will
|
11
|
+
no longer be matched by the Integer class matcher.
|
12
|
+
|
13
|
+
= Backwards Compatibility
|
14
|
+
|
15
|
+
* If the input bytesize limits in the typecast_params plugin cause
|
16
|
+
issues in your application, you can use the :skip_bytesize_checking
|
17
|
+
option when loading the plugin to disable the checks.
|
18
|
+
|
19
|
+
* If the default Integer class matcher limit causes problems in your
|
20
|
+
application, you can use the class_matchers plugin to override the
|
21
|
+
matcher to not use a limit:
|
22
|
+
|
23
|
+
plugin :class_matchers
|
24
|
+
class_matcher(Integer, /(\d+)/){|a| [a.to_i]}
|
@@ -0,0 +1,41 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* An Integer_matcher_max plugin has been added for setting the
|
4
|
+
maximum value matched by the Integer matcher (the minimum is
|
5
|
+
always 0, since the Integer matcher does not match negative
|
6
|
+
integers). The default maximum value when using the plugin
|
7
|
+
is 2**63-1, the maximum value for a signed 64-bit integer.
|
8
|
+
You can specify a different maximum value by passing an argument
|
9
|
+
when loading the plugin.
|
10
|
+
|
11
|
+
* A typecast_params_sized_integers plugin has been added for
|
12
|
+
converting parameters to integers only if the integer is within a
|
13
|
+
specific size. By default, the plugin supports 8-bit, 16-bit,
|
14
|
+
32-bit, and 64-bit signed and unsigned integer types, with the
|
15
|
+
following typecast_params methods added by the plugin:
|
16
|
+
|
17
|
+
* int8, uint8, pos_int8, pos_uint8, Integer8, Integeru8
|
18
|
+
* int16, uint16, pos_int16, pos_uint16, Integer16, Integeru16
|
19
|
+
* int32, uint32, pos_int32, pos_uint32, Integer32, Integeru32
|
20
|
+
* int64, uint64, pos_int64, pos_uint64, Integer64, Integeru64
|
21
|
+
|
22
|
+
You can override what sizes are added by default by using the
|
23
|
+
:sizes option. You can also specify a :default_size option,
|
24
|
+
in which case the default int, pos_int, and Integer conversions
|
25
|
+
will use the given size. So if you want to change the default
|
26
|
+
typecast_params integer conversion behavior to only support
|
27
|
+
integer values that can fit in 64-bit signed integers, you can
|
28
|
+
use:
|
29
|
+
|
30
|
+
plugin :typecast_params_sized_integers, sizes: [64],
|
31
|
+
default_size: 64
|
32
|
+
|
33
|
+
= Other Improvements
|
34
|
+
|
35
|
+
* The block passed to the class_matcher method in the class_matchers
|
36
|
+
plugin can now return nil/false to signal that it should not match.
|
37
|
+
This is useful when the regexp argument provided matches segments
|
38
|
+
not valid for the class.
|
39
|
+
|
40
|
+
* RodaRequest#matched_path now works correctly when using the
|
41
|
+
unescape_path plugin.
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
class Roda
|
5
|
+
module RodaPlugins
|
6
|
+
# The Integer_matcher_max plugin sets the maximum integer value
|
7
|
+
# value that the Integer class matcher will match by default.
|
8
|
+
# By default, loading this plugin sets the maximum value to
|
9
|
+
# 2**63-1, the largest signed 64-bit integer value:
|
10
|
+
#
|
11
|
+
# plugin :Integer_matcher_max
|
12
|
+
# route do |r|
|
13
|
+
# r.is Integer do
|
14
|
+
# # Matches /9223372036854775807
|
15
|
+
# # Does not match /9223372036854775808
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# To specify a different maximum value, you can pass a different
|
20
|
+
# maximum value when loading the plugin:
|
21
|
+
#
|
22
|
+
# plugin :Integer_matcher_max, 2**64-1
|
23
|
+
module IntegerMatcherMax
|
24
|
+
def self.configure(app, max=nil)
|
25
|
+
if max
|
26
|
+
app::RodaRequest.class_eval do
|
27
|
+
define_method(:_match_class_max_Integer){max}
|
28
|
+
alias_method :_match_class_max_Integer, :_match_class_max_Integer
|
29
|
+
private :_match_class_max_Integer
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
module RequestMethods
|
35
|
+
private
|
36
|
+
|
37
|
+
# Do not have the Integer matcher max when over the maximum
|
38
|
+
# configured Integer value.
|
39
|
+
def _match_class_convert_Integer(value)
|
40
|
+
value = super
|
41
|
+
value if value <= _match_class_max_Integer
|
42
|
+
end
|
43
|
+
|
44
|
+
# Use 2**63-1 as the default maximum value for the Integer
|
45
|
+
# matcher.
|
46
|
+
def _match_class_max_Integer
|
47
|
+
9223372036854775807
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
register_plugin(:Integer_matcher_max, IntegerMatcherMax)
|
53
|
+
end
|
54
|
+
end
|
@@ -52,9 +52,9 @@ class Roda
|
|
52
52
|
end
|
53
53
|
end
|
54
54
|
elsif matcher == Integer
|
55
|
-
if matchdata = /\A\/(\d
|
55
|
+
if (matchdata = /\A\/(\d{1,100})(?=\/|\z)/.match(@remaining_path)) && (value = _match_class_convert_Integer(matchdata[1]))
|
56
56
|
@remaining_path = matchdata.post_match
|
57
|
-
always{yield(
|
57
|
+
always{yield(value)}
|
58
58
|
end
|
59
59
|
else
|
60
60
|
path = @remaining_path
|
@@ -151,9 +151,9 @@ class Roda
|
|
151
151
|
always{yield rp[1, len]}
|
152
152
|
end
|
153
153
|
elsif matcher == Integer
|
154
|
-
if matchdata = /\A\/(\d
|
154
|
+
if (matchdata = /\A\/(\d{1,100})\z/.match(@remaining_path)) && (value = _match_class_convert_Integer(matchdata[1]))
|
155
155
|
@remaining_path = ''
|
156
|
-
always{yield(
|
156
|
+
always{yield(value)}
|
157
157
|
end
|
158
158
|
else
|
159
159
|
path = @remaining_path
|
@@ -28,6 +28,18 @@ class Roda
|
|
28
28
|
# This is useful to DRY up code if you are using the same type of pattern and
|
29
29
|
# type conversion in multiple places in your application.
|
30
30
|
#
|
31
|
+
# If you have a segment match the passed regexp, but decide during block
|
32
|
+
# processing that you do not want to treat it as a match, you can have the
|
33
|
+
# block return nil or false. This is useful if you want to make sure you
|
34
|
+
# are using valid data:
|
35
|
+
#
|
36
|
+
# class_matcher(Date, /(\dd\d)-(\d\d)-(\d\d)/) do |y, m, d|
|
37
|
+
# y = y.to_i
|
38
|
+
# m = m.to_i
|
39
|
+
# d = d.to_i
|
40
|
+
# [Date.new(y, m, d)] if Date.valid_date?(y, m, d)
|
41
|
+
# end
|
42
|
+
#
|
31
43
|
# This plugin does not work with the params_capturing plugin, as it does not
|
32
44
|
# offer the ability to associate block arguments with named keys.
|
33
45
|
module ClassMatchers
|
@@ -11,7 +11,7 @@ class Roda
|
|
11
11
|
# * Doesn't include middleware timing
|
12
12
|
# * Doesn't proxy the body
|
13
13
|
# * Doesn't support different capitalization of the Content-Length response header
|
14
|
-
# * Logs to
|
14
|
+
# * Logs to +$stderr+ instead of <tt>env['rack.errors']</tt> if explicit logger not passed
|
15
15
|
#
|
16
16
|
# Example:
|
17
17
|
#
|
data/lib/roda/plugins/halt.rb
CHANGED
@@ -56,7 +56,7 @@ class Roda
|
|
56
56
|
# r.halt(:template) if r.params['a']
|
57
57
|
#
|
58
58
|
# # symbol_views plugin, specifying status code, headers, and template file to render as body
|
59
|
-
# r.halt(500, 'header=>'value', :other_template) if r.params['c']
|
59
|
+
# r.halt(500, {'header'=>'value'}, :other_template) if r.params['c']
|
60
60
|
#
|
61
61
|
# # json plugin, specifying status code and JSON body
|
62
62
|
# r.halt(500, [{'error'=>'foo'}]) if r.params['b']
|
@@ -5,34 +5,26 @@ require 'time'
|
|
5
5
|
|
6
6
|
class Roda
|
7
7
|
module RodaPlugins
|
8
|
-
# The typecast_params plugin allows for
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
# cases it makes sense to explicitly convert the parameter to the
|
8
|
+
# The typecast_params plugin allows for type conversion of submitted parameters.
|
9
|
+
# Submitted parameters should be considered untrusted input, and in standard use
|
10
|
+
# with browsers, parameters are # submitted as strings (or a hash/array containing
|
11
|
+
# strings). In most # cases it makes sense to explicitly convert the parameter to the
|
13
12
|
# desired type. While this can be done via manual conversion:
|
14
13
|
#
|
15
|
-
#
|
16
|
-
#
|
14
|
+
# val = request.params['key'].to_i
|
15
|
+
# val = nil unless val > 0
|
17
16
|
#
|
18
17
|
# the typecast_params plugin adds a friendlier interface:
|
19
18
|
#
|
20
|
-
#
|
19
|
+
# val = typecast_params.pos_int('key')
|
21
20
|
#
|
22
|
-
# As +typecast_params+ is a fairly long method name,
|
23
|
-
# consider aliasing it to something more terse in your
|
24
|
-
# such as +tp+.
|
25
|
-
#
|
26
|
-
# One advantage of using typecast_params is that access or conversion
|
27
|
-
# errors are raised as a specific exception class
|
28
|
-
# (+Roda::RodaPlugins::TypecastParams::Error+). This allows you to handle
|
29
|
-
# this specific exception class globally and return an appropriate 4xx
|
30
|
-
# response to the client. You can use the Error#param_name and Error#reason
|
31
|
-
# methods to get more information about the error.
|
21
|
+
# As +typecast_params+ is a fairly long method name, and may be a method you call
|
22
|
+
# frequently, you may want to consider aliasing it to something more terse in your
|
23
|
+
# application, such as +tp+.
|
32
24
|
#
|
33
25
|
# typecast_params offers support for default values:
|
34
26
|
#
|
35
|
-
#
|
27
|
+
# val = typecast_params.pos_int('key', 1)
|
36
28
|
#
|
37
29
|
# The default value is only used if no value has been submitted for the parameter,
|
38
30
|
# or if the conversion of the value results in +nil+. Handling defaults for parameter
|
@@ -43,35 +35,41 @@ class Roda
|
|
43
35
|
# In many cases, parameters should be required, and if they aren't submitted, that
|
44
36
|
# should be considered an error. typecast_params handles this with ! methods:
|
45
37
|
#
|
46
|
-
#
|
38
|
+
# val = typecast_params.pos_int!('key')
|
47
39
|
#
|
48
40
|
# These ! methods raise an error instead of returning +nil+, and do not allow defaults.
|
49
41
|
#
|
42
|
+
# The errors raised by this plugin use a specific exception class,
|
43
|
+
# +Roda::RodaPlugins::TypecastParams::Error+. This allows you to handle
|
44
|
+
# this specific exception class globally and return an appropriate 4xx
|
45
|
+
# response to the client. You can use the Error#param_name and Error#reason
|
46
|
+
# methods to get more information about the error.
|
47
|
+
#
|
50
48
|
# To make it easy to handle cases where many parameters need the same conversion
|
51
49
|
# done, you can pass an array of keys to a conversion method, and it will return an array
|
52
50
|
# of converted values:
|
53
51
|
#
|
54
|
-
#
|
52
|
+
# val1, val2 = typecast_params.pos_int(['key1', 'key2'])
|
55
53
|
#
|
56
54
|
# This is equivalent to:
|
57
55
|
#
|
58
|
-
#
|
59
|
-
#
|
56
|
+
# val1 = typecast_params.pos_int('key1')
|
57
|
+
# val2 = typecast_params.pos_int('key2')
|
60
58
|
#
|
61
59
|
# The ! methods also support arrays, ensuring that all parameters have a value:
|
62
60
|
#
|
63
|
-
#
|
61
|
+
# val1, val2 = typecast_params.pos_int!(['key1', 'key2'])
|
64
62
|
#
|
65
63
|
# For handling of array parameters, where all entries in the array use the
|
66
64
|
# same conversion, there is an +array+ method which takes the type as the first argument
|
67
65
|
# and the keys to convert as the second argument:
|
68
66
|
#
|
69
|
-
#
|
67
|
+
# vals = typecast_params.array(:pos_int, 'keys')
|
70
68
|
#
|
71
69
|
# If you want to ensure that all entries in the array are converted successfully and that
|
72
70
|
# there is a value for the array itself, you can use +array!+:
|
73
71
|
#
|
74
|
-
#
|
72
|
+
# vals = typecast_params.array!(:pos_int, 'keys')
|
75
73
|
#
|
76
74
|
# This will raise an exception if any of the values in the array for parameter +keys+ cannot
|
77
75
|
# be converted to integer.
|
@@ -79,8 +77,8 @@ class Roda
|
|
79
77
|
# Both +array+ and +array!+ support default values which are used if no value is present
|
80
78
|
# for the parameter:
|
81
79
|
#
|
82
|
-
#
|
83
|
-
#
|
80
|
+
# vals1 = typecast_params.array(:pos_int, 'keys1', [])
|
81
|
+
# vals2 = typecast_params.array!(:pos_int, 'keys2', [])
|
84
82
|
#
|
85
83
|
# You can also pass an array of keys to +array+ or +array!+, if you would like to perform
|
86
84
|
# the same conversion on multiple arrays:
|
@@ -88,7 +86,7 @@ class Roda
|
|
88
86
|
# foo_ids, bar_ids = typecast_params.array!(:pos_int, ['foo_ids', 'bar_ids'])
|
89
87
|
#
|
90
88
|
# The previous examples have shown use of the +pos_int+ method, which uses +to_i+ to convert the
|
91
|
-
# value to an integer, but returns nil if the resulting integer is not positive. Unless you need
|
89
|
+
# value to an integer, but returns +nil+ if the resulting integer is not positive. Unless you need
|
92
90
|
# to handle negative numbers, it is recommended to use +pos_int+ instead of +int+ as +int+ will
|
93
91
|
# convert invalid values to 0 (since that is how <tt>String#to_i</tt> works).
|
94
92
|
#
|
@@ -224,10 +222,10 @@ class Roda
|
|
224
222
|
# # }
|
225
223
|
#
|
226
224
|
# Using the +:symbolize+ option makes it simpler to transition from untrusted external
|
227
|
-
# data (string keys), to
|
228
|
-
# the expected types are used).
|
225
|
+
# data (string keys), to semitrusted data that can be used internally (trusted in the sense that
|
226
|
+
# the expected types are used, not that you trust the values).
|
229
227
|
#
|
230
|
-
# Note that if there are multiple conversion
|
228
|
+
# Note that if there are multiple conversion errors raised inside a +convert!+ or +convert_each!+
|
231
229
|
# block, they are recorded and a single TypecastParams::Error instance is raised after
|
232
230
|
# processing the block. TypecastParams::Error#param_names can be called on the exception to
|
233
231
|
# get an array of all parameter names with conversion issues, and TypecastParams::Error#all_errors
|
@@ -245,14 +243,18 @@ class Roda
|
|
245
243
|
# specific to the Roda application. You can add support for custom types by passing a block
|
246
244
|
# when loading the typecast_params plugin. This block is executed in the context of the
|
247
245
|
# subclass, and calling +handle_type+ in the block can be used to add conversion methods.
|
248
|
-
# +handle_type+ accepts a type name and the block used to convert the type
|
246
|
+
# +handle_type+ accepts a type name, an options hash, and the block used to convert the type.
|
247
|
+
# The only currently supported option is +:max_input_bytesize+, specifying the maximum bytesize of
|
248
|
+
# string input. You can also override the max input bytesize of an existing type using the
|
249
|
+
# +max_input_bytesize+ method.
|
249
250
|
#
|
250
251
|
# plugin :typecast_params do
|
251
|
-
# handle_type(:album) do |value|
|
252
|
+
# handle_type(:album, max_input_bytesize: 100) do |value|
|
252
253
|
# if id = convert_pos_int(val)
|
253
254
|
# Album[id]
|
254
255
|
# end
|
255
256
|
# end
|
257
|
+
# max_input_bytesize(:date, 256)
|
256
258
|
# end
|
257
259
|
#
|
258
260
|
# By default, the typecast_params conversion procs are passed the parameter value directly
|
@@ -260,10 +262,18 @@ class Roda
|
|
260
262
|
# strip leading and trailing whitespace from parameter string values before processing, which
|
261
263
|
# you can do by passing the <tt>strip: :all</tt> option when loading the plugin.
|
262
264
|
#
|
263
|
-
# By default, the
|
264
|
-
#
|
265
|
+
# By default, the typecasting methods for some types check whether the bytesize of input
|
266
|
+
# strings is over the maximum expected values, and raise an error in such cases. The input
|
267
|
+
# bytesize is checked prior to any type conversion. If you would like to skip this check
|
268
|
+
# and allow any bytesize when doing type conversion for param string values, you can do so by
|
269
|
+
# passing the # <tt>:skip_bytesize_checking</tt> option when loading the plugin. By default,
|
270
|
+
# there is an 100 byte limit on integer input, an 1000 byte input on float input, and a 128
|
271
|
+
# byte limit on date/time input.
|
272
|
+
#
|
273
|
+
# By default, the typecasting methods check whether input strings have null bytes, and raise
|
274
|
+
# an error in such cases. This check for null bytes occurs prior to any type conversion.
|
265
275
|
# 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.
|
276
|
+
# you can do so by passing the <tt>:allow_null_bytes</tt> option when loading the plugin.
|
267
277
|
#
|
268
278
|
# You can use the :date_parse_input_handler option to specify custom handling of date
|
269
279
|
# parsing input. Modern versions of Ruby and the date gem internally raise if the input to
|
@@ -282,6 +292,11 @@ class Roda
|
|
282
292
|
# string.b[0, 128]
|
283
293
|
# }
|
284
294
|
#
|
295
|
+
# The +date_parse_input_handler+ is only called if the value is under the max input
|
296
|
+
# bytesize, so you may need to call +max_input_bytesize+ for the +:date+, +:time+, and
|
297
|
+
# +:datetime+ methods to override the max input bytesize if you want to use this option
|
298
|
+
# for input strings over 128 bytes.
|
299
|
+
#
|
285
300
|
# By design, typecast_params only deals with string keys, it is not possible to use
|
286
301
|
# symbol keys as arguments to the conversion methods and have them converted.
|
287
302
|
module TypecastParams
|
@@ -409,6 +424,14 @@ class Roda
|
|
409
424
|
end
|
410
425
|
end
|
411
426
|
|
427
|
+
module SkipBytesizeChecking
|
428
|
+
private
|
429
|
+
|
430
|
+
# Do not check max input bytesize
|
431
|
+
def check_allowed_bytesize(v, max)
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
412
435
|
# Class handling conversion of submitted parameters to desired types.
|
413
436
|
class Params
|
414
437
|
# Handle conversions for the given type using the given block.
|
@@ -422,23 +445,29 @@ class Roda
|
|
422
445
|
# This method is used to define all type conversions, even the built
|
423
446
|
# in ones. It can be called in subclasses to setup subclass-specific
|
424
447
|
# types.
|
425
|
-
def self.handle_type(type, &block)
|
448
|
+
def self.handle_type(type, opts=OPTS, &block)
|
426
449
|
convert_meth = :"convert_#{type}"
|
427
450
|
define_method(convert_meth, &block)
|
428
451
|
|
452
|
+
max_input_bytesize = opts[:max_input_bytesize]
|
453
|
+
max_input_bytesize_meth = :"_max_input_bytesize_for_#{type}"
|
454
|
+
define_method(max_input_bytesize_meth){max_input_bytesize}
|
455
|
+
|
429
456
|
convert_array_meth = :"_convert_array_#{type}"
|
430
457
|
define_method(convert_array_meth) do |v|
|
431
458
|
raise Error, "expected array but received #{v.inspect}" unless v.is_a?(Array)
|
432
459
|
v.map! do |val|
|
460
|
+
check_allowed_bytesize(val, send(max_input_bytesize_meth))
|
433
461
|
check_null_byte(val)
|
434
462
|
send(convert_meth, val)
|
435
463
|
end
|
436
464
|
end
|
437
465
|
|
438
|
-
private convert_meth, convert_array_meth
|
466
|
+
private convert_meth, convert_array_meth, max_input_bytesize_meth
|
467
|
+
alias_method max_input_bytesize_meth, max_input_bytesize_meth
|
439
468
|
|
440
469
|
define_method(type) do |key, default=nil|
|
441
|
-
process_arg(convert_meth, key, default) if require_hash!
|
470
|
+
process_arg(convert_meth, key, default, send(max_input_bytesize_meth)) if require_hash!
|
442
471
|
end
|
443
472
|
|
444
473
|
define_method(:"#{type}!") do |key|
|
@@ -446,6 +475,15 @@ class Roda
|
|
446
475
|
end
|
447
476
|
end
|
448
477
|
|
478
|
+
# Override the maximum input bytesize for the given type. This is mostly useful
|
479
|
+
# for overriding the sizes for the default input types.
|
480
|
+
def self.max_input_bytesize(type, bytesize)
|
481
|
+
max_input_bytesize_meth = :"_max_input_bytesize_for_#{type}"
|
482
|
+
define_method(max_input_bytesize_meth){bytesize}
|
483
|
+
private max_input_bytesize_meth
|
484
|
+
alias_method max_input_bytesize_meth, max_input_bytesize_meth
|
485
|
+
end
|
486
|
+
|
449
487
|
# Create a new instance with the given object and nesting level.
|
450
488
|
# +obj+ should be an array or hash, and +nesting+ should be an
|
451
489
|
# array. Designed for internal use, should not be called by
|
@@ -485,17 +523,18 @@ class Roda
|
|
485
523
|
end
|
486
524
|
end
|
487
525
|
|
488
|
-
handle_type(:int) do |v|
|
526
|
+
handle_type(:int, :max_input_bytesize=>100) do |v|
|
489
527
|
string_or_numeric!(v) && v.to_i
|
490
528
|
end
|
529
|
+
alias base_convert_int convert_int
|
491
530
|
|
492
|
-
handle_type(:pos_int) do |v|
|
493
|
-
if (v =
|
531
|
+
handle_type(:pos_int, :max_input_bytesize=>100) do |v|
|
532
|
+
if (v = base_convert_int(v)) && v > 0
|
494
533
|
v
|
495
534
|
end
|
496
535
|
end
|
497
536
|
|
498
|
-
handle_type(:Integer) do |v|
|
537
|
+
handle_type(:Integer, :max_input_bytesize=>100) do |v|
|
499
538
|
if string_or_numeric!(v)
|
500
539
|
case v
|
501
540
|
when String
|
@@ -509,12 +548,13 @@ class Roda
|
|
509
548
|
end
|
510
549
|
end
|
511
550
|
end
|
551
|
+
alias base_convert_Integer convert_Integer
|
512
552
|
|
513
|
-
handle_type(:float) do |v|
|
553
|
+
handle_type(:float, :max_input_bytesize=>1000) do |v|
|
514
554
|
string_or_numeric!(v) && v.to_f
|
515
555
|
end
|
516
556
|
|
517
|
-
handle_type(:Float) do |v|
|
557
|
+
handle_type(:Float, :max_input_bytesize=>1000) do |v|
|
518
558
|
string_or_numeric!(v) && ::Kernel::Float(v)
|
519
559
|
end
|
520
560
|
|
@@ -523,15 +563,15 @@ class Roda
|
|
523
563
|
v
|
524
564
|
end
|
525
565
|
|
526
|
-
handle_type(:date) do |v|
|
566
|
+
handle_type(:date, :max_input_bytesize=>128) do |v|
|
527
567
|
parse!(::Date, v)
|
528
568
|
end
|
529
569
|
|
530
|
-
handle_type(:time) do |v|
|
570
|
+
handle_type(:time, :max_input_bytesize=>128) do |v|
|
531
571
|
parse!(::Time, v)
|
532
572
|
end
|
533
573
|
|
534
|
-
handle_type(:datetime) do |v|
|
574
|
+
handle_type(:datetime, :max_input_bytesize=>128) do |v|
|
535
575
|
parse!(::DateTime, v)
|
536
576
|
end
|
537
577
|
|
@@ -712,7 +752,7 @@ class Roda
|
|
712
752
|
def array(type, key, default=nil)
|
713
753
|
meth = :"_convert_array_#{type}"
|
714
754
|
raise ProgrammerError, "no typecast_params type registered for #{type.inspect}" unless respond_to?(meth, true)
|
715
|
-
process_arg(meth, key, default) if require_hash!
|
755
|
+
process_arg(meth, key, default, send(:"_max_input_bytesize_for_#{type}")) if require_hash!
|
716
756
|
end
|
717
757
|
|
718
758
|
# Call +array+ with the +type+, +key+, and +default+, but if the return value is nil or any value in
|
@@ -945,10 +985,10 @@ class Roda
|
|
945
985
|
|
946
986
|
# If +key+ is not an array, convert the value at the given +key+ using the +meth+ method and +default+
|
947
987
|
# value. If +key+ is an array, return an array with the conversion done for each respective member of +key+.
|
948
|
-
def process_arg(meth, key, default)
|
988
|
+
def process_arg(meth, key, default, max_input_bytesize=nil)
|
949
989
|
case key
|
950
990
|
when String
|
951
|
-
v = process(meth, key, default)
|
991
|
+
v = process(meth, key, default, max_input_bytesize)
|
952
992
|
|
953
993
|
if @capture
|
954
994
|
key = key.to_sym if symbolize?
|
@@ -961,13 +1001,20 @@ class Roda
|
|
961
1001
|
when Array
|
962
1002
|
key.map do |k|
|
963
1003
|
raise ProgrammerError, "non-String element in array argument passed to typecast_params: #{k.inspect}" unless k.is_a?(String)
|
964
|
-
process_arg(meth, k, default)
|
1004
|
+
process_arg(meth, k, default, max_input_bytesize)
|
965
1005
|
end
|
966
1006
|
else
|
967
1007
|
raise ProgrammerError, "Unsupported argument for typecast_params conversion method: #{key.inspect}"
|
968
1008
|
end
|
969
1009
|
end
|
970
1010
|
|
1011
|
+
# Raise an Error if the value is a string with bytesize over max (if max is given)
|
1012
|
+
def check_allowed_bytesize(v, max)
|
1013
|
+
if max && v.is_a?(String) && v.bytesize > max
|
1014
|
+
handle_error(nil, :too_long, "string parameter is too long for type", true)
|
1015
|
+
end
|
1016
|
+
end
|
1017
|
+
|
971
1018
|
# Raise an Error if the value is a string containing a null byte.
|
972
1019
|
def check_null_byte(v)
|
973
1020
|
if v.is_a?(String) && v.index("\0")
|
@@ -977,10 +1024,11 @@ class Roda
|
|
977
1024
|
|
978
1025
|
# Get the value of +key+ for the object, and convert it to the expected type using +meth+.
|
979
1026
|
# If the value either before or after conversion is nil, return the +default+ value.
|
980
|
-
def process(meth, key, default)
|
1027
|
+
def process(meth, key, default, max_input_bytesize=nil)
|
981
1028
|
v = param_value(key)
|
982
1029
|
|
983
1030
|
unless v.nil?
|
1031
|
+
check_allowed_bytesize(v, max_input_bytesize)
|
984
1032
|
check_null_byte(v)
|
985
1033
|
v = send(meth, v)
|
986
1034
|
end
|
@@ -1049,6 +1097,9 @@ class Roda
|
|
1049
1097
|
if opts[:allow_null_bytes]
|
1050
1098
|
app::TypecastParams.send(:include, AllowNullByte)
|
1051
1099
|
end
|
1100
|
+
if opts[:skip_bytesize_checking]
|
1101
|
+
app::TypecastParams.send(:include, SkipBytesizeChecking)
|
1102
|
+
end
|
1052
1103
|
if opts[:date_parse_input_handler]
|
1053
1104
|
app::TypecastParams.class_eval do
|
1054
1105
|
include DateParseInputHandler
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
class Roda
|
5
|
+
module RodaPlugins
|
6
|
+
# The typecast_params_sized_integers plugin adds sized integer conversion
|
7
|
+
# methods to typecast_params:
|
8
|
+
#
|
9
|
+
# * int8, uint8, pos_int8, pos_uint8, Integer8, Integeru8
|
10
|
+
# * int16, uint16, pos_int16, pos_uint16, Integer16, Integeru16
|
11
|
+
# * int32, uint32, pos_int32, pos_uint32, Integer32, Integeru32
|
12
|
+
# * int64, uint64, pos_int64, pos_uint64, Integer64, Integeru64
|
13
|
+
#
|
14
|
+
# The int*, pos_int*, and Integer* methods operate the same as the
|
15
|
+
# standard int, pos_int, and Integer methods in typecast_params,
|
16
|
+
# except that they will only handle parameter values in the given
|
17
|
+
# range for the signed integer type. The uint*, pos_int*, and
|
18
|
+
# Integeru* methods are similar to the int*, pos_int*, and
|
19
|
+
# Integer* methods, except they use the range of the unsigned
|
20
|
+
# integer type instead of the range of the signed integer type.
|
21
|
+
#
|
22
|
+
# Here are the signed and unsigned integer type ranges:
|
23
|
+
# 8 :: [-128, 127], [0, 255]
|
24
|
+
# 16 :: [-32768, 32767], [0, 65535]
|
25
|
+
# 32 :: [-2147483648, 2147483647], [0, 4294967295]
|
26
|
+
# 64 :: [-9223372036854775808, 9223372036854775807], [0, 18446744073709551615]
|
27
|
+
#
|
28
|
+
# To only create methods for certain integer sizes, you can pass a
|
29
|
+
# :sizes option when loading the plugin, and it will only create
|
30
|
+
# methods for the sizes you specify.
|
31
|
+
#
|
32
|
+
# You can provide a :default_size option when loading the plugin,
|
33
|
+
# in which case the int, uint, pos_int, pos_uint, Integer, and Integeru,
|
34
|
+
# typecast_params conversion methods will be aliases to the conversion
|
35
|
+
# methods for the given sized type:
|
36
|
+
#
|
37
|
+
# plugin :typecast_params_sized_integers, default_size: 64
|
38
|
+
#
|
39
|
+
# route do |r|
|
40
|
+
# # Returns nil unless param.to_i > 0 && param.to_i <= 9223372036854775807
|
41
|
+
# typecast_params.pos_int('param_name')
|
42
|
+
# end
|
43
|
+
module TypecastParamsSizedIntegers
|
44
|
+
def self.load_dependencies(app, opts=OPTS)
|
45
|
+
app.plugin :typecast_params do
|
46
|
+
(opts[:sizes] || [8, 16, 32, 64]).each do |i|
|
47
|
+
# Avoid defining the same methods more than once
|
48
|
+
next if method_defined?(:"pos_int#{i}")
|
49
|
+
|
50
|
+
min_signed = -(2**(i-1))
|
51
|
+
max_signed = 2**(i-1)-1
|
52
|
+
max_unsigned = 2**i-1
|
53
|
+
|
54
|
+
handle_type(:"int#{i}", :max_input_bytesize=>100) do |v|
|
55
|
+
if (v = base_convert_int(v)) && v >= min_signed && v <= max_signed
|
56
|
+
v
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
handle_type(:"uint#{i}", :max_input_bytesize=>100) do |v|
|
61
|
+
if (v = base_convert_int(v)) && v >= 0 && v <= max_unsigned
|
62
|
+
v
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
handle_type(:"pos_int#{i}", :max_input_bytesize=>100) do |v|
|
67
|
+
if (v = base_convert_int(v)) && v > 0 && v <= max_signed
|
68
|
+
v
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
handle_type(:"pos_uint#{i}", :max_input_bytesize=>100) do |v|
|
73
|
+
if (v = base_convert_int(v)) && v > 0 && v <= max_unsigned
|
74
|
+
v
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
handle_type(:"Integer#{i}", :max_input_bytesize=>100) do |v|
|
79
|
+
if (v = base_convert_Integer(v)) && v >= min_signed && v <= max_signed
|
80
|
+
v
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
handle_type(:"Integeru#{i}", :max_input_bytesize=>100) do |v|
|
85
|
+
if (v = base_convert_Integer(v)) && v >= 0 && v <= max_unsigned
|
86
|
+
v
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
if default = opts[:default_size]
|
93
|
+
app::TypecastParams.class_eval do
|
94
|
+
%w[int uint pos_int pos_uint Integer Integeru].each do |type|
|
95
|
+
['', 'convert_', '_convert_array_', '_max_input_bytesize_for_'].each do |prefix|
|
96
|
+
alias_method :"#{prefix}#{type}", :"#{prefix}#{type}#{default}"
|
97
|
+
end
|
98
|
+
alias_method :"#{type}!", :"#{type}#{default}!"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
register_plugin(:typecast_params_sized_integers, TypecastParamsSizedIntegers)
|
106
|
+
end
|
107
|
+
end
|
@@ -20,6 +20,13 @@ class Roda
|
|
20
20
|
# end
|
21
21
|
module UnescapePath
|
22
22
|
module RequestMethods
|
23
|
+
# Make sure the matched path calculation handles the unescaping
|
24
|
+
# of the remaining path.
|
25
|
+
def matched_path
|
26
|
+
e = @env
|
27
|
+
Rack::Utils.unescape(e["SCRIPT_NAME"] + e["PATH_INFO"]).chomp(@remaining_path)
|
28
|
+
end
|
29
|
+
|
23
30
|
private
|
24
31
|
|
25
32
|
# Unescape the path.
|
data/lib/roda/request.rb
CHANGED
@@ -440,10 +440,19 @@ class Roda
|
|
440
440
|
hash.all?{|k,v| send("match_#{k}", v)}
|
441
441
|
end
|
442
442
|
|
443
|
-
# Match integer segment, and yield resulting value as an
|
443
|
+
# Match integer segment of up to 100 decimal characters, and yield resulting value as an
|
444
444
|
# integer.
|
445
445
|
def _match_class_Integer
|
446
|
-
consume(/\A\/(\d
|
446
|
+
consume(/\A\/(\d{1,100})(?=\/|\z)/) do |i|
|
447
|
+
if i = _match_class_convert_Integer(i)
|
448
|
+
[i]
|
449
|
+
end
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
# Convert the segment matched by the Integer matcher to an integer.
|
454
|
+
def _match_class_convert_Integer(value)
|
455
|
+
value.to_i
|
447
456
|
end
|
448
457
|
|
449
458
|
# Match only if all of the arguments in the given array match.
|
@@ -548,9 +557,11 @@ class Roda
|
|
548
557
|
# path from PATH_INFO, and updates captures with any regex captures.
|
549
558
|
def consume(pattern)
|
550
559
|
if matchdata = pattern.match(@remaining_path)
|
551
|
-
@remaining_path = matchdata.post_match
|
552
560
|
captures = matchdata.captures
|
553
|
-
|
561
|
+
if defined?(yield)
|
562
|
+
return unless captures = yield(*captures)
|
563
|
+
end
|
564
|
+
@remaining_path = matchdata.post_match
|
554
565
|
@captures.concat(captures)
|
555
566
|
end
|
556
567
|
end
|
data/lib/roda/version.rb
CHANGED
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.62.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Evans
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-11-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -164,7 +164,7 @@ dependencies:
|
|
164
164
|
- - ">="
|
165
165
|
- !ruby/object:Gem::Version
|
166
166
|
version: '0'
|
167
|
-
description:
|
167
|
+
description:
|
168
168
|
email:
|
169
169
|
- code@jeremyevans.net
|
170
170
|
executables: []
|
@@ -233,6 +233,8 @@ extra_rdoc_files:
|
|
233
233
|
- doc/release_notes/3.59.0.txt
|
234
234
|
- doc/release_notes/3.6.0.txt
|
235
235
|
- doc/release_notes/3.60.0.txt
|
236
|
+
- doc/release_notes/3.61.0.txt
|
237
|
+
- doc/release_notes/3.62.0.txt
|
236
238
|
- doc/release_notes/3.7.0.txt
|
237
239
|
- doc/release_notes/3.8.0.txt
|
238
240
|
- doc/release_notes/3.9.0.txt
|
@@ -300,12 +302,15 @@ files:
|
|
300
302
|
- doc/release_notes/3.59.0.txt
|
301
303
|
- doc/release_notes/3.6.0.txt
|
302
304
|
- doc/release_notes/3.60.0.txt
|
305
|
+
- doc/release_notes/3.61.0.txt
|
306
|
+
- doc/release_notes/3.62.0.txt
|
303
307
|
- doc/release_notes/3.7.0.txt
|
304
308
|
- doc/release_notes/3.8.0.txt
|
305
309
|
- doc/release_notes/3.9.0.txt
|
306
310
|
- lib/roda.rb
|
307
311
|
- lib/roda/cache.rb
|
308
312
|
- lib/roda/plugins.rb
|
313
|
+
- lib/roda/plugins/Integer_matcher_max.rb
|
309
314
|
- lib/roda/plugins/_after_hook.rb
|
310
315
|
- lib/roda/plugins/_before_hook.rb
|
311
316
|
- lib/roda/plugins/_optimized_matching.rb
|
@@ -421,6 +426,7 @@ files:
|
|
421
426
|
- lib/roda/plugins/timestamp_public.rb
|
422
427
|
- lib/roda/plugins/type_routing.rb
|
423
428
|
- lib/roda/plugins/typecast_params.rb
|
429
|
+
- lib/roda/plugins/typecast_params_sized_integers.rb
|
424
430
|
- lib/roda/plugins/unescape_path.rb
|
425
431
|
- lib/roda/plugins/view_options.rb
|
426
432
|
- lib/roda/request.rb
|
@@ -436,7 +442,7 @@ metadata:
|
|
436
442
|
documentation_uri: https://roda.jeremyevans.net/documentation.html
|
437
443
|
mailing_list_uri: https://github.com/jeremyevans/roda/discussions
|
438
444
|
source_code_uri: https://github.com/jeremyevans/roda
|
439
|
-
post_install_message:
|
445
|
+
post_install_message:
|
440
446
|
rdoc_options: []
|
441
447
|
require_paths:
|
442
448
|
- lib
|
@@ -452,7 +458,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
452
458
|
version: '0'
|
453
459
|
requirements: []
|
454
460
|
rubygems_version: 3.3.7
|
455
|
-
signing_key:
|
461
|
+
signing_key:
|
456
462
|
specification_version: 4
|
457
463
|
summary: Routing tree web toolkit
|
458
464
|
test_files: []
|