diff --git a/API.md b/API.md index 2ef55b2..fc9916e 100644 --- a/API.md +++ b/API.md @@ -52,15 +52,211 @@ public enum HTTPBodyChunk { } /// Headers structure. -public struct HTTPHeaders { - var storage: [String:[String]] /* lower cased keys */ - var original: [(String, String)] /* original casing */ - let description: String - - public subscript(key: String) -> [String] - func makeIterator() -> IndexingIterator> - - public init(_ headers: [(String, String)] = []) +public struct HTTPHeaders : Sequence, ExpressibleByDictionaryLiteral { + public subscript(name: Name) -> String? + public subscript(valuesFor name: Name) -> [String] + + public struct Literal : ExpressibleByDictionaryLiteral { } + public mutating func append(_ literal: HTTPHeaders.Literal) + public mutating func replace(_ literal: HTTPHeaders.Literal) + + public func makeIterator() -> AnyIterator<(name: Name, value: String)> + + public struct Name : Hashable, ExpressibleByStringLiteral, CustomStringConvertible { + public init(_ name: String) + + // https://www.iana.org/assignments/message-headers/message-headers.xhtml + // Permanent Message Header Field Names + public static let aIM + public static let accept + public static let acceptAdditions + public static let acceptCharset + public static let acceptDatetime + public static let acceptEncoding + public static let acceptFeatures + public static let acceptLanguage + public static let acceptPatch + public static let acceptPost + public static let acceptRanges + public static let age + public static let allow + public static let alpn + public static let altSvc + public static let altUsed + public static let alternates + public static let applyToRedirectRef + public static let authenticationControl + public static let authenticationInfo + public static let authorization + public static let cExt + public static let cMan + public static let cOpt + public static let cPEP + public static let cPEPInfo + public static let cacheControl + public static let calDAVTimezones + public static let close + public static let connection + public static let contentBase + public static let contentDisposition + public static let contentEncoding + public static let contentID + public static let contentLanguage + public static let contentLength + public static let contentLocation + public static let contentMD5 + public static let contentRange + public static let contentScriptType + public static let contentStyleType + public static let contentType + public static let contentVersion + public static let cookie + public static let cookie2 + public static let dasl + public static let dav + public static let date + public static let defaultStyle + public static let deltaBase + public static let depth + public static let derivedFrom + public static let destination + public static let differentialID + public static let digest + public static let eTag + public static let expect + public static let expires + public static let ext + public static let forwarded + public static let from + public static let getProfile + public static let hobareg + public static let host + public static let http2Settings + public static let im + public static let `if` + public static let ifMatch + public static let ifModifiedSince + public static let ifNoneMatch + public static let ifRange + public static let ifScheduleTagMatch + public static let ifUnmodifiedSince + public static let keepAlive + public static let label + public static let lastModified + public static let link + public static let location + public static let lockToken + public static let man + public static let maxForwards + public static let mementoDatetime + public static let meter + public static let mimeVersion + public static let negotiate + public static let opt + public static let optionalWWWAuthenticate + public static let orderingType + public static let origin + public static let overwrite + public static let p3p + public static let pep + public static let picsLabel + public static let pepInfo + public static let position + public static let pragma + public static let prefer + public static let preferenceApplied + public static let profileObject + public static let `protocol` + public static let protocolInfo + public static let protocolQuery + public static let protocolRequest + public static let proxyAuthenticate + public static let proxyAuthenticationInfo + public static let proxyAuthorization + public static let proxyFeatures + public static let proxyInstruction + public static let `public` + public static let publicKeyPins + public static let publicKeyPinsReportOnly + public static let range + public static let redirectRef + public static let referer + public static let retryAfter + public static let safe + public static let scheduleReply + public static let scheduleTag + public static let secWebSocketAccept + public static let secWebSocketExtensions + public static let secWebSocketKey + public static let secWebSocketProtocol + public static let secWebSocketVersion + public static let securityScheme + public static let server + public static let setCookie + public static let setCookie2 + public static let setProfile + public static let slug + public static let soapAction + public static let statusURI + public static let strictTransportSecurity + public static let surrogateCapability + public static let surrogateControl + public static let tcn + public static let te + public static let timeout + public static let topic + public static let trailer + public static let transferEncoding + public static let ttl + public static let urgency + public static let uri + public static let upgrade + public static let userAgent + public static let variantVary + public static let vary + public static let via + public static let wwwAuthenticate + public static let wantDigest + public static let warning + public static let xFrameOptions + + // https://www.iana.org/assignments/message-headers/message-headers.xhtml + // Provisional Message Header Field Names + public static let accessControl + public static let accessControlAllowCredentials + public static let accessControlAllowHeaders + public static let accessControlAllowMethods + public static let accessControlAllowOrigin + public static let accessControlMaxAge + public static let accessControlRequestMethod + public static let accessControlRequestHeaders + public static let compliance + public static let contentTransferEncoding + public static let cost + public static let ediintFeatures + public static let messageID + public static let methodCheck + public static let methodCheckExpires + public static let nonCompliance + public static let optional + public static let refererRoot + public static let resolutionHint + public static let resolverLocation + public static let subOK + public static let subst + public static let title + public static let uaColor + public static let uaMedia + public static let uaPixels + public static let uaResolution + public static let uaWindowpixels + public static let version + public static let xDeviceAccept + public static let xDeviceAcceptCharset + public static let xDeviceAcceptEncoding + public static let xDeviceAcceptLanguage + public static let xDeviceUserAgent + } } /// Version number of the HTTP Protocol diff --git a/Sources/HTTP/HTTPCommon.swift b/Sources/HTTP/HTTPCommon.swift index 1ec333b..447c390 100644 --- a/Sources/HTTP/HTTPCommon.swift +++ b/Sources/HTTP/HTTPCommon.swift @@ -17,55 +17,6 @@ public protocol WebAppContaining: class { func serve(req: HTTPRequest, res: HTTPResponseWriter ) -> HTTPBodyProcessing } -/// Headers structure. -public struct HTTPHeaders { - var storage: [String: [String]] /* lower cased keys */ - var original: [(String, String)] /* original casing */ - let description: String - - public subscript(key: String) -> [String] { - get { - return storage[key.lowercased()] ?? [] - } - mutating set { - original = original.filter { $0.0 != key.lowercased() } - storage[key.lowercased()]=nil - for val in newValue { - self.append(newHeader: (key, val)) - } - } - } - - func makeIterator() -> IndexingIterator> { - return original.makeIterator() - } - - public mutating func append(newHeader: (String, String)) { - original.append(newHeader) - let key = newHeader.0.lowercased() - let val = newHeader.1 - - var existing = storage[key] ?? [] - existing.append(val) - storage[key] = existing - } - - /// Create Header structure from an array of string pairs - public init(_ headers: [(String, String)] = []) { - original = headers - description = "" - storage = [String: [String]]() - makeIterator().forEach { (element: (String, String)) in - let key = element.0.lowercased() - let val = element.1 - - var existing = storage[key] ?? [] - existing.append(val) - storage[key] = existing - } - } -} - public enum Result { case success(()) case failure(POSIXError) diff --git a/Sources/HTTP/HTTPHeaders.swift b/Sources/HTTP/HTTPHeaders.swift new file mode 100644 index 0000000..8cfc366 --- /dev/null +++ b/Sources/HTTP/HTTPHeaders.swift @@ -0,0 +1,336 @@ +/// Headers structure. +public struct HTTPHeaders { + var original: [(name: Name, value: String)]? + var storage: [Name: [String]] { + didSet { original = nil } + } + + public subscript(name: Name) -> String? { + get { + guard let value = storage[name] else { return nil } + switch name { + case Name.setCookie: // Exception, see note in [RFC7230, section 3.2.2] + return value.isEmpty ? nil : value[0] + default: + return value.joined(separator: ",") + } + } + set { + storage[name] = newValue.map { [$0] } + } + } + + public subscript(valuesFor name: Name) -> [String] { + get { return storage[name] ?? [] } + set { storage[name] = newValue.isEmpty ? nil : newValue } + } +} + +extension HTTPHeaders : ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral: (Name, String)...) { + storage = [:] + for (name, value) in dictionaryLiteral { +#if swift(>=4.0) + storage[name, default: []].append(value) +#else + if storage[name] == nil { + storage[name] = [value] + } else { + storage[name]!.append(value) + } +#endif + } + original = dictionaryLiteral + } +} + +extension HTTPHeaders { + // Used instead of HTTPHeaders to save CPU on dictionary construction + public struct Literal : ExpressibleByDictionaryLiteral { + let fields: [(name: Name, value: String)] + + public init(dictionaryLiteral: (Name, String)...) { + fields = dictionaryLiteral + } + } + + public mutating func append(_ literal: HTTPHeaders.Literal) { + for (name, value) in literal.fields { +#if swift(>=4.0) + storage[name, default: []].append(value) +#else + if storage[name] == nil { + storage[name] = [value] + } else { + storage[name]!.append(value) + } +#endif + } + } + + public mutating func replace(_ literal: HTTPHeaders.Literal) { + for (name, _) in literal.fields { + storage[name] = [] + } + for (name, value) in literal.fields { + storage[name]!.append(value) + } + } +} + +extension HTTPHeaders : Sequence { + public func makeIterator() -> AnyIterator<(name: Name, value: String)> { + if let original = original { + return AnyIterator(original.makeIterator()) + } else { + return AnyIterator(StorageIterator(storage.makeIterator())) + } + } + + struct StorageIterator : IteratorProtocol { + var headers: DictionaryIterator + var header: (name: Name, values: IndexingIterator<[String]>)? + + init(_ iterator: DictionaryIterator) { + headers = iterator + header = headers.next().map { (name: $0.key, values: $0.value.makeIterator()) } + } + + mutating func next() -> (name: Name, value: String)? { + while header != nil { + if let value = header!.values.next() { + return (name: header!.name, value: value) + } else { + header = headers.next().map { (name: $0.key, values: $0.value.makeIterator()) } + } + } + return nil + } + } +} + +extension HTTPHeaders { + public struct Name : Hashable, ExpressibleByStringLiteral, CustomStringConvertible { + let original: String + let lowercased: String + public let hashValue: Int + + public init(_ name: String) { + original = name + lowercased = name.lowercased() + hashValue = lowercased.hashValue + } + + public init(stringLiteral: String) { + self.init(stringLiteral) + } + + public init(unicodeScalarLiteral: String) { + self.init(unicodeScalarLiteral) + } + + public init(extendedGraphemeClusterLiteral: String) { + self.init(extendedGraphemeClusterLiteral) + } + + public var description: String { + return original + } + + public static func == (lhs: Name, rhs: Name) -> Bool { + return lhs.lowercased == rhs.lowercased + } + + // https://www.iana.org/assignments/message-headers/message-headers.xhtml + // Permanent Message Header Field Names + public static let aIM = Name("A-IM") + public static let accept = Name("Accept") + public static let acceptAdditions = Name("Accept-Additions") + public static let acceptCharset = Name("Accept-Charset") + public static let acceptDatetime = Name("Accept-Datetime") + public static let acceptEncoding = Name("Accept-Encoding") + public static let acceptFeatures = Name("Accept-Features") + public static let acceptLanguage = Name("Accept-Language") + public static let acceptPatch = Name("Accept-Patch") + public static let acceptPost = Name("Accept-Post") + public static let acceptRanges = Name("Accept-Ranges") + public static let age = Name("Age") + public static let allow = Name("Allow") + public static let alpn = Name("ALPN") + public static let altSvc = Name("Alt-Svc") + public static let altUsed = Name("Alt-Used") + public static let alternates = Name("Alternates") + public static let applyToRedirectRef = Name("Apply-To-Redirect-Ref") + public static let authenticationControl = Name("Authentication-Control") + public static let authenticationInfo = Name("Authentication-Info") + public static let authorization = Name("Authorization") + public static let cExt = Name("C-Ext") + public static let cMan = Name("C-Man") + public static let cOpt = Name("C-Opt") + public static let cPEP = Name("C-PEP") + public static let cPEPInfo = Name("C-PEP-Info") + public static let cacheControl = Name("Cache-Control") + public static let calDAVTimezones = Name("CalDAV-Timezones") + public static let close = Name("Close") + public static let connection = Name("Connection") + public static let contentBase = Name("Content-Base") + public static let contentDisposition = Name("Content-Disposition") + public static let contentEncoding = Name("Content-Encoding") + public static let contentID = Name("Content-ID") + public static let contentLanguage = Name("Content-Language") + public static let contentLength = Name("Content-Length") + public static let contentLocation = Name("Content-Location") + public static let contentMD5 = Name("Content-MD5") + public static let contentRange = Name("Content-Range") + public static let contentScriptType = Name("Content-Script-Type") + public static let contentStyleType = Name("Content-Style-Type") + public static let contentType = Name("Content-Type") + public static let contentVersion = Name("Content-Version") + public static let cookie = Name("Cookie") + public static let cookie2 = Name("Cookie2") + public static let dasl = Name("DASL") + public static let dav = Name("DAV") + public static let date = Name("Date") + public static let defaultStyle = Name("Default-Style") + public static let deltaBase = Name("Delta-Base") + public static let depth = Name("Depth") + public static let derivedFrom = Name("Derived-From") + public static let destination = Name("Destination") + public static let differentialID = Name("Differential-ID") + public static let digest = Name("Digest") + public static let eTag = Name("ETag") + public static let expect = Name("Expect") + public static let expires = Name("Expires") + public static let ext = Name("Ext") + public static let forwarded = Name("Forwarded") + public static let from = Name("From") + public static let getProfile = Name("GetProfile") + public static let hobareg = Name("Hobareg") + public static let host = Name("Host") + public static let http2Settings = Name("HTTP2-Settings") + public static let im = Name("IM") + public static let `if` = Name("If") + public static let ifMatch = Name("If-Match") + public static let ifModifiedSince = Name("If-Modified-Since") + public static let ifNoneMatch = Name("If-None-Match") + public static let ifRange = Name("If-Range") + public static let ifScheduleTagMatch = Name("If-Schedule-Tag-Match") + public static let ifUnmodifiedSince = Name("If-Unmodified-Since") + public static let keepAlive = Name("Keep-Alive") + public static let label = Name("Label") + public static let lastModified = Name("Last-Modified") + public static let link = Name("Link") + public static let location = Name("Location") + public static let lockToken = Name("Lock-Token") + public static let man = Name("Man") + public static let maxForwards = Name("Max-Forwards") + public static let mementoDatetime = Name("Memento-Datetime") + public static let meter = Name("Meter") + public static let mimeVersion = Name("MIME-Version") + public static let negotiate = Name("Negotiate") + public static let opt = Name("Opt") + public static let optionalWWWAuthenticate = Name("Optional-WWW-Authenticate") + public static let orderingType = Name("Ordering-Type") + public static let origin = Name("Origin") + public static let overwrite = Name("Overwrite") + public static let p3p = Name("P3P") + public static let pep = Name("PEP") + public static let picsLabel = Name("PICS-Label") + public static let pepInfo = Name("Pep-Info") + public static let position = Name("Position") + public static let pragma = Name("Pragma") + public static let prefer = Name("Prefer") + public static let preferenceApplied = Name("Preference-Applied") + public static let profileObject = Name("ProfileObject") + public static let `protocol` = Name("Protocol") + public static let protocolInfo = Name("Protocol-Info") + public static let protocolQuery = Name("Protocol-Query") + public static let protocolRequest = Name("Protocol-Request") + public static let proxyAuthenticate = Name("Proxy-Authenticate") + public static let proxyAuthenticationInfo = Name("Proxy-Authentication-Info") + public static let proxyAuthorization = Name("Proxy-Authorization") + public static let proxyFeatures = Name("Proxy-Features") + public static let proxyInstruction = Name("Proxy-Instruction") + public static let `public` = Name("Public") + public static let publicKeyPins = Name("Public-Key-Pins") + public static let publicKeyPinsReportOnly = Name("Public-Key-Pins-Report-Only") + public static let range = Name("Range") + public static let redirectRef = Name("Redirect-Ref") + public static let referer = Name("Referer") + public static let retryAfter = Name("Retry-After") + public static let safe = Name("Safe") + public static let scheduleReply = Name("Schedule-Reply") + public static let scheduleTag = Name("Schedule-Tag") + public static let secWebSocketAccept = Name("Sec-WebSocket-Accept") + public static let secWebSocketExtensions = Name("Sec-WebSocket-Extensions") + public static let secWebSocketKey = Name("Sec-WebSocket-Key") + public static let secWebSocketProtocol = Name("Sec-WebSocket-Protocol") + public static let secWebSocketVersion = Name("Sec-WebSocket-Version") + public static let securityScheme = Name("Security-Scheme") + public static let server = Name("Server") + public static let setCookie = Name("Set-Cookie") + public static let setCookie2 = Name("Set-Cookie2") + public static let setProfile = Name("SetProfile") + public static let slug = Name("SLUG") + public static let soapAction = Name("SoapAction") + public static let statusURI = Name("Status-URI") + public static let strictTransportSecurity = Name("Strict-Transport-Security") + public static let surrogateCapability = Name("Surrogate-Capability") + public static let surrogateControl = Name("Surrogate-Control") + public static let tcn = Name("TCN") + public static let te = Name("TE") + public static let timeout = Name("Timeout") + public static let topic = Name("Topic") + public static let trailer = Name("Trailer") + public static let transferEncoding = Name("Transfer-Encoding") + public static let ttl = Name("TTL") + public static let urgency = Name("Urgency") + public static let uri = Name("URI") + public static let upgrade = Name("Upgrade") + public static let userAgent = Name("User-Agent") + public static let variantVary = Name("Variant-Vary") + public static let vary = Name("Vary") + public static let via = Name("Via") + public static let wwwAuthenticate = Name("WWW-Authenticate") + public static let wantDigest = Name("Want-Digest") + public static let warning = Name("Warning") + public static let xFrameOptions = Name("X-Frame-Options") + + // https://www.iana.org/assignments/message-headers/message-headers.xhtml + // Provisional Message Header Field Names + public static let accessControl = Name("Access-Control") + public static let accessControlAllowCredentials = Name("Access-Control-Allow-Credentials") + public static let accessControlAllowHeaders = Name("Access-Control-Allow-Headers") + public static let accessControlAllowMethods = Name("Access-Control-Allow-Methods") + public static let accessControlAllowOrigin = Name("Access-Control-Allow-Origin") + public static let accessControlMaxAge = Name("Access-Control-Max-Age") + public static let accessControlRequestMethod = Name("Access-Control-Request-Method") + public static let accessControlRequestHeaders = Name("Access-Control-Request-Headers") + public static let compliance = Name("Compliance") + public static let contentTransferEncoding = Name("Content-Transfer-Encoding") + public static let cost = Name("Cost") + public static let ediintFeatures = Name("EDIINT-Features") + public static let messageID = Name("Message-ID") + public static let methodCheck = Name("Method-Check") + public static let methodCheckExpires = Name("Method-Check-Expires") + public static let nonCompliance = Name("Non-Compliance") + public static let optional = Name("Optional") + public static let refererRoot = Name("Referer-Root") + public static let resolutionHint = Name("Resolution-Hint") + public static let resolverLocation = Name("Resolver-Location") + public static let subOK = Name("SubOK") + public static let subst = Name("Subst") + public static let title = Name("Title") + public static let uaColor = Name("UA-Color") + public static let uaMedia = Name("UA-Media") + public static let uaPixels = Name("UA-Pixels") + public static let uaResolution = Name("UA-Resolution") + public static let uaWindowpixels = Name("UA-Windowpixels") + public static let version = Name("Version") + public static let xDeviceAccept = Name("X-Device-Accept") + public static let xDeviceAcceptCharset = Name("X-Device-Accept-Charset") + public static let xDeviceAcceptEncoding = Name("X-Device-Accept-Encoding") + public static let xDeviceAcceptLanguage = Name("X-Device-Accept-Language") + public static let xDeviceUserAgent = Name("X-Device-User-Agent") + } +} diff --git a/Sources/HTTP/HTTPStreamingParser.swift b/Sources/HTTP/HTTPStreamingParser.swift index 9096f2b..40c11f6 100644 --- a/Sources/HTTP/HTTPStreamingParser.swift +++ b/Sources/HTTP/HTTPStreamingParser.swift @@ -179,7 +179,7 @@ public class StreamingParser: HTTPResponseWriter { } case .headerValueReceived: if let parserBuffer = self.parserBuffer, let lastHeaderName = self.lastHeaderName, let headerValue = String(data:parserBuffer, encoding: .utf8) { - self.parsedHeaders.append(newHeader: (lastHeaderName, headerValue)) + self.parsedHeaders.append([HTTPHeaders.Name(lastHeaderName): headerValue]) self.lastHeaderName = nil self.parserBuffer=nil } else { diff --git a/Tests/BlueSocketHTTPTests/Helpers/EchoWebApp.swift b/Tests/BlueSocketHTTPTests/Helpers/EchoWebApp.swift index cb253e1..f997833 100644 --- a/Tests/BlueSocketHTTPTests/Helpers/EchoWebApp.swift +++ b/Tests/BlueSocketHTTPTests/Helpers/EchoWebApp.swift @@ -17,7 +17,7 @@ class EchoWebApp: WebAppContaining { res.writeResponse(HTTPResponse(httpVersion: req.httpVersion, status: .ok, transferEncoding: .chunked, - headers: HTTPHeaders([("X-foo", "bar")]))) + headers: ["X-foo": "bar"])) return .processBody { (chunk, stop) in switch chunk { case .chunk(let data, let finishedProcessing): diff --git a/Tests/BlueSocketHTTPTests/Helpers/HelloWorldKeepAliveWebApp.swift b/Tests/BlueSocketHTTPTests/Helpers/HelloWorldKeepAliveWebApp.swift index f8cacb6..ce84333 100644 --- a/Tests/BlueSocketHTTPTests/Helpers/HelloWorldKeepAliveWebApp.swift +++ b/Tests/BlueSocketHTTPTests/Helpers/HelloWorldKeepAliveWebApp.swift @@ -16,7 +16,7 @@ class HelloWorldKeepAliveWebApp: WebAppContaining { res.writeResponse(HTTPResponse(httpVersion: req.httpVersion, status: .ok, transferEncoding: .chunked, - headers: HTTPHeaders([("Connection","Keep-Alive"),("Keep-Alive","timeout=5, max=10")]))) + headers: ["Connection": "Keep-Alive", "Keep-Alive": "timeout=5, max=10"])) return .processBody { (chunk, stop) in switch chunk { case .chunk(_, let finishedProcessing): diff --git a/Tests/BlueSocketHTTPTests/Helpers/HelloWorldWebApp.swift b/Tests/BlueSocketHTTPTests/Helpers/HelloWorldWebApp.swift index 9353389..5294b95 100644 --- a/Tests/BlueSocketHTTPTests/Helpers/HelloWorldWebApp.swift +++ b/Tests/BlueSocketHTTPTests/Helpers/HelloWorldWebApp.swift @@ -16,7 +16,7 @@ class HelloWorldWebApp: WebAppContaining { res.writeResponse(HTTPResponse(httpVersion: req.httpVersion, status: .ok, transferEncoding: .chunked, - headers: HTTPHeaders([("X-foo", "bar")]))) + headers: ["X-foo": "bar"])) return .processBody { (chunk, stop) in switch chunk { case .chunk(_, let finishedProcessing): diff --git a/Tests/BlueSocketHTTPTests/ServerTests.swift b/Tests/BlueSocketHTTPTests/ServerTests.swift index 0a026ef..569aac0 100644 --- a/Tests/BlueSocketHTTPTests/ServerTests.swift +++ b/Tests/BlueSocketHTTPTests/ServerTests.swift @@ -13,7 +13,7 @@ import XCTest class ServerTests: XCTestCase { func testResponseOK() { - let request = HTTPRequest(method: .GET, target:"/echo", httpVersion: HTTPVersion(major: 1, minor: 1), headers: HTTPHeaders([("X-foo", "bar")])) + let request = HTTPRequest(method: .GET, target:"/echo", httpVersion: HTTPVersion(major: 1, minor: 1), headers: ["X-foo": "bar"]) let resolver = TestResponseResolver(request: request, requestBody: Data()) resolver.resolveHandler(EchoWebApp().serve) XCTAssertNotNil(resolver.response) @@ -23,7 +23,7 @@ class ServerTests: XCTestCase { func testEcho() { let testString="This is a test" - let request = HTTPRequest(method: .POST, target:"/echo", httpVersion: HTTPVersion(major: 1, minor: 1), headers: HTTPHeaders([("X-foo", "bar")])) + let request = HTTPRequest(method: .POST, target:"/echo", httpVersion: HTTPVersion(major: 1, minor: 1), headers: ["X-foo": "bar"]) let resolver = TestResponseResolver(request: request, requestBody: testString.data(using: .utf8)!) resolver.resolveHandler(EchoWebApp().serve) XCTAssertNotNil(resolver.response) @@ -33,7 +33,7 @@ class ServerTests: XCTestCase { } func testHello() { - let request = HTTPRequest(method: .GET, target:"/helloworld", httpVersion: HTTPVersion(major: 1, minor: 1), headers: HTTPHeaders([("X-foo", "bar")])) + let request = HTTPRequest(method: .GET, target:"/helloworld", httpVersion: HTTPVersion(major: 1, minor: 1), headers: ["X-foo": "bar"]) let resolver = TestResponseResolver(request: request, requestBody: Data()) resolver.resolveHandler(HelloWorldWebApp().serve) XCTAssertNotNil(resolver.response) @@ -43,13 +43,13 @@ class ServerTests: XCTestCase { } func testSimpleHello() { - let request = HTTPRequest(method: .GET, target:"/helloworld", httpVersion: HTTPVersion(major: 1, minor: 1), headers: HTTPHeaders([("X-foo", "bar")])) + let request = HTTPRequest(method: .GET, target:"/helloworld", httpVersion: HTTPVersion(major: 1, minor: 1), headers: ["X-foo": "bar"]) let resolver = TestResponseResolver(request: request, requestBody: Data()) let simpleHelloWebApp = SimpleResponseCreator { (request, body) -> (reponse: HTTPResponse, responseBody: Data) in return (HTTPResponse(httpVersion: request.httpVersion, status: .ok, transferEncoding: .chunked, - headers: HTTPHeaders([("X-foo", "bar")])), + headers: ["X-foo": "bar"]), "Hello, World!".data(using: .utf8)!) } @@ -96,7 +96,7 @@ class ServerTests: XCTestCase { return (HTTPResponse(httpVersion: request.httpVersion, status: .ok, transferEncoding: .chunked, - headers: HTTPHeaders([("X-foo", "bar")])), + headers: ["X-foo": "bar"]), "Hello, World!".data(using: .utf8)!) } diff --git a/Tests/HTTPTests/HeadersTests.swift b/Tests/HTTPTests/HeadersTests.swift index 785463a..072f819 100644 --- a/Tests/HTTPTests/HeadersTests.swift +++ b/Tests/HTTPTests/HeadersTests.swift @@ -12,21 +12,51 @@ import XCTest class HeadersTests: XCTestCase { func testHeaders() { - var headers = HTTPHeaders() + var headers: HTTPHeaders = [ + .accept: "text/html", + "Accept": "application/xhtml+xml", + "Accept": "application/xml;q=0.9", + "accept": "image/webp", + .accept: "*/*;q=0.8", + "Accept-Language": "ru-RU,ru;q=0.8", + .acceptLanguage: "en-US;q=0.6", + "accept-language": "en;q=0.4", + "Content-Length": "200", + "Set-Cookie": "test1=0; expires=Tue, 21 Jun 2016 16:26:50 GMT; path=/; domain=.my.mail.ru", + "Set-Cookie": "test2=0; expires=Tue, 21 Jun 2016 16:26:50 GMT; path=/; domain=.my.mail.ru", + ] + + XCTAssertEqual(headers["Content-Length"], "200") + XCTAssertEqual(headers["content-length"], "200") + XCTAssertEqual(headers[.contentLength], "200") + XCTAssertEqual(headers[.accept], "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8") + XCTAssertEqual(headers[.acceptLanguage], "ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4") + XCTAssertEqual(headers[.setCookie], "test1=0; expires=Tue, 21 Jun 2016 16:26:50 GMT; path=/; domain=.my.mail.ru") + + XCTAssertEqual(headers[valuesFor: "Content-Length"], ["200"]) + XCTAssertEqual(headers[valuesFor: "content-length"], ["200"]) + XCTAssertEqual(headers[valuesFor: .contentLength], ["200"]) + XCTAssertEqual(headers[valuesFor: .accept], ["text/html", "application/xhtml+xml", "application/xml;q=0.9", "image/webp", "*/*;q=0.8"]) + XCTAssertEqual(headers[valuesFor: .setCookie], [ + "test1=0; expires=Tue, 21 Jun 2016 16:26:50 GMT; path=/; domain=.my.mail.ru", + "test2=0; expires=Tue, 21 Jun 2016 16:26:50 GMT; path=/; domain=.my.mail.ru", + ]) + + headers = HTTPHeaders() let initialCount = headers.makeIterator().reduce(0) { (last, element) -> Int in return last + 1 } XCTAssertEqual(0, initialCount) - headers.append(newHeader: ("Test-Header","Test Value")) + headers.append(["Test-Header": "Test Value"]) let nextCount = headers.makeIterator().reduce(0) { (last, element) -> Int in return last + 1 } XCTAssertEqual(1, nextCount) - let testHeaderValueArray = headers["test-header"] + let testHeaderValueArray = headers[valuesFor: "test-header"] XCTAssertNotNil(testHeaderValueArray) XCTAssertEqual(1,testHeaderValueArray.count) XCTAssertEqual("Test Value",testHeaderValueArray.first ?? "Not Found") - headers.append(newHeader: ("Test-header","Test Value 2")) - let testHeaderValueArray2 = headers["test-header"] + headers.append(["Test-header": "Test Value 2"]) + let testHeaderValueArray2 = headers[valuesFor: "test-header"] XCTAssertNotNil(testHeaderValueArray2) XCTAssertEqual(2,testHeaderValueArray2.count) XCTAssertEqual("Test Value",testHeaderValueArray2.first ?? "Not Found") @@ -34,14 +64,14 @@ class HeadersTests: XCTestCase { XCTAssertEqual("Test Value 2",testHeaderValueArray2Remainder.first ?? "Not Found") //This should overwrites, since the subscript is documented to use lowercase keys - headers["TEST-HEADER"]=["Test Value 3"] - let testHeaderValueArray3 = headers["test-header"] + headers[valuesFor: "TEST-HEADER"]=["Test Value 3"] + let testHeaderValueArray3 = headers[valuesFor: "test-header"] XCTAssertNotNil(testHeaderValueArray3) XCTAssertEqual(1,testHeaderValueArray3.count) //Overwrite - headers["TEST-HEADER"]=["Test Value 4a","Test Value 4b"] - let testHeaderValueArray4 = headers["test-header"] + headers[valuesFor: "TEST-HEADER"]=["Test Value 4a","Test Value 4b"] + let testHeaderValueArray4 = headers[valuesFor: "test-header"] XCTAssertNotNil(testHeaderValueArray4) XCTAssertEqual(2,testHeaderValueArray4.count) XCTAssertEqual("Test Value 4a",testHeaderValueArray4.first ?? "Not Found")