-
Notifications
You must be signed in to change notification settings - Fork 84
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
api: make iOS Headers and HeadersBuilder case-insensitive #2383
Changes from 38 commits
8ffc556
c3c9015
8137392
15f722b
3576622
f7538f2
516a1fc
2ff4fb6
42897f2
0e328a0
8430f4f
86ce3fc
37c6e5c
35c0536
1fd9938
6389322
3ac922c
6d6caf2
cc40134
eea7eff
eac2d9d
302db00
9069668
15cb362
ca4441a
fe79cb3
a80ef6a
1a4d915
2a5085a
b9d1dd9
75a586d
7295362
723c76f
079a01a
62885af
702bc24
d3bd1ea
d45fa9f
42b38bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,45 +4,64 @@ import Foundation | |
/// To instantiate new instances, see `{Request|Response}HeadersBuilder`. | ||
@objcMembers | ||
public class Headers: NSObject { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we merge the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One of my previous solutions was to merge
|
||
let headers: [String: [String]] | ||
let container: HeadersContainer | ||
|
||
/// Get the value for the provided header name. | ||
/// | ||
/// - note: The lookup for a header name is a case-insensitive operation. | ||
/// | ||
Augustyniak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// - parameter name: Header name for which to get the current value. | ||
/// | ||
/// - returns: The current headers specified for the provided name. | ||
public func value(forName name: String) -> [String]? { | ||
return self.headers[name] | ||
return self.container.value(forName: name) | ||
} | ||
|
||
/// Accessor for all underlying headers as a map. | ||
/// Accessor for all underlying case-sensitive headers. When possible, | ||
/// use case-insensitive accessors instead. | ||
/// | ||
/// - warning: It's discouraged to use this dictionary for equality | ||
/// key-based lookups as this may lead to issues with headers | ||
/// that do not follow expected casing i.e., "Content-Length" | ||
/// instead of "content-length". | ||
/// | ||
/// - returns: The underlying headers. | ||
public func allHeaders() -> [String: [String]] { | ||
return self.headers | ||
/// - returns: The underlying case-sensitive headers. | ||
public func caseSensitiveHeaders() -> [String: [String]] { | ||
return self.container.allHeaders() | ||
} | ||
|
||
/// Internal initializer used by builders. | ||
/// | ||
/// - parameter headers: Headers to set. | ||
required init(headers: [String: [String]]) { | ||
self.headers = headers | ||
/// - parameter container: Headers to set. | ||
required init(container: HeadersContainer) { | ||
self.container = container | ||
super.init() | ||
} | ||
|
||
/// Inialize the receiver with a given headers map. | ||
/// | ||
/// - parameter headers: The headers map to use. | ||
convenience init(headers: [String: [String]]) { | ||
self.init(container: HeadersContainer(headers: headers)) | ||
} | ||
|
||
override convenience init() { | ||
self.init(headers: [:]) | ||
} | ||
} | ||
|
||
// MARK: - Equatable | ||
|
||
extension Headers { | ||
public override func isEqual(_ object: Any?) -> Bool { | ||
return (object as? Self)?.headers == self.headers | ||
return (object as? Self)?.container == self.container | ||
} | ||
} | ||
|
||
// MARK: - CustomStringConvertible | ||
|
||
extension Headers { | ||
public override var description: String { | ||
return "\(type(of: self)) \(self.headers.description)" | ||
return "\(type(of: self)) \(self.caseSensitiveHeaders())" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
/// The container which manages the underlying headers map. | ||
/// It maintains the original casing of passed header names. | ||
/// It treats headers names as case-insensitive for the purpose | ||
/// of header lookups and header name conflict resolutions. | ||
struct HeadersContainer: Equatable { | ||
private var headers: [String: Header] | ||
|
||
/// Represents a headers name together with all of its values. | ||
/// It preserves the original casing of the header name. | ||
struct Header: Equatable { | ||
private(set) var name: String | ||
private(set) var value: [String] | ||
|
||
init(name: String, value: [String] = []) { | ||
self.name = name | ||
self.value = value | ||
} | ||
|
||
mutating func addValue(_ value: [String]) { | ||
self.value.append(contentsOf: value) | ||
} | ||
|
||
mutating func addValue(_ value: String) { | ||
self.value.append(value) | ||
} | ||
} | ||
|
||
/// Initialize a new instance of the receiver using the provided headers map. | ||
/// | ||
/// - parameter headers: The headers map. | ||
init(headers: [String: [String]]) { | ||
var underlyingHeaders = [String: Header]() | ||
for (name, value) in headers { | ||
let lowercasedName = name.lowercased() | ||
/// Dictionaries in Swift are unordered collections. Process headers with names | ||
/// that are the same when lowercased in an alphabetical order to avoid a situation | ||
/// in which the result of the initialization is non-derministic i.e., we want | ||
/// "[A: ["1"]", "a: ["2"]]" headers to be always converted to ["A": ["1", "2"]] and | ||
/// never to "a": ["2", "1"]. | ||
/// | ||
/// If a given header name already exists in the processed headers map, check | ||
/// if the currently processed header name is before the existing header name as | ||
/// determined by an alphabetical order. | ||
guard let existingHeader = underlyingHeaders[lowercasedName] else { | ||
underlyingHeaders[lowercasedName] = Header(name: name, value: value) | ||
continue | ||
} | ||
|
||
if existingHeader.name > name { | ||
underlyingHeaders[lowercasedName] = | ||
Header(name: name, value: value + existingHeader.value) | ||
} else { | ||
underlyingHeaders[lowercasedName]?.addValue(value) | ||
} | ||
} | ||
self.headers = underlyingHeaders | ||
} | ||
|
||
/// Initialize an empty headers container. | ||
init() { | ||
self.headers = [:] | ||
} | ||
|
||
/// Add a value to a header with a given name. | ||
/// | ||
/// - parameter name: The name of the header. For the purpose of headers lookup | ||
/// and header name conflict resolution, the name of the header | ||
/// is considered to be case-insensitive. | ||
/// - parameter value: The value to add. | ||
mutating func add(name: String, value: String) { | ||
self.headers[name.lowercased(), default: Header(name: name)].addValue(value) | ||
} | ||
|
||
/// Set the value of a given header. | ||
/// | ||
/// - parameter name: The name of the header. | ||
/// - parameter value: The value to set the header value to. | ||
mutating func set(name: String, value: [String]?) { | ||
guard let value = value else { | ||
self.headers[name.lowercased()] = nil | ||
return | ||
} | ||
self.headers[name.lowercased()] = Header(name: name, value: value) | ||
} | ||
|
||
/// Get the value for the provided header name. | ||
/// | ||
/// - parameter name: The case-insensitive header name for which to | ||
/// get the current value. | ||
/// | ||
/// - returns: The value associated with a given header. | ||
func value(forName name: String) -> [String]? { | ||
return self.headers[name.lowercased()]?.value | ||
} | ||
|
||
/// Return all underlying headers. | ||
/// | ||
/// - returns: The underlying headers. | ||
func allHeaders() -> [String: [String]] { | ||
return Dictionary(uniqueKeysWithValues: self.headers.map { _, value in | ||
return (value.name, value.value) | ||
}) | ||
} | ||
} | ||
|
||
extension HeadersContainer: CustomStringConvertible { | ||
var description: String { | ||
return self.headers.description | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this rename. 👍