forked from httprb/http
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathheaders.rb
More file actions
343 lines (309 loc) · 8.33 KB
/
Copy pathheaders.rb
File metadata and controls
343 lines (309 loc) · 8.33 KB
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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# frozen_string_literal: true
require "forwardable"
require "http/errors"
require "http/headers/normalizer"
require "http/headers/known"
module HTTP
# HTTP Headers container.
class Headers
extend Forwardable
include Enumerable
class << self
# Coerces given object into Headers
#
# @example
# headers = HTTP::Headers.coerce("Content-Type" => "text/plain")
#
# @raise [Error] if object can't be coerced
# @param [#to_hash, #to_h, #to_a] object
# @return [Headers]
# @api public
def coerce(object)
object = if object.respond_to?(:to_hash) then object.to_hash
elsif object.respond_to?(:to_h) then object.to_h
elsif object.respond_to?(:to_a) then object.to_a
else raise Error, "Can't coerce #{object.inspect} to Headers"
end
headers = new
object.each { |k, v| headers.add k, v }
headers
end
# @!method [](object)
# Coerces given object into Headers
#
# @example
# headers = HTTP::Headers["Content-Type" => "text/plain"]
#
# @see .coerce
# @return [Headers]
# @api public
alias [] coerce
# Returns the shared normalizer instance
#
# @example
# HTTP::Headers.normalizer
#
# @return [Headers::Normalizer]
# @api public
def normalizer
@normalizer ||= Normalizer.new #: Headers::Normalizer
end
end
# Creates a new empty headers container
#
# @example
# headers = HTTP::Headers.new
#
# @return [Headers]
# @api public
def initialize
# The @pile stores each header value using a three element array:
# 0 - the normalized header key, used for lookup
# 1 - the header key as it will be sent with a request
# 2 - the value
@pile = []
end
# Sets header, replacing any existing values
#
# @example
# headers.set("Content-Type", "text/plain")
#
# @param (see #add)
# @return [void]
# @api public
def set(name, value)
delete(name)
add(name, value)
end
# @!method []=(name, value)
# Sets header, replacing any existing values
#
# @example
# headers["Content-Type"] = "text/plain"
#
# @see #set
# @return [void]
# @api public
alias []= set
# Removes header with the given name
#
# @example
# headers.delete("Content-Type")
#
# @param [#to_s] name header name
# @return [void]
# @api public
def delete(name)
name = normalize_header name
@pile.delete_if { |k, _| k.eql?(name) }
end
# Appends header value(s) to the given name
#
# @example
# headers.add("Accept", "text/html")
#
# @param [String, Symbol] name header name. When specified as a string, the
# name is sent as-is. When specified as a symbol, the name is converted
# to a string of capitalized words separated by a dash. Word boundaries
# are determined by an underscore (`_`) or a dash (`-`).
# Ex: `:content_type` is sent as `"Content-Type"`, and `"auth_key"` (string)
# is sent as `"auth_key"`.
# @param [Array<#to_s>, #to_s] value header value(s) to be appended
# @return [void]
# @api public
def add(name, value)
lookup_name = normalize_header(name)
wire_name = wire_name_for(name, lookup_name)
Array(value).each do |v|
@pile << [
lookup_name,
wire_name,
validate_value(v)
]
end
end
# Returns list of header values if any
#
# @example
# headers.get("Content-Type")
#
# @return [Array<String>]
# @api public
def get(name)
name = normalize_header name
@pile.filter_map { |k, _, v| v if k.eql?(name) }
end
# Smart version of {#get}
#
# @example
# headers["Content-Type"]
#
# @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
# @api public
def [](name)
values = get(name)
return if values.empty?
return values unless values.one?
values.join
end
# Tells whether header with given name is set
#
# @example
# headers.include?("Content-Type")
#
# @return [Boolean]
# @api public
def include?(name)
name = normalize_header name
@pile.any? { |k, _| k.eql?(name) }
end
# Returns Rack-compatible headers Hash
#
# @example
# headers.to_h
#
# @return [Hash]
# @api public
def to_h
keys.to_h { |k| [k, self[k]] } # steep:ignore
end
# @!method to_hash
# @see #to_h
# @return [Hash]
alias to_hash to_h
# Pattern matching interface
#
# @example
# headers.deconstruct_keys(%i[content_type])
#
# @param keys [Array<Symbol>, nil] keys to extract, or nil for all
# @return [Hash{Symbol => Object}]
# @api public
def deconstruct_keys(keys)
hash = @pile.map { |_, k, _| k }.to_h { |k| [k.tr("A-Z-", "a-z_").to_sym, self[k]] } # steep:ignore
keys ? hash.slice(*keys) : hash
end
# Returns human-readable representation of self instance
#
# @example
# headers.inspect
#
# @return [String]
# @api public
def inspect = "#<#{self.class}>"
# Returns list of header names
#
# @example
# headers.keys
#
# @return [Array<String>]
# @api public
def keys
@pile.map { |_, k, _| k }.uniq
end
# Compares headers to another Headers or Array of pairs
#
# @example
# headers == other_headers
#
# @return [Boolean]
# @api public
def ==(other)
return false unless other.respond_to? :to_a
to_a.eql?(other.to_a)
end
# Calls the given block once for each key/value pair
#
# @example
# headers.each { |name, value| puts "#{name}: #{value}" }
#
# @return [Enumerator] if no block given
# @return [Headers] self-reference
# @api public
def each
return to_enum unless block_given?
@pile.each { |item| yield(item.drop(1)) }
self
end
# @!method empty?
# Returns true if self has no key/value pairs
#
# @example
# headers.empty?
#
# @return [Boolean]
# @api public
def_delegator :@pile, :empty?
# @!method hash
# Computes a hash-code for this headers container
#
# @example
# headers.hash
#
# @see http://www.ruby-doc.org/core/Object.html#method-i-hash
# @return [Fixnum]
# @api public
def_delegator :@pile, :hash
# Properly clones internal key/value storage
#
# @return [void]
# @api private
def initialize_copy(_orig)
@pile = @pile.map(&:dup)
end
# Merges other headers into self
#
# @example
# headers.merge!("Accept" => "text/html")
#
# @see #merge
# @return [void]
# @api public
def merge!(other)
coerced = self.class.coerce(other)
names = coerced.keys
names.each { |name| set name, coerced.get(name) }
end
# Returns new instance with other headers merged in
#
# @example
# new_headers = headers.merge("Accept" => "text/html")
#
# @see #merge!
# @return [Headers]
# @api public
def merge(other)
dup.tap { |dupped| dupped.merge! other }
end
private
# Returns the wire name for a header
#
# @return [String]
# @api private
def wire_name_for(name, lookup_name)
case name
when String then name
when Symbol then lookup_name
else raise HeaderError, "HTTP header must be a String or Symbol: #{name.inspect}"
end
end
# Transforms name to canonical HTTP header capitalization
#
# @return [String]
# @api private
def normalize_header(name) = self.class.normalizer.call(name)
# Ensures there is no new line character in the header value
#
# @param [String] value
# @raise [HeaderError] if value includes new line character
# @return [String] stringified header value
# @api private
def validate_value(value)
v = value.to_s
return v unless v.include?("\n") || v.include?("\r")
raise HeaderError, "Invalid HTTP header field value: #{v.inspect}"
end
end
end