roda 3.17.0 → 3.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +48 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +22 -4
  5. data/doc/release_notes/3.18.0.txt +170 -0
  6. data/lib/roda.rb +249 -26
  7. data/lib/roda/plugins/_after_hook.rb +4 -26
  8. data/lib/roda/plugins/_before_hook.rb +30 -2
  9. data/lib/roda/plugins/branch_locals.rb +2 -2
  10. data/lib/roda/plugins/class_level_routing.rb +9 -7
  11. data/lib/roda/plugins/default_headers.rb +15 -1
  12. data/lib/roda/plugins/default_status.rb +9 -10
  13. data/lib/roda/plugins/direct_call.rb +38 -0
  14. data/lib/roda/plugins/error_email.rb +1 -1
  15. data/lib/roda/plugins/error_handler.rb +37 -11
  16. data/lib/roda/plugins/hooks.rb +28 -30
  17. data/lib/roda/plugins/mail_processor.rb +16 -11
  18. data/lib/roda/plugins/mailer.rb +1 -1
  19. data/lib/roda/plugins/middleware.rb +13 -3
  20. data/lib/roda/plugins/multi_route.rb +3 -3
  21. data/lib/roda/plugins/named_templates.rb +4 -4
  22. data/lib/roda/plugins/path.rb +13 -8
  23. data/lib/roda/plugins/render.rb +2 -2
  24. data/lib/roda/plugins/route_block_args.rb +4 -3
  25. data/lib/roda/plugins/route_csrf.rb +9 -4
  26. data/lib/roda/plugins/sessions.rb +2 -1
  27. data/lib/roda/plugins/shared_vars.rb +1 -1
  28. data/lib/roda/plugins/static_routing.rb +7 -17
  29. data/lib/roda/plugins/status_handler.rb +5 -3
  30. data/lib/roda/plugins/view_options.rb +2 -2
  31. data/lib/roda/version.rb +1 -1
  32. data/spec/define_roda_method_spec.rb +257 -0
  33. data/spec/plugin/class_level_routing_spec.rb +0 -27
  34. data/spec/plugin/default_headers_spec.rb +7 -0
  35. data/spec/plugin/default_status_spec.rb +31 -1
  36. data/spec/plugin/direct_call_spec.rb +28 -0
  37. data/spec/plugin/error_handler_spec.rb +27 -0
  38. data/spec/plugin/hooks_spec.rb +21 -0
  39. data/spec/plugin/middleware_spec.rb +108 -36
  40. data/spec/plugin/multi_route_spec.rb +12 -0
  41. data/spec/plugin/route_csrf_spec.rb +27 -0
  42. data/spec/plugin/sessions_spec.rb +26 -1
  43. data/spec/plugin/static_routing_spec.rb +25 -3
  44. data/spec/plugin/status_handler_spec.rb +17 -0
  45. data/spec/route_spec.rb +39 -0
  46. data/spec/spec_helper.rb +2 -2
  47. metadata +9 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea47f91d6b930ef7822fef3781ef11f959ace8f9e6a907f24481c5f797678482
4
- data.tar.gz: 13b182e1651d996e1493078301888b6671dd91ea0516f441ad1bc59cba940d6d
3
+ metadata.gz: f39ffb4b00598e7e113f06c2f6e43e968f96b2a447059de8b351c7f5cc35675a
4
+ data.tar.gz: eacc16913dcfd72ad822fd8b6f25e132be80123c25a6a0c29869b39a13f04b50
5
5
  SHA512:
6
- metadata.gz: 2bc04891739a93f46839abf14fd915b2f470e835e6c200fa1f8fd8a168cda13406c424fa0e1cc1eee5ce0e1e50b0dfbbe6440d27bec2112a2c6cf3dc83951ffc
7
- data.tar.gz: 68a1ec7f9ead2ce2fda252d36c7edf0df3c53d1aa02a246669d8d0700afd65d1a010179be92c1973b9c511386df39e2b7d1fd041228add83d2ef344e9d49a475
6
+ metadata.gz: 8aec053c2bda39f201cb03043cfe382d5982e5e16abca381ba76220603a25d680425a8aaf4b405f4134caef4336e3586e83d2ae0f43e8c7a40279650927237d3
7
+ data.tar.gz: 104867afa6a526d13acd76eab634eaf9688f44131606e1c7c67f73f1464c662d16d76938217c0b878deb0d7e6217ae5586fc2764be13cdd64298b5256bd4b3b8
data/CHANGELOG CHANGED
@@ -1,3 +1,51 @@
1
+ = 3.18.0 (2019-03-15)
2
+
3
+ * Add direct_call plugin for making Roda.call skip middleware, allowing more optimization when dispatching routes (jeremyevans)
4
+
5
+ * Improve performance of default_headers plugin by directly defining set_default_headers (jeremyevans)
6
+
7
+ * Improve performance when freezing app if certain methods have not been overridden (jeremyevans)
8
+
9
+ * Support :check_arity and :check_dynamic_arity app options for whether/how to check arity for blocks used to define methods (jeremyevans)
10
+
11
+ * Improve performance of the status_handler plugin by using methods instead of instance_exec (jeremyevans)
12
+
13
+ * Remove r.static_route method from the static_routing plugin (jeremyevans)
14
+
15
+ * Improve performance of the static_routing plugin by using methods instead of instance_exec (jeremyevans)
16
+
17
+ * Add support for the route_block_args plugin to the route_csrf plugin (jeremyevans)
18
+
19
+ * Improve performance of the route_csrf plugin by using a method instead of instance_exec (jeremyevans)
20
+
21
+ * Improve performance of the route_block_args plugin by using a method instead of instance_exec (jeremyevans)
22
+
23
+ * Improve performance of the path plugin by using methods instead of instance_exec (jeremyevans)
24
+
25
+ * Improve performance of the named_templates plugin by using methods instead of instance_exec (jeremyevans)
26
+
27
+ * Improve performance of the multi_route plugin by using methods instead of instance_exec (jeremyevans)
28
+
29
+ * Improve performance of the hooks plugin by using methods instead of instance_exec (jeremyevans)
30
+
31
+ * Improve performance of the mail_processor plugin by using methods instead of instance_exec (jeremyevans)
32
+
33
+ * Improve performance of the default_status plugin by directly defining the default_status method (jeremyevans)
34
+
35
+ * Improve performance of class_level_routing plugin using methods instead of instance_exec (jeremyevans)
36
+
37
+ * Do not have route_block_args plugin affect class_level_routes plugin (jeremyevans)
38
+
39
+ * Integrate internal after hook with error_handler plugin (jeremyevans)
40
+
41
+ * Improve performance of internal before and after hooks (jeremyevans)
42
+
43
+ * Improve performance by using method instead of instance_exec for main route block (jeremyevans)
44
+
45
+ * Add Roda.define_roda_method for defining instance methods instead of using instance_exec (jeremyevans)
46
+
47
+ * Include cookie_options when clearing the cookie (#162, #163) (eiko, jeremyevans)
48
+
1
49
  = 3.17.0 (2019-02-15)
2
50
 
3
51
  * Improve performance in the common case for RodaResponse#finish (jeremyevans)
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014-2018 Jeremy Evans
1
+ Copyright (c) 2014-2019 Jeremy Evans
2
2
  Copyright (c) 2010-2014 Michel Martens, Damian Janowski and Cyril David
3
3
  Copyright (c) 2008-2009 Christian Neukirchen
4
4
 
data/README.rdoc CHANGED
@@ -103,7 +103,7 @@ The primary way routes are matched in Roda is by calling
103
103
  Each of these "routing methods" takes a "match block".
104
104
 
105
105
  Each routing method takes each of the arguments (called matchers)
106
- that is given and tries to match it to the current request.
106
+ that are given and tries to match it to the current request.
107
107
  If the method is able to match all of the arguments, it yields to the match block;
108
108
  otherwise, the block is skipped and execution continues.
109
109
 
@@ -592,8 +592,9 @@ with your application code. Some of the things Roda does:
592
592
  are <tt>@_request</tt> and <tt>@_response</tt>. All instance variables in the
593
593
  scope of the +route+ block used by plugins that ship with Roda are prefixed
594
594
  with an underscore.
595
- - The only methods defined (beyond the default methods for +Object+) are:
596
- +call+, +env+, +opts+, +request+, +response+, and +session+.
595
+ - The main methods defined, beyond the default methods for +Object+, are
596
+ +env+, +opts+, +request+, +response+, and +session+. +call+ and +_call+ are also
597
+ defined, but are deprecated. All other methods defined are prefixed with +_roda_+
597
598
  - Constants inside the Roda namespace are all prefixed with +Roda+
598
599
  (e.g., <tt>Roda::RodaRequest</tt>).
599
600
 
@@ -698,6 +699,23 @@ The following options are respected by the default library or multiple plugins:
698
699
 
699
700
  :add_script_name :: Prepend the SCRIPT_NAME for the request to paths. This is
700
701
  useful if you mount the app as a path under another app.
702
+ :check_arity :: Whether arity for blocks passed to Roda should be checked
703
+ to determine if they can be used directly to define methods
704
+ or need to be wrapped. By default, for backwards compatibility,
705
+ this is true, so Roda will check blocks and handle cases where
706
+ the arity of the block does not match the expected arity. This
707
+ can be set to +:warn+ to issue warnings whenever Roda detects an
708
+ arity mismatch. If set to +false+, Roda does not check the arity
709
+ of blocks, which can result in failures at runtime if the arity
710
+ of the block does not match what Roda expects. Note that Roda
711
+ does not check the arity for lambda blocks, as those are strict
712
+ by default.
713
+ :check_dynamic_arity :: Similar to :check_arity, but used for checking blocks
714
+ where the number of arguments Roda will call the blocks
715
+ with is not possible to determine when defining the
716
+ method. By default, Roda checks arity for such methods,
717
+ but doing so actually slows the method down even if the
718
+ number of arguments matches the expected number of arguments.
701
719
  :freeze_middleware :: Whether to freeze all middleware when building the rack app.
702
720
  :json_parser :: A callable for parsing JSON (+JSON.parse+ in general used by
703
721
  default).
@@ -831,7 +849,7 @@ but before handling other requests:
831
849
 
832
850
  The easiest way to prevent XSS with Roda is to use a template library
833
851
  that automatically escapes output by default.
834
- The +:escape+ option to the +render+ plugin sets the ERb template processor
852
+ The +:escape+ option to the +render+ plugin sets the ERB template processor
835
853
  to escape by default, so that in your templates:
836
854
 
837
855
  <%= '<>' %> # outputs &lt;&gt;
@@ -0,0 +1,170 @@
1
+ = New Features
2
+
3
+ * A direct_call plugin has been added. This plugin makes Roda.call
4
+ call the app directly, skipping any middleware. This plugin
5
+ can be used for performance reasons, as the class itself can be
6
+ used as the base rack app, instead of using a lambda as the base
7
+ rack app. Roda.app.call will still call all middleware when
8
+ using this plugin.
9
+
10
+ = Other Improvements
11
+
12
+ * Blocks that are given during application configuration, and
13
+ previously executed with instance_exec, instead now define methods,
14
+ and Roda now calls these methods. This is a much faster approach.
15
+ This new approach, combined with the direct_call plugin and the
16
+ Roda.freeze optimizations, can be over 80% faster for trivial
17
+ applications, with measureable improvements in most applications.
18
+
19
+ As methods are strict in regards to arity and instance_exec is
20
+ not, Roda now checks all such blocks for arity mismatches, and
21
+ attempts to compensate for arity mismatches. In case of an arity
22
+ mismatch, Roda will define a method that will call instance_exec,
23
+ in which case there will not be a performance improvement.
24
+
25
+ For some methods, Roda may not know the expected arity until
26
+ runtime. In that case, Roda will check the arity at runtime and
27
+ try to call the method with the arity that it supports if there is
28
+ an arity mismatch.
29
+
30
+ You can control the checking of arity via two options:
31
+
32
+ :check_arity :: Set to false to turn off all arity checking. Set to
33
+ :warn to issue a warning when defining the method if
34
+ there is an arity mismatch (for methods where the
35
+ expected arity is known in advance).
36
+ :check_dynamic_arity :: Set to false to turn off arity checking for
37
+ methods defined where the arity is not known
38
+ at compile time. Set to :warn to issue a
39
+ warning at runtime every time the method is
40
+ called and there is an arity mismatch (for
41
+ methods where the expected arity is not
42
+ known in advance). Note that checking the
43
+ arity at runtime has a performance cost,
44
+ so for maximum performance this should be
45
+ set to false.
46
+
47
+ Note that this arity checking is only done to keep backwards
48
+ compatibility. Since lambdas already used strict arity, no arity
49
+ checking is done if the block is a lambda and not a regular proc.
50
+
51
+ Roda has a new dispatch API that works with these defined methods.
52
+ The new dispatch API uses the following methods:
53
+
54
+ * _roda_handle_main_route: Entry point for normal request dispatch.
55
+ * _roda_handle_route: Yields to the routing block, catching any
56
+ halts inside the block, treating the block as a routing block.
57
+ * _roda_main_route: Roda.route defines this method using the
58
+ block provided, it accepts the request as an argument.
59
+ * _roda_run_main_route: Calls _roda_main_route with the request,
60
+ allowing for plugins to execute code around the main routing,
61
+ while still being able to throw :halt to return a response.
62
+
63
+ All instance methods defined by Roda use the _roda_ prefix.
64
+
65
+ * When deleting the session cookie in the sessions plugin, the
66
+ Set-Cookie response header now uses the same path and domain
67
+ that was originally used to set the cookie. This can fix cases
68
+ where the cookie was not being cleared as expected.
69
+
70
+ * Freezing a Roda app now can add performance improvements in
71
+ addition to reliability improvements. When freezing the class,
72
+ if certain methods in the class have not been overridden, Roda
73
+ now defines aliases or more optimized methods to improve
74
+ performance.
75
+
76
+ * Roda now warns if the Roda#call method is overridden in a module,
77
+ without the module also overridding _roda_handle_main_route or
78
+ _roda_run_main_route. This indicates that the module needs
79
+ to be updated to use Roda's new dispatch API. Roda will continue
80
+ to work in this case, but it will be slower than the Roda's now
81
+ default behavior, as it will force usage of the old dispatch API.
82
+ This check will be removed in Roda 4, which will remove support
83
+ for Roda#call (and Roda#_call).
84
+
85
+ * When there is only a single internal before or after hook defined,
86
+ the hook is now faster by using a method alias.
87
+
88
+ * The route_csrf plugin block or :csrf_failure option proc now
89
+ integrates with the route_block_args plugin.
90
+
91
+ * The default_status plugin is now faster by defining the
92
+ default_status method directly.
93
+
94
+ * The default_headers plugin is now faster by defining an optimized
95
+ set_default_headers method directly.
96
+
97
+ * The hooks plugin is now faster by defining methods for each
98
+ hook block, with a main hook method that dispatches to each
99
+ of the hook block methods. If only a single hook block is
100
+ used, the main hook method is an alias to the hook block
101
+ method to avoid an extra method call.
102
+
103
+ * The following plugins now use define_method instead of
104
+ instance_exec for better performance:
105
+
106
+ * defaults_setter
107
+ * mail_processor
108
+ * multi_route
109
+ * named_templates
110
+ * path
111
+ * route_block_args
112
+ * route_csrf
113
+ * static_routing
114
+ * status_handler
115
+
116
+ * The internal after hook implementation has now been merged into
117
+ the error_handler plugin. This is faster in cases where the
118
+ error_handler plugin is used, and slower in cases where the
119
+ internal after hook plugin was used without the error_handler
120
+ plugin.
121
+
122
+ * The route_block_args plugin now handles cases where
123
+ Roda.convert_route_block has already been overridden.
124
+
125
+ * Performance of routing methods that can yield captures has been
126
+ improved.
127
+
128
+ * Hash#merge is now used in preference to Hash[].merge! in cases
129
+ where the receiver of Hash#merge would not be provided by the
130
+ user. This is because Hash#merge is faster than Hash[].merge!
131
+ in recent ruby versions. If the receiver of #merge is provided
132
+ by the user, then Hash[].merge! is still used to ensure that the
133
+ resulting value is plain hash.
134
+
135
+ * The static_routing plugin no longer removes existing static
136
+ routes if loaded more than once.
137
+
138
+ * Roda now warns when calling Roda.route without a block.
139
+
140
+ = Backwards Compatibility
141
+
142
+ * The route_block_args plugin no longer affects the
143
+ class_level_routing plugin. Support for this was added in Roda
144
+ 3.17.0 when the route_block_args plugin was added, but this was a
145
+ mistake as class_level_routing blocks should be called with the
146
+ captures for their matchers, not with the route block args.
147
+
148
+ * Some of the internal state was changed in the following plugins:
149
+
150
+ * class_level_routing
151
+ * mail_processor
152
+ * multi_route
153
+ * named_templates
154
+ * static_routing
155
+ * status_handler
156
+
157
+ This only affects you if you were accessing the internal state
158
+ via the opts hash.
159
+
160
+ * The static_routing plugin no longer defines the r.static_route
161
+ method.
162
+
163
+ * The mailer plugin was switched to use the new dispatch API, and
164
+ will no longer handle cases where the old dispatch API (Roda#call)
165
+ was overrridden.
166
+
167
+ * The static_route method in the static_routing plugin must
168
+ now be called with a block. Previously, that would not
169
+ cause a failure until runtime, where it would fail when
170
+ you tried to execute the route.
data/lib/roda.rb CHANGED
@@ -145,6 +145,106 @@ class Roda
145
145
  build_rack_app
146
146
  end
147
147
 
148
+ # Define an instance method using the block with the provided name and
149
+ # expected arity. If the name is given as a Symbol, it is used directly.
150
+ # If the name is given as a String, a unique name will be generated using
151
+ # that string. The expected arity should be either 0 (no arguments),
152
+ # 1 (single argument), or :any (any number of arguments).
153
+ #
154
+ # If the :check_arity app option is not set to false, Roda will check that
155
+ # the arity of the block matches the expected arity, and compensate for
156
+ # cases where it does not. If it is set to :warn, Roda will warn in the
157
+ # cases where the arity does not match what is expected.
158
+ #
159
+ # If the expected arity is :any, Roda must perform a dynamic arity check
160
+ # when the method is called, which can hurt performance even in the case
161
+ # where the arity matches. The :check_dynamic_arity app option can be
162
+ # set to false to turn off the dynamic arity checks. The
163
+ # :check_dynamic_arity app option can be to :warn to warn if Roda needs
164
+ # to adjust arity dynamically.
165
+ #
166
+ # Roda only checks arity for regular blocks, not lambda blocks, as the
167
+ # fixes Roda uses for regular blocks would not work for lambda blocks.
168
+ #
169
+ # Roda does not support blocks with required keyword arguments if the
170
+ # expected arity is 0 or 1.
171
+ def define_roda_method(meth, expected_arity, &block)
172
+ if meth.is_a?(String)
173
+ meth = roda_method_name(meth)
174
+ end
175
+
176
+ if (check_arity = opts.fetch(:check_arity, true)) && !block.lambda?
177
+ required_args, optional_args, rest, keyword = _define_roda_method_arg_numbers(block)
178
+
179
+ if keyword == :required && (expected_arity == 0 || expected_arity == 1)
180
+ raise RodaError, "cannot use block with required keyword arguments when calling define_roda_method with expected arity #{expected_arity}"
181
+ end
182
+
183
+ case expected_arity
184
+ when 0
185
+ unless required_args == 0
186
+ if check_arity == :warn
187
+ RodaPlugins.warn "Arity mismatch in block passed to define_roda_method. Expected Arity 0, but arguments required for #{block.inspect}"
188
+ end
189
+ b = block
190
+ block = lambda{instance_exec(&b)} # Fallback
191
+ end
192
+ when 1
193
+ if required_args == 0 && optional_args == 0 && !rest
194
+ if check_arity == :warn
195
+ RodaPlugins.warn "Arity mismatch in block passed to define_roda_method. Expected Arity 1, but no arguments accepted for #{block.inspect}"
196
+ end
197
+ b = block
198
+ block = lambda{|_| instance_exec(&b)} # Fallback
199
+ end
200
+ when :any
201
+ if check_dynamic_arity = opts.fetch(:check_dynamic_arity, check_arity)
202
+ if keyword
203
+ # Complexity of handling keyword arguments using define_method is too high,
204
+ # Fallback to instance_exec in this case.
205
+ b = block
206
+ block = lambda{|*a| instance_exec(*a, &b)} # Keyword arguments fallback
207
+ else
208
+ arity_meth = meth
209
+ meth = :"#{meth}_arity"
210
+ end
211
+ end
212
+ else
213
+ raise RodaError, "unexpected arity passed to define_roda_method: #{expected_arity.inspect}"
214
+ end
215
+ end
216
+
217
+ define_method(meth, &block)
218
+ private meth
219
+
220
+ if arity_meth
221
+ required_args, optional_args, rest, keyword = _define_roda_method_arg_numbers(instance_method(meth))
222
+ max_args = required_args + optional_args
223
+ define_method(arity_meth) do |*a|
224
+ arity = a.length
225
+ if arity > required_args
226
+ if arity > max_args && !rest
227
+ if check_dynamic_arity == :warn
228
+ RodaPlugins.warn "Dynamic arity mismatch in block passed to define_roda_method. At most #{max_args} arguments accepted, but #{arity} arguments given for #{block.inspect}"
229
+ end
230
+ a = a.slice(0, max_args)
231
+ end
232
+ elsif arity < required_args
233
+ if check_dynamic_arity == :warn
234
+ RodaPlugins.warn "Dynamic arity mismatch in block passed to define_roda_method. #{required_args} args required, but #{arity} arguments given for #{block.inspect}"
235
+ end
236
+ a.concat([nil] * (required_args - arity))
237
+ end
238
+
239
+ send(meth, *a)
240
+ end
241
+ private arity_meth
242
+ arity_meth
243
+ else
244
+ meth
245
+ end
246
+ end
247
+
148
248
  # Expand the given path, using the root argument as the base directory.
149
249
  def expand_path(path, root=opts[:root])
150
250
  ::File.expand_path(path, root)
@@ -160,6 +260,24 @@ class Roda
160
260
  def freeze
161
261
  @opts.freeze
162
262
  @middleware.freeze
263
+
264
+ unless opts[:subclassed]
265
+ # If the _roda_run_main_route instance method has not been overridden,
266
+ # make it an alias to _roda_main_route for performance
267
+ if instance_method(:_roda_run_main_route).owner == InstanceMethods
268
+ class_eval("alias _roda_run_main_route _roda_main_route")
269
+ end
270
+ self::RodaResponse.class_eval do
271
+ if instance_method(:set_default_headers).owner == ResponseMethods &&
272
+ instance_method(:default_headers).owner == ResponseMethods
273
+
274
+ def set_default_headers
275
+ @headers['Content-Type'] ||= 'text/html'
276
+ end
277
+ end
278
+ end
279
+ end
280
+
163
281
  super
164
282
  end
165
283
 
@@ -176,10 +294,16 @@ class Roda
176
294
  # and setup the request and response subclasses.
177
295
  def inherited(subclass)
178
296
  raise RodaError, "Cannot subclass a frozen Roda class" if frozen?
297
+
298
+ # Mark current class as having been subclassed, as some optimizations
299
+ # depend on the class not being subclassed
300
+ opts[:subclassed] = true
301
+
179
302
  super
180
303
  subclass.instance_variable_set(:@inherit_middleware, @inherit_middleware)
181
304
  subclass.instance_variable_set(:@middleware, @inherit_middleware ? @middleware.dup : [])
182
305
  subclass.instance_variable_set(:@opts, opts.dup)
306
+ subclass.opts.delete(:subclassed)
183
307
  subclass.opts.to_a.each do |k,v|
184
308
  if (v.is_a?(Array) || v.is_a?(Hash)) && !v.frozen?
185
309
  subclass.opts[k] = v.dup
@@ -233,9 +357,15 @@ class Roda
233
357
  # This should only be called once per class, and if called multiple
234
358
  # times will overwrite the previous routing.
235
359
  def route(&block)
360
+ unless block
361
+ RodaPlugins.warn "no block passed to Roda.route"
362
+ return
363
+ end
364
+
236
365
  @raw_route_block = block
237
366
  @route_block = block = convert_route_block(block)
238
- @rack_app_route_block = rack_app_route_block(block)
367
+ @rack_app_route_block = block = rack_app_route_block(block)
368
+ public define_roda_method(:_roda_main_route, 1, &block)
239
369
  build_rack_app
240
370
  end
241
371
 
@@ -250,10 +380,68 @@ class Roda
250
380
 
251
381
  private
252
382
 
383
+ # Return the number of required argument, optional arguments,
384
+ # whether the callable accepts any additional arguments,
385
+ # and whether the callable accepts keyword arguments (true, false
386
+ # or :required).
387
+ def _define_roda_method_arg_numbers(callable)
388
+ optional_args = 0
389
+ rest = false
390
+ keyword = false
391
+ callable.parameters.map(&:first).each do |arg_type, _|
392
+ case arg_type
393
+ when :opt
394
+ optional_args += 1
395
+ when :rest
396
+ rest = true
397
+ when :keyreq
398
+ keyword = :required
399
+ when :key, :keyrest
400
+ keyword ||= true
401
+ end
402
+ end
403
+ arity = callable.arity
404
+ if arity < 0
405
+ arity = arity.abs - 1
406
+ end
407
+ required_args = arity
408
+ arity -= 1 if keyword == :required
409
+
410
+ if callable.is_a?(Proc) && !callable.lambda?
411
+ optional_args -= arity
412
+ end
413
+
414
+ [required_args, optional_args, rest, keyword]
415
+ end
416
+
417
+ # The base rack app to use, before middleware is added.
418
+ def base_rack_app_callable(new_api=true)
419
+ if new_api
420
+ lambda{|env| new(env)._roda_handle_main_route}
421
+ else
422
+ block = @rack_app_route_block
423
+ lambda{|env| new(env).call(&block)}
424
+ end
425
+ end
426
+
253
427
  # Build the rack app to use
254
428
  def build_rack_app
255
- if block = @rack_app_route_block
256
- app = lambda{|env| new(env).call(&block)}
429
+ if @rack_app_route_block
430
+ # RODA4: Assume optimize is true
431
+ optimize = ancestors.each do |mod|
432
+ break true if mod == InstanceMethods
433
+ meths = mod.instance_methods(false)
434
+ if meths.include?(:call) && !(meths.include?(:_roda_handle_main_route) || meths.include?(:_roda_run_main_route))
435
+ RodaPlugins.warn <<WARNING
436
+ Falling back to using #call for dispatching for #{self}, due to #call override in #{mod}.
437
+ #{mod} should be fixed to adjust to Roda's new dispatch API, and override _roda_handle_main_route or _roda_run_main_route
438
+ WARNING
439
+ break false
440
+ end
441
+ end
442
+
443
+ app = base_rack_app_callable(optimize)
444
+
257
445
  @middleware.reverse_each do |args, bl|
258
446
  mid, *args = args
259
447
  app = mid.new(app, *args, &bl)
@@ -274,13 +462,15 @@ class Roda
274
462
  # in order, if any _roda_before_* methods are defined. Also, rebuild
275
463
  # the route block if a _roda_before method is defined.
276
464
  def def_roda_before
277
- meths = private_instance_methods.grep(/\A_roda_before_\d\d/).sort.join(';')
465
+ meths = private_instance_methods.grep(/\A_roda_before_\d\d/).sort
278
466
  unless meths.empty?
279
- class_eval("def _roda_before; #{meths} end", __FILE__, __LINE__)
280
- private :_roda_before
281
- if @raw_route_block
282
- route(&@raw_route_block)
467
+ plugin :_before_hook unless private_method_defined?(:_roda_before)
468
+ if meths.length == 1
469
+ class_eval("alias _roda_before #{meths.first}", __FILE__, __LINE__)
470
+ else
471
+ class_eval("def _roda_before; #{meths.join(';')} end", __FILE__, __LINE__)
283
472
  end
473
+ private :_roda_before
284
474
  end
285
475
  end
286
476
 
@@ -288,10 +478,14 @@ class Roda
288
478
  # in order, if any _roda_after_* methods are defined. Also, use
289
479
  # the internal after hook plugin if the _roda_after method is defined.
290
480
  def def_roda_after
291
- meths = private_instance_methods.grep(/\A_roda_after_\d\d/).sort.map{|s| "#{s}(res)"}.join(';')
481
+ meths = private_instance_methods.grep(/\A_roda_after_\d\d/).sort
292
482
  unless meths.empty?
293
- plugin :_after_hook unless private_method_defined?(:_roda_after)
294
- class_eval("def _roda_after(res); #{meths} end", __FILE__, __LINE__)
483
+ plugin :error_handler unless private_method_defined?(:_roda_after)
484
+ if meths.length == 1
485
+ class_eval("alias _roda_after #{meths.first}", __FILE__, __LINE__)
486
+ else
487
+ class_eval("def _roda_after(res); #{meths.map{|s| "#{s}(res)"}.join(';')} end", __FILE__, __LINE__)
488
+ end
295
489
  private :_roda_after
296
490
  end
297
491
  end
@@ -302,14 +496,14 @@ class Roda
302
496
  # if any before hooks are defined.
303
497
  # Can be modified by plugins.
304
498
  def rack_app_route_block(block)
305
- if private_method_defined?(:_roda_before)
306
- lambda do |r|
307
- _roda_before
308
- instance_exec(r, &block)
309
- end
310
- else
311
- block
312
- end
499
+ block
500
+ end
501
+
502
+ method_num = 0
503
+ method_num_mutex = Mutex.new
504
+ # Return a unique method name symbol for the given suffix.
505
+ define_method(:roda_method_name) do |suffix|
506
+ :"_roda_#{suffix}_#{method_num_mutex.synchronize{method_num += 1}}"
313
507
  end
314
508
  end
315
509
 
@@ -328,20 +522,49 @@ class Roda
328
522
  @_response = klass::RodaResponse.new
329
523
  end
330
524
 
331
- # instance_exec the route block in the scope of the
332
- # receiver, with the related request. Catch :halt so that
333
- # the route block can throw :halt at any point with the
334
- # rack response to use.
525
+ # Handle dispatching to the main route, catching :halt and handling
526
+ # the result of the block.
527
+ def _roda_handle_main_route
528
+ catch(:halt) do
529
+ r = @_request
530
+ r.block_result(_roda_run_main_route(r))
531
+ @_response.finish
532
+ end
533
+ end
534
+
535
+ # Treat the given block as a routing block, catching :halt if
536
+ # thrown by the block.
537
+ def _roda_handle_route
538
+ catch(:halt) do
539
+ @_request.block_result(yield)
540
+ @_response.finish
541
+ end
542
+ end
543
+
544
+ # Default implementation of the main route, usually overridden
545
+ # by Roda.route.
546
+ def _roda_main_route(_)
547
+ end
548
+
549
+ # Run the main route block with the request. Designed for
550
+ # extension by plugins
551
+ def _roda_run_main_route(r)
552
+ _roda_main_route(r)
553
+ end
554
+
555
+ # Deprecated method for the previous main route dispatch API.
335
556
  def call(&block)
557
+ # RODA4: Remove
336
558
  catch(:halt) do
337
559
  r = @_request
338
- r.block_result(instance_exec(r, &block))
560
+ r.block_result(instance_exec(r, &block)) # Fallback
339
561
  @_response.finish
340
562
  end
341
563
  end
342
564
 
343
- # Private alias for internal use
565
+ # Deprecated private alias for internal use
344
566
  alias _call call
567
+ # RODA4: Remove
345
568
  private :_call
346
569
 
347
570
  # The environment hash for the current request. Example:
@@ -904,7 +1127,7 @@ class Roda
904
1127
  path = @remaining_path
905
1128
  # For every block, we make sure to reset captures so that
906
1129
  # nesting matchers won't mess with each other's captures.
907
- @captures.clear
1130
+ captures = @captures.clear
908
1131
 
909
1132
  if match_all(args)
910
1133
  block_result(yield(*captures))