roda 3.68.0 → 3.70.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: faad040bd1e251d705afbfe6ced40794aa2f135658168c85603a6c8a83a497be
4
- data.tar.gz: 6e1d78ffdf0442835a754e54fc6216d8f668f3d0cb8e9c5635c1a582b17d5746
3
+ metadata.gz: 0d8ea04ca767f8be110f6ba14d1fd877a389a2e733e2925020f2ac589f6a9571
4
+ data.tar.gz: c9a2d26f41724c05ea1de5e8fb0058093e5fad34fb0c6479dd74badcfea67016
5
5
  SHA512:
6
- metadata.gz: a663960b5f5c5391a44102b1b255fbb5546f241c2386b6fdbe8a5050f434407332f239eb0c862ab877de239ec1b4ea39c4f9202c488672a846c5f8eabaef6379
7
- data.tar.gz: 508fa08ad66e1b11b5abc552345067fc2517aeb10ea060c6298a337b44db453c5daf35a0bbafe399dd9167286fd9ea35c07d75c340f640a6198beb52367352d5
6
+ metadata.gz: 288a793976f389dfa2fe8fd55fb649590748cbe7ebb9da48f351117bb93da33ceee5848077f53c73430fd943b6be5b69a6d189e8b7424fe9ec60892dbaacff56
7
+ data.tar.gz: 3cad6d6823a2fbcd5534521b6e5fbcc7b85c04d1a80e9ca758cac6ab9b4b967a5f019b653de7d4de3c1d32069832f362fd2564ec219252b0f2dd8ccf1c0785c1
data/CHANGELOG CHANGED
@@ -1,3 +1,13 @@
1
+ = 3.70.0 (2023-07-12)
2
+
3
+ * Add plain_hash_response_headers plugin, using a plain hash for response headers on Rack 3 for much better performance (jeremyevans)
4
+
5
+ * Use lower case response header keys by default on Rack 3, instead of relying on Rack::Headers conversion (jeremyevans)
6
+
7
+ = 3.69.0 (2023-06-13)
8
+
9
+ * Allow symbol_matcher in symbol_matchers plugin to take a block to allow type conversion (jeremyevans)
10
+
1
11
  = 3.68.0 (2023-05-11)
2
12
 
3
13
  * Make Roda.run in multi_run plugin accept blocks to allow autoloading the apps to dispatch to (jeremyevans)
@@ -0,0 +1,33 @@
1
+ = New Feature
2
+
3
+ * The symbol_matcher method in the symbol_matchers plugin now
4
+ supports a block to allow for type conversion of matched
5
+ segments:
6
+
7
+ symbol_matcher(:date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d|
8
+ [Date.new(y.to_i, m.to_i, d.to_i)]
9
+ end
10
+
11
+ route do |r|
12
+ r.on :date do |date|
13
+ # date is an instance of Date
14
+ end
15
+ end
16
+
17
+ As shown above, the block should return an array of objects to yield
18
+ to the match block.
19
+
20
+ If you have a segment match the passed regexp, but decide during block
21
+ processing that you do not want to treat it as a match, you can have the
22
+ block return nil or false. This is useful if you want to make sure you
23
+ are using valid data:
24
+
25
+ symbol_matcher(:date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d|
26
+ y = y.to_i
27
+ m = m.to_i
28
+ d = d.to_i
29
+ [Date.new(y, m, d)] if Date.valid_date?(y, m, d)
30
+ end
31
+
32
+ When providing a block when using the symbol_matchers method, that
33
+ symbol may not work with the params_capturing plugin.
@@ -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.
@@ -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)
@@ -33,7 +33,7 @@ class Roda
33
33
  # block return nil or false. This is useful if you want to make sure you
34
34
  # are using valid data:
35
35
  #
36
- # class_matcher(Date, /(\dd\d)-(\d\d)-(\d\d)/) do |y, m, d|
36
+ # class_matcher(Date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d|
37
37
  # y = y.to_i
38
38
  # m = m.to_i
39
39
  # d = d.to_i
@@ -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
@@ -86,7 +86,7 @@ class Roda
86
86
  # Handle a result for one of the registered JSON result classes
87
87
  # by converting the result to JSON.
88
88
  def handle_json_block_result(result)
89
- @_response['Content-Type'] ||= opts[:json_result_content_type]
89
+ @_response[RodaResponseHeaders::CONTENT_TYPE] ||= opts[:json_result_content_type]
90
90
  @_request.send(:convert_to_json, result)
91
91
  end
92
92
  end
@@ -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
 
@@ -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.
@@ -37,6 +37,35 @@ class Roda
37
37
  #
38
38
  # If using this plugin with the params_capturing plugin, this plugin should
39
39
  # be loaded first.
40
+ #
41
+ # You can provide a block when calling +symbol_matcher+, and it will be called
42
+ # for all matches to allow for type conversion. The block must return an
43
+ # array:
44
+ #
45
+ # symbol_matcher(:date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d|
46
+ # [Date.new(y.to_i, m.to_i, d.to_i)]
47
+ # end
48
+ #
49
+ # route do |r|
50
+ # r.on :date do |date|
51
+ # # date is an instance of Date
52
+ # end
53
+ # end
54
+ #
55
+ # If you have a segment match the passed regexp, but decide during block
56
+ # processing that you do not want to treat it as a match, you can have the
57
+ # block return nil or false. This is useful if you want to make sure you
58
+ # are using valid data:
59
+ #
60
+ # symbol_matcher(:date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d|
61
+ # y = y.to_i
62
+ # m = m.to_i
63
+ # d = d.to_i
64
+ # [Date.new(y, m, d)] if Date.valid_date?(y, m, d)
65
+ # end
66
+ #
67
+ # However, if providing a block to the symbol_matchers plugin, the symbol may
68
+ # not work with the params_capturing plugin.
40
69
  module SymbolMatchers
41
70
  def self.load_dependencies(app)
42
71
  app.plugin :_symbol_regexp_matchers
@@ -50,9 +79,10 @@ class Roda
50
79
 
51
80
  module ClassMethods
52
81
  # Set the regexp to use for the given symbol, instead of the default.
53
- def symbol_matcher(s, re)
82
+ def symbol_matcher(s, re, &block)
54
83
  meth = :"match_symbol_#{s}"
55
- self::RodaRequest.send(:define_method, meth){re}
84
+ array = [re, block].freeze
85
+ self::RodaRequest.send(:define_method, meth){array}
56
86
  self::RodaRequest.send(:private, meth)
57
87
  end
58
88
  end
@@ -67,8 +97,8 @@ class Roda
67
97
  meth = :"match_symbol_#{s}"
68
98
  if respond_to?(meth, true)
69
99
  # Allow calling private match methods
70
- re = send(meth)
71
- consume(self.class.cached_matcher(re){re})
100
+ re, block = send(meth)
101
+ consume(self.class.cached_matcher(re){re}, &block)
72
102
  else
73
103
  super
74
104
  end
@@ -80,7 +110,8 @@ class Roda
80
110
  meth = :"match_symbol_#{s}"
81
111
  if respond_to?(meth, true)
82
112
  # Allow calling private match methods
83
- send(meth)
113
+ re, = send(meth)
114
+ re
84
115
  else
85
116
  super
86
117
  end
@@ -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
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 = 68
7
+ RodaMinorVersion = 70
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.68.0
4
+ version: 3.70.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-05-11 00:00:00.000000000 Z
11
+ date: 2023-07-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -241,7 +241,9 @@ extra_rdoc_files:
241
241
  - doc/release_notes/3.66.0.txt
242
242
  - doc/release_notes/3.67.0.txt
243
243
  - doc/release_notes/3.68.0.txt
244
+ - doc/release_notes/3.69.0.txt
244
245
  - doc/release_notes/3.7.0.txt
246
+ - doc/release_notes/3.70.0.txt
245
247
  - doc/release_notes/3.8.0.txt
246
248
  - doc/release_notes/3.9.0.txt
247
249
  files:
@@ -316,7 +318,9 @@ files:
316
318
  - doc/release_notes/3.66.0.txt
317
319
  - doc/release_notes/3.67.0.txt
318
320
  - doc/release_notes/3.68.0.txt
321
+ - doc/release_notes/3.69.0.txt
319
322
  - doc/release_notes/3.7.0.txt
323
+ - doc/release_notes/3.70.0.txt
320
324
  - doc/release_notes/3.8.0.txt
321
325
  - doc/release_notes/3.9.0.txt
322
326
  - lib/roda.rb
@@ -410,6 +414,7 @@ files:
410
414
  - lib/roda/plugins/path_matchers.rb
411
415
  - lib/roda/plugins/path_rewriter.rb
412
416
  - lib/roda/plugins/placeholder_string_matchers.rb
417
+ - lib/roda/plugins/plain_hash_response_headers.rb
413
418
  - lib/roda/plugins/precompile_templates.rb
414
419
  - lib/roda/plugins/public.rb
415
420
  - lib/roda/plugins/r.rb