roda 3.69.0 → 3.71.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2871ec8db3a18a77238fdae1aeb786c7a7eb93f14b0827428fab1b07f6e7bee8
4
- data.tar.gz: 75db2ec79978055f407c2270cfd9119782322402c2c536a504fbfc08f4fedac7
3
+ metadata.gz: d5460d6cecb4f9b9acfedd05f5b14793bb9aefa2df2a8c29c559da319df87ee4
4
+ data.tar.gz: 75c3c8803abef4e27cac592a88597e8f9cd098be39bf1d0c1b1192175fd1f8e2
5
5
  SHA512:
6
- metadata.gz: 1ca4fdcac9bcba88309b8ccb4315ea2243530b08313135f97a643b81600b98821521fbbe08e9c7ab757732887326e8af492f0f3e883a3f54e7be73f0811d19e6
7
- data.tar.gz: 6b313eecbd4f62d07bca53fa23db2325b4f53f3faf55344c86d8f543ff92c5dcba94235f963582ce227d3602e3ea8633e981393ef6aedcbe20fb8152e6409c08
6
+ metadata.gz: 6efc49bb205012a7ce50bc3c300d924a149de33416a35870f11efed492251c331804b970c967e47a481b0ddb0a83507c5926c1dc32069941b4edd2bd56df09e5
7
+ data.tar.gz: 698b7e7daae4cd0712a20a1e3e274f7ba3cdb9068eefdeb3bb5f043f2d437cdd5dda961ce91d88794e22a8ca3ef5825e3f8bd3f92ff933eab772c352dd17c2ee
data/CHANGELOG CHANGED
@@ -1,3 +1,13 @@
1
+ = 3.71.0 (2023-08-14)
2
+
3
+ * Add match_hook_args plugin, similar to match_hooks but support matchers and block args as hook arguments (jeremyevans)
4
+
5
+ = 3.70.0 (2023-07-12)
6
+
7
+ * Add plain_hash_response_headers plugin, using a plain hash for response headers on Rack 3 for much better performance (jeremyevans)
8
+
9
+ * Use lower case response header keys by default on Rack 3, instead of relying on Rack::Headers conversion (jeremyevans)
10
+
1
11
  = 3.69.0 (2023-06-13)
2
12
 
3
13
  * Allow symbol_matcher in symbol_matchers plugin to take a block to allow type conversion (jeremyevans)
@@ -0,0 +1,19 @@
1
+ = New Features
2
+
3
+ * A plain_hash_response_headers plugin has been added. On Rack 3,
4
+ this changes Roda to use a plain hash for response headers (as it
5
+ does on Rack 2), instead of using Rack::Headers (the default on
6
+ Rack 3). For a minimal app, using this plugin can almost double
7
+ the performance on Rack 3. Before using this plugin, you should
8
+ make sure that all response headers set explictly in your
9
+ application are already lower-case.
10
+
11
+ = Improvements
12
+
13
+ * Roda now natively uses lower-case for all response headers set
14
+ implicitly when using Rack 3. Previously, Roda used mixed-case
15
+ response headers and had Rack::Headers handle the conversion to
16
+ lower-case (Rack 3 requires lower-case response headers). Note
17
+ that Rack::Headers is still used for response headers by default
18
+ on Rack 3, as applications may not have converted to using
19
+ lower-case response headers.
@@ -0,0 +1,33 @@
1
+ = New Feature
2
+
3
+ * A match_hook_args plugin has been added. This is similar to the
4
+ existing match_hook plugin, but passes through the matchers and
5
+ block arguments (values yielded to the match block). Example:
6
+
7
+ plugin :match_hook_args
8
+
9
+ add_match_hook do |matchers, block_args|
10
+ logger.debug("matchers: #{matchers.inspect}. #{block_args.inspect} yielded.")
11
+ end
12
+
13
+ # Term is an implicit matcher used for terminating matches, and
14
+ # will be included in the array of matchers yielded to the match hook
15
+ # if a terminating match is used.
16
+ term = self.class::RodaRequest::TERM
17
+
18
+ route do |r|
19
+ r.root do
20
+ # for a request for /
21
+ # matchers: nil, block_args: nil
22
+ end
23
+
24
+ r.on 'a', ['b', 'c'], Integer do |segment, id|
25
+ # for a request for /a/b/1
26
+ # matchers: ["a", ["b", "c"], Integer], block_args: ["b", 1]
27
+ end
28
+
29
+ r.get 'd' do
30
+ # for a request for /d
31
+ # matchers: ["d", term], block_args: []
32
+ end
33
+ end
@@ -430,8 +430,8 @@ class Roda
430
430
  opts[:css_headers] = headers.merge(opts[:css_headers])
431
431
  opts[:js_headers] = headers.merge(opts[:js_headers])
432
432
  end
433
- opts[:css_headers]['Content-Type'] ||= "text/css; charset=UTF-8".freeze
434
- opts[:js_headers]['Content-Type'] ||= "application/javascript; charset=UTF-8".freeze
433
+ opts[:css_headers][RodaResponseHeaders::CONTENT_TYPE] ||= "text/css; charset=UTF-8".freeze
434
+ opts[:js_headers][RodaResponseHeaders::CONTENT_TYPE] ||= "application/javascript; charset=UTF-8".freeze
435
435
 
436
436
  [:css_headers, :js_headers, :css_opts, :js_opts, :dependencies, :expanded_dependencies].each do |s|
437
437
  opts[s].freeze
@@ -754,7 +754,7 @@ class Roda
754
754
  file = "#{o[:"compiled_#{type}_path"]}#{file}"
755
755
 
756
756
  if o[:gzip] && env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/
757
- @_response['Content-Encoding'] = 'gzip'
757
+ @_response[RodaResponseHeaders::CONTENT_ENCODING] = 'gzip'
758
758
  file += '.gz'
759
759
  end
760
760
 
@@ -86,7 +86,7 @@ class Roda
86
86
  return unless time
87
87
  res = response
88
88
  e = env
89
- res['Last-Modified'] = time.httpdate
89
+ res[RodaResponseHeaders::LAST_MODIFIED] = time.httpdate
90
90
  return if e['HTTP_IF_NONE_MATCH']
91
91
  status = res.status
92
92
 
@@ -122,7 +122,7 @@ class Roda
122
122
 
123
123
  res = response
124
124
  e = env
125
- res['ETag'] = etag = "#{'W/' if weak}\"#{value}\""
125
+ res[RodaResponseHeaders::ETAG] = etag = "#{'W/' if weak}\"#{value}\""
126
126
  status = res.status
127
127
 
128
128
  if (!status || (status >= 200 && status < 300) || status == 304)
@@ -176,7 +176,7 @@ class Roda
176
176
  values << (v == true ? k : "#{k}=#{v}")
177
177
  end
178
178
 
179
- self['Cache-Control'] = values.join(', ') unless values.empty?
179
+ @headers[RodaResponseHeaders::CACHE_CONTROL] = values.join(', ') unless values.empty?
180
180
  end
181
181
 
182
182
  # Set Cache-Control header with the max_age given. max_age should
@@ -185,7 +185,7 @@ class Roda
185
185
  # HTTP 1.0 clients (Cache-Control is an HTTP 1.1 header).
186
186
  def expires(max_age, opts=OPTS)
187
187
  cache_control(Hash[opts].merge!(:max_age=>max_age))
188
- self['Expires'] = (Time.now + max_age).httpdate
188
+ @headers[RodaResponseHeaders::EXPIRES] = (Time.now + max_age).httpdate
189
189
  end
190
190
 
191
191
  # Remove Content-Type and Content-Length for 304 responses.
@@ -193,8 +193,8 @@ class Roda
193
193
  a = super
194
194
  if a[0] == 304
195
195
  h = a[1]
196
- h.delete('Content-Type')
197
- h.delete('Content-Length')
196
+ h.delete(RodaResponseHeaders::CONTENT_TYPE)
197
+ h.delete(RodaResponseHeaders::CONTENT_LENGTH)
198
198
  end
199
199
  a
200
200
  end
@@ -257,7 +257,7 @@ class Roda
257
257
  headers.merge!(chunk_headers)
258
258
  end
259
259
  if self.opts[:force_chunked_encoding]
260
- headers['Transfer-Encoding'] = 'chunked'
260
+ res[RodaResponseHeaders::TRANSFER_ENCODING] = 'chunked'
261
261
  body = Body.new(self)
262
262
  else
263
263
  body = StreamBody.new(self)
@@ -46,7 +46,7 @@ class Roda
46
46
 
47
47
  # Log request/response information in common log format to logger.
48
48
  def _roda_after_90__common_logger(result)
49
- return unless result && result[0] && result[1]
49
+ return unless result && (status = result[0]) && (headers = result[1])
50
50
 
51
51
  elapsed_time = if timer = @_request_timer
52
52
  '%0.4f' % (CommonLogger.start_timer - timer)
@@ -56,7 +56,7 @@ class Roda
56
56
 
57
57
  env = @_request.env
58
58
 
59
- line = "#{env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-"} - #{env["REMOTE_USER"] || "-"} [#{Time.now.strftime("%d/%b/%Y:%H:%M:%S %z")}] \"#{env["REQUEST_METHOD"]} #{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}#{"?#{env["QUERY_STRING"]}" if ((qs = env["QUERY_STRING"]) && !qs.empty?)} #{@_request.http_version}\" #{result[0]} #{((length = result[1]['Content-Length']) && (length unless length == '0')) || '-'} #{elapsed_time}\n"
59
+ line = "#{env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-"} - #{env["REMOTE_USER"] || "-"} [#{Time.now.strftime("%d/%b/%Y:%H:%M:%S %z")}] \"#{env["REQUEST_METHOD"]} #{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}#{"?#{env["QUERY_STRING"]}" if ((qs = env["QUERY_STRING"]) && !qs.empty?)} #{@_request.http_version}\" #{status} #{((length = headers[RodaResponseHeaders::CONTENT_LENGTH]) && (length unless length == '0')) || '-'} #{elapsed_time}\n"
60
60
  if MUTATE_LINE
61
61
  line.gsub!(/[^[:print:]\n]/){|c| sprintf("\\x%x", c.ord)}
62
62
  # :nocov:
@@ -200,7 +200,7 @@ class Roda
200
200
 
201
201
  # The header name to use, depends on whether report only mode has been enabled.
202
202
  def header_key
203
- @report_only ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy'
203
+ @report_only ? RodaResponseHeaders::CONTENT_SECURITY_POLICY_REPORT_ONLY : RodaResponseHeaders::CONTENT_SECURITY_POLICY
204
204
  end
205
205
 
206
206
  # The header value to use.
@@ -309,7 +309,7 @@ class Roda
309
309
  # Set the appropriate content security policy header.
310
310
  def set_default_headers
311
311
  super
312
- (@content_security_policy || roda_class.opts[:content_security_policy]).set_header(@headers)
312
+ (@content_security_policy || roda_class.opts[:content_security_policy]).set_header(headers)
313
313
  end
314
314
  end
315
315
  end
@@ -27,8 +27,8 @@ class Roda
27
27
  when DROP_BODY_RANGE, 204, 304
28
28
  r[2] = EMPTY_ARRAY
29
29
  h = r[1]
30
- h.delete("Content-Length")
31
- h.delete("Content-Type")
30
+ h.delete(RodaResponseHeaders::CONTENT_LENGTH)
31
+ h.delete(RodaResponseHeaders::CONTENT_TYPE)
32
32
  when 205
33
33
  r[2] = EMPTY_ARRAY
34
34
  empty_205_headers(r[1])
@@ -198,7 +198,7 @@ END
198
198
  def exception_page(exception, opts=OPTS)
199
199
  message = exception_page_exception_message(exception)
200
200
  if opts[:json]
201
- @_response['Content-Type'] = "application/json"
201
+ @_response[RodaResponseHeaders::CONTENT_TYPE] = "application/json"
202
202
  {
203
203
  "exception"=>{
204
204
  "class"=>exception.class.to_s,
@@ -207,7 +207,7 @@ END
207
207
  }
208
208
  }
209
209
  elsif env['HTTP_ACCEPT'] =~ /text\/html/
210
- @_response['Content-Type'] = "text/html"
210
+ @_response[RodaResponseHeaders::CONTENT_TYPE] = "text/html"
211
211
 
212
212
  context = opts[:context] || 7
213
213
  css_file = opts[:css_file]
@@ -394,7 +394,7 @@ END1
394
394
  </html>
395
395
  END
396
396
  else
397
- @_response['Content-Type'] = "text/plain"
397
+ @_response[RodaResponseHeaders::CONTENT_TYPE] = "text/plain"
398
398
  "#{exception.class}: #{message}\n#{exception.backtrace.map{|l| "\t#{l}"}.join("\n")}"
399
399
  end
400
400
  end
@@ -429,11 +429,11 @@ END
429
429
  # Serve exception page assets
430
430
  def exception_page_assets
431
431
  get 'exception_page.css' do
432
- response['Content-Type'] = "text/css"
432
+ response[RodaResponseHeaders::CONTENT_TYPE] = "text/css"
433
433
  scope.exception_page_css
434
434
  end
435
435
  get 'exception_page.js' do
436
- response['Content-Type'] = "application/javascript"
436
+ response[RodaResponseHeaders::CONTENT_TYPE] = "application/javascript"
437
437
  scope.exception_page_js
438
438
  end
439
439
  end
@@ -45,7 +45,7 @@ class Roda
45
45
  # Match if the given mimetype is one of the accepted mimetypes.
46
46
  def match_accept(mimetype)
47
47
  if @env["HTTP_ACCEPT"].to_s.split(',').any?{|s| s.strip == mimetype}
48
- response["Content-Type"] = mimetype
48
+ response[RodaResponseHeaders::CONTENT_TYPE] = mimetype
49
49
  end
50
50
  end
51
51
 
@@ -27,7 +27,7 @@ class Roda
27
27
  if env['PATH_INFO'] == opts[:heartbeat_path]
28
28
  response = @_response
29
29
  response.status = 200
30
- response['Content-Type'] = 'text/plain'
30
+ response[RodaResponseHeaders::CONTENT_TYPE] = 'text/plain'
31
31
  response.write 'OK'
32
32
  throw :halt, response.finish
33
33
  end
@@ -52,6 +52,9 @@ class Roda
52
52
  # using the +:content_type+ option:
53
53
  #
54
54
  # plugin :json, content_type: 'application/xml'
55
+ #
56
+ # This plugin depends on the custom_block_results plugin, and therefore does
57
+ # not support treating String, FalseClass, or NilClass values as JSON.
55
58
  module Json
56
59
  # Set the classes to automatically convert to JSON, and the serializer to use.
57
60
  def self.configure(app, opts=OPTS)
@@ -86,7 +89,7 @@ class Roda
86
89
  # Handle a result for one of the registered JSON result classes
87
90
  # by converting the result to JSON.
88
91
  def handle_json_block_result(result)
89
- @_response['Content-Type'] ||= opts[:json_result_content_type]
92
+ @_response[RodaResponseHeaders::CONTENT_TYPE] ||= opts[:json_result_content_type]
90
93
  @_request.send(:convert_to_json, result)
91
94
  end
92
95
  end
@@ -379,7 +379,7 @@ class Roda
379
379
  end
380
380
 
381
381
  # Perform the processing of mail for this request, first considering
382
- # routes defined via the the class-level +rcpt+ method, and then the
382
+ # routes defined via the class-level +rcpt+ method, and then the
383
383
  # normal routing tree passed in as the block.
384
384
  def process_mail(&block)
385
385
  if string_routes = opts[:mail_processor_string_routes]
@@ -186,7 +186,7 @@ class Roda
186
186
  # that the routing tree did not handle the request.
187
187
  def finish
188
188
  if m = mail
189
- header_content_type = @headers.delete('Content-Type')
189
+ header_content_type = @headers.delete(RodaResponseHeaders::CONTENT_TYPE)
190
190
  m.headers(@headers)
191
191
  m.body(@body.join) unless @body.empty?
192
192
  mail_attachments.each do |a, block|
@@ -241,7 +241,7 @@ class Roda
241
241
  if mail = env['roda.mail']
242
242
  res = @_response
243
243
  res.mail = mail
244
- res.headers.delete('Content-Type')
244
+ res.headers.delete(RodaResponseHeaders::CONTENT_TYPE)
245
245
  end
246
246
  end
247
247
 
@@ -4,7 +4,9 @@
4
4
  class Roda
5
5
  module RodaPlugins
6
6
  # The match_hook plugin adds hooks that are called upon a successful match
7
- # by any of the matchers.
7
+ # by any of the matchers. The hooks do not take any arguments. If you would
8
+ # like hooks that pass the arguments/matchers and values yielded to the route block,
9
+ # use the match_hook_args plugin.
8
10
  #
9
11
  # plugin :match_hook
10
12
  #
@@ -0,0 +1,93 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The match_hook_args plugin adds hooks that are called upon a successful match
7
+ # by any of the matchers. It is similar to the match_hook plugin, but it allows
8
+ # for passing the matchers and block arguments for each match method.
9
+ #
10
+ # plugin :match_hook_args
11
+ #
12
+ # add_match_hook do |matchers, block_args|
13
+ # logger.debug("matchers: #{matchers.inspect}. #{block_args.inspect} yielded.")
14
+ # end
15
+ #
16
+ # # Term is an implicit matcher used for terminating matches, and
17
+ # # will be included in the array of matchers yielded to the match hook
18
+ # # if a terminating match is used.
19
+ # term = self.class::RodaRequest::TERM
20
+ #
21
+ # route do |r|
22
+ # r.root do
23
+ # # for a request for /
24
+ # # matchers: nil, block_args: nil
25
+ # end
26
+ #
27
+ # r.on 'a', ['b', 'c'], Integer do |segment, id|
28
+ # # for a request for /a/b/1
29
+ # # matchers: ["a", ["b", "c"], Integer], block_args: ["b", 1]
30
+ # end
31
+ #
32
+ # r.get 'd' do
33
+ # # for a request for /d
34
+ # # matchers: ["d", term], block_args: []
35
+ # end
36
+ # end
37
+ module MatchHookArgs
38
+ def self.configure(app)
39
+ app.opts[:match_hook_args] ||= []
40
+ end
41
+
42
+ module ClassMethods
43
+ # Freeze the array of hook methods when freezing the app
44
+ def freeze
45
+ opts[:match_hook_args].freeze
46
+ super
47
+ end
48
+
49
+ # Add a match hook that will be called with matchers and block args.
50
+ def add_match_hook(&block)
51
+ opts[:match_hook_args] << define_roda_method("match_hook_args", :any, &block)
52
+
53
+ if opts[:match_hook_args].length == 1
54
+ class_eval("alias _match_hook_args #{opts[:match_hook_args].first}", __FILE__, __LINE__)
55
+ else
56
+ class_eval("def _match_hook_args(v, a); #{opts[:match_hook_args].map{|m| "#{m}(v, a)"}.join(';')} end", __FILE__, __LINE__)
57
+ end
58
+
59
+ public :_match_hook_args
60
+
61
+ nil
62
+ end
63
+ end
64
+
65
+ module InstanceMethods
66
+ # Default empty method if no match hooks are defined.
67
+ def _match_hook_args(matchers, block_args)
68
+ end
69
+ end
70
+
71
+ module RequestMethods
72
+ private
73
+
74
+ # Call the match hook with matchers and block args if yielding to the block before yielding to the block.
75
+ def if_match(v)
76
+ super do |*a|
77
+ scope._match_hook_args(v, a)
78
+ yield(*a)
79
+ end
80
+ end
81
+
82
+ # Call the match hook with nil matchers and blocks before yielding to the block
83
+ def always
84
+ scope._match_hook_args(nil, nil)
85
+ super
86
+ end
87
+ end
88
+ end
89
+
90
+ register_plugin :match_hook_args, MatchHookArgs
91
+ end
92
+ end
93
+
@@ -138,7 +138,7 @@ class Roda
138
138
  def method_not_allowed(verbs)
139
139
  res = response
140
140
  res.status = 405
141
- res['Allow'] = verbs
141
+ res[RodaResponseHeaders::ALLOW] = verbs
142
142
  nil
143
143
  end
144
144
  end
@@ -0,0 +1,32 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The response_headers_plain_hash plugin will change Roda to
7
+ # use a plain hash for response headers. This is Roda's
8
+ # default behavior on Rack 2, but on Rack 3+, Roda defaults
9
+ # to using Rack::Headers for response headers for backwards
10
+ # compatibility (Rack::Headers automatically lower cases header
11
+ # keys).
12
+ #
13
+ # On Rack 3+, you should use this plugin for better performance
14
+ # if you are sure all headers in your application and middleware
15
+ # are already lower case (lower case response header keys are
16
+ # required by the Rack 3 spec).
17
+ module PlainHashResponseHeaders
18
+ if defined?(Rack::Headers) && Rack::Headers.is_a?(Class)
19
+ module ResponseMethods
20
+ private
21
+
22
+ # Use plain hash for headers
23
+ def _initialize_headers
24
+ {}
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ register_plugin(:plain_hash_response_headers, PlainHashResponseHeaders)
31
+ end
32
+ end
@@ -123,8 +123,8 @@ class Roda
123
123
  headers.replace(h)
124
124
 
125
125
  unless s == 304
126
- headers['Content-Type'] = ::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
127
- headers['Content-Encoding'] = encoding
126
+ headers[RodaResponseHeaders::CONTENT_TYPE] = ::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
127
+ headers[RodaResponseHeaders::CONTENT_ENCODING] = encoding
128
128
  end
129
129
 
130
130
  halt [s, headers, b]
@@ -193,7 +193,10 @@ class Roda
193
193
  raise InvalidToken, msg
194
194
  when :empty_403
195
195
  @_response.status = 403
196
- @_response.headers.replace('Content-Type'=>'text/html', 'Content-Length'=>'0')
196
+ headers = @_response.headers
197
+ headers.clear
198
+ headers[RodaResponseHeaders::CONTENT_TYPE] = 'text/html'
199
+ headers[RodaResponseHeaders::CONTENT_LENGTH] ='0'
197
200
  throw :halt, @_response.finish_with_body([])
198
201
  when :clear_session
199
202
  session.clear
@@ -327,7 +327,7 @@ class Roda
327
327
  def send_file(path, opts = OPTS)
328
328
  res = response
329
329
  headers = res.headers
330
- if opts[:type] || !headers["Content-Type"]
330
+ if opts[:type] || !headers[RodaResponseHeaders::CONTENT_TYPE]
331
331
  res.content_type(opts[:type] || ::File.extname(path), :default => 'application/octet-stream')
332
332
  end
333
333
 
@@ -352,7 +352,7 @@ class Roda
352
352
  end
353
353
 
354
354
  res.status = opts[:status] || s
355
- headers.delete("Content-Length")
355
+ headers.delete(RodaResponseHeaders::CONTENT_LENGTH)
356
356
  headers.replace(h.merge!(headers))
357
357
  res.body = b
358
358
 
@@ -407,7 +407,7 @@ class Roda
407
407
 
408
408
  # If the body is a DelayedBody, set the appropriate length for it.
409
409
  def finish
410
- @length = @body.length if @body.is_a?(DelayedBody) && !@headers["Content-Length"]
410
+ @length = @body.length if @body.is_a?(DelayedBody) && !@headers[RodaResponseHeaders::CONTENT_LENGTH]
411
411
  super
412
412
  end
413
413
 
@@ -424,7 +424,7 @@ class Roda
424
424
 
425
425
  # Set the Content-Type of the response body given a media type or file
426
426
  # extension. See plugin documentation for options.
427
- def content_type(type = nil || (return @headers["Content-Type"]), opts = OPTS)
427
+ def content_type(type = nil || (return @headers[RodaResponseHeaders::CONTENT_TYPE]), opts = OPTS)
428
428
  unless (mime_type = mime_type(type) || opts[:default])
429
429
  raise RodaError, "Unknown media type: #{type}"
430
430
  end
@@ -437,7 +437,7 @@ class Roda
437
437
  end
438
438
  end
439
439
 
440
- @headers["Content-Type"] = mime_type
440
+ @headers[RodaResponseHeaders::CONTENT_TYPE] = mime_type
441
441
  end
442
442
 
443
443
  # Set the Content-Disposition to "attachment" with the specified filename,
@@ -463,14 +463,14 @@ class Roda
463
463
  encoded_params = "; filename*=#{encoding.to_s}''#{encoded_filename}"
464
464
  end
465
465
 
466
- unless @headers["Content-Type"]
466
+ unless @headers[RodaResponseHeaders::CONTENT_TYPE]
467
467
  ext = File.extname(filename)
468
468
  unless ext.empty?
469
469
  content_type(ext)
470
470
  end
471
471
  end
472
472
  end
473
- @headers["Content-Disposition"] = "#{disposition}#{params}#{encoded_params}"
473
+ @headers[RodaResponseHeaders::CONTENT_DISPOSITION] = "#{disposition}#{params}#{encoded_params}"
474
474
  end
475
475
 
476
476
  # Whether or not the status is set to 1xx. Returns nil if status not yet set.
@@ -152,7 +152,7 @@ class Roda
152
152
  # the request afterwards, returning the result of the block.
153
153
  def on_type(type, &block)
154
154
  return unless type == requested_type
155
- response['Content-Type'] ||= @scope.opts[:type_routing][:types][type]
155
+ response[RodaResponseHeaders::CONTENT_TYPE] ||= @scope.opts[:type_routing][:types][type]
156
156
  always(&block)
157
157
  end
158
158
 
@@ -200,7 +200,7 @@ class Roda
200
200
  @env['HTTP_ACCEPT'].to_s.split(/\s*,\s*/).map do |part|
201
201
  mime, _= part.split(/\s*;\s*/, 2)
202
202
  if sym = mimes[mime]
203
- response['Vary'] = (vary = response['Vary']) ? "#{vary}, Accept" : 'Accept'
203
+ response[RodaResponseHeaders::VARY] = (vary = response[RodaResponseHeaders::VARY]) ? "#{vary}, Accept" : 'Accept'
204
204
  return sym
205
205
  end
206
206
  end
@@ -346,7 +346,7 @@ class Roda
346
346
  end
347
347
 
348
348
  # The reason behind this error. If this error was caused by a conversion method,
349
- # this will be the the conversion method symbol. If this error was caused
349
+ # this will be the conversion method symbol. If this error was caused
350
350
  # because a value was missing, then it will be +:missing+. If this error was
351
351
  # caused because a value was not the correct type, then it will be +:invalid_type+.
352
352
  attr_accessor :reason
data/lib/roda/response.rb CHANGED
@@ -6,6 +6,25 @@ rescue LoadError
6
6
  end
7
7
 
8
8
  class Roda
9
+ # Contains constants for response headers. This approach is used so that all
10
+ # headers used internally by Roda can be lower case on Rack 3, so that it is
11
+ # possible to use a plain hash of response headers instead of using Rack::Headers.
12
+ module RodaResponseHeaders
13
+ headers = %w'Allow Cache-Control Content-Disposition Content-Encoding Content-Length
14
+ Content-Security-Policy Content-Security-Policy-Report-Only Content-Type
15
+ ETag Expires Last-Modified Link Location Set-Cookie Transfer-Encoding Vary'.freeze.each(&:freeze)
16
+
17
+ if defined?(Rack::Headers) && Rack::Headers.is_a?(Class)
18
+ headers.each do |mixed_case|
19
+ const_set(mixed_case.gsub('-', '_').upcase!.to_sym, mixed_case.downcase.freeze)
20
+ end
21
+ else
22
+ headers.each do |mixed_case|
23
+ const_set(mixed_case.gsub('-', '_').upcase!.to_sym, mixed_case.freeze)
24
+ end
25
+ end
26
+ end
27
+
9
28
  # Base class used for Roda responses. The instance methods for this
10
29
  # class are added by Roda::RodaPlugins::Base::ResponseMethods, the class
11
30
  # methods are added by Roda::RodaPlugins::Base::ResponseClassMethods.
@@ -30,7 +49,7 @@ class Roda
30
49
 
31
50
  # Instance methods for RodaResponse
32
51
  module ResponseMethods
33
- DEFAULT_HEADERS = {"Content-Type" => "text/html".freeze}.freeze
52
+ DEFAULT_HEADERS = {RodaResponseHeaders::CONTENT_TYPE => "text/html".freeze}.freeze
34
53
 
35
54
  # The body for the current response.
36
55
  attr_reader :body
@@ -42,20 +61,11 @@ class Roda
42
61
  # code for non-empty responses and a 404 code for empty responses.
43
62
  attr_accessor :status
44
63
 
45
- if defined?(Rack::Headers) && Rack::Headers.is_a?(Class)
46
- # Set the default headers when creating a response.
47
- def initialize
48
- @headers = Rack::Headers.new
49
- @body = []
50
- @length = 0
51
- end
52
- else
53
- # Set the default headers when creating a response.
54
- def initialize
55
- @headers = {}
56
- @body = []
57
- @length = 0
58
- end
64
+ # Set the default headers when creating a response.
65
+ def initialize
66
+ @headers = _initialize_headers
67
+ @body = []
68
+ @length = 0
59
69
  end
60
70
 
61
71
  # Return the response header with the given key. Example:
@@ -108,15 +118,15 @@ class Roda
108
118
  if b.empty?
109
119
  s = @status || 404
110
120
  if (s == 304 || s == 204 || (s >= 100 && s <= 199))
111
- h.delete("Content-Type")
121
+ h.delete(RodaResponseHeaders::CONTENT_TYPE)
112
122
  elsif s == 205
113
123
  empty_205_headers(h)
114
124
  else
115
- h["Content-Length"] ||= '0'
125
+ h[RodaResponseHeaders::CONTENT_LENGTH] ||= '0'
116
126
  end
117
127
  else
118
128
  s = @status || default_status
119
- h["Content-Length"] ||= @length.to_s
129
+ h[RodaResponseHeaders::CONTENT_LENGTH] ||= @length.to_s
120
130
  end
121
131
 
122
132
  [s, h, b]
@@ -149,7 +159,7 @@ class Roda
149
159
  # response.redirect('foo', 301)
150
160
  # response.redirect('bar')
151
161
  def redirect(path, status = 302)
152
- @headers["Location"] = path
162
+ @headers[RodaResponseHeaders::LOCATION] = path
153
163
  @status = status
154
164
  nil
155
165
  end
@@ -171,18 +181,30 @@ class Roda
171
181
 
172
182
  private
173
183
 
184
+ if defined?(Rack::Headers) && Rack::Headers.is_a?(Class)
185
+ # Use Rack::Headers for headers by default on Rack 3
186
+ def _initialize_headers
187
+ Rack::Headers.new
188
+ end
189
+ else
190
+ # Use plain hash for headers by default on Rack 1-2
191
+ def _initialize_headers
192
+ {}
193
+ end
194
+ end
195
+
174
196
  if Rack.release < '2.0.2'
175
197
  # Don't use a content length for empty 205 responses on
176
198
  # rack 1, as it violates Rack::Lint in that version.
177
199
  def empty_205_headers(headers)
178
- headers.delete("Content-Type")
179
- headers.delete("Content-Length")
200
+ headers.delete(RodaResponseHeaders::CONTENT_TYPE)
201
+ headers.delete(RodaResponseHeaders::CONTENT_LENGTH)
180
202
  end
181
203
  else
182
204
  # Set the content length for empty 205 responses to 0
183
205
  def empty_205_headers(headers)
184
- headers.delete("Content-Type")
185
- headers["Content-Length"] = '0'
206
+ headers.delete(RodaResponseHeaders::CONTENT_TYPE)
207
+ headers[RodaResponseHeaders::CONTENT_LENGTH] = '0'
186
208
  end
187
209
  end
188
210
 
data/lib/roda/version.rb CHANGED
@@ -4,7 +4,7 @@ class Roda
4
4
  RodaMajorVersion = 3
5
5
 
6
6
  # The minor version of Roda, updated for new feature releases of Roda.
7
- RodaMinorVersion = 69
7
+ RodaMinorVersion = 71
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
data/lib/roda.rb CHANGED
@@ -204,7 +204,7 @@ class Roda
204
204
 
205
205
  alias set_default_headers set_default_headers
206
206
  def set_default_headers
207
- @headers['Content-Type'] ||= 'text/html'
207
+ @headers[RodaResponseHeaders::CONTENT_TYPE] ||= 'text/html'
208
208
  end
209
209
  end
210
210
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roda
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.69.0
4
+ version: 3.71.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-06-13 00:00:00.000000000 Z
11
+ date: 2023-08-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -243,6 +243,8 @@ extra_rdoc_files:
243
243
  - doc/release_notes/3.68.0.txt
244
244
  - doc/release_notes/3.69.0.txt
245
245
  - doc/release_notes/3.7.0.txt
246
+ - doc/release_notes/3.70.0.txt
247
+ - doc/release_notes/3.71.0.txt
246
248
  - doc/release_notes/3.8.0.txt
247
249
  - doc/release_notes/3.9.0.txt
248
250
  files:
@@ -319,6 +321,8 @@ files:
319
321
  - doc/release_notes/3.68.0.txt
320
322
  - doc/release_notes/3.69.0.txt
321
323
  - doc/release_notes/3.7.0.txt
324
+ - doc/release_notes/3.70.0.txt
325
+ - doc/release_notes/3.71.0.txt
322
326
  - doc/release_notes/3.8.0.txt
323
327
  - doc/release_notes/3.9.0.txt
324
328
  - lib/roda.rb
@@ -389,6 +393,7 @@ files:
389
393
  - lib/roda/plugins/mailer.rb
390
394
  - lib/roda/plugins/match_affix.rb
391
395
  - lib/roda/plugins/match_hook.rb
396
+ - lib/roda/plugins/match_hook_args.rb
392
397
  - lib/roda/plugins/middleware.rb
393
398
  - lib/roda/plugins/middleware_stack.rb
394
399
  - lib/roda/plugins/module_include.rb
@@ -412,6 +417,7 @@ files:
412
417
  - lib/roda/plugins/path_matchers.rb
413
418
  - lib/roda/plugins/path_rewriter.rb
414
419
  - lib/roda/plugins/placeholder_string_matchers.rb
420
+ - lib/roda/plugins/plain_hash_response_headers.rb
415
421
  - lib/roda/plugins/precompile_templates.rb
416
422
  - lib/roda/plugins/public.rb
417
423
  - lib/roda/plugins/r.rb