diff --git a/lib/http/headers/normalizer.rb b/lib/http/headers/normalizer.rb index b4a53b42..be623079 100644 --- a/lib/http/headers/normalizer.rb +++ b/lib/http/headers/normalizer.rb @@ -41,8 +41,9 @@ def initialize # Transforms `name` to canonical HTTP header capitalization def call(name) - name = -name.to_s + name = -name.to_s value = (@cache[name] ||= -normalize_header(name)) + value.dup end diff --git a/spec/lib/http/headers/normalizer_spec.rb b/spec/lib/http/headers/normalizer_spec.rb index a512af08..a304e3d3 100644 --- a/spec/lib/http/headers/normalizer_spec.rb +++ b/spec/lib/http/headers/normalizer_spec.rb @@ -22,39 +22,31 @@ expect(cache_store.keys).to eq(max_headers[1..] + ["New-Header"]) end - describe "multiple invocations with the same input" do - let(:normalized_values) { Array.new(3) { normalizer.call("content_type") } } + it "retuns mutable strings" do + normalized_headers = Array.new(3) { normalizer.call("content_type") } - it "returns the same result each time" do - expect(normalized_values.uniq.size).to eq 1 - end - - it "returns different string objects each time" do - expect(normalized_values.map(&:object_id).uniq.size).to eq normalized_values.size - end - end - - it "limits allocation counts for first normalization of a header" do - expected_allocations = { - Array => 1, - described_class => 1, - Hash => 1, - described_class::Cache => 1, - MatchData => 1, - String => 6 - } - - expect do - normalizer.call("content_type") - end.to limit_allocations(**expected_allocations) + expect(normalized_headers) + .to satisfy { |arr| arr.uniq.size == 1 } + .and satisfy { |arr| arr.map(&:object_id).uniq.size == normalized_headers.size } + .and satisfy { |arr| arr.none?(&:frozen?) } end - it "allocates minimal memory for subsequent normalization of the same header" do - normalizer.call("content_type") - - expect do - normalizer.call("content_type") - end.to limit_allocations(String => 1) + it "allocates minimal memory for normalization of the same header" do + normalizer.call("accept") # XXX: Ensure normalizer is pre-allocated + + # On first call it is expected to allocate during normalization + expect { normalizer.call("content_type") }.to limit_allocations( + Array => 1, + MatchData => 1, + String => 6 + ) + + # On subsequent call it is expected to only allocate copy of a cached string + expect { normalizer.call("content_type") }.to limit_allocations( + Array => 0, + MatchData => 0, + String => 1 + ) end end end