roda 3.54.0 → 3.57.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +32 -0
  3. data/doc/conventions.rdoc +14 -11
  4. data/doc/release_notes/3.55.0.txt +12 -0
  5. data/doc/release_notes/3.56.0.txt +33 -0
  6. data/doc/release_notes/3.57.0.txt +34 -0
  7. data/lib/roda/plugins/chunked.rb +2 -2
  8. data/lib/roda/plugins/common_logger.rb +12 -1
  9. data/lib/roda/plugins/cookies.rb +2 -0
  10. data/lib/roda/plugins/hash_branch_view_subdir.rb +76 -0
  11. data/lib/roda/plugins/hash_branches.rb +145 -0
  12. data/lib/roda/plugins/hash_paths.rb +128 -0
  13. data/lib/roda/plugins/hash_routes.rb +13 -176
  14. data/lib/roda/plugins/json_parser.rb +6 -2
  15. data/lib/roda/plugins/middleware.rb +17 -2
  16. data/lib/roda/plugins/multi_public.rb +8 -0
  17. data/lib/roda/plugins/multi_route.rb +1 -1
  18. data/lib/roda/plugins/multi_view.rb +0 -4
  19. data/lib/roda/plugins/named_routes.rb +1 -2
  20. data/lib/roda/plugins/not_allowed.rb +13 -0
  21. data/lib/roda/plugins/public.rb +8 -0
  22. data/lib/roda/plugins/render.rb +5 -3
  23. data/lib/roda/plugins/route_csrf.rb +1 -0
  24. data/lib/roda/plugins/run_append_slash.rb +1 -1
  25. data/lib/roda/plugins/run_require_slash.rb +46 -0
  26. data/lib/roda/plugins/sessions.rb +1 -0
  27. data/lib/roda/plugins/sinatra_helpers.rb +10 -0
  28. data/lib/roda/plugins/static.rb +2 -0
  29. data/lib/roda/plugins/static_routing.rb +1 -1
  30. data/lib/roda/plugins/status_303.rb +6 -3
  31. data/lib/roda/plugins/status_handler.rb +35 -9
  32. data/lib/roda/plugins/symbol_status.rb +2 -0
  33. data/lib/roda/plugins/unescape_path.rb +2 -0
  34. data/lib/roda/request.rb +35 -1
  35. data/lib/roda/response.rb +5 -0
  36. data/lib/roda/version.rb +1 -1
  37. metadata +30 -6
@@ -3,58 +3,9 @@
3
3
  #
4
4
  class Roda
5
5
  module RodaPlugins
6
- # The hash_routes plugin combines the O(1) dispatching speed of the static_routing plugin with
7
- # the flexibility of the multi_route plugin. For any point in the routing tree,
8
- # it allows you dispatch to multiple routes where the next segment or the remaining path
9
- # is a static string.
10
- #
11
- # For a basic replacement of the multi_route plugin, you can replace class level
12
- # <tt>route('segment')</tt> calls with <tt>hash_branch('segment')</tt>:
13
- #
14
- # class App < Roda
15
- # plugin :hash_routes
16
- #
17
- # hash_branch("a") do |r|
18
- # # /a branch
19
- # end
20
- #
21
- # hash_branch("b") do |r|
22
- # # /b branch
23
- # end
24
- #
25
- # route do |r|
26
- # r.hash_branches
27
- # end
28
- # end
29
- #
30
- # With the above routing tree, the +r.hash_branches+ call in the main routing tree,
31
- # will dispatch requests for the +/a+ and +/b+ branches of the tree to the appropriate
32
- # routing blocks.
33
- #
34
- # In addition to supporting routing via the next segment, you can also support similar
35
- # routing for entire remaining path using the +hash_path+ class method:
36
- #
37
- # class App < Roda
38
- # plugin :hash_routes
39
- #
40
- # hash_path("/a") do |r|
41
- # # /a path
42
- # end
43
- #
44
- # hash_path("/a/b") do |r|
45
- # # /a/b path
46
- # end
47
- #
48
- # route do |r|
49
- # r.hash_paths
50
- # end
51
- # end
52
- #
53
- # With the above routing tree, the +r.hash_paths+ call will dispatch requests for the +/a+ and
54
- # +/a/b+ request paths.
55
- #
56
- # You can combine the two approaches, and use +r.hash_routes+ to first try routing the
57
- # full path, and then try routing the next segment:
6
+ # The hash_routes plugin builds on top of the hash_branches and hash_paths plugins, and adds
7
+ # a DSL for configuring hash branches and paths. It also adds an +r.hash_routes+ method for
8
+ # first attempting dispatch to the configured hash_paths, then to the configured hash_branches:
58
9
  #
59
10
  # class App < Roda
60
11
  # plugin :hash_routes
@@ -84,60 +35,14 @@ class Roda
84
35
  # +hash_path+ block. Other requests for the +/a+ branch, and all requests for the +/b+
85
36
  # branch will be routed to the appropriate +hash_branch+ block.
86
37
  #
87
- # Both +hash_branch+ and +hash_path+ support namespaces, which allows them to be used at
88
- # any level of the routing tree. Here is an example that uses namespaces for sub-branches:
89
- #
90
- # class App < Roda
91
- # plugin :hash_routes
92
- #
93
- # # Only one argument used, so the namespace defaults to '', and the argument
94
- # # specifies the route name
95
- # hash_branch("a") do |r|
96
- # # uses '/a' as the namespace when looking up routes,
97
- # # as that part of the path has been routed now
98
- # r.hash_routes
99
- # end
100
- #
101
- # # Two arguments used, so first specifies the namespace and the second specifies
102
- # # the route name
103
- # hash_branch('', "b") do |r|
104
- # # uses :b as the namespace when looking up routes, as that was explicitly specified
105
- # r.hash_routes(:b)
106
- # end
107
- #
108
- # hash_path("/a", "/b") do |r|
109
- # # /a/b path
110
- # end
111
- #
112
- # hash_path("/a", "/c") do |r|
113
- # # /a/c path
114
- # end
115
- #
116
- # hash_path(:b, "/b") do |r|
117
- # # /b/b path
118
- # end
119
- #
120
- # hash_path(:b, "/c") do |r|
121
- # # /b/c path
122
- # end
123
- #
124
- # route do |r|
125
- # # uses '' as the namespace, as no part of the path has been routed yet
126
- # r.hash_branches
127
- # end
128
- # end
129
- #
130
- # With the above routing tree, requests for the +/a+ and +/b+ branches will be
131
- # dispatched to the appropriate +hash_branch+ block. Those blocks will the dispatch
132
- # to the +hash_path+ blocks, with the +/a+ branch using the implicit namespace of
133
- # +/a+, and the +/b+ branch using the explicit namespace of +:b+. In general, it
134
- # is best for performance to explicitly specify the namespace when calling
135
- # +r.hash_branches+, +r.hash_paths+, and +r.hash_routes+.
38
+ # It is best for performance to explicitly specify the namespace when calling
39
+ # +r.hash_routes+.
136
40
  #
137
41
  # Because specifying routes explicitly using the +hash_branch+ and +hash_path+
138
42
  # class methods can get repetitive, the hash_routes plugin offers a DSL for DRYing
139
- # the code up. This DSL is used by calling the +hash_routes+ class method. Below
140
- # is a translation of the previous example to using the +hash_routes+ DSL:
43
+ # the code up. This DSL is used by calling the +hash_routes+ class method. The
44
+ # DSL used tries to mirror the standard Roda DSL, but it is not a normal routing
45
+ # tree (it's not possible to execute arbitrary code between branches during routing).
141
46
  #
142
47
  # class App < Roda
143
48
  # plugin :hash_routes
@@ -264,9 +169,12 @@ class Roda
264
169
  # * views
265
170
  # * all verb methods (get, post, etc.)
266
171
  module HashRoutes
172
+ def self.load_dependencies(app)
173
+ app.plugin :hash_branches
174
+ app.plugin :hash_paths
175
+ end
176
+
267
177
  def self.configure(app)
268
- app.opts[:hash_branches] ||= {}
269
- app.opts[:hash_paths] ||= {}
270
178
  app.opts[:hash_routes_methods] ||= {}
271
179
  end
272
180
 
@@ -359,24 +267,10 @@ class Roda
359
267
  module ClassMethods
360
268
  # Freeze the hash_routes metadata when freezing the app.
361
269
  def freeze
362
- opts[:hash_branches].freeze.each_value(&:freeze)
363
- opts[:hash_paths].freeze.each_value(&:freeze)
364
270
  opts[:hash_routes_methods].freeze
365
271
  super
366
272
  end
367
273
 
368
- # Duplicate hash_routes metadata in subclass.
369
- def inherited(subclass)
370
- super
371
-
372
- [:hash_branches, :hash_paths].each do |k|
373
- h = subclass.opts[k]
374
- opts[k].each do |namespace, routes|
375
- h[namespace] = routes.dup
376
- end
377
- end
378
- end
379
-
380
274
  # Invoke the DSL for configuring hash routes, see DSL for methods inside the
381
275
  # block. If the block accepts an argument, yield the DSL instance. If the
382
276
  # block does not accept an argument, instance_exec the block in the context
@@ -393,66 +287,9 @@ class Roda
393
287
 
394
288
  dsl
395
289
  end
396
-
397
- # Add branch handler for the given namespace and segment. If called without
398
- # a block, removes the existing branch handler if it exists.
399
- def hash_branch(namespace='', segment, &block)
400
- segment = "/#{segment}"
401
- routes = opts[:hash_branches][namespace] ||= {}
402
- if block
403
- routes[segment] = define_roda_method(routes[segment] || "hash_branch_#{namespace}_#{segment}", 1, &convert_route_block(block))
404
- elsif meth = routes[segment]
405
- routes.delete(segment)
406
- remove_method(meth)
407
- end
408
- end
409
-
410
- # Add path handler for the given namespace and path. When the
411
- # r.hash_paths method is called, checks the matching namespace
412
- # for the full remaining path, and dispatch to that block if
413
- # there is one. If called without a block, removes the existing
414
- # path handler if it exists.
415
- def hash_path(namespace='', path, &block)
416
- routes = opts[:hash_paths][namespace] ||= {}
417
- if block
418
- routes[path] = define_roda_method(routes[path] || "hash_path_#{namespace}_#{path}", 1, &convert_route_block(block))
419
- elsif meth = routes[path]
420
- routes.delete(path)
421
- remove_method(meth)
422
- end
423
- end
424
290
  end
425
291
 
426
292
  module RequestMethods
427
- # Checks the matching hash_branch namespace for a branch matching the next
428
- # segment in the remaining path, and dispatch to that block if there is one.
429
- def hash_branches(namespace=matched_path)
430
- rp = @remaining_path
431
-
432
- return unless rp.getbyte(0) == 47 # "/"
433
-
434
- if routes = roda_class.opts[:hash_branches][namespace]
435
- if segment_end = rp.index('/', 1)
436
- if meth = routes[rp[0, segment_end]]
437
- @remaining_path = rp[segment_end, 100000000]
438
- always{scope.send(meth, self)}
439
- end
440
- elsif meth = routes[rp]
441
- @remaining_path = ''
442
- always{scope.send(meth, self)}
443
- end
444
- end
445
- end
446
-
447
- # Checks the matching hash_path namespace for a branch matching the
448
- # remaining path, and dispatch to that block if there is one.
449
- def hash_paths(namespace=matched_path)
450
- if (routes = roda_class.opts[:hash_paths][namespace]) && (meth = routes[@remaining_path])
451
- @remaining_path = ''
452
- always{scope.send(meth, self)}
453
- end
454
- end
455
-
456
293
  # Check for matches in both the hash_path and hash_branch namespaces for
457
294
  # a matching remaining path or next segment in the remaining path, respectively.
458
295
  def hash_routes(namespace=matched_path)
@@ -4,12 +4,16 @@ require 'json'
4
4
 
5
5
  class Roda
6
6
  module RodaPlugins
7
- # The json_parser plugin parses request bodies in json format
7
+ # The json_parser plugin parses request bodies in JSON format
8
8
  # if the request's content type specifies json. This is mostly
9
9
  # designed for use with JSON API sites.
10
10
  #
11
11
  # This only parses the request body as JSON if the Content-Type
12
12
  # header for the request includes "json".
13
+ #
14
+ # The parsed JSON body will be available in +r.POST+, just as a
15
+ # parsed HTML form body would be. It will also be available in
16
+ # +r.params+ (which merges +r.GET+ with +r.POST+).
13
17
  module JsonParser
14
18
  DEFAULT_ERROR_HANDLER = proc{|r| r.halt [400, {}, []]}
15
19
 
@@ -25,7 +29,7 @@ class Roda
25
29
  # object as the second argument, so the parser needs
26
30
  # to respond to +call(str, request)+.
27
31
  # :wrap :: Whether to wrap uploaded JSON data in a hash with a "_json"
28
- # key. Without this, calls to r.params will fail if a non-Hash
32
+ # key. Without this, calls to +r.params+ will fail if a non-Hash
29
33
  # (such as an array) is uploaded in JSON format. A value of
30
34
  # :always will wrap all values, and a value of :unless_hash will
31
35
  # only wrap values that are not already hashes.
@@ -73,11 +73,16 @@ class Roda
73
73
  # and rack response for all requests passing through the middleware,
74
74
  # after either the middleware or next app handles the request
75
75
  # and returns a response.
76
+ # :forward_response_headers :: Whether changes to the response headers made inside
77
+ # the middleware's route block should be applied to the
78
+ # final response when the request is forwarded to the app.
79
+ # Defaults to false.
76
80
  def self.configure(app, opts={}, &block)
77
81
  app.opts[:middleware_env_var] = opts[:env_var] if opts.has_key?(:env_var)
78
82
  app.opts[:middleware_env_var] ||= 'roda.forward_next'
79
83
  app.opts[:middleware_configure] = block if block
80
84
  app.opts[:middleware_handle_result] = opts[:handle_result]
85
+ app.opts[:middleware_forward_response_headers] = opts[:forward_response_headers]
81
86
  end
82
87
 
83
88
  # Forwarder instances are what is actually used as middleware.
@@ -108,6 +113,10 @@ class Roda
108
113
 
109
114
  if call_next
110
115
  res = @app.call(env)
116
+
117
+ if modified_headers = env.delete('roda.response_headers')
118
+ res[1] = modified_headers.merge(res[1])
119
+ end
111
120
  end
112
121
 
113
122
  if handle_result = @mid.opts[:middleware_handle_result]
@@ -135,7 +144,10 @@ class Roda
135
144
  def call(&block)
136
145
  super do |r|
137
146
  res = instance_exec(r, &block) # call Fallback
138
- throw :next, true if r.forward_next
147
+ if r.forward_next
148
+ r.env['roda.response_headers'] = response.headers if opts[:middleware_forward_response_headers]
149
+ throw :next, true
150
+ end
139
151
  res
140
152
  end
141
153
  end
@@ -144,7 +156,10 @@ class Roda
144
156
  # that the next middleware is called.
145
157
  def _roda_run_main_route(r)
146
158
  res = super
147
- throw :next, true if r.forward_next
159
+ if r.forward_next
160
+ r.env['roda.response_headers'] = response.headers if opts[:middleware_forward_response_headers]
161
+ throw :next, true
162
+ end
148
163
  res
149
164
  end
150
165
  end
@@ -1,5 +1,13 @@
1
1
  # frozen-string-literal: true
2
2
 
3
+ begin
4
+ require 'rack/files'
5
+ rescue LoadError
6
+ # :nocov:
7
+ require 'rack/file'
8
+ # :nocov:
9
+ end
10
+
3
11
  #
4
12
  class Roda
5
13
  module RodaPlugins
@@ -8,7 +8,7 @@ class Roda
8
8
  # which will check # if the first segment in the path matches a named route,
9
9
  # and dispatch to that named route.
10
10
  #
11
- # The hash_routes plugin offers a +r.hash_routes+ method that is similar to
11
+ # The hash_branches plugin offers a +r.hash_branches+ method that is similar to
12
12
  # and performs better than the +r.multi_route+ method, and it is recommended
13
13
  # to consider using that instead of this plugin.
14
14
  #
@@ -16,10 +16,6 @@ class Roda
16
16
  # +multi_view_compile+ class method that will take an array of view template
17
17
  # names and construct a regexp that can be passed to +r.multi_view+.
18
18
  #
19
- # The hash_routes plugin offers a views method that is similar to and performs
20
- # better than the +r.multi_view+ method, and it is recommended to consider
21
- # using that instead of this plugin.
22
- #
23
19
  # Example:
24
20
  #
25
21
  # plugin :multi_view
@@ -145,8 +145,7 @@ class Roda
145
145
  routes = opts[:namespaced_routes][namespace] ||= {}
146
146
  if block
147
147
  routes[name] = define_roda_method(routes[name] || "named_routes_#{namespace}_#{name}", 1, &convert_route_block(block))
148
- elsif meth = routes[name]
149
- routes.delete(name)
148
+ elsif meth = routes.delete(name)
150
149
  remove_method(meth)
151
150
  end
152
151
  else
@@ -17,6 +17,9 @@ class Roda
17
17
  # will return a 200 response for <tt>GET /</tt> and a 405
18
18
  # response for <tt>POST /</tt>.
19
19
  #
20
+ # This plugin changes the +r.root+ method to return a 405 status
21
+ # for non-GET requests to +/+.
22
+ #
20
23
  # This plugin also changes the +r.is+ method so that if you use
21
24
  # a verb method inside +r.is+, it returns a 405 status if none
22
25
  # of the verb methods match. So this code:
@@ -100,6 +103,15 @@ class Roda
100
103
  end
101
104
  end
102
105
 
106
+ # Treat +r.root+ similar to <tt>r.get ''</tt>, using a 405
107
+ # response for non-GET requests.
108
+ def root
109
+ super
110
+ if @remaining_path == "/" && !is_get?
111
+ always{method_not_allowed("GET")}
112
+ end
113
+ end
114
+
103
115
  # Setup methods for all verbs. If inside an is block and not given
104
116
  # arguments, record the verb used. If given an argument, add an is
105
117
  # check with the arguments.
@@ -129,6 +141,7 @@ class Roda
129
141
  res = response
130
142
  res.status = 405
131
143
  res['Allow'] = verbs
144
+ nil
132
145
  end
133
146
  end
134
147
  end
@@ -2,6 +2,14 @@
2
2
 
3
3
  require 'uri'
4
4
 
5
+ begin
6
+ require 'rack/files'
7
+ rescue LoadError
8
+ # :nocov:
9
+ require 'rack/file'
10
+ # :nocov:
11
+ end
12
+
5
13
  #
6
14
  class Roda
7
15
  module RodaPlugins
@@ -495,8 +495,10 @@ class Roda
495
495
 
496
496
  # Render the given template. If there is a default layout
497
497
  # for the class, take the result of the template rendering
498
- # and render it inside the layout. See Render for details.
499
- def view(template, opts = (content = _optimized_view_content(template); OPTS))
498
+ # and render it inside the layout. Blocks passed to view
499
+ # are passed to render when rendering the template.
500
+ # See Render for details.
501
+ def view(template, opts = (content = _optimized_view_content(template) unless defined?(yield); OPTS), &block)
500
502
  if content
501
503
  # First, check if the optimized layout method has already been created,
502
504
  # and use it if so. This way avoids the extra conditional and local variable
@@ -516,7 +518,7 @@ class Roda
516
518
  end
517
519
  else
518
520
  opts = parse_template_opts(template, opts)
519
- content = opts[:content] || render_template(opts)
521
+ content = opts[:content] || render_template(opts, &block)
520
522
  end
521
523
 
522
524
  if layout_opts = view_layout_opts(opts)
@@ -4,6 +4,7 @@ require 'base64'
4
4
  require 'openssl'
5
5
  require 'securerandom'
6
6
  require 'uri'
7
+ require 'rack/utils'
7
8
 
8
9
  class Roda
9
10
  module RodaPlugins
@@ -34,7 +34,7 @@ class Roda
34
34
  # path internally, or a redirect is issued when configured with
35
35
  # <tt>use_redirects: true</tt>.
36
36
  def run(*)
37
- if remaining_path.empty?
37
+ if @remaining_path.empty?
38
38
  if scope.opts[:run_append_slash_redirect]
39
39
  redirect("#{path}/")
40
40
  else
@@ -0,0 +1,46 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The run_require_slash plugin makes +r.run+ a no-op if the remaining
7
+ # path is not empty and does not start with +/+. The Rack SPEC requires that
8
+ # +PATH_INFO+ start with a slash if not empty, so this plugin prevents
9
+ # dispatching to the application with an environment that would violate the
10
+ # Rack SPEC.
11
+ #
12
+ # You are unlikely to want to use this plugin unless are consuming partial
13
+ # segments of the request path, or using the match_affix plugin to change
14
+ # how routing is done:
15
+ #
16
+ # plugin :match_affix, "", /(\/|\z)/
17
+ # route do |r|
18
+ # r.on "/a" do
19
+ # r.on "b" do
20
+ # r.run App
21
+ # end
22
+ # end
23
+ # end
24
+ #
25
+ # # with run_require_slash:
26
+ # # GET /a/b/e => App not dispatched to
27
+ # # GET /a/b => App gets "" as PATH_INFO
28
+ #
29
+ # # with run_require_slash:
30
+ # # GET /a/b/e => App gets "e" as PATH_INFO, violating rack SPEC
31
+ # # GET /a/b => App gets "" as PATH_INFO
32
+ module RunRequireSlash
33
+ module RequestMethods
34
+ # Calls the given rack app only if the remaining patch is empty or
35
+ # starts with a slash.
36
+ def run(*)
37
+ if @remaining_path.empty? || @remaining_path.start_with?('/')
38
+ super
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ register_plugin(:run_require_slash, RunRequireSlash)
45
+ end
46
+ end
@@ -14,6 +14,7 @@ require 'base64'
14
14
  require 'json'
15
15
  require 'securerandom'
16
16
  require 'zlib'
17
+ require 'rack/utils'
17
18
 
18
19
  class Roda
19
20
  module RodaPlugins
@@ -1,5 +1,15 @@
1
1
  # frozen-string-literal: true
2
2
 
3
+ require 'rack/mime'
4
+ begin
5
+ require 'rack/files'
6
+ rescue LoadError
7
+ # :nocov:
8
+ require 'rack/file'
9
+ # :nocov:
10
+ end
11
+
12
+
3
13
  #
4
14
  class Roda
5
15
  module RodaPlugins
@@ -1,5 +1,7 @@
1
1
  # frozen-string-literal: true
2
2
 
3
+ require 'rack/static'
4
+
3
5
  #
4
6
  class Roda
5
7
  module RodaPlugins
@@ -50,7 +50,7 @@ class Roda
50
50
  # while still handling the request methods differently.
51
51
  module StaticRouting
52
52
  def self.load_dependencies(app)
53
- app.plugin :hash_routes
53
+ app.plugin :hash_paths
54
54
  end
55
55
 
56
56
  module ClassMethods
@@ -17,10 +17,13 @@ class Roda
17
17
  private
18
18
 
19
19
  def default_redirect_status
20
- if env['HTTP_VERSION'] == 'HTTP/1.1' && !is_get?
21
- 303
22
- else
20
+ return super if is_get?
21
+
22
+ case http_version
23
+ when 'HTTP/1.0', 'HTTP/0.9', nil
23
24
  super
25
+ else
26
+ 303
24
27
  end
25
28
  end
26
29
  end
@@ -15,24 +15,53 @@ class Roda
15
15
  # status_handler(403) do
16
16
  # "You are forbidden from seeing that!"
17
17
  # end
18
+ #
18
19
  # status_handler(404) do
19
20
  # "Where did it go?"
20
21
  # end
21
22
  #
23
+ # status_handler(405, keep_headers: ['Accept']) do
24
+ # "Use a different method!"
25
+ # end
26
+ #
22
27
  # Before a block is called, any existing headers on the response will be
23
- # cleared. So if you want to be sure the headers are set even in your block,
24
- # you need to reset them in the block.
28
+ # cleared, unless the +:keep_headers+ option is used. If the +:keep_headers+
29
+ # option is used, the value should be an array, and only the headers listed
30
+ # in the array will be kept.
25
31
  module StatusHandler
32
+ CLEAR_HEADERS = :clear.to_proc
33
+ private_constant :CLEAR_HEADERS
34
+
26
35
  def self.configure(app)
27
36
  app.opts[:status_handler] ||= {}
28
37
  end
29
38
 
30
39
  module ClassMethods
31
40
  # Install the given block as a status handler for the given HTTP response code.
32
- def status_handler(code, &block)
41
+ def status_handler(code, opts=OPTS, &block)
33
42
  # For backwards compatibility, pass request argument if block accepts argument
34
43
  arity = block.arity == 0 ? 0 : 1
35
- opts[:status_handler][code] = [define_roda_method(:"_roda_status_handler_#{code}", arity, &block), arity]
44
+ handle_headers = case keep_headers = opts[:keep_headers]
45
+ when nil, false
46
+ CLEAR_HEADERS
47
+ when Array
48
+ # :nocov:
49
+ if Rack.release >= '2.3'
50
+ keep_headers = keep_headers.map(&:downcase)
51
+ end
52
+ # :nocov:
53
+ lambda{|headers| headers.delete_if{|k,_| !keep_headers.include?(k)}}
54
+ else
55
+ raise RodaError, "Invalid :keep_headers option"
56
+ end
57
+
58
+ meth = define_roda_method(:"_roda_status_handler__#{code}", arity, &block)
59
+ self.opts[:status_handler][code] = define_roda_method(:"_roda_status_handler_#{code}", 1) do |result|
60
+ res = @_response
61
+ res.status = result[0]
62
+ handle_headers.call(res.headers)
63
+ result.replace(_roda_handle_route{arity == 1 ? send(meth, @_request) : send(meth)})
64
+ end
36
65
  end
37
66
 
38
67
  # Freeze the hash of status handlers so that there can be no thread safety issues at runtime.
@@ -47,11 +76,8 @@ class Roda
47
76
 
48
77
  # If routing returns a response we have a handler for, call that handler.
49
78
  def _roda_after_20__status_handler(result)
50
- if result && (meth, arity = opts[:status_handler][result[0]]; meth) && (v = result[2]).is_a?(Array) && v.empty?
51
- res = @_response
52
- res.headers.clear
53
- res.status = result[0]
54
- result.replace(_roda_handle_route{arity == 1 ? send(meth, @_request) : send(meth)})
79
+ if result && (meth = opts[:status_handler][result[0]]) && (v = result[2]).is_a?(Array) && v.empty?
80
+ send(meth, result)
55
81
  end
56
82
  end
57
83
  end
@@ -1,5 +1,7 @@
1
1
  # frozen-string-literal: true
2
2
 
3
+ require 'rack/utils'
4
+
3
5
  class Roda
4
6
  module RodaPlugins
5
7
  # The symbol_status plugin patches the +status=+ response method to
@@ -1,5 +1,7 @@
1
1
  # frozen-string-literal: true
2
2
 
3
+ require 'rack/utils'
4
+
3
5
  #
4
6
  class Roda
5
7
  module RodaPlugins