roda 3.44.0 → 3.48.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b60182ef0483279ac1d1983495ba020c1803d6c1fe0fa84af882142f816f72bc
4
- data.tar.gz: 20f6053b7c73fed454dc3f39e55a516d7a5461a4005356b5788480b8aab36f14
3
+ metadata.gz: 34691daba87a890b955df8a9c89ac11e3b5b2ce3ac4726b5d4dbd526aaa5c8ce
4
+ data.tar.gz: e2d8399a4a17a432d39e531ef25db62028622bd258194e930ef3d2b55a722bb8
5
5
  SHA512:
6
- metadata.gz: 89359f0c972c236bee671606fc7254f4999c6fc03f1b0ef8ff7c9691b581b6e796197b9fbee6fd109ae4d1026624f041d2e64c6182dbf8c80818756543923c9d
7
- data.tar.gz: 2d786d8641f5195a65874f3326abe07057748e4de2df7d0a8e8cd9439b6afceb08e65b6a3733e15b0289e273cc070033c1dc466e1909880410b4d05541fa04ae
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
- For most applications, {rack-unreloader}[https://github.com/jeremyevans/rack-unreloader]
969
- is probably the fastest approach to reloading while still being fairly safe, as it
970
- reloads just files that have been modified, and unloads constants defined in the files
971
- before reloading them. However, it requires modifying your application code to use
972
- rack-unreloader specific APIs.
973
-
974
- A similar solution that reloads files and unloads constants is ActiveSupport::Dependencies.
975
- ActiveSupport::Dependencies doesn't require modifying your application code, but it modifies
976
- some core methods, including +require+ and +const_missing+. It requires less configuration,
977
- but depends that you follow Rails' file and class naming conventions. It also provides
978
- autoloading (on the fly) of files when a missing constant is accessed. If your application
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
@@ -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 to set data. Overwrite is default, use
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
@@ -566,7 +566,7 @@ class Roda
566
566
  end
567
567
  false
568
568
  when Regexp
569
- if md = content.match(val)
569
+ if md = val.match(content)
570
570
  @captures.concat(md.captures)
571
571
  end
572
572
  else
@@ -3,15 +3,10 @@
3
3
  #
4
4
  class Roda
5
5
  module RodaPlugins
6
- # The multi_route plugin allows for multiple named routes, which the
7
- # main route block can dispatch to by name at any point by calling +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
- # 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 in multi-threaded code, you should not attempt to add a
51
- # named route after accepting requests.
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
- # If you want to use the +r.multi_route+ method, use string names for the
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 namespaced routes and setup the regexp matchers so that there can be no thread safety issues at runtime.
94
+ # Freeze the multi_route regexp matchers so that there can be no thread safety issues at runtime.
141
95
  def freeze
142
- opts[:namespaced_routes].freeze.each do |k,v|
143
- v.freeze
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
- # The names for the currently stored named routes
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
- r = route(section, namespace)
142
+ res = route(section, namespace)
210
143
  if block_given?
211
144
  yield
212
145
  else
213
- r
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
@@ -184,7 +184,7 @@ class Roda
184
184
  path = super
185
185
 
186
186
  if opts[:use_extension]
187
- if m = path.match(opts[:extension_regexp])
187
+ if m = opts[:extension_regexp].match(path)
188
188
  @type_routing_extension = @requested_type = m[2].to_sym
189
189
  path = m[1]
190
190
  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!{|val| send(convert_meth, val)}
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
- def real_remaining_path
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 = remaining_path.match(pattern)
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
@@ -4,7 +4,7 @@ class Roda
4
4
  RodaMajorVersion = 3
5
5
 
6
6
  # The minor version of Roda, updated for new feature releases of Roda.
7
- RodaMinorVersion = 44
7
+ RodaMinorVersion = 48
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
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.44.0
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-05-12 00:00:00.000000000 Z
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.15
420
+ rubygems_version: 3.2.22
411
421
  signing_key:
412
422
  specification_version: 4
413
423
  summary: Routing tree web toolkit