summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeremy Evans <code@jeremyevans.net>2022-02-02 16:19:59 -0800
committerJeremy Evans <code@jeremyevans.net>2022-02-02 16:19:59 -0800
commit79328deacf599b92c82b9e38aced48033f3f4d2f (patch)
tree7947351ec70b2d2ef7e291dd922e8dd2f99284e7
parent568dee5a2de80a9da20e2baf084b215abeee4ea8 (diff)
downloadrack-79328deacf599b92c82b9e38aced48033f3f4d2f.tar.gz
Add Rack::Headers to support lower-case header keys
This implements a Hash subclass that lower-cases keys passed to it. The expected usage is for Rack 2 libraries/frameworks that want to be compatible with Rack 3, assuming that we require response header keys to be lower-case in Rack 3 (as discussed in #1592). I've tested this implementation in Roda (a web framework), and only minimal changes were needed to get Roda's tests passing with it, even though Roda uses mixed-case headers throughout its implementation and tests. It was simple to get the tests passing both with the implementation and without, showing that it's possible to get a framework that can support both Rack 2 and Rack 3. Additionally, I tested Rodauth (an authentication framework built on top of Roda), with the modified version of Roda, and again, only minimal changes were needed to keep the tests passing. I also tested a handful of small applications based on Roda using the modified version of Roda, and none of those applications needed any changes to keep their tests passing. I think that shows that requiring lower-case response header keys is probably acceptable, since it should result in minimal if any churn to applications, and only minor churn to libraries. The implementation of Rack::Headers is based on ruby-cicphash (a case-insensitive, case-preserving hash implementation), but streamlined as case doesn't need to be preserved, and it is a Hash subclass (as required by Rack 3 SPEC), instead of an Object subclass that uses a hash internally.
-rw-r--r--CHANGELOG.md1
-rw-r--r--lib/rack.rb1
-rw-r--r--lib/rack/headers.rb150
-rwxr-xr-xtest/spec_headers.rb515
4 files changed, 667 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67a0a27b..176a2be7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. For info on
### Added
+- `Rack::Headers` added to support lower-case header keys. ([@jeremyevans](https://github.com/jeremyevans))
- `Rack::RewindableInput` supports size. ([@ahorek](https://github.com/ahorek))
- Rack::Session::Pool now accepts `:allow_fallback` option to disable fallback to public id. ([#1431](https://github.com/rack/rack/issues/1431), [@jeremyevans](https://github.com/jeremyevans))
diff --git a/lib/rack.rb b/lib/rack.rb
index ad3efec6..1df65b47 100644
--- a/lib/rack.rb
+++ b/lib/rack.rb
@@ -90,6 +90,7 @@ module Rack
autoload :ForwardRequest, "rack/recursive"
autoload :Handler, "rack/handler"
autoload :Head, "rack/head"
+ autoload :Headers, "rack/headers"
autoload :Lint, "rack/lint"
autoload :Lock, "rack/lock"
autoload :Logger, "rack/logger"
diff --git a/lib/rack/headers.rb b/lib/rack/headers.rb
new file mode 100644
index 00000000..cc764a0d
--- /dev/null
+++ b/lib/rack/headers.rb
@@ -0,0 +1,150 @@
+module Rack
+ # Rack::Headers is a Hash subclass that downcases all keys. It's designed
+ # to be used by rack applications that don't implement the Rack 3 SPEC
+ # (by using non-lowercase response header keys), automatically handling
+ # the downcasing of keys.
+ class Headers < Hash
+ def self.[](*items)
+ if items.length % 2 != 0
+ if items.length == 1 && items.first.is_a?(Hash)
+ new.merge!(items.first)
+ else
+ raise ArgumentError, "odd number of arguments for Rack::Headers"
+ end
+ else
+ hash = new
+ loop do
+ break if items.length == 0
+ key = items.shift
+ value = items.shift
+ hash[key] = value
+ end
+ hash
+ end
+ end
+
+ def [](key)
+ super(downcase_key(key))
+ end
+
+ def []=(key, value)
+ super(key.downcase.freeze, value)
+ end
+ alias store []=
+
+ def assoc(key)
+ super(downcase_key(key))
+ end
+
+ def compare_by_identity
+ raise TypeError, "Rack::Headers cannot compare by identity, use regular Hash"
+ end
+
+ def delete(key)
+ super(downcase_key(key))
+ end
+
+ def dig(key, *a)
+ super(downcase_key(key), *a)
+ end
+
+ def fetch(key, *default, &block)
+ key = downcase_key(key)
+ super
+ end
+
+ def fetch_values(*a)
+ super(*a.map!{|key| downcase_key(key)})
+ end
+
+ def has_key?(key)
+ super(downcase_key(key))
+ end
+ alias include? has_key?
+ alias key? has_key?
+ alias member? has_key?
+
+ def invert
+ hash = self.class.new
+ each{|key, value| hash[value] = key}
+ hash
+ end
+
+ def merge(hash, &block)
+ dup.merge!(hash, &block)
+ end
+
+ def reject(&block)
+ hash = dup
+ hash.reject!(&block)
+ hash
+ end
+
+ def replace(hash)
+ clear
+ update(hash)
+ end
+
+ def select(&block)
+ hash = dup
+ hash.select!(&block)
+ hash
+ end
+
+ def to_proc
+ lambda{|x| self[x]}
+ end
+
+ def transform_values(&block)
+ dup.transform_values!(&block)
+ end
+
+ def update(hash, &block)
+ hash.each do |key, value|
+ self[key] = if block_given? && include?(key)
+ block.call(key, self[key], value)
+ else
+ value
+ end
+ end
+ self
+ end
+ alias merge! update
+
+ def values_at(*keys)
+ keys.map{|key| self[key]}
+ end
+
+ if RUBY_VERSION >= '2.5'
+ def slice(*a)
+ h = self.class.new
+ a.each{|k| h[k] = self[k] if has_key?(k)}
+ h
+ end
+
+ def transform_keys(&block)
+ dup.transform_keys!(&block)
+ end
+
+ def transform_keys!
+ hash = self.class.new
+ each do |k, v|
+ hash[yield k] = v
+ end
+ replace(hash)
+ end
+ end
+
+ if RUBY_VERSION >= '3.0'
+ def except(*a)
+ super(*a.map!{|key| downcase_key(key)})
+ end
+ end
+
+ private
+
+ def downcase_key(key)
+ key.is_a?(String) ? key.downcase : key
+ end
+ end
+end
diff --git a/test/spec_headers.rb b/test/spec_headers.rb
new file mode 100755
index 00000000..a7fd617c
--- /dev/null
+++ b/test/spec_headers.rb
@@ -0,0 +1,515 @@
+# frozen_string_literal: true
+
+require_relative 'helper'
+
+class RackHeadersTest < Minitest::Spec
+ before do
+ @h = Rack::Headers.new
+ @fh = Rack::Headers['AB'=>'1', 'cd'=>'2', '3'=>'4']
+ end
+
+ def test_public_interface
+ headers_methods = Rack::Headers.public_instance_methods.sort
+ hash_methods = Hash.public_instance_methods.sort
+ assert_empty(headers_methods - hash_methods)
+ assert_empty(hash_methods - headers_methods)
+ end
+
+ def test_class_aref
+ assert_equal Hash[], Rack::Headers[]
+ assert_equal Hash['a'=>'2'], Rack::Headers['A'=>'2']
+ assert_equal Hash['a'=>'2', 'b'=>'4'], Rack::Headers['A'=>'2', 'B'=>'4']
+ assert_equal Hash['a','2','b','4'], Rack::Headers['A','2','B','4']
+ assert_raises(ArgumentError){Rack::Headers['A']}
+ assert_raises(ArgumentError){Rack::Headers['A',2,'B']}
+ end
+
+ def test_default_values
+ h, ch = Hash.new, Rack::Headers.new
+ assert_equal h, ch
+ h, ch = Hash.new('1'), Rack::Headers.new('1')
+ assert_equal h, ch
+ assert_equal h['3'], ch['3']
+ h['a'], ch['A'] = ['2', '2']
+ assert_equal h['a'], ch['a']
+ h, ch = Hash.new{|h,k| k*2}, Rack::Headers.new{|h,k| k*2}
+ assert_equal h['3'], ch['3']
+ h['c'], ch['C'] = ['2', '2']
+ assert_equal h['c'], ch['c']
+ assert_raises(ArgumentError){Rack::Headers.new('1'){|hash,k| key}}
+
+ assert_nil @fh.default
+ assert_nil @fh.default_proc
+ assert_nil @fh['55']
+ assert_equal '3', Rack::Headers.new('3').default
+ assert_nil Rack::Headers.new('3').default_proc
+ assert_equal '3', Rack::Headers.new('3')['1']
+
+ @fh.default = '4'
+ assert_equal '4', @fh.default
+ assert_nil @fh.default_proc
+ assert_equal '4', @fh['55']
+
+ h = Rack::Headers.new('5')
+ assert_equal '5', h.default
+ assert_nil h.default_proc
+ assert_equal '5', h['55']
+
+ h = Rack::Headers.new{|hash, key| '1234'}
+ assert_nil h.default
+ refute_equal nil, h.default_proc
+ assert_equal '1234', h['55']
+
+ h = Rack::Headers.new{|hash, key| hash[key] = '1234'; nil}
+ assert_nil h.default
+ refute_equal nil, h.default_proc
+ assert_nil h['Ac']
+ assert_equal '1234', h['aC']
+ end
+
+ def test_store_and_retrieve
+ assert_nil @h['a']
+ @h['A'] = '2'
+ assert_equal '2', @h['a']
+ assert_equal '2', @h['A']
+ @h['a'] = '3'
+ assert_equal '3', @h['a']
+ assert_equal '3', @h['A']
+ @h['AB'] = '5'
+ assert_equal '5', @h['ab']
+ assert_equal '5', @h['AB']
+ assert_equal '5', @h['aB']
+ assert_equal '5', @h['Ab']
+ @h.store('C', '8')
+ assert_equal '8', @h['c']
+ assert_equal '8', @h['C']
+ end
+
+ def test_clear
+ assert_equal 3, @fh.length
+ @fh.clear
+ assert_equal Hash[], @fh
+ assert_equal 0, @fh.length
+ end
+
+ def test_delete
+ assert_equal 3, @fh.length
+ assert_equal '1', @fh.delete('aB')
+ assert_equal 2, @fh.length
+ assert_nil @fh.delete('Ab')
+ assert_equal 2, @fh.length
+ end
+
+ def test_delete_if_and_reject
+ assert_equal 3, @fh.length
+ hash = @fh.reject{|key, value| key == 'ab' || key == 'cd'}
+ assert_equal 1, hash.length
+ assert_equal Hash['3'=>'4'], hash
+ assert_equal 3, @fh.length
+ hash = @fh.delete_if{|key, value| key == 'ab' || key == 'cd'}
+ assert_equal 1, hash.length
+ assert_equal Hash['3'=>'4'], hash
+ assert_equal 1, @fh.length
+ assert_equal Hash['3'=>'4'], @fh
+ assert_nil @fh.reject!{|key, value| key == 'ab' || key == 'cd'}
+ hash = @fh.reject!{|key, value| key == '3'}
+ assert_equal 0, hash.length
+ assert_equal Hash[], hash
+ assert_equal 0, @fh.length
+ assert_equal Hash[], @fh
+ end
+
+ def test_dup_and_clone
+ def @h.foo; 1; end
+ h2 = @h.dup
+ h3 = @h.clone
+ h2['A'] = '2'
+ h3['B'] = '3'
+ assert_equal Rack::Headers[], @h
+ assert_raises NoMethodError do h2.foo end
+ assert_equal 1, h3.foo
+ assert_equal '2', h2['a']
+ assert_equal '3', h3['b']
+ end
+
+ def test_each
+ i = 0
+ @h.each{i+=1}
+ assert_equal 0, i
+ items = [['ab','1'], ['cd','2'], ['3','4']]
+ @fh.each do |k,v|
+ assert items.include?([k,v])
+ items -= [[k,v]]
+ end
+ assert_equal [], items
+ end
+
+ def test_each_key
+ i = 0
+ @h.each{i+=1}
+ assert_equal 0, i
+ keys = ['ab', 'cd', '3']
+ @fh.each_key do |k|
+ assert keys.include?(k)
+ assert k.frozen?
+ keys -= [k]
+ end
+ assert_equal [], keys
+ end
+
+ def test_each_value
+ i = 0
+ @h.each{i+=1}
+ assert_equal 0, i
+ values = ['1', '2', '4']
+ @fh.each_value do |v|
+ assert values.include?(v)
+ values -= [v]
+ end
+ assert_equal [], values
+ end
+
+ def test_empty
+ assert @h.empty?
+ assert !@fh.empty?
+ end
+
+ def test_fetch
+ assert_raises(ArgumentError){@h.fetch(1,2,3)}
+ assert_raises(ArgumentError){@h.fetch(1,2,3){4}}
+ assert_raises(IndexError){@h.fetch(1)}
+ @h.default = '33'
+ assert_raises(IndexError){@h.fetch(1)}
+ @h['1'] = '8'
+ assert_equal '8', @h.fetch('1')
+ assert_equal '3', @h.fetch(2, '3')
+ assert_equal '222', @h.fetch('2'){|k| k*3}
+ assert_equal '1', @fh.fetch('Ab')
+ assert_equal '2', @fh.fetch('cD', '3')
+ assert_equal '4', @fh.fetch("3", 3)
+ assert_equal '4', @fh.fetch("3"){|k| k*3}
+ assert_raises(IndexError){Rack::Headers.new{34}.fetch(1)}
+ end
+
+ def test_has_key
+ %i'include? has_key? key? member?'.each do |meth|
+ assert !@h.send(meth,1)
+ assert @fh.send(meth,'Ab')
+ assert @fh.send(meth,'cD')
+ assert @fh.send(meth,'3')
+ assert @fh.send(meth,'ab')
+ assert @fh.send(meth,'CD')
+ assert @fh.send(meth,'3')
+ assert !@fh.send(meth,1)
+ end
+ end
+
+ def test_has_value
+ %i'value? has_value?'.each do |meth|
+ assert !@h.send(meth,'1')
+ assert @fh.send(meth,'1')
+ assert @fh.send(meth,'2')
+ assert @fh.send(meth,'4')
+ assert !@fh.send(meth,'3')
+ end
+ end
+
+ def test_inspect
+ %i'inspect to_s'.each do |meth|
+ assert_equal '{}', @h.send(meth)
+ assert_equal '{"ab"=>"1", "cd"=>"2", "3"=>"4"}', @fh.send(meth)
+ end
+ end
+
+ def test_invert
+ assert_kind_of(Rack::Headers, @h.invert)
+ assert_equal({}, @h.invert)
+ assert_equal({"1"=>"ab", "2"=>"cd", "4"=>"3"}, @fh.invert)
+ assert_equal({'cd'=>'ab'}, Rack::Headers['AB'=>'CD'].invert)
+ assert_equal({'cd'=>'xy'}, Rack::Headers['AB'=>'Cd', 'xY'=>'cD'].invert)
+ end
+
+ def test_keys
+ assert_equal [], @h.keys
+ assert_equal %w'ab cd 3', @fh.keys
+ end
+
+ def test_length
+ %i'length size'.each do |meth|
+ assert_equal 0, @h.send(meth)
+ assert_equal 3, @fh.send(meth)
+ end
+ end
+
+ def test_merge_and_update
+ assert_equal @h, @h.merge({})
+ assert_equal @fh, @fh.merge({})
+ assert_equal Rack::Headers['ab'=>'55'], @h.merge({'ab'=>'55'})
+ assert_equal Rack::Headers[], @h
+ assert_equal Rack::Headers['ab'=>'55'], @h.update({'ab'=>'55'})
+ assert_equal Rack::Headers['ab'=>'55'], @h
+ assert_equal Rack::Headers['ab'=>'55', 'cd'=>'2', '3'=>'4'], @fh.merge({'ab'=>'55'})
+ assert_equal Rack::Headers['ab'=>'1', 'cd'=>'2', '3'=>'4'], @fh
+ assert_equal Rack::Headers['ab'=>'55', 'cd'=>'2', '3'=>'4'], @fh.merge!({'ab'=>'55'})
+ assert_equal Rack::Headers['ab'=>'55', 'cd'=>'2', '3'=>'4'], @fh
+ assert_equal Rack::Headers['ab'=>'abss55', 'cd'=>'2', '3'=>'4'], @fh.merge({'ab'=>'ss'}){|k,ov,nv| [k,nv,ov].join}
+ assert_equal Rack::Headers['ab'=>'55', 'cd'=>'2', '3'=>'4'], @fh
+ assert_equal Rack::Headers['ab'=>'abss55', 'cd'=>'2', '3'=>'4'], @fh.update({'ab'=>'ss'}){|k,ov,nv| [k,nv,ov].join}
+ assert_equal Rack::Headers['ab'=>'abss55', 'cd'=>'2', '3'=>'4'], @fh
+ assert_equal Rack::Headers['ab'=>'abssabss55', 'cd'=>'2', '3'=>'4'], @fh.merge!({'ab'=>'ss'}){|k,ov,nv| [k,nv,ov].join}
+ assert_equal Rack::Headers['ab'=>'abssabss55', 'cd'=>'2', '3'=>'4'], @fh
+ end
+
+ def test_replace
+ h = @h.dup
+ fh = @fh.dup
+ h1 = fh.replace(@h)
+ assert_equal @h, h1
+ assert_same fh, h1
+
+ h2 = h.replace(@fh)
+ assert_equal @fh, h2
+ assert_same h, h2
+
+ assert_equal @h, fh.replace({})
+ assert_equal @fh, h.replace('AB'=>'1', 'cd'=>'2', '3'=>'4')
+ end
+
+ def test_select
+ assert_equal({}, @h.select{true})
+ assert_equal({}, @h.select{false})
+ assert_equal({'3' => '4', "ab" => '1', 'cd' => '2'}, @fh.select{true})
+ assert_equal({}, @fh.select{false})
+ assert_equal({'cd' => '2'}, @fh.select{|k,v| k.start_with?('c')})
+ assert_equal({'3' => '4'}, @fh.select{|k,v| v == '4'})
+ end
+
+ def test_shift
+ assert_nil @h.shift
+ array = @fh.to_a
+ i = 3
+ while true
+ assert i >= 0
+ kv = @fh.shift
+ if kv.nil?
+ assert_equal [], array
+ break
+ else
+ i -= 1
+ assert array.include?(kv)
+ array -= [kv]
+ end
+ end
+ assert_equal [], array
+ assert_equal 0, i
+ end
+
+ def test_sort
+ assert_equal [], @h.sort
+ assert_equal [], @h.sort{|a,b| a.to_s<=>b.to_s}
+ assert_equal [['ab', '1'], ['cd', '4'], ['ef', '2']], Rack::Headers['CD','4','AB','1','EF','2'].sort
+ assert_equal [['3', '4'], ['ab', '1'], ['cd', '2']], @fh.sort{|(ak,av),(bk,bv)| ak.to_s<=>bk.to_s}
+ end
+
+ def test_to_a
+ assert_equal [], @h.to_a
+ assert_equal [['ab', '1'], ['cd', '2'], ['3', '4']], @fh.to_a
+ end
+
+ def test_to_hash
+ assert_equal Hash[], @h.to_hash
+ assert_equal Hash['3','4','ab','1','cd','2'], @fh.to_hash
+ end
+
+ def test_values
+ assert_equal [], @h.values
+ assert_equal ['f', 'c'], Rack::Headers['aB','f','1','c'].values
+ end
+
+ def test_values_at
+ assert_equal [], @h.values_at()
+ assert_equal [nil], @h.values_at(1)
+ assert_equal [nil, nil], @h.values_at(1, 1)
+ assert_equal [], @fh.values_at()
+ assert_equal ['1'], @fh.values_at('AB')
+ assert_equal ['2', '1'], @fh.values_at('CD', 'Ab')
+ assert_equal ['2', nil, '1'], @fh.values_at('CD', 32, 'aB')
+ assert_equal ['4', '2', nil, '1'], @fh.values_at('3', 'CD', 32, 'ab')
+ end
+
+ def test_assoc
+ assert_nil @h.assoc(1)
+ assert_equal ['ab', '1'], @fh.assoc('Ab')
+ assert_equal ['cd', '2'], @fh.assoc('CD')
+ assert_nil @fh.assoc('4')
+ assert_equal ['3', '4'], @fh.assoc('3')
+ end
+
+ def test_default_proc=
+ @h.default_proc = proc{|h, k| k * 2}
+ assert_equal 'aa', @h['A']
+ @h['Ab'] = '2'
+ assert_equal '2', @h['aB']
+ end
+
+ def test_flatten
+ assert_equal [], @h.flatten
+ assert_equal ['ab', '1', 'cd', '2', '3', '4'], @fh.flatten
+ @fh['X'] = '56'
+ assert_equal ['ab', '1', 'cd', '2', '3', '4', 'x', '56'], @fh.flatten
+ assert_equal ['ab', '1', 'cd', '2', '3', '4', 'x', '56'], @fh.flatten(2)
+ end
+
+ def test_keep_if
+ assert_equal @h, @h.keep_if{|k, v| true}
+ assert_equal @fh, @fh.keep_if{|k, v| true}
+ assert_equal @h, @fh.dup.keep_if{|k, v| false}
+ assert_equal Rack::Headers["AB"=>'1'], @fh.keep_if{|k, v| k == "ab"}
+ end
+
+ def test_key
+ assert_nil @h.key('1')
+ assert_nil @fh.key(1)
+ assert_equal 'ab', @fh.key('1')
+ assert_equal 'cd', @fh.key('2')
+ assert_nil @fh.key('3')
+ assert_equal '3', @fh.key('4')
+ end
+
+ def test_rassoc
+ assert_nil @h.rassoc('1')
+ assert_equal ['ab', '1'], @fh.rassoc('1')
+ assert_equal ['cd', '2'], @fh.rassoc('2')
+ assert_nil @fh.rassoc('3')
+ assert_equal ['3', '4'], @fh.rassoc('4')
+ end
+
+ def test_select!
+ assert_nil @h.select!{|k, v| true}
+ assert_nil @fh.select!{|k, v| true}
+ assert_equal @h, @fh.dup.select!{|k, v| false}
+ assert_equal Rack::Headers["AB"=>'1'], @fh.select!{|k, v| k == "ab"}
+ end
+
+ def test_compare_by_identity
+ assert_raises(TypeError){@fh.compare_by_identity}
+ end
+
+ def test_compare_by_identity?
+ assert_equal(false, @fh.compare_by_identity?)
+ end
+
+ def test_to_h
+ assert_equal Hash[], @h.to_h
+ assert_equal Hash['3','4','ab','1','cd','2'], @fh.to_h
+ end
+
+ def test_dig
+ assert_equal('1', @fh.dig('AB'))
+ assert_equal('2', @fh.dig('Cd'))
+ assert_equal('4', @fh.dig('3'))
+ assert_nil(@fh.dig('4'))
+
+ assert_raises(TypeError){@fh.dig('AB', 1)}
+ assert_raises(TypeError){@fh.dig('cd', 2)}
+ assert_raises(TypeError){@fh.dig('3', 3)}
+ assert_nil(@fh.dig('4', 5))
+ end
+
+ def test_fetch_values
+ assert_equal(['1'], @fh.fetch_values('AB'))
+ assert_equal(['1', '2', '4'], @fh.fetch_values('AB', 'Cd', '3'))
+ assert_raises(KeyError){@fh.fetch_values('AB', 'cD', '4')}
+ end
+
+ def test_to_proc
+ pr = @fh.to_proc
+ assert_equal('1', pr['AB'])
+ assert_equal('2', pr['cD'])
+ assert_equal('4', pr['3'])
+ assert_nil(pr['4'])
+ end
+
+ def test_compact
+ assert_equal(false, @fh.compact.equal?(@fh))
+ assert_equal(@fh, @fh.compact)
+ assert_equal(Rack::Headers['Ab'=>1], Rack::Headers['aB'=>1, 'cd'=>nil].compact)
+ end
+
+ def test_compact!
+ fh = @fh.dup
+ assert_nil(@fh.compact!)
+ assert_equal(fh, @fh)
+
+ h = Rack::Headers['Ab'=>1, 'cd'=>nil]
+ assert_equal(Rack::Headers['aB'=>1], h.compact!)
+ assert_equal(Rack::Headers['AB'=>1], h)
+ end
+
+ def test_transform_values
+ fh = @fh.transform_values{|v| v.to_s*2}
+ assert_equal('1', @fh['aB'])
+ assert_equal(Rack::Headers['AB'=>'11', 'cD'=>'22', '3'=>'44'], fh)
+ assert_equal('11', fh['Ab'])
+ end
+
+ def test_transform_values!
+ @fh.transform_values!{|v| v.to_s*2}
+ assert_equal('11', @fh['AB'])
+ assert_equal(Rack::Headers['Ab'=>'11', 'CD'=>'22', '3'=>'44'], @fh)
+ assert_equal('11', @fh['aB'])
+ end
+
+ if RUBY_VERSION >= '2.5'
+ def test_slice
+ assert_equal(Rack::Headers['Ab'=>'1', 'cD'=>'2', '3'=>'4'], @fh.slice('aB', 'Cd', '3'))
+ assert_equal(Rack::Headers['AB'=>'1', 'CD'=>'2'], @fh.slice('Ab', 'CD'))
+ assert_equal('1', @fh.slice('AB', 'cd')['Ab'])
+ end
+
+ def test_transform_keys
+ map = {'ab'=>'Xy', 'cd'=>'dC', '3'=>'5'}
+ dh = @fh.dup
+ fh = @fh.transform_keys{|k| map[k]}
+ assert_equal(dh, @fh)
+ assert_equal('1', fh['xY'])
+ assert_equal('2', fh['Dc'])
+ assert_equal('4', fh['5'])
+ end
+
+ def test_transform_keys!
+ map = {'ab'=>'Xy', 'cd'=>'dC', '3'=>'5'}
+ dh = @fh.dup
+ @fh.transform_keys!{|k| map[k]}
+ assert_equal(false, dh == @fh)
+ assert_equal('1', @fh['xY'])
+ assert_equal('2', @fh['DC'])
+ assert_equal('4', @fh['5'])
+ end
+ end
+
+ if RUBY_VERSION >= '2.6'
+ def test_filter!
+ assert_nil @h.filter!{|k, v| true}
+ assert_nil @fh.filter!{|k, v| true}
+ assert_equal @h, @fh.dup.filter!{|k, v| false}
+ assert_equal Rack::Headers["AB"=>'1'], @fh.filter!{|k, v| k == "ab"}
+ end
+ end
+
+ if RUBY_VERSION >= '2.7'
+ def test_deconstruct_keys
+ assert_equal(@fh.to_hash, @fh.deconstruct_keys([]))
+ assert_equal(Rack::Headers, @fh.deconstruct_keys([]).class)
+ end
+ end
+
+ if RUBY_VERSION >= '3.0'
+ def test_except
+ @fh = Rack::Headers['AB'=>'1', 'Cd'=>'2', '3'=>'4']
+ assert_equal(@fh, @fh.except)
+ assert_equal(Rack::Headers['cD'=>'2', '3'=>'4'], @fh.except('AB', 5))
+ assert_equal(Rack::Headers['AB'=>'1'], @fh.except('cD', '3'))
+ end
+ end
+end