-
Notifications
You must be signed in to change notification settings - Fork 324
/
Copy pathheaders.rb
212 lines (182 loc) · 5.24 KB
/
headers.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# frozen_string_literal: true
require "forwardable"
require "http/errors"
require "http/headers/mixin"
require "http/headers/known"
module HTTP
# HTTP Headers container.
class Headers
extend Forwardable
include Enumerable
# Matches HTTP header names when in "Canonical-Http-Format"
CANONICAL_NAME_RE = /^[A-Z][a-z]*(?:-[A-Z][a-z]*)*$/
# Matches valid header field name according to RFC.
# @see http://tools.ietf.org/html/rfc7230#section-3.2
COMPLIANT_NAME_RE = /^[A-Za-z0-9!#\$%&'*+\-.^_`|~]+$/
# Class constructor.
def initialize
@pile = []
end
# Sets header.
#
# @param (see #add)
# @return [void]
def set(name, value)
delete(name)
add(name, value)
end
alias []= set
# Removes header.
#
# @param [#to_s] name header name
# @return [void]
def delete(name)
name = normalize_header name.to_s
@pile.delete_if { |k, _| k == name }
end
# Appends header.
#
# @param [#to_s] name header name
# @param [Array<#to_s>, #to_s] value header value(s) to be appended
# @return [void]
def add(name, value)
name = normalize_header name.to_s
Array(value).each { |v| @pile << [name, v.to_s] }
end
# Returns list of header values if any.
#
# @return [Array<String>]
def get(name)
name = normalize_header name.to_s
@pile.select { |k, _| k == name }.map { |_, v| v }
end
# Smart version of {#get}.
#
# @return [nil] if header was not set
# @return [String] if header has exactly one value
# @return [Array<String>] if header has more than one value
def [](name)
values = get(name)
case values.count
when 0 then nil
when 1 then values.first
else values
end
end
# Tells whenever header with given `name` is set or not.
#
# @return [Boolean]
def include?(name)
name = normalize_header name.to_s
@pile.any? { |k, _| k == name }
end
# Returns Rack-compatible headers Hash
#
# @return [Hash]
def to_h
Hash[keys.map { |k| [k, self[k]] }]
end
alias to_hash to_h
# Returns headers key/value pairs.
#
# @return [Array<[String, String]>]
def to_a
@pile.map { |pair| pair.map(&:dup) }
end
# Returns human-readable representation of `self` instance.
#
# @return [String]
def inspect
"#<#{self.class} #{to_h.inspect}>"
end
# Returns 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
# Calls the given block once for each key/value pair in headers container.
#
# @return [Enumerator] if no block given
# @return [Headers] self-reference
def each
return to_enum(__method__) unless block_given?
@pile.each { |arr| yield(arr) }
self
end
# @!method empty?
# Returns `true` if `self` has no key/value pairs
#
# @return [Boolean]
def_delegator :@pile, :empty?
# @!method hash
# Compute a hash-code for this headers container.
# Two conatiners with the same content will have the same hash code.
#
# @see http://www.ruby-doc.org/core/Object.html#method-i-hash
# @return [Fixnum]
def_delegator :@pile, :hash
# Properly clones internal key/value storage.
#
# @api private
def initialize_copy(orig)
super
@pile = to_a
end
# Merges `other` headers into `self`.
#
# @see #merge
# @return [void]
def merge!(other)
self.class.coerce(other).to_h.each { |name, values| set name, values }
end
# Returns new instance with `other` headers merged in.
#
# @see #merge!
# @return [Headers]
def merge(other)
dup.tap { |dupped| dupped.merge! other }
end
class << self
# Coerces given `object` into Headers.
#
# @raise [Error] if object can't be coerced
# @param [#to_hash, #to_h, #to_a] object
# @return [Headers]
def coerce(object)
unless object.is_a? self
object = case
when object.respond_to?(:to_hash) then object.to_hash
when object.respond_to?(:to_h) then object.to_h
when object.respond_to?(:to_a) then object.to_a
else raise Error, "Can't coerce #{object.inspect} to Headers"
end
end
headers = new
object.each { |k, v| headers.add k, v }
headers
end
alias [] coerce
end
private
# Transforms `name` to canonical HTTP header capitalization
#
# @param [String] name
# @raise [InvalidHeaderNameError] if normalized name does not
# match {HEADER_NAME_RE}
# @return [String] canonical HTTP header name
def normalize_header(name)
return name if name =~ CANONICAL_NAME_RE
normalized = name.split(/[\-_]/).each(&:capitalize!).join("-")
return normalized if normalized =~ COMPLIANT_NAME_RE
raise InvalidHeaderNameError, "Invalid HTTP header field name: #{name.inspect}"
end
end
end