roda 3.69.0 → 3.71.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: 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