Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor headers #92

Merged
merged 6 commits into from
Mar 18, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 125 additions & 30 deletions lib/http/headers.rb
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
11 changes: 11 additions & 0 deletions lib/http/headers/mixin.rb
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
4 changes: 2 additions & 2 deletions lib/http/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,15 @@ def initialize(verb, uri, headers = {}, proxy = {}, body = nil, version = '1.1')

@proxy, @body, @version = proxy, body, version

@headers = HTTP::Headers.new(headers)
@headers = HTTP::Headers.from_hash(headers || {})
@headers['Host'] ||= @uri.host
end

# Returns new Request with updated uri
def redirect(uri)
uri = @uri.merge uri.to_s
req = self.class.new(verb, uri, headers, proxy, body, version)
req.headers.merge!('Host' => req.uri.host)
req['Host'] = req.uri.host
req
end

Expand Down
4 changes: 2 additions & 2 deletions lib/http/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class Response

def initialize(status, version, headers, body, uri = nil) # rubocop:disable ParameterLists
@status, @version, @body, @uri = status, version, body, uri
@headers = HTTP::Headers.new(headers)
@headers = HTTP::Headers.from_hash(headers || {})
end

# Obtain the 'Reason-Phrase' for the response
Expand All @@ -86,7 +86,7 @@ def reason

# Returns an Array ala Rack: `[status, headers, body]`
def to_a
[status, headers, body.to_s]
[status, headers.to_h, body.to_s]
end

# Return the response body as a string
Expand Down
36 changes: 36 additions & 0 deletions spec/http/headers/mixin_spec.rb
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
Loading