-
Notifications
You must be signed in to change notification settings - Fork 320
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #92 from tarcieri/feature/improve-headers
Refactor headers
- Loading branch information
Showing
8 changed files
with
569 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,55 +1,150 @@ | ||
require 'forwardable' | ||
require 'delegate' | ||
|
||
require 'http/headers/mixin' | ||
|
||
module HTTP | ||
# Headers Hash wraper with keys normalization | ||
class Headers < ::Delegator | ||
module Mixin | ||
extend Forwardable | ||
attr_reader :headers | ||
def_delegators :headers, :[], :[]= | ||
end | ||
class Headers | ||
extend Forwardable | ||
|
||
# Matches HTTP header names when in "Canonical-Http-Format" | ||
CANONICAL_HEADER = /^[A-Z][a-z]*(-[A-Z][a-z]*)*$/ | ||
|
||
def initialize(obj = {}) | ||
super({}) | ||
__setobj__ obj | ||
# :nodoc: | ||
def initialize | ||
@pile = [] | ||
end | ||
|
||
# Transform to canonical HTTP header capitalization | ||
def canonicalize_header(header) | ||
header.to_s.split(/[\-_]/).map(&:capitalize).join('-') | ||
# Sets header | ||
# | ||
# @return [void] | ||
def set(name, value) | ||
delete(name) | ||
add(name, value) | ||
end | ||
alias_method :[]=, :set | ||
|
||
# Obtain the given header | ||
def [](name) | ||
super(name) || super(canonicalize_header name) | ||
# Removes header | ||
# | ||
# @return [void] | ||
def delete(name) | ||
name = canonicalize_header name.to_s | ||
@pile.delete_if { |k, _| k == name } | ||
end | ||
|
||
# Append header | ||
# | ||
# @return [void] | ||
def add(name, value) | ||
name = canonicalize_header name.to_s | ||
Array(value).each { |v| @pile << [name, v] } | ||
end | ||
alias_method :append, :add | ||
|
||
# Set a header | ||
def []=(name, value) | ||
# If we have a canonical header, we're done, canonicalize otherwise | ||
name = name.to_s[CANONICAL_HEADER] || canonicalize_header(name) | ||
# Return array of header values if any. | ||
# | ||
# @return [Array] | ||
def get(name) | ||
name = canonicalize_header name.to_s | ||
@pile.select { |k, _| k == name }.map { |_, v| v } | ||
end | ||
|
||
# Check if the header has already been set and group | ||
value = Array(self[name]) + Array(value) if key? name | ||
# Smart version of {#get} | ||
# | ||
# @return [NilClass] if header was not set | ||
# @return [Object] if header has exactly one value | ||
# @return [Array<Object>] if header has more than one value | ||
def [](name) | ||
values = get(name) | ||
|
||
super name, value | ||
case values.count | ||
when 0 then nil | ||
when 1 then values.first | ||
else values | ||
end | ||
end | ||
|
||
protected | ||
# Converts headers into a Rack-compatible Hash | ||
# | ||
# @return [Hash] | ||
def to_h | ||
Hash[keys.map { |k| [k, self[k]] }] | ||
end | ||
|
||
# Array of key/value pairs | ||
# | ||
# @return [Array<[String, String]>] | ||
def to_a | ||
@pile.map { |pair| pair.map(&:dup) } | ||
end | ||
|
||
# :nodoc: | ||
def __getobj__ | ||
@headers | ||
def inspect | ||
"#<#{self.class} #{to_h.inspect}>" | ||
end | ||
|
||
# List of header names | ||
# | ||
# @return [Array<String>] | ||
def keys | ||
@pile.map { |k, _| k }.uniq | ||
end | ||
|
||
# Compares headers to another Headers or Array of key/value pairs | ||
# | ||
# @return [Boolean] | ||
def ==(other) | ||
return false unless other.respond_to? :to_a | ||
@pile == other.to_a | ||
end | ||
|
||
def_delegators :@pile, :each, :hash | ||
|
||
# :nodoc: | ||
def __setobj__(obj) | ||
@headers = {} | ||
obj.each { |k, v| self[k] = v } if obj.respond_to? :each | ||
def initialize_copy(orig) | ||
super | ||
@pile = to_a | ||
end | ||
|
||
# Merge in `other` headers | ||
# | ||
# @see #merge | ||
# @return [void] | ||
def merge!(other) | ||
self.class.from_hash(other).to_h.each { |name, values| set name, values } | ||
end | ||
|
||
# Returns new Headers instance with `other` headers merged in. | ||
# | ||
# @see #merge! | ||
# @return [Headers] | ||
def merge(other) | ||
dup.tap { |dupped| dupped.merge! other } | ||
end | ||
|
||
# Initiates new Headers object from given Hash | ||
# | ||
# @raise [Error] if given hash does not respond to `#to_hash` or `#to_h` | ||
# @param [#to_hash, #to_h] hash | ||
# @return [Headers] | ||
def self.from_hash(hash) | ||
hash = case | ||
when hash.respond_to?(:to_hash) then hash.to_hash | ||
when hash.respond_to?(:to_h) then hash.to_h | ||
else fail Error, '#to_hash or #to_h object expected' | ||
end | ||
|
||
headers = new | ||
hash.each { |k, v| headers.add k, v } | ||
headers | ||
end | ||
|
||
private | ||
|
||
# Transform to canonical HTTP header capitalization | ||
# @param [String] name | ||
# @return [String] | ||
def canonicalize_header(name) | ||
name[CANONICAL_HEADER] || name.split(/[\-_]/).map(&:capitalize).join('-') | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
require 'forwardable' | ||
|
||
module HTTP | ||
class Headers | ||
module Mixin | ||
extend Forwardable | ||
attr_reader :headers | ||
def_delegators :headers, :[], :[]= | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
require 'spec_helper' | ||
|
||
describe HTTP::Headers::Mixin do | ||
let :dummy_class do | ||
Class.new do | ||
include HTTP::Headers::Mixin | ||
|
||
def initialize(headers) | ||
@headers = headers | ||
end | ||
end | ||
end | ||
|
||
let(:headers) { HTTP::Headers.new } | ||
let(:dummy) { dummy_class.new headers } | ||
|
||
describe '#headers' do | ||
it 'returns @headers instance variable' do | ||
expect(dummy.headers).to be headers | ||
end | ||
end | ||
|
||
describe '#[]' do | ||
it 'proxies to headers#[]' do | ||
expect(headers).to receive(:[]).with(:accept) | ||
dummy[:accept] | ||
end | ||
end | ||
|
||
describe '#[]=' do | ||
it 'proxies to headers#[]' do | ||
expect(headers).to receive(:[]=).with(:accept, 'text/plain') | ||
dummy[:accept] = 'text/plain' | ||
end | ||
end | ||
end |
Oops, something went wrong.