roda 3.17.0 → 3.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -1,33 +1,11 @@
1
1
  # frozen-string-literal: true
2
2
 
3
+ require_relative 'error_handler'
4
+
3
5
  #
4
6
  class Roda
5
7
  module RodaPlugins
6
- # Internal after hook module, not for external use.
7
- # Allows for plugins to configure the order in which
8
- # after processing is done by using _roda_after_*
9
- # private instance methods that are called in sorted order.
10
- # Loaded automatically by the base library if any _roda_after_*
11
- # methods are defined.
12
- module AfterHook # :nodoc:
13
- # Module for internal after hook support.
14
- module InstanceMethods
15
- # Run internal after hooks with the response
16
- def call
17
- res = super
18
- ensure
19
- _roda_after(res)
20
- end
21
-
22
- private
23
-
24
- # Empty roda_after method, so nothing breaks if the module is included.
25
- # This method will be overridden in most classes using this module.
26
- def _roda_after(res)
27
- end
28
- end
29
- end
30
-
31
- register_plugin(:_after_hook, AfterHook)
8
+ # RODA4: Remove
9
+ register_plugin(:_after_hook, ErrorHandler)
32
10
  end
33
11
  end
@@ -3,9 +3,37 @@
3
3
  #
4
4
  class Roda
5
5
  module RodaPlugins
6
- # Deprecated plugin, only exists for backwards compatibility.
7
- # Features are now part of base library.
6
+ # Internal before hook module, not for external use.
7
+ # Allows for plugins to configure the order in which
8
+ # before processing is done by using _roda_before_*
9
+ # private instance methods that are called in sorted order.
10
+ # Loaded automatically by the base library if any _roda_before_*
11
+ # methods are defined.
8
12
  module BeforeHook # :nodoc:
13
+ module InstanceMethods
14
+ # Run internal before hooks - Old Dispatch API.
15
+ def call(&block)
16
+ # RODA4: Remove
17
+ super do
18
+ _roda_before
19
+ instance_exec(@_request, &block) # call Fallback
20
+ end
21
+ end
22
+
23
+ # Run internal before hooks before running the main
24
+ # roda route.
25
+ def _roda_run_main_route(r)
26
+ _roda_before
27
+ super
28
+ end
29
+
30
+ private
31
+
32
+ # Default empty implementation of _roda_before, usually
33
+ # overridden by Roda.def_roda_before.
34
+ def _roda_before
35
+ end
36
+ end
9
37
  end
10
38
 
11
39
  register_plugin(:_before_hook, BeforeHook)
@@ -31,7 +31,7 @@ class Roda
31
31
  # Update the default layout locals to use in this branch.
32
32
  def set_layout_locals(opts)
33
33
  if locals = @_layout_locals
34
- @_layout_locals = Hash[locals].merge!(opts)
34
+ @_layout_locals = locals.merge(opts)
35
35
  else
36
36
  @_layout_locals = opts
37
37
  end
@@ -40,7 +40,7 @@ class Roda
40
40
  # Update the default view locals to use in this branch.
41
41
  def set_view_locals(opts)
42
42
  if locals = @_view_locals
43
- @_view_locals = Hash[locals].merge!(opts)
43
+ @_view_locals = locals.merge(opts)
44
44
  else
45
45
  @_view_locals = opts
46
46
  end
@@ -62,9 +62,10 @@ class Roda
62
62
 
63
63
  module ClassMethods
64
64
  # Define routing methods that will store class level routes.
65
- [:root, :on, :is, :get, :post, :delete, :head, :options, :link, :patch, :put, :trace, :unlink].each do |meth|
66
- define_method(meth) do |*args, &block|
67
- opts[:class_level_routes] << [meth, args, convert_route_block(block)].freeze
65
+ [:root, :on, :is, :get, :post, :delete, :head, :options, :link, :patch, :put, :trace, :unlink].each do |request_meth|
66
+ define_method(request_meth) do |*args, &block|
67
+ meth = define_roda_method("class_level_routing_#{request_meth}", :any, &block)
68
+ opts[:class_level_routes] << [request_meth, args, meth].freeze
68
69
  end
69
70
  end
70
71
 
@@ -91,11 +92,12 @@ class Roda
91
92
  # the original response.
92
93
  @_response.send(:initialize)
93
94
  @_response.status = nil
94
- result.replace(_call do |r|
95
- opts[:class_level_routes].each do |meth, args, blk|
95
+ result.replace(_roda_handle_route do
96
+ r = @_request
97
+ opts[:class_level_routes].each do |request_meth, args, meth|
96
98
  r.instance_variable_set(:@remaining_path, @_original_remaining_path)
97
- r.public_send(meth, *args) do |*a|
98
- instance_exec(*a, &blk)
99
+ r.public_send(request_meth, *args) do |*a|
100
+ send(meth, *a)
99
101
  end
100
102
  end
101
103
  nil
@@ -23,7 +23,21 @@ class Roda
23
23
  module DefaultHeaders
24
24
  # Merge the given headers into the existing default headers, if any.
25
25
  def self.configure(app, headers={})
26
- app.opts[:default_headers] = (app.default_headers || app::RodaResponse::DEFAULT_HEADERS).merge(headers).freeze
26
+ headers = app.opts[:default_headers] = (app.default_headers || app::RodaResponse::DEFAULT_HEADERS).merge(headers).freeze
27
+
28
+ if headers.all?{|k, v| k.is_a?(String) && v.is_a?(String)}
29
+ response_class = app::RodaResponse
30
+ owner = response_class.instance_method(:set_default_headers).owner
31
+ if owner == Base::ResponseMethods || (owner == response_class && app.opts[:set_default_headers_overridder] == response_class)
32
+ app.opts[:set_default_headers_overridder] = response_class
33
+ response_class.class_eval(<<-END, __FILE__, __LINE__+1)
34
+ def set_default_headers
35
+ h = @headers
36
+ #{headers.map{|k,v| "h[#{k.inspect}] ||= #{v.inspect}"}.join('; ')}
37
+ end
38
+ END
39
+ end
40
+ end
27
41
  end
28
42
 
29
43
  module ClassMethods
@@ -7,8 +7,6 @@ class Roda
7
7
  # return a response status integer. This integer will be used as
8
8
  # the default response status (usually 200) if the body has been
9
9
  # written to, and you have not explicitly set a response status.
10
- # The block given to the block is instance_execed in the context
11
- # of the response.
12
10
  #
13
11
  # Example:
14
12
  #
@@ -19,15 +17,16 @@ class Roda
19
17
  module DefaultStatus
20
18
  def self.configure(app, &block)
21
19
  raise RodaError, "default_status plugin requires a block" unless block
22
- app.opts[:default_status] = block
23
- end
24
-
25
- module ResponseMethods
26
- # instance_exec the default_status plugin block to get the response
27
- # status.
28
- def default_status
29
- instance_exec(&roda_class.opts[:default_status])
20
+ if check_arity = app.opts.fetch(:check_arity, true)
21
+ unless block.arity == 0
22
+ if check_arity == :warn
23
+ RodaPlugins.warn "Arity mismatch in block passed to plugin :default_status. Expected Arity 0, but arguments required for #{block.inspect}"
24
+ end
25
+ b = block
26
+ block = lambda{instance_exec(&b)} # Fallback
27
+ end
30
28
  end
29
+ app::RodaResponse.send(:define_method, :default_status, &block)
31
30
  end
32
31
  end
33
32
 
@@ -0,0 +1,38 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The direct_call plugin makes the call class method skip the middleware stack
7
+ # (app.call will still call the middleware).
8
+ # This can be used as an optimization, as the Roda class itself can be used
9
+ # as the callable, which is faster than using a lambda.
10
+ module DirectCall
11
+ def self.configure(app)
12
+ app.send(:build_rack_app)
13
+ end
14
+
15
+ module ClassMethods
16
+ # Call the application without middlware.
17
+ def call(env)
18
+ new(env)._roda_handle_main_route
19
+ end
20
+
21
+ private
22
+
23
+ # If new_api is true, use the receiver as the base rack app for better
24
+ # performance.
25
+ def base_rack_app_callable(new_api=true)
26
+ if new_api
27
+ self
28
+ else
29
+ super
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ register_plugin(:direct_call, DirectCall)
36
+ end
37
+ end
38
+
@@ -123,7 +123,7 @@ END
123
123
  def error_email_content(exception)
124
124
  email_opts = self.class.opts[:error_email]
125
125
  headers = email_opts[:default_headers].call(email_opts, exception)
126
- headers = Hash[headers].merge!(email_opts[:headers])
126
+ headers = headers.merge(email_opts[:headers])
127
127
  headers = headers.map{|k,v| "#{k}: #{v.gsub(/\r?\n/m, "\r\n ")}"}.sort.join("\r\n")
128
128
  body = email_opts[:body].call(self, exception)
129
129
  "#{headers}\r\n\r\n#{body}"
@@ -44,10 +44,6 @@ class Roda
44
44
  #
45
45
  # plugin :error_handler, classes: [StandardError, ScriptError, NoMemoryError]
46
46
  module ErrorHandler
47
- def self.load_dependencies(app, *)
48
- app.plugin :_after_hook
49
- end
50
-
51
47
  DEFAULT_ERROR_HANDLER_CLASSES = [StandardError, ScriptError].freeze
52
48
 
53
49
  # If a block is given, automatically call the +error+ method on
@@ -72,13 +68,45 @@ class Roda
72
68
 
73
69
  module InstanceMethods
74
70
  # If an error occurs, set the response status to 500 and call
75
- # the error handler.
71
+ # the error handler. Old Dispatch API.
76
72
  def call
77
- super
73
+ # RODA4: Remove
74
+ begin
75
+ res = super
76
+ ensure
77
+ _roda_after(res)
78
+ end
79
+ rescue *opts[:error_handler_classes] => e
80
+ _handle_error(e)
81
+ end
82
+
83
+ # If an error occurs, set the response status to 500 and call
84
+ # the error handler.
85
+ def _roda_handle_main_route
86
+ begin
87
+ res = super
88
+ ensure
89
+ _roda_after(res)
90
+ end
78
91
  rescue *opts[:error_handler_classes] => e
79
- @_response.send(:initialize)
80
- @_response.status = 500
81
- res = _call{handle_error(e)}
92
+ _handle_error(e)
93
+ end
94
+
95
+ private
96
+
97
+ # Default empty implementation of _roda_after, usually
98
+ # overridden by Roda.def_roda_before.
99
+ def _roda_after(res)
100
+ end
101
+
102
+ # Handle the given exception using handle_error, using a default status
103
+ # of 500. Run after hooks on the rack response, but if any error occurs
104
+ # when doing so, log the error using rack.errors and return the response.
105
+ def _handle_error(e)
106
+ res = @_response
107
+ res.send(:initialize)
108
+ res.status = 500
109
+ res = _roda_handle_route{handle_error(e)}
82
110
  begin
83
111
  _roda_after(res)
84
112
  rescue => e2
@@ -90,8 +118,6 @@ class Roda
90
118
  res
91
119
  end
92
120
 
93
- private
94
-
95
121
  # By default, have the error handler reraise the error, so using
96
122
  # the plugin without installing an error handler doesn't change
97
123
  # behavior.
@@ -34,57 +34,55 @@ class Roda
34
34
  # hooks can be called with nil if an exception is raised during routing.
35
35
  module Hooks
36
36
  def self.configure(app)
37
- app.opts[:before_hook] ||= nil
38
- app.opts[:after_hook] ||= nil
37
+ app.opts[:after_hooks] ||= []
38
+ app.opts[:before_hooks] ||= []
39
39
  end
40
40
 
41
41
  module ClassMethods
42
- # Add an after hook. If there is already an after hook defined,
43
- # use a proc that instance_execs the existing after proc and
44
- # then instance_execs the given after proc, so that the given
45
- # after proc always executes after the previous one.
42
+ # Freeze the array of hook methods when freezing the app.
43
+ def freeze
44
+ opts[:after_hooks].freeze
45
+ opts[:before_hooks].freeze
46
+
47
+ super
48
+ end
49
+
50
+ # Add an after hook.
46
51
  def after(&block)
47
- opts[:after_hook] = if b = opts[:after_hook]
48
- proc do |res|
49
- instance_exec(res, &b)
50
- instance_exec(res, &block)
51
- end
52
+ opts[:after_hooks] << define_roda_method("after_hook", 1, &block)
53
+ if opts[:after_hooks].length == 1
54
+ class_eval("alias _roda_after_80__hooks #{opts[:after_hooks].first}", __FILE__, __LINE__)
52
55
  else
53
- block
56
+ class_eval("def _roda_after_80__hooks(res) #{opts[:after_hooks].map{|m| "#{m}(res)"}.join(';')} end", __FILE__, __LINE__)
54
57
  end
58
+ private :_roda_after_80__hooks
59
+ def_roda_after
60
+ nil
55
61
  end
56
62
 
57
- # Add a before hook. If there is already a before hook defined,
58
- # use a proc that instance_execs the give before proc and
59
- # then instance_execs the existing before proc, so that the given
60
- # before proc always executes before the previous one.
63
+ # Add a before hook.
61
64
  def before(&block)
62
- opts[:before_hook] = if b = opts[:before_hook]
63
- proc do
64
- instance_exec(&block)
65
- instance_exec(&b)
66
- end
65
+ opts[:before_hooks].unshift(define_roda_method("before_hook", 0, &block))
66
+ if opts[:before_hooks].length == 1
67
+ class_eval("alias _roda_before_10__hooks #{opts[:before_hooks].first}", __FILE__, __LINE__)
67
68
  else
68
- block
69
+ class_eval("def _roda_before_10__hooks; #{opts[:before_hooks].join(';')} end", __FILE__, __LINE__)
69
70
  end
71
+ private :_roda_before_10__hooks
72
+ def_roda_before
73
+ nil
70
74
  end
71
75
  end
72
76
 
73
77
  module InstanceMethods
74
78
  private
75
79
 
76
- # Run after hooks.
80
+ # Default method if no after hooks are defined.
77
81
  def _roda_after_80__hooks(res)
78
- if b = opts[:after_hook]
79
- instance_exec(res, &b)
80
- end
81
82
  end
82
83
 
83
- # Run before hooks.
84
+ # Default method if no before hooks are defined.
84
85
  def _roda_before_10__hooks
85
- if b = opts[:before_hook]
86
- instance_exec(&b)
87
- end
88
86
  end
89
87
  end
90
88
  end
@@ -301,7 +301,7 @@ class Roda
301
301
 
302
302
  begin
303
303
  begin
304
- scope.process_mail(&@rack_app_route_block)
304
+ scope.process_mail
305
305
  rescue UnhandledMail
306
306
  scope.unhandled_mail_hook
307
307
  else
@@ -329,16 +329,23 @@ class Roda
329
329
  def rcpt(*addresses, &block)
330
330
  opts[:mail_processor_string_routes] ||= {}
331
331
  opts[:mail_processor_regexp_routes] ||= {}
332
+ string_meth = nil
333
+ regexp_meth = nil
332
334
  addresses.each do |address|
333
335
  key = case address
334
336
  when String
335
- :mail_processor_string_routes
337
+ unless string_meth
338
+ string_meth = define_roda_method("mail_processor_string_route_#{address}", 1, &convert_route_block(block))
339
+ end
340
+ opts[:mail_processor_string_routes][address] = string_meth
336
341
  when Regexp
337
- :mail_processor_regexp_routes
342
+ unless regexp_meth
343
+ regexp_meth = define_roda_method("mail_processor_regexp_route_#{address}", :any, &convert_route_block(block))
344
+ end
345
+ opts[:mail_processor_regexp_routes][address] = regexp_meth
338
346
  else
339
347
  raise RodaError, "invalid address format passed to rcpt, should be Array or String"
340
348
  end
341
- opts[key][address] = block
342
349
  end
343
350
  nil
344
351
  end
@@ -379,25 +386,23 @@ class Roda
379
386
  addresses = mail_recipients
380
387
 
381
388
  addresses.each do |address|
382
- if blk = string_routes[address.to_s.downcase]
383
- call(&blk)
389
+ if meth = string_routes[address.to_s.downcase]
390
+ _roda_handle_route{send(meth, @_request)}
384
391
  return
385
392
  end
386
393
  end
387
394
 
388
- opts[:mail_processor_regexp_routes].each do |regexp, blk|
395
+ opts[:mail_processor_regexp_routes].each do |regexp, meth|
389
396
  addresses.each do |address|
390
397
  if md = regexp.match(address)
391
- call do |r|
392
- instance_exec(r, *md.captures, &blk)
393
- end
398
+ _roda_handle_route{send(meth, @_request, *md.captures)}
394
399
  return
395
400
  end
396
401
  end
397
402
  end
398
403
  end
399
404
 
400
- call(&block)
405
+ _roda_handle_main_route
401
406
 
402
407
  nil
403
408
  end