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
@@ -53,10 +53,13 @@ class Roda
53
53
  end
54
54
 
55
55
  module ClassMethods
56
+ RodaPlugins.opt_attr_reader(self, :remaining_path_rewrites)
57
+ RodaPlugins.opt_attr_reader(self, :path_info_rewrites)
58
+
56
59
  # Freeze the path rewrite metadata.
57
60
  def freeze
58
- opts[:remaining_path_rewrites].freeze
59
- opts[:path_info_rewrites].freeze
61
+ remaining_path_rewrites.freeze
62
+ path_info_rewrites.freeze
60
63
  super
61
64
  end
62
65
 
@@ -75,7 +78,7 @@ class Roda
75
78
  end
76
79
 
77
80
  was = /\A#{Regexp.escape(was)}/ unless was.is_a?(Regexp)
78
- array = @opts[opts[:path_info] ? :path_info_rewrites : :remaining_path_rewrites]
81
+ array = opts[:path_info] ? path_info_rewrites : remaining_path_rewrites
79
82
  array << [was, is.dup.freeze].freeze
80
83
  end
81
84
  end
@@ -85,10 +88,10 @@ class Roda
85
88
  def initialize(scope, env)
86
89
  path_info = env['PATH_INFO']
87
90
 
88
- rewrite_path(scope.class.opts[:path_info_rewrites], path_info)
91
+ rewrite_path(scope.class.path_info_rewrites, path_info)
89
92
  super
90
93
  remaining_path = @remaining_path = @remaining_path.dup
91
- rewrite_path(scope.class.opts[:remaining_path_rewrites], remaining_path)
94
+ rewrite_path(scope.class.remaining_path_rewrites, remaining_path)
92
95
  end
93
96
 
94
97
  private
@@ -280,7 +280,7 @@ class Roda
280
280
 
281
281
  # Yield the current Permissions Policy to the block.
282
282
  def self.configure(app, opts=OPTS)
283
- policy = app.opts[:permissions_policy] = if policy = app.opts[:permissions_policy]
283
+ policy = app.opts[:permissions_policy] = if policy = app.permissions_policy
284
284
  policy.dup
285
285
  else
286
286
  Policy.new
@@ -296,6 +296,10 @@ class Roda
296
296
  policy.freeze
297
297
  end
298
298
 
299
+ module ClassMethods
300
+ RodaPlugins.opt_attr_reader(self, :permissions_policy)
301
+ end
302
+
299
303
  module InstanceMethods
300
304
  # If a block is given, yield the current permission policy. Returns the
301
305
  # current permissions policy.
@@ -315,7 +319,7 @@ class Roda
315
319
 
316
320
  # The current permissions policy to be used for this response.
317
321
  def permissions_policy
318
- @permissions_policy ||= roda_class.opts[:permissions_policy].dup
322
+ @permissions_policy ||= roda_class.permissions_policy.dup
319
323
  end
320
324
 
321
325
  # Do not set a permissions policy header for this response.
@@ -329,7 +333,7 @@ class Roda
329
333
  def set_default_headers
330
334
  super
331
335
  unless @skip_permissions_policy
332
- (@permissions_policy || roda_class.opts[:permissions_policy]).set_header(headers)
336
+ (@permissions_policy || roda_class.permissions_policy).set_header(headers)
333
337
  end
334
338
  end
335
339
  end
@@ -69,19 +69,15 @@ class Roda
69
69
  # :zstd :: Whether to serve already zstd-compressed files with a .zst extension
70
70
  # for clients supporting "zstd" transfer encoding.
71
71
  def self.configure(app, opts={})
72
- if opts[:root]
73
- app.opts[:public_root] = app.expand_path(opts[:root])
74
- elsif !app.opts[:public_root]
75
- app.opts[:public_root] = app.expand_path("public")
76
- end
77
- app.opts[:public_server] = RACK_FILES.new(app.opts[:public_root], opts[:headers]||{}, opts[:default_mime] || 'text/plain')
72
+ app.opts[:public_root] = app.expand_path(opts[:root] || app.public_root || "public")
73
+ app.opts[:public_server] = RACK_FILES.new(app.public_root, opts[:headers]||{}, opts[:default_mime] || 'text/plain')
78
74
 
79
75
  unless encodings = opts[:encodings]
80
76
  if ENCODING_MAP.any?{|k,| opts.has_key?(k)}
81
77
  encodings = ENCODING_MAP.map{|k, v| [v, ENCODING_EXTENSIONS[v]] if opts[k]}.compact
82
78
  end
83
79
  end
84
- encodings = (encodings || app.opts[:public_encodings] || EMPTY_ARRAY).map(&:dup).freeze
80
+ encodings = (encodings || app.public_encodings || EMPTY_ARRAY).map(&:dup).freeze
85
81
  encodings.each do |a|
86
82
  a << /\b#{a[0]}\b/
87
83
  end
@@ -89,10 +85,16 @@ class Roda
89
85
  app.opts[:public_encodings] = encodings
90
86
  end
91
87
 
88
+ module ClassMethods
89
+ RodaPlugins.opt_attr_reader(self, :public_root)
90
+ RodaPlugins.opt_attr_reader(self, :public_server)
91
+ RodaPlugins.opt_attr_reader(self, :public_encodings)
92
+ end
93
+
92
94
  module RequestMethods
93
95
  # Serve files from the public directory if the file exists and this is a GET request.
94
96
  def public
95
- public_serve_with(roda_class.opts[:public_server])
97
+ public_serve_with(roda_class.public_server)
96
98
  end
97
99
 
98
100
  private
@@ -124,11 +126,10 @@ class Roda
124
126
  path = PARSER.unescape(real_remaining_path)
125
127
  return if path.include?("\0")
126
128
 
127
- roda_opts = roda_class.opts
128
129
  path = ::File.join(server.root, *public_path_segments(path))
129
130
 
130
131
  if accept_encoding = env['HTTP_ACCEPT_ENCODING']
131
- roda_opts[:public_encodings].each do |enc, ext, regexp|
132
+ roda_class.public_encodings.each do |enc, ext, regexp|
132
133
  if regexp.send(MATCH_METHOD, accept_encoding)
133
134
  public_serve_compressed(server, path, ext, enc)
134
135
  end
@@ -57,7 +57,7 @@ class Roda
57
57
  precompiled_file = app.assets_opts[:precompiled]
58
58
  prev_mtime = ::File.mtime(precompiled_file)
59
59
  app.instance_exec do
60
- opts[:assets] = opts[:assets].merge(:compiled=>_compiled_assets_initial_hash.replace(assets_opts[:compiled])).freeze
60
+ opts[:assets] = assets_opts.merge(:compiled=>_compiled_assets_initial_hash.replace(assets_opts[:compiled])).freeze
61
61
 
62
62
  define_singleton_method(:recheck_precompiled_assets) do
63
63
  new_mtime = ::File.mtime(precompiled_file)
@@ -53,13 +53,16 @@ class Roda
53
53
  # uses a hash that redirects GET and HEAD requests with a 301 status,
54
54
  # and other request methods with a 307 status.
55
55
  def self.configure(app, opts=OPTS)
56
- previous = app.opts[:redirect_http_to_https] || DEFAULTS
57
- opts = app.opts[:redirect_http_to_https] = previous.merge(opts)
56
+ opts = app.opts[:redirect_http_to_https] = (app.redirect_http_to_https_opts || DEFAULTS).merge(opts)
58
57
  opts[:port_string] = opts[:port] ? ":#{opts[:port]}".freeze : "".freeze
59
58
  opts[:prefix] = opts[:host] ? "https://#{opts[:host]}#{opts[:port_string]}".freeze : nil
60
59
  opts.freeze
61
60
  end
62
61
 
62
+ module ClassMethods
63
+ RodaPlugins.opt_attr_reader(self, :redirect_http_to_https, name: :redirect_http_to_https_opts)
64
+ end
65
+
63
66
  module RequestMethods
64
67
  # Redirect HTTP requests to HTTPS. While this doesn't secure the
65
68
  # current request, it makes it more likely that the browser will submit
@@ -67,7 +70,7 @@ class Roda
67
70
  def redirect_http_to_https
68
71
  return if ssl?
69
72
 
70
- opts = roda_class.opts[:redirect_http_to_https]
73
+ opts = roda_class.redirect_http_to_https_opts
71
74
 
72
75
  res = response
73
76
 
@@ -323,15 +323,15 @@ class Roda
323
323
 
324
324
  # Setup default rendering options. See Render for details.
325
325
  def self.configure(app, opts=OPTS)
326
- if app.opts[:render]
327
- orig_cache = app.opts[:render][:cache]
328
- orig_method_cache = app.opts[:render][:template_method_cache]
329
- opts = app.opts[:render][:orig_opts].merge(opts)
326
+ if app.render_opts
327
+ orig_cache = app.render_opts[:cache]
328
+ orig_method_cache = app.render_opts[:template_method_cache]
329
+ opts = app.render_opts[:orig_opts].merge(opts)
330
330
  end
331
331
  app.opts[:render] = opts.dup
332
- app.opts[:render][:orig_opts] = opts
332
+ app.render_opts[:orig_opts] = opts
333
333
 
334
- opts = app.opts[:render]
334
+ opts = app.render_opts
335
335
  opts[:engine] = (opts[:engine] || "erb").dup.freeze
336
336
  opts[:views] = app.expand_path(opts[:views]||"views").freeze
337
337
  opts[:check_paths] = true unless opts.has_key?(:check_paths)
@@ -580,7 +580,7 @@ class Roda
580
580
  # affecting the parent class.
581
581
  def inherited(subclass)
582
582
  super
583
- opts = subclass.opts[:render] = subclass.opts[:render].dup
583
+ opts = subclass.opts[:render] = subclass.render_opts.dup
584
584
  if COMPILED_METHOD_SUPPORT
585
585
  opts[:template_method_cache] = (opts[:cache_class] || RodaCache).new
586
586
  end
@@ -588,10 +588,7 @@ class Roda
588
588
  opts.freeze
589
589
  end
590
590
 
591
- # Return the render options for this class.
592
- def render_opts
593
- opts[:render]
594
- end
591
+ RodaPlugins.opt_attr_reader(self, :render, name: :render_opts)
595
592
 
596
593
  private
597
594
 
@@ -603,13 +600,13 @@ class Roda
603
600
  # in order for the precompile_templates plugin to work correctly.
604
601
  instance.send(:retrieve_template, instance.send(:view_layout_opts, OPTS))
605
602
 
606
- if COMPILED_METHOD_SUPPORT && (layout_template = render_opts[:optimize_layout]) && !opts[:render][:optimized_layout_method_created]
603
+ if COMPILED_METHOD_SUPPORT && (layout_template = render_opts[:optimize_layout]) && !render_opts[:optimized_layout_method_created]
607
604
  instance.send(:retrieve_template, :template=>layout_template, :cache_key=>nil, :template_method_cache_key => :_roda_layout)
608
- layout_method = opts[:render][:template_method_cache][:_roda_layout]
605
+ layout_method = render_opts[:template_method_cache][:_roda_layout]
609
606
  define_method(:_layout_method){layout_method}
610
607
  private :_layout_method
611
608
  alias_method(:_layout_method, :_layout_method)
612
- opts[:render] = opts[:render].merge(:optimized_layout_method_created=>true)
609
+ opts[:render] = render_opts.merge(:optimized_layout_method_created=>true)
613
610
  end
614
611
  end
615
612
  end
@@ -650,7 +647,7 @@ class Roda
650
647
  # If we have an optimized template method but no optimized layout method, create the
651
648
  # optimized layout method if possible and use it. If you can't create the optimized
652
649
  # layout method, fall through to the slower approach.
653
- if layout_template = self.class.opts[:render][:optimize_layout]
650
+ if layout_template = self.class.render_opts[:optimize_layout]
654
651
  retrieve_template(:template=>layout_template, :cache_key=>nil, :template_method_cache_key => :_roda_layout)
655
652
  if layout_method = _layout_method
656
653
  return _call_optimized_template_method(layout_method, OPTS){content}
@@ -695,7 +692,7 @@ class Roda
695
692
 
696
693
  # Return a symbol containing the optimized layout method
697
694
  def _layout_method
698
- self.class.opts[:render][:template_method_cache][:_roda_layout]
695
+ self.class.render_opts[:template_method_cache][:_roda_layout]
699
696
  end
700
697
 
701
698
  # Use an optimized render path for templates with a hash of locals. Returns the result
@@ -766,37 +763,25 @@ class Roda
766
763
  end
767
764
 
768
765
  if RUBY_VERSION >= '3'
769
- class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
770
- def _call_optimized_template_method((meth, fixed_locals), locals, &block)
771
- if fixed_locals
772
- send(meth, **locals, &block)
773
- else
774
- send(meth, locals, &block)
775
- end
766
+ def _call_optimized_template_method((meth, fixed_locals), locals, &block)
767
+ if fixed_locals
768
+ send(meth, **locals, &block)
769
+ else
770
+ send(meth, locals, &block)
776
771
  end
777
- RUBY
772
+ end
778
773
  # simplecov:disable
779
- elsif RUBY_VERSION >= '2'
780
- class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
781
- def _call_optimized_template_method((meth, fixed_locals), locals, &block)
782
- if fixed_locals
783
- if locals.empty?
784
- send(meth, &block)
785
- else
786
- send(meth, **locals, &block)
787
- end
774
+ else
775
+ def _call_optimized_template_method((meth, fixed_locals), locals, &block)
776
+ if fixed_locals
777
+ if locals.empty?
778
+ send(meth, &block)
788
779
  else
789
- send(meth, locals, &block)
780
+ send(meth, **locals, &block)
790
781
  end
782
+ else
783
+ send(meth, locals, &block)
791
784
  end
792
- RUBY
793
- else
794
- # Call the optimized template method. This is designed to be used with the
795
- # method cache, which caches the method name and whether the method uses
796
- # fixed locals. Methods with fixed locals need to be called with a keyword
797
- # splat.
798
- def _call_optimized_template_method((meth, fixed_locals), locals, &block)
799
- send(meth, locals, &block)
800
785
  end
801
786
  end
802
787
  # simplecov:enable
@@ -844,7 +829,7 @@ class Roda
844
829
  # Given the template name and options, set the template class, template path/content,
845
830
  # template block, and locals to use for the render in the passed options.
846
831
  def find_template(opts)
847
- render_opts = self.class.opts[:render]
832
+ render_opts = self.class.render_opts
848
833
  engine_override = opts[:engine]
849
834
  engine = opts[:engine] ||= render_opts[:engine]
850
835
  if content = opts[:inline]
@@ -908,7 +893,7 @@ class Roda
908
893
  end
909
894
  cached_template(opts) do
910
895
  opts = found_template_opts || find_template(opts)
911
- render_opts = self.class.opts[:render]
896
+ render_opts = self.class.render_opts
912
897
  template_opts = render_opts[:template_opts]
913
898
  if engine_opts = render_opts[:engine_opts][opts[:engine]]
914
899
  template_opts = template_opts.merge(engine_opts)
@@ -993,14 +978,14 @@ class Roda
993
978
  end
994
979
 
995
980
  module AssumeFixedLocalsInstanceMethods
981
+ private
982
+
996
983
  # simplecov:disable
997
984
  if RUBY_VERSION >= '3.0'
998
985
  # simplecov:enable
999
- class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
1000
- def _call_optimized_template_method((meth,_), locals, &block)
1001
- send(meth, **locals, &block)
1002
- end
1003
- RUBY
986
+ def _call_optimized_template_method((meth,_), locals, &block)
987
+ send(meth, **locals, &block)
988
+ end
1004
989
  end
1005
990
  end
1006
991
  end
@@ -42,11 +42,14 @@ class Roda
42
42
  # strip.
43
43
  def self.configure(app, opts=OPTS)
44
44
  app.opts[:render_coverage_strip_paths] = opts[:strip_paths].map{|f| File.expand_path(f)} if opts.has_key?(:strip_paths)
45
- coverage_dir = app.opts[:render_coverage_dir] = opts[:dir] || app.opts[:render_coverage_dir] || 'coverage/views'
45
+ coverage_dir = app.opts[:render_coverage_dir] = opts[:dir] || app.render_coverage_dir || 'coverage/views'
46
46
  Dir.mkdir(coverage_dir) unless File.directory?(coverage_dir)
47
47
  end
48
48
 
49
49
  module ClassMethods
50
+ RodaPlugins.opt_attr_reader(self, :render_coverage_strip_paths)
51
+ RodaPlugins.opt_attr_reader(self, :render_coverage_dir)
52
+
50
53
  # Set a compiled path on the created template, if the path for
51
54
  # the template is in one of the allowed_views.
52
55
  def create_template(opts, template_opts)
@@ -54,10 +57,10 @@ class Roda
54
57
 
55
58
  path = File.expand_path(opts[:path])
56
59
  compiled_path = nil
57
- (self.opts[:render_coverage_strip_paths] || render_opts[:allowed_paths]).each do |dir|
60
+ (render_coverage_strip_paths || render_opts[:allowed_paths]).each do |dir|
58
61
  dir += "/" unless dir.end_with?("/")
59
62
  if path.start_with?(dir)
60
- compiled_path = File.join(self.opts[:render_coverage_dir], path[dir.length, 10000000].tr('/', '-'))
63
+ compiled_path = File.join(render_coverage_dir, path[dir.length, 10000000].tr('/', '-'))
61
64
  break
62
65
  end
63
66
  end
@@ -32,14 +32,20 @@ class Roda
32
32
  end
33
33
 
34
34
  def self.configure(app, opts=OPTS)
35
- app.opts[:render_locals] = (app.opts[:render_locals] || {}).merge(opts[:render]||{}).freeze
36
- app.opts[:layout_locals] = (app.opts[:layout_locals] || {}).merge(opts[:layout]||{}).freeze
35
+ app.opts[:render_locals] = (app.render_locals || {}).merge(opts[:render]||{}).freeze
36
+ app.opts[:layout_locals] = (app.layout_locals || {}).merge(opts[:layout]||{}).freeze
37
37
  if opts.has_key?(:merge)
38
38
  app.opts[:merge_locals] = opts[:merge]
39
- app.opts[:layout_locals] = app.opts[:render_locals].merge(app.opts[:layout_locals]).freeze
39
+ app.opts[:layout_locals] = app.render_locals.merge(app.layout_locals).freeze
40
40
  end
41
41
  end
42
42
 
43
+ module ClassMethods
44
+ RodaPlugins.opt_attr_reader(self, :render_locals)
45
+ RodaPlugins.opt_attr_reader(self, :layout_locals)
46
+ RodaPlugins.opt_attr_reader(self, :merge_locals, name: :merge_locals?)
47
+ end
48
+
43
49
  module InstanceMethods
44
50
  private
45
51
 
@@ -56,11 +62,11 @@ class Roda
56
62
  end
57
63
 
58
64
  def render_locals
59
- opts[:render_locals]
65
+ self.class.render_locals
60
66
  end
61
67
 
62
68
  def layout_locals
63
- opts[:layout_locals]
69
+ self.class.layout_locals
64
70
  end
65
71
 
66
72
  # If this isn't the layout template, then use the plugin's render locals as the default locals.
@@ -79,7 +85,7 @@ class Roda
79
85
  # If using a layout, then use the plugin's layout locals as the default locals.
80
86
  def view_layout_opts(opts)
81
87
  if layout_opts = super
82
- merge_locals = layout_opts.has_key?(:merge_locals) ? layout_opts[:merge_locals] : self.opts[:merge_locals]
88
+ merge_locals = layout_opts.fetch(:merge_locals){self.class.merge_locals?}
83
89
 
84
90
  locals = {}
85
91
  locals.merge!(layout_locals)
@@ -20,7 +20,7 @@ class Roda
20
20
  def self.configure(app, setting)
21
21
  case setting
22
22
  when :allow, :raise, :warn
23
- app::RodaRequest.class_eval do
23
+ app::RodaRequest.class_exec do
24
24
  alias_method(:[], :"request_aref_#{setting}")
25
25
  alias_method(:[]=, :"request_aset_#{setting}")
26
26
  public :[], :[]=
@@ -50,12 +50,16 @@ class Roda
50
50
  else
51
51
  mime_types.dup
52
52
  end
53
- app.opts[:repsonse_content_types] = mime_types.freeze
53
+ app.opts[:response_content_types] = mime_types.freeze
54
54
  else
55
- app.opts[:repsonse_content_types] ||= {}
55
+ app.opts[:response_content_types] ||= {}
56
56
  end
57
57
  end
58
58
 
59
+ module ClassMethods
60
+ RodaPlugins.opt_attr_reader(self, :response_content_types)
61
+ end
62
+
59
63
  module ResponseMethods
60
64
  # Return the content-type of the response. Will be nil if it has
61
65
  # not yet been explicitly set.
@@ -68,7 +72,7 @@ class Roda
68
72
  # type with the given file extension. If the symbol is not
69
73
  # a recognized mime type, raises KeyError.
70
74
  def content_type=(mime_type)
71
- mime_type = roda_class.opts[:repsonse_content_types].fetch(mime_type) if mime_type.is_a?(Symbol)
75
+ mime_type = roda_class.response_content_types.fetch(mime_type) if mime_type.is_a?(Symbol)
72
76
  @headers[RodaResponseHeaders::CONTENT_TYPE] = mime_type
73
77
  end
74
78
  end
@@ -196,7 +196,7 @@ class Roda
196
196
  end
197
197
 
198
198
  def self.configure(app, opts=OPTS, &block)
199
- options = app.opts[:route_csrf] = (app.opts[:route_csrf] || DEFAULTS).merge(opts)
199
+ options = app.opts[:route_csrf] = (app.route_csrf_opts || DEFAULTS).merge(opts)
200
200
  if block || opts[:csrf_failure].is_a?(Proc)
201
201
  if block && opts[:csrf_failure]
202
202
  raise RodaError, "Cannot specify both route_csrf plugin block and :csrf_failure option"
@@ -212,6 +212,10 @@ class Roda
212
212
  options.freeze
213
213
  end
214
214
 
215
+ module ClassMethods
216
+ RodaPlugins.opt_attr_reader(self, :route_csrf, name: :route_csrf_opts)
217
+ end
218
+
215
219
  module InstanceMethods
216
220
  # Check that the submitted CSRF token is valid, if the request requires a CSRF token.
217
221
  # If the CSRF token is valid or the request does not require a CSRF token, return nil.
@@ -358,6 +362,7 @@ class Roda
358
362
  end
359
363
 
360
364
  if (rack_csrf_key = opts[:upgrade_from_rack_csrf_key]) && (rack_csrf_value = session[rack_csrf_key]) && csrf_compare(rack_csrf_value, encoded_token)
365
+ # RODA4: Remove :upgrade_from_rack_csrf_key support
361
366
  return
362
367
  end
363
368
 
@@ -392,7 +397,7 @@ class Roda
392
397
 
393
398
  # Helper for getting the plugin options.
394
399
  def csrf_options
395
- opts[:route_csrf]
400
+ self.class.route_csrf_opts
396
401
  end
397
402
 
398
403
  # Perform a constant-time comparison of the two strings, returning true if they match and false otherwise.
@@ -31,6 +31,10 @@ class Roda
31
31
  app.opts[:run_append_slash_redirect] = !!opts[:use_redirects]
32
32
  end
33
33
 
34
+ module ClassMethods
35
+ RodaPlugins.opt_attr_reader(self, :run_append_slash_redirect, name: :run_append_slash_redirect?)
36
+ end
37
+
34
38
  module RequestMethods
35
39
  # Calls the given rack app. If the path matches the root of the app but
36
40
  # does not contain a trailing slash, a trailing slash is appended to the
@@ -39,7 +43,7 @@ class Roda
39
43
  # the redirect approach will not be used.
40
44
  def run(*)
41
45
  if @remaining_path.empty?
42
- if scope.opts[:run_append_slash_redirect] && !(path = self.path).start_with?('//')
46
+ if roda_class.run_append_slash_redirect? && !(path = self.path).start_with?('//')
43
47
  redirect("#{path}/")
44
48
  else
45
49
  @remaining_path += '/'
@@ -69,7 +69,7 @@ class Roda
69
69
  class CsrfFailure < RodaError; end
70
70
 
71
71
  def self.configure(app, opts=OPTS, &block)
72
- options = app.opts[:sec_fetch_site_csrf] = (app.opts[:sec_fetch_site_csrf] || DEFAULTS).merge(opts)
72
+ options = app.opts[:sec_fetch_site_csrf] = (app.sec_fetch_site_csrf_opts || DEFAULTS).merge(opts)
73
73
 
74
74
  allowed_values = options[:allowed_values] = ["same-origin"]
75
75
  allowed_values << "same-site" if opts[:allow_same_site]
@@ -94,6 +94,10 @@ class Roda
94
94
  options.freeze
95
95
  end
96
96
 
97
+ module ClassMethods
98
+ RodaPlugins.opt_attr_reader(self, :sec_fetch_site_csrf, name: :sec_fetch_site_csrf_opts)
99
+ end
100
+
97
101
  module InstanceMethods
98
102
  # Check that the Sec-Fetch-Site header is valid, if the request requires it.
99
103
  # If the header is valid or the request does not require the header, return nil.
@@ -101,7 +105,7 @@ class Roda
101
105
  # if a block is not given, use the plugin :csrf_failure option to determine how to
102
106
  # handle it.
103
107
  def check_sec_fetch_site!(&block)
104
- plugin_opts = self.class.opts[:sec_fetch_site_csrf]
108
+ plugin_opts = self.class.sec_fetch_site_csrf_opts
105
109
  return unless plugin_opts[:check_request_methods].include?(request.request_method)
106
110
 
107
111
  sec_fetch_site = env["HTTP_SEC_FETCH_SITE"]
@@ -10,7 +10,6 @@ rescue OpenSSL::Cipher::CipherError
10
10
  # simplecov:enable
11
11
  end
12
12
 
13
- require 'json'
14
13
  require 'securerandom'
15
14
  require 'zlib'
16
15
  require 'rack/utils'
@@ -175,15 +174,16 @@ class Roda
175
174
 
176
175
  def self.load_dependencies(app, opts=OPTS)
177
176
  app.plugin :_base64
177
+ app.plugin :_json
178
178
  end
179
179
 
180
180
  # Configure the plugin, see Sessions for details on options.
181
181
  def self.configure(app, opts=OPTS)
182
- opts = (app.opts[:sessions] || DEFAULT_OPTIONS).merge(opts)
182
+ opts = (app.sessions_opts || DEFAULT_OPTIONS).merge(opts)
183
183
  co = opts[:cookie_options] = DEFAULT_COOKIE_OPTIONS.merge(opts[:cookie_options] || OPTS).freeze
184
184
  opts[:remove_cookie_options] = co.merge(:max_age=>'0', :expires=>Time.at(0))
185
- opts[:parser] ||= app.opts[:json_parser] || JSON.method(:parse)
186
- opts[:serializer] ||= app.opts[:json_serializer] || :to_json.to_proc
185
+ opts[:parser] ||= app.json_parser
186
+ opts[:serializer] ||= app.json_serializer
187
187
 
188
188
  opts[:per_cookie_cipher_secret] = true unless opts.has_key?(:per_cookie_cipher_secret)
189
189
  opts[:session_version_num] = opts[:per_cookie_cipher_secret] ? 1 : 0
@@ -216,6 +216,10 @@ class Roda
216
216
  app.opts[:sessions_convert_symbols] = true unless app.opts.has_key?(:sessions_convert_symbols)
217
217
  end
218
218
 
219
+ module ClassMethods
220
+ RodaPlugins.opt_attr_reader(self, :sessions, name: :sessions_opts)
221
+ end
222
+
219
223
  module InstanceMethods
220
224
  # Clear data from the session, and update the request environment
221
225
  # so that the session cookie will use a new creation timestamp
@@ -233,7 +237,7 @@ class Roda
233
237
  # update the rack response headers to set the session cookie in
234
238
  # the response.
235
239
  def _roda_after_50__sessions(res)
236
- if res && (session = env[self.class.opts[:sessions][:env_key]])
240
+ if res && (session = env[self.class.sessions_opts[:env_key]])
237
241
  @_request.persist_session(res[1], session)
238
242
  end
239
243
  end
@@ -247,7 +251,7 @@ class Roda
247
251
  # this method stores the session in 'rack.session' in the request environment,
248
252
  # but that does not happen until this method is called.
249
253
  def session
250
- @env[roda_class.opts[:sessions][:env_key]] ||= _load_session
254
+ @env[roda_class.sessions_opts[:env_key]] ||= _load_session
251
255
  end
252
256
 
253
257
  # The time the session was originally created. nil if there is no active session.
@@ -266,7 +270,7 @@ class Roda
266
270
  # Rack::Session::Cookie, mark the related cookie for expiration so it isn't
267
271
  # sent in the future.
268
272
  def persist_session(headers, session)
269
- opts = roda_class.opts[:sessions]
273
+ opts = roda_class.sessions_opts
270
274
 
271
275
  if session.empty?
272
276
  if env[SESSION_SERIALIZED]
@@ -317,7 +321,7 @@ class Roda
317
321
  # Load the session by looking for the appropriate cookie, or falling
318
322
  # back to the rack session cookie if configured.
319
323
  def _load_session
320
- opts = roda_class.opts[:sessions]
324
+ opts = roda_class.sessions_opts
321
325
  cs = cookies
322
326
 
323
327
  if data = cs[opts[:key]]
@@ -340,7 +344,7 @@ class Roda
340
344
  # serialized session using the default Rack::Session::Cookie
341
345
  # hmac and coder.
342
346
  def _deserialize_rack_session(data)
343
- opts = roda_class.opts[:sessions]
347
+ opts = roda_class.sessions_opts
344
348
  data, digest = data.split("--", 2)
345
349
  unless digest
346
350
  return _session_serialization_error("Not decoding Rack::Session::Cookie session: invalid format")
@@ -374,7 +378,7 @@ class Roda
374
378
 
375
379
  # Interpret given cookie data as a Rack::Session::Cookie
376
380
  def _deserialize_session(data)
377
- opts = roda_class.opts[:sessions]
381
+ opts = roda_class.sessions_opts
378
382
 
379
383
  begin
380
384
  data = Base64_.urlsafe_decode64(data)
@@ -466,7 +470,7 @@ class Roda
466
470
  end
467
471
 
468
472
  def _serialize_session(session)
469
- opts = roda_class.opts[:sessions]
473
+ opts = roda_class.sessions_opts
470
474
  env = @env
471
475
  now = Time.now.to_i
472
476
  json_data = opts[:serializer].call(session).force_encoding('BINARY')
@@ -74,6 +74,8 @@ class Roda
74
74
  end
75
75
 
76
76
  module ClassMethods
77
+ RodaPlugins.opt_attr_reader(self, :shape_friendly_scope_instance_variables)
78
+
77
79
  # Automatically refresh the instance variables used if the plugin
78
80
  # sets instance variables.
79
81
  def plugin(plugin, *args, &block)
@@ -107,7 +109,7 @@ class Roda
107
109
  ivs.concat(mod.const_get(const)) if mod.const_defined?(const)
108
110
  end
109
111
 
110
- if const == :SCOPE_INSTANCE_VARIABLES && (scope_ivs = opts[:shape_friendly_scope_instance_variables])
112
+ if const == :SCOPE_INSTANCE_VARIABLES && (scope_ivs = shape_friendly_scope_instance_variables)
111
113
  ivs.concat(scope_ivs)
112
114
  end
113
115