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
@@ -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