roda 3.108.0 → 3.109.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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/lib/roda/opts.rb +22 -0
  3. data/lib/roda/plugins/Integer_matcher_max.rb +1 -1
  4. data/lib/roda/plugins/_add_script_name.rb +14 -0
  5. data/lib/roda/plugins/_json.rb +22 -0
  6. data/lib/roda/plugins/_symbol_class_matchers.rb +6 -6
  7. data/lib/roda/plugins/additional_render_engines.rb +5 -1
  8. data/lib/roda/plugins/additional_view_directories.rb +5 -1
  9. data/lib/roda/plugins/assets.rb +7 -10
  10. data/lib/roda/plugins/autoload_hash_branches.rb +9 -3
  11. data/lib/roda/plugins/autoload_named_routes.rb +9 -3
  12. data/lib/roda/plugins/capture_erb.rb +5 -1
  13. data/lib/roda/plugins/chunked.rb +11 -5
  14. data/lib/roda/plugins/class_level_routing.rb +5 -3
  15. data/lib/roda/plugins/class_matchers.rb +3 -1
  16. data/lib/roda/plugins/common_logger.rb +7 -3
  17. data/lib/roda/plugins/conditional_sessions.rb +1 -1
  18. data/lib/roda/plugins/content_for.rb +5 -1
  19. data/lib/roda/plugins/content_security_policy.rb +6 -2
  20. data/lib/roda/plugins/cookie_flags.rb +6 -2
  21. data/lib/roda/plugins/cookies.rb +7 -3
  22. data/lib/roda/plugins/custom_block_results.rb +5 -3
  23. data/lib/roda/plugins/custom_matchers.rb +4 -2
  24. data/lib/roda/plugins/default_headers.rb +2 -5
  25. data/lib/roda/plugins/default_status.rb +1 -0
  26. data/lib/roda/plugins/each_part.rb +19 -23
  27. data/lib/roda/plugins/environments.rb +3 -7
  28. data/lib/roda/plugins/error_email.rb +9 -5
  29. data/lib/roda/plugins/error_handler.rb +5 -3
  30. data/lib/roda/plugins/error_mail.rb +6 -2
  31. data/lib/roda/plugins/hash_branch_view_subdir.rb +6 -4
  32. data/lib/roda/plugins/hash_branches.rb +7 -5
  33. data/lib/roda/plugins/hash_paths.rb +7 -5
  34. data/lib/roda/plugins/hash_public.rb +11 -7
  35. data/lib/roda/plugins/hash_public_cache.rb +9 -7
  36. data/lib/roda/plugins/hash_routes.rb +5 -3
  37. data/lib/roda/plugins/heartbeat.rb +6 -2
  38. data/lib/roda/plugins/hmac_paths.rb +9 -3
  39. data/lib/roda/plugins/hooks.rb +13 -10
  40. data/lib/roda/plugins/host_authorization.rb +7 -2
  41. data/lib/roda/plugins/host_routing.rb +7 -2
  42. data/lib/roda/plugins/invalid_request_body.rb +20 -4
  43. data/lib/roda/plugins/ip_from_header.rb +5 -1
  44. data/lib/roda/plugins/json.rb +10 -8
  45. data/lib/roda/plugins/json_parser.rb +26 -12
  46. data/lib/roda/plugins/mail_processor.rb +9 -6
  47. data/lib/roda/plugins/mailer.rb +5 -3
  48. data/lib/roda/plugins/match_affix.rb +6 -1
  49. data/lib/roda/plugins/match_hook_args.rb +7 -5
  50. data/lib/roda/plugins/middleware.rb +13 -7
  51. data/lib/roda/plugins/module_include.rb +1 -1
  52. data/lib/roda/plugins/multi_public.rb +6 -2
  53. data/lib/roda/plugins/multi_route.rb +1 -1
  54. data/lib/roda/plugins/multi_run.rb +13 -17
  55. data/lib/roda/plugins/named_routes.rb +8 -6
  56. data/lib/roda/plugins/named_templates.rb +5 -3
  57. data/lib/roda/plugins/path.rb +16 -14
  58. data/lib/roda/plugins/path_rewriter.rb +8 -5
  59. data/lib/roda/plugins/permissions_policy.rb +7 -3
  60. data/lib/roda/plugins/public.rb +11 -10
  61. data/lib/roda/plugins/recheck_precompiled_assets.rb +1 -1
  62. data/lib/roda/plugins/redirect_http_to_https.rb +6 -3
  63. data/lib/roda/plugins/render.rb +34 -49
  64. data/lib/roda/plugins/render_coverage.rb +6 -3
  65. data/lib/roda/plugins/render_locals.rb +12 -6
  66. data/lib/roda/plugins/request_aref.rb +1 -1
  67. data/lib/roda/plugins/response_content_type.rb +7 -3
  68. data/lib/roda/plugins/route_csrf.rb +7 -2
  69. data/lib/roda/plugins/run_append_slash.rb +5 -1
  70. data/lib/roda/plugins/sec_fetch_site_csrf.rb +6 -2
  71. data/lib/roda/plugins/sessions.rb +15 -11
  72. data/lib/roda/plugins/shape_friendly.rb +3 -1
  73. data/lib/roda/plugins/sinatra_helpers.rb +7 -3
  74. data/lib/roda/plugins/status_handler.rb +5 -3
  75. data/lib/roda/plugins/strip_path_prefix.rb +4 -2
  76. data/lib/roda/plugins/symbol_matchers.rb +3 -1
  77. data/lib/roda/plugins/symbol_views.rb +1 -1
  78. data/lib/roda/plugins/timestamp_public.rb +8 -4
  79. data/lib/roda/plugins/type_routing.rb +9 -5
  80. data/lib/roda/plugins/typecast_params.rb +2 -2
  81. data/lib/roda/plugins/typecast_params_sized_integers.rb +1 -1
  82. data/lib/roda/plugins/url_escape.rb +2 -14
  83. data/lib/roda/plugins.rb +10 -0
  84. data/lib/roda/version.rb +1 -1
  85. data/lib/roda.rb +25 -14
  86. metadata +6 -3
@@ -38,11 +38,9 @@ class Roda
38
38
 
39
39
  module InstanceMethods
40
40
  if RUBY_VERSION >= '2.7'
41
- class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
42
- def each_part(enum, template, **locals, &block)
43
- render_each(enum, template, :locals=>locals, &block)
44
- end
45
- RUBY
41
+ def each_part(enum, template, **locals, &block)
42
+ render_each(enum, template, :locals=>locals, &block)
43
+ end
46
44
  # simplecov:disable
47
45
  else
48
46
  def each_part(enum, template, locals=OPTS, &block)
@@ -56,28 +54,26 @@ class Roda
56
54
  # simplecov:disable
57
55
  if RUBY_VERSION >= '3.0'
58
56
  # simplecov:enable
59
- class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
60
- def each_part(enum, template, **locals, &block)
61
- if optimized_method = _cached_render_each_template_method(template)
62
- optimized_method = optimized_method[0]
63
- as = render_each_default_local(template)
64
- if defined?(yield)
65
- enum.each do |v|
66
- locals[as] = v
67
- yield send(optimized_method, **locals)
68
- end
69
- nil
70
- else
71
- enum.map do |v|
72
- locals[as] = v
73
- send(optimized_method, **locals)
74
- end.join
57
+ def each_part(enum, template, **locals, &block)
58
+ if optimized_method = _cached_render_each_template_method(template)
59
+ optimized_method = optimized_method[0]
60
+ as = render_each_default_local(template)
61
+ if defined?(yield)
62
+ enum.each do |v|
63
+ locals[as] = v
64
+ yield send(optimized_method, **locals)
75
65
  end
66
+ nil
76
67
  else
77
- render_each(enum, template, :locals=>locals, &block)
68
+ enum.map do |v|
69
+ locals[as] = v
70
+ send(optimized_method, **locals)
71
+ end.join
78
72
  end
73
+ else
74
+ render_each(enum, template, :locals=>locals, &block)
79
75
  end
80
- RUBY
76
+ end
81
77
  end
82
78
  end
83
79
  end
@@ -44,10 +44,12 @@ class Roda
44
44
  # if no environment is given. If ENV['RACK_ENV'] is not set and
45
45
  # no environment is given, assume the development environment.
46
46
  def self.configure(app, env=ENV["RACK_ENV"])
47
- app.environment = (env || 'development').to_sym
47
+ app.opts[:environment] = (env || 'development').to_sym
48
48
  end
49
49
 
50
50
  module ClassMethods
51
+ RodaPlugins.opt_attr_reader(self, :environment)
52
+
51
53
  # If no environments are given or one of the given environments
52
54
  # matches the current environment, yield the receiver to the block.
53
55
  def configure(*envs)
@@ -56,12 +58,6 @@ class Roda
56
58
  end
57
59
  end
58
60
 
59
- # The current environment for the application, which should be stored
60
- # as a symbol.
61
- def environment
62
- opts[:environment]
63
- end
64
-
65
61
  # Override the environment for the application, instead of using
66
62
  # RACK_ENV.
67
63
  def environment=(v)
@@ -56,7 +56,7 @@ class Roda
56
56
  {'From'=>h[:from], 'To'=>h[:to], 'Subject'=>"#{h[:prefix]}#{subject}"}
57
57
  end,
58
58
  :body=>lambda do |s, e|
59
- filter = s.opts[:error_email][:filter]
59
+ filter = s.class.error_email_opts[:filter]
60
60
  format = lambda do |h|
61
61
  h = h.map{|k, v| "#{k.inspect} => #{filter.call(k, v) ? 'FILTERED' : v.inspect}"}
62
62
  h.sort!
@@ -120,8 +120,12 @@ END
120
120
  raise RodaError, "must provide :to and :from options to error_email plugin"
121
121
  end
122
122
  app.opts[:error_email] = email_opts
123
- app.opts[:error_email][:headers].freeze
124
- app.opts[:error_email].freeze
123
+ app.error_email_opts[:headers].freeze
124
+ app.error_email_opts.freeze
125
+ end
126
+
127
+ module ClassMethods
128
+ RodaPlugins.opt_attr_reader(self, :error_email, name: :error_email_opts)
125
129
  end
126
130
 
127
131
  module InstanceMethods
@@ -129,7 +133,7 @@ END
129
133
  # instance, but it can be a plain string which is used as the subject for
130
134
  # the email.
131
135
  def error_email(exception)
132
- email_opts = self.class.opts[:error_email].dup
136
+ email_opts = self.class.error_email_opts.dup
133
137
  email_opts[:message] = error_email_content(exception)
134
138
  email_opts[:emailer].call(email_opts)
135
139
  end
@@ -137,7 +141,7 @@ END
137
141
  # The content of the email to send, include the headers and the body.
138
142
  # Takes the same argument as #error_email.
139
143
  def error_email_content(exception)
140
- email_opts = self.class.opts[:error_email]
144
+ email_opts = self.class.error_email_opts
141
145
  headers = email_opts[:default_headers].call(email_opts, exception)
142
146
  headers = headers.merge(email_opts[:headers])
143
147
  headers = headers.map{|k,v| "#{k}: #{v.gsub(/\r?\n/m, "\r\n ")}"}.sort.join("\r\n")
@@ -49,7 +49,7 @@ class Roda
49
49
  # If a block is given, automatically call the +error+ method on
50
50
  # the Roda class with it.
51
51
  def self.configure(app, opts={}, &block)
52
- app.opts[:error_handler_classes] = (opts[:classes] || app.opts[:error_handler_classes] || DEFAULT_ERROR_HANDLER_CLASSES).dup.freeze
52
+ app.opts[:error_handler_classes] = (opts[:classes] || app.error_handler_classes || DEFAULT_ERROR_HANDLER_CLASSES).dup.freeze
53
53
 
54
54
  if block
55
55
  app.error(&block)
@@ -57,6 +57,8 @@ class Roda
57
57
  end
58
58
 
59
59
  module ClassMethods
60
+ RodaPlugins.opt_attr_reader(self, :error_handler_classes)
61
+
60
62
  # Install the given block as the error handler, so that if routing
61
63
  # the request raises an exception, the block will be called with
62
64
  # the exception in the scope of the Roda instance.
@@ -77,7 +79,7 @@ class Roda
77
79
  ensure
78
80
  _roda_after(res)
79
81
  end
80
- rescue *opts[:error_handler_classes] => e
82
+ rescue *self.class.error_handler_classes => e
81
83
  _handle_error(e)
82
84
  end
83
85
 
@@ -89,7 +91,7 @@ class Roda
89
91
  ensure
90
92
  _roda_after(res)
91
93
  end
92
- rescue *opts[:error_handler_classes] => e
94
+ rescue *self.class.error_handler_classes => e
93
95
  _handle_error(e)
94
96
  end
95
97
 
@@ -44,12 +44,16 @@ class Roda
44
44
 
45
45
  # Set default opts for plugin. See ErrorEmail module RDoc for options.
46
46
  def self.configure(app, opts=OPTS)
47
- app.opts[:error_mail] = email_opts = (app.opts[:error_mail] || {:filter=>DEFAULT_FILTER}).merge(opts).freeze
47
+ app.opts[:error_mail] = email_opts = (app.error_mail_opts || {:filter=>DEFAULT_FILTER}).merge(opts).freeze
48
48
  unless email_opts[:to] && email_opts[:from]
49
49
  raise RodaError, "must provide :to and :from options to error_mail plugin"
50
50
  end
51
51
  end
52
52
 
53
+ module ClassMethods
54
+ RodaPlugins.opt_attr_reader(self, :error_mail, name: :error_mail_opts)
55
+ end
56
+
53
57
  module InstanceMethods
54
58
  # Send an email for the given error. +exception+ is usually an exception
55
59
  # instance, but it can be a plain string which is used as the subject for
@@ -67,7 +71,7 @@ class Roda
67
71
  private
68
72
 
69
73
  def _error_mail(e)
70
- email_opts = self.class.opts[:error_mail]
74
+ email_opts = self.class.error_mail_opts
71
75
  subject = if e.respond_to?(:message)
72
76
  "#{e.class}: #{e.message}"
73
77
  else
@@ -33,9 +33,11 @@ class Roda
33
33
  end
34
34
 
35
35
  module ClassMethods
36
+ RodaPlugins.opt_attr_reader(self, :hash_branch_view_subdir_methods)
37
+
36
38
  # Freeze the hash_branch_view_subdir metadata when freezing the app.
37
39
  def freeze
38
- opts[:hash_branch_view_subdir_methods].freeze.each_value(&:freeze)
40
+ hash_branch_view_subdir_methods.freeze.each_value(&:freeze)
39
41
  super
40
42
  end
41
43
 
@@ -43,8 +45,8 @@ class Roda
43
45
  def inherited(subclass)
44
46
  super
45
47
 
46
- h = subclass.opts[:hash_branch_view_subdir_methods]
47
- opts[:hash_branch_view_subdir_methods].each do |namespace, routes|
48
+ h = subclass.hash_branch_view_subdir_methods
49
+ hash_branch_view_subdir_methods.each do |namespace, routes|
48
50
  h[namespace] = routes.dup
49
51
  end
50
52
  end
@@ -53,7 +55,7 @@ class Roda
53
55
  # by modifying the generated method to append the view subdirectory before
54
56
  # dispatching to the original block.
55
57
  def hash_branch(namespace='', segment, &block)
56
- meths = opts[:hash_branch_view_subdir_methods][namespace] ||= {}
58
+ meths = hash_branch_view_subdir_methods[namespace] ||= {}
57
59
 
58
60
  if block
59
61
  meth = meths[segment] = define_roda_method(meths[segment] || "_hash_branch_view_subdir_#{namespace}_#{segment}", 1, &convert_route_block(block))
@@ -88,9 +88,11 @@ class Roda
88
88
  end
89
89
 
90
90
  module ClassMethods
91
+ RodaPlugins.opt_attr_reader(self, :hash_branches)
92
+
91
93
  # Freeze the hash_branches metadata when freezing the app.
92
94
  def freeze
93
- opts[:hash_branches].freeze.each_value(&:freeze)
95
+ hash_branches.freeze.each_value(&:freeze)
94
96
  super
95
97
  end
96
98
 
@@ -98,8 +100,8 @@ class Roda
98
100
  def inherited(subclass)
99
101
  super
100
102
 
101
- h = subclass.opts[:hash_branches]
102
- opts[:hash_branches].each do |namespace, routes|
103
+ h = subclass.hash_branches
104
+ hash_branches.each do |namespace, routes|
103
105
  h[namespace] = routes.dup
104
106
  end
105
107
  end
@@ -108,7 +110,7 @@ class Roda
108
110
  # a block, removes the existing branch handler if it exists.
109
111
  def hash_branch(namespace='', segment, &block)
110
112
  segment = "/#{segment}"
111
- routes = opts[:hash_branches][namespace] ||= {}
113
+ routes = hash_branches[namespace] ||= {}
112
114
  if block
113
115
  routes[segment] = define_roda_method(routes[segment] || "hash_branch_#{namespace}_#{segment}", 1, &convert_route_block(block))
114
116
  elsif meth = routes.delete(segment)
@@ -125,7 +127,7 @@ class Roda
125
127
 
126
128
  return unless rp.getbyte(0) == 47 # "/"
127
129
 
128
- if routes = roda_class.opts[:hash_branches][namespace]
130
+ if routes = roda_class.hash_branches[namespace]
129
131
  if segment_end = rp.index('/', 1)
130
132
  if meth = routes[rp[0, segment_end]]
131
133
  always do
@@ -80,9 +80,11 @@ class Roda
80
80
  end
81
81
 
82
82
  module ClassMethods
83
+ RodaPlugins.opt_attr_reader(self, :hash_paths)
84
+
83
85
  # Freeze the hash_paths metadata when freezing the app.
84
86
  def freeze
85
- opts[:hash_paths].freeze.each_value(&:freeze)
87
+ hash_paths.freeze.each_value(&:freeze)
86
88
  super
87
89
  end
88
90
 
@@ -90,8 +92,8 @@ class Roda
90
92
  def inherited(subclass)
91
93
  super
92
94
 
93
- h = subclass.opts[:hash_paths]
94
- opts[:hash_paths].each do |namespace, routes|
95
+ h = subclass.hash_paths
96
+ hash_paths.each do |namespace, routes|
95
97
  h[namespace] = routes.dup
96
98
  end
97
99
  end
@@ -102,7 +104,7 @@ class Roda
102
104
  # there is one. If called without a block, removes the existing
103
105
  # path handler if it exists.
104
106
  def hash_path(namespace='', path, &block)
105
- routes = opts[:hash_paths][namespace] ||= {}
107
+ routes = hash_paths[namespace] ||= {}
106
108
  if block
107
109
  routes[path] = define_roda_method(routes[path] || "hash_path_#{namespace}_#{path}", 1, &convert_route_block(block))
108
110
  elsif meth = routes.delete(path)
@@ -115,7 +117,7 @@ class Roda
115
117
  # Checks the matching hash_path namespace for a branch matching the
116
118
  # remaining path, and dispatch to that block if there is one.
117
119
  def hash_paths(namespace=matched_path)
118
- if (routes = roda_class.opts[:hash_paths][namespace]) && (meth = routes[@remaining_path])
120
+ if (routes = roda_class.hash_paths[namespace]) && (meth = routes[@remaining_path])
119
121
  always do
120
122
  @remaining_path = ''
121
123
  scope.send(meth, self)
@@ -78,16 +78,20 @@ class Roda
78
78
  end
79
79
 
80
80
  module ClassMethods
81
+ RodaPlugins.opt_attr_reader(self, :hash_public_prefix)
82
+ RodaPlugins.opt_attr_reader(self, :hash_public_length)
83
+ RodaPlugins.opt_attr_reader(self, :hash_public_mutex)
84
+ RodaPlugins.opt_attr_reader(self, :hash_public_cache)
85
+
81
86
  # The digest for the given file to use in hash_path.
82
87
  def hash_path_digest(file)
83
- opts = self.opts
84
- cache = opts[:hash_public_cache]
85
- mutex = opts[:hash_public_mutex]
88
+ cache = hash_public_cache
89
+ mutex = hash_public_mutex
86
90
  unless digest = mutex.synchronize{cache[file]}
87
- digest = Digest::SHA256.file(File.join(opts[:public_root], file)).base64digest
91
+ digest = Digest::SHA256.file(File.join(public_root, file)).base64digest
88
92
  digest.chomp!("=")
89
93
  digest.tr!("+/", "-_")
90
- if length = opts[:hash_public_length]
94
+ if length = hash_public_length
91
95
  digest = digest[0, length]
92
96
  end
93
97
  digest.freeze
@@ -102,7 +106,7 @@ class Roda
102
106
  # This does not check the file is inside the directory for performance
103
107
  # reasons, so this should not be called with untrusted input.
104
108
  def hash_path(file)
105
- "/#{opts[:hash_public_prefix]}/#{self.class.hash_path_digest(file)}/#{file}"
109
+ "/#{self.class.hash_public_prefix}/#{self.class.hash_path_digest(file)}/#{file}"
106
110
  end
107
111
  end
108
112
 
@@ -112,7 +116,7 @@ class Roda
112
116
  # a string segment for the content hash, and this is a GET request.
113
117
  def hash_public
114
118
  if is_get?
115
- on roda_class.opts[:hash_public_prefix], String do |_|
119
+ on roda_class.hash_public_prefix, String do |_|
116
120
  public
117
121
  end
118
122
  end
@@ -1,6 +1,5 @@
1
1
  # frozen-string-literal: true
2
2
 
3
- require 'json'
4
3
  require 'find'
5
4
 
6
5
  class Roda
@@ -29,6 +28,7 @@ class Roda
29
28
  # load_hash_public_cache_file
30
29
  module HashPublicCache
31
30
  def self.load_dependencies(app, _cache_file, opts = OPTS)
31
+ app.plugin :_json
32
32
  app.plugin :hash_public, opts
33
33
  end
34
34
 
@@ -40,13 +40,15 @@ class Roda
40
40
  end
41
41
 
42
42
  module ClassMethods
43
+ RodaPlugins.opt_attr_reader(self, :hash_public_cache_file)
44
+
43
45
  # Load the hash public cache file, if it exists. This replaces the hash
44
46
  # public cache with the values from the file.
45
47
  def load_hash_public_cache_file
46
- file = opts[:hash_public_cache_file]
48
+ file = hash_public_cache_file
47
49
  return unless File.file?(file)
48
50
 
49
- cache = opts[:hash_public_cache] = (opts[:json_parser] || ::JSON.method(:parse)).call(::File.read(file))
51
+ cache = opts[:hash_public_cache] = json_parser.call(::File.read(file))
50
52
  cache.each_value(&:freeze)
51
53
  nil
52
54
  end
@@ -56,13 +58,13 @@ class Roda
56
58
  # If a block is given, it will only calculate the digest for the file
57
59
  # if the block returns truthy.
58
60
  def scan_hash_public_cache_dir
59
- cache = opts[:hash_public_cache]
61
+ cache = hash_public_cache
60
62
 
61
63
  # Public root doesn't have trailing slash even if given, as
62
64
  # File.expand_path removes it.
63
- root = opts[:public_root] + File::SEPARATOR
65
+ root = public_root + File::SEPARATOR
64
66
 
65
- Find.find(opts[:public_root]) do |file|
67
+ Find.find(public_root) do |file|
66
68
  if File.file?(file)
67
69
  file = file.sub(root, '')
68
70
  next if cache[file]
@@ -80,7 +82,7 @@ class Roda
80
82
 
81
83
  # Write the current hash public cache to the cache file.
82
84
  def dump_hash_public_cache_file
83
- File.write(opts[:hash_public_cache_file], (opts[:json_serializer] || :to_json.to_proc).call(opts[:hash_public_cache]))
85
+ File.write(hash_public_cache_file, json_serializer.call(hash_public_cache))
84
86
  nil
85
87
  end
86
88
  end
@@ -191,7 +191,7 @@ class Roda
191
191
  def dispatch_from(namespace='', branch, &block)
192
192
  ns = @namespace
193
193
  if block
194
- meth_hash = @roda.opts[:hash_routes_methods]
194
+ meth_hash = @roda.hash_routes_methods
195
195
  key = [:dispatch_from, namespace, branch].freeze
196
196
  meth = meth_hash[key] = @roda.define_roda_method(meth_hash[key] || "hash_routes_dispatch_from_#{namespace}_#{branch}", 1, &block)
197
197
  @roda.hash_branch(namespace, branch) do |r|
@@ -253,7 +253,7 @@ class Roda
253
253
  # Returns 404 for requests for the path with a different request method.
254
254
  def verb(verb, path, &block)
255
255
  path = path == true ? "" : "/#{path}"
256
- meth_hash = @roda.opts[:hash_routes_methods]
256
+ meth_hash = @roda.hash_routes_methods
257
257
  key = [@namespace, path].freeze
258
258
  meth = meth_hash[key] = @roda.define_roda_method(meth_hash[key] || "hash_routes_#{@namespace}_#{path}", 0, &block)
259
259
  @roda.hash_path(@namespace, path) do |r|
@@ -265,9 +265,11 @@ class Roda
265
265
  end
266
266
 
267
267
  module ClassMethods
268
+ RodaPlugins.opt_attr_reader(self, :hash_routes_methods)
269
+
268
270
  # Freeze the hash_routes metadata when freezing the app.
269
271
  def freeze
270
- opts[:hash_routes_methods].freeze
272
+ hash_routes_methods.freeze
271
273
  super
272
274
  end
273
275
 
@@ -16,7 +16,11 @@ class Roda
16
16
  module Heartbeat
17
17
  # Set the heartbeat path to the given path.
18
18
  def self.configure(app, opts=OPTS)
19
- app.opts[:heartbeat_path] = (opts[:path] || app.opts[:heartbeat_path] || "/heartbeat").dup.freeze
19
+ app.opts[:heartbeat_path] = (opts[:path] || app.heartbeat_path || "/heartbeat").dup.freeze
20
+ end
21
+
22
+ module ClassMethods
23
+ RodaPlugins.opt_attr_reader(self, :heartbeat_path)
20
24
  end
21
25
 
22
26
  module InstanceMethods
@@ -24,7 +28,7 @@ class Roda
24
28
 
25
29
  # If the request is for a heartbeat path, return the heartbeat response.
26
30
  def _roda_before_20__heartbeat
27
- if env['PATH_INFO'] == opts[:heartbeat_path]
31
+ if env['PATH_INFO'] == self.class.heartbeat_path
28
32
  response = @_response
29
33
  response.status = 200
30
34
  response[RodaResponseHeaders::CONTENT_TYPE] = 'text/plain'
@@ -231,6 +231,12 @@ class Roda
231
231
  end
232
232
  end
233
233
 
234
+ module ClassMethods
235
+ RodaPlugins.opt_attr_reader(self, :hmac_paths_secret)
236
+ RodaPlugins.opt_attr_reader(self, :hmac_paths_old_secret)
237
+ RodaPlugins.opt_attr_reader(self, :hmac_paths_namespace_session_key)
238
+ end
239
+
234
240
  module InstanceMethods
235
241
  # Return a path with an HMAC. Designed to be used with r.hmac_path, to make sure
236
242
  # users can only request paths that they have been provided by the application
@@ -315,7 +321,7 @@ class Roda
315
321
  # using the secret given in the plugin, for the given root and options.
316
322
  # This always returns a hexidecimal string.
317
323
  def hmac_path_hmac_secret(root, opts=OPTS)
318
- secret = opts[:secret] || self.opts[:hmac_paths_secret]
324
+ secret = opts[:secret] || self.class.hmac_paths_secret
319
325
 
320
326
  if namespace = hmac_path_namespace(opts)
321
327
  secret = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, secret, namespace)
@@ -326,7 +332,7 @@ class Roda
326
332
 
327
333
  # The default namespace to use for hmac_path, if a :namespace option is not provided.
328
334
  def hmac_path_default_namespace
329
- if (key = opts[:hmac_paths_namespace_session_key]) && (value = session[key])
335
+ if (key = self.class.hmac_paths_namespace_session_key) && (value = session[key])
330
336
  value.to_s
331
337
  end
332
338
  end
@@ -395,7 +401,7 @@ class Roda
395
401
  def hmac_path_valid?(root, path, hmac, opts=OPTS)
396
402
  if Rack::Utils.secure_compare(scope.hmac_path_hmac(root, path, opts), hmac)
397
403
  true
398
- elsif old_secret = roda_class.opts[:hmac_paths_old_secret]
404
+ elsif old_secret = roda_class.hmac_paths_old_secret
399
405
  opts = opts.dup
400
406
  opts[:secret] = old_secret
401
407
  Rack::Utils.secure_compare(scope.hmac_path_hmac(root, path, opts), hmac)
@@ -39,21 +39,24 @@ class Roda
39
39
  end
40
40
 
41
41
  module ClassMethods
42
+ RodaPlugins.opt_attr_reader(self, :after_hooks)
43
+ RodaPlugins.opt_attr_reader(self, :before_hooks)
44
+
42
45
  # Freeze the array of hook methods when freezing the app.
43
46
  def freeze
44
- opts[:after_hooks].freeze
45
- opts[:before_hooks].freeze
47
+ after_hooks.freeze
48
+ before_hooks.freeze
46
49
 
47
50
  super
48
51
  end
49
52
 
50
53
  # Add an after hook.
51
54
  def after(&block)
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__)
55
+ after_hooks << define_roda_method("after_hook", 1, &block)
56
+ if after_hooks.length == 1
57
+ class_eval("alias _roda_after_80__hooks #{after_hooks.first}", __FILE__, __LINE__)
55
58
  else
56
- class_eval("def _roda_after_80__hooks(res) #{opts[:after_hooks].map{|m| "#{m}(res)"}.join(';')} end", __FILE__, __LINE__)
59
+ class_eval("def _roda_after_80__hooks(res) #{after_hooks.map{|m| "#{m}(res)"}.join(';')} end", __FILE__, __LINE__)
57
60
  end
58
61
  private :_roda_after_80__hooks
59
62
  def_roda_after
@@ -62,11 +65,11 @@ class Roda
62
65
 
63
66
  # Add a before hook.
64
67
  def before(&block)
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__)
68
+ before_hooks.unshift(define_roda_method("before_hook", 0, &block))
69
+ if before_hooks.length == 1
70
+ class_eval("alias _roda_before_10__hooks #{before_hooks.first}", __FILE__, __LINE__)
68
71
  else
69
- class_eval("def _roda_before_10__hooks; #{opts[:before_hooks].join(';')} end", __FILE__, __LINE__)
72
+ class_eval("def _roda_before_10__hooks; #{before_hooks.join(';')} end", __FILE__, __LINE__)
70
73
  end
71
74
  private :_roda_before_10__hooks
72
75
  def_roda_before
@@ -101,6 +101,11 @@ class Roda
101
101
  end
102
102
  end
103
103
 
104
+ module ClassMethods
105
+ RodaPlugins.opt_attr_reader(self, :host_authorization_host)
106
+ RodaPlugins.opt_attr_reader(self, :host_authorization_check_forwarded, name: :host_authorization_check_forwarded?)
107
+ end
108
+
104
109
  module InstanceMethods
105
110
  # Check whether the host is authorized. If not authorized, return a response
106
111
  # immediately based on the plugin block.
@@ -108,7 +113,7 @@ class Roda
108
113
  r = @_request
109
114
  return if host_authorized?(_convert_host_for_authorization(r.env["HTTP_HOST"].to_s.dup))
110
115
 
111
- if opts[:host_authorization_check_forwarded] && (host = r.env["HTTP_X_FORWARDED_HOST"])
116
+ if self.class.host_authorization_check_forwarded? && (host = r.env["HTTP_X_FORWARDED_HOST"])
112
117
  if i = host.rindex(',')
113
118
  host = host[i+1, 10000000].to_s
114
119
  end
@@ -133,7 +138,7 @@ class Roda
133
138
  end
134
139
 
135
140
  # Whether the host given is one of the authorized hosts for this application.
136
- def host_authorized?(host, authorized_host = opts[:host_authorization_host])
141
+ def host_authorized?(host, authorized_host = self.class.host_authorization_host)
137
142
  case authorized_host
138
143
  when Array
139
144
  authorized_host.any?{|auth_host| host_authorized?(host, auth_host)}
@@ -176,6 +176,11 @@ class Roda
176
176
  end
177
177
  private_constant :DSL
178
178
 
179
+ module ClassMethods
180
+ RodaPlugins.opt_attr_reader(self, :host_routing_hash)
181
+ RodaPlugins.opt_attr_reader(self, :host_routing_default_host)
182
+ end
183
+
179
184
  module InstanceMethods
180
185
  # Handle case where plugin is used without providing a block to
181
186
  # +hosts.default+. This returns nil, ensuring that the hostname
@@ -207,9 +212,9 @@ class Roda
207
212
  host = ""
208
213
  end
209
214
 
210
- roda_class.opts[:host_routing_hash][host] ||
215
+ roda_class.host_routing_hash[host] ||
211
216
  scope._host_routing_default(host) ||
212
- roda_class.opts[:host_routing_default_host]
217
+ roda_class.host_routing_default_host
213
218
  end
214
219
  end
215
220
  end
@@ -37,13 +37,25 @@ class Roda
37
37
  # # To treat the exception raised as a submitted parameter
38
38
  # {body_error: exception}
39
39
  # end
40
+ #
41
+ # You can use the +rescue_classes+ argument when loading the plugin to only rescue
42
+ # specific exception classes:
43
+ #
44
+ # plugin :invalid_request_body, :empty_hash, rescue_classes: Rack::BadRequest
45
+ #
46
+ # However, this can be risky, because there have been many cases in the past where
47
+ # Rack can rescue classes such as +ArgumentError+ or +NoMethodError+ when parsing
48
+ # invalid request bodies. The default for +rescue_classes+ is +StandardError+.
40
49
  module InvalidRequestBody
41
50
  # Exception class raised for invalid request bodies.
42
51
  Error = Class.new(RodaError)
43
52
 
44
53
  # Set the action to use (:empty_400, :empty_hash, :raise) for invalid request bodies,
45
- # or use a block for custom behavior.
46
- def self.configure(app, action=nil, &block)
54
+ # or use a block for custom behavior. The rescue_classes argument is an exception
55
+ # class or array of exception classes to rescue.
56
+ def self.configure(app, action=nil, rescue_classes: StandardError, &block)
57
+ app.opts[:invalid_response_body_rescue_classes] = Array(rescue_classes).dup.freeze
58
+
47
59
  if action
48
60
  if block
49
61
  raise RodaError, "cannot provide both block and action when loading invalid_request_body plugin"
@@ -56,7 +68,7 @@ class Roda
56
68
 
57
69
  app::RodaRequest.send(:alias_method, :handle_invalid_request_body, method)
58
70
  elsif block
59
- app::RodaRequest.class_eval do
71
+ app::RodaRequest.class_exec do
60
72
  define_method(:handle_invalid_request_body, &block)
61
73
  alias handle_invalid_request_body handle_invalid_request_body
62
74
  end
@@ -67,6 +79,10 @@ class Roda
67
79
  app::RodaRequest.send(:private, :handle_invalid_request_body)
68
80
  end
69
81
 
82
+ module ClassMethods
83
+ RodaPlugins.opt_attr_reader(self, :invalid_response_body_rescue_classes)
84
+ end
85
+
70
86
  module RequestMethods
71
87
  # Handle invalid request bodies as configured if the default behavior
72
88
  # raises an exception.
@@ -74,7 +90,7 @@ class Roda
74
90
  super
75
91
  rescue RodaError
76
92
  raise
77
- rescue => e
93
+ rescue *roda_class.invalid_response_body_rescue_classes => e
78
94
  handle_invalid_request_body(e)
79
95
  end
80
96