Skip to content

Commit 6cf1ea4

Browse files
committed
Added better header API's in accordance with #8 and corresponding tests.
1 parent 12e3c28 commit 6cf1ea4

File tree

7 files changed

+317
-20
lines changed

7 files changed

+317
-20
lines changed

Sources/HTTP/Message.swift

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//
2+
// Message.swift
3+
// Edge
4+
//
5+
// Created by Tyler Fleming Cloutier on 10/30/16.
6+
//
7+
//
8+
9+
import Foundation
10+
11+
public protocol HTTPMessage {
12+
var version: Version { get set }
13+
var rawHeaders: [String] { get set }
14+
var headers: [String:String] { get }
15+
var cookies: [String] { get }
16+
var body: [UInt8] { get set }
17+
}
18+
19+
public extension HTTPMessage {
20+
21+
/// Groups the `rawHeaders` into key-value pairs. If there is an odd number
22+
/// of `rawHeaders`, the last value will be discarded.
23+
var rawHeaderPairs: [(String, String)] {
24+
return stride(from: 0, to: self.rawHeaders.count, by: 2).flatMap {
25+
let chunk = rawHeaders[$0..<min($0 + 2, rawHeaders.count)]
26+
if let first = chunk.first, let last = chunk.last {
27+
return (first, last)
28+
}
29+
return nil
30+
}
31+
}
32+
33+
/// The same as `rawHeaderPairs` with the key lowercased.
34+
var lowercasedRawHeaderPairs: [(String, String)] {
35+
return rawHeaderPairs.map { ($0.0.lowercased(), $0.1) }
36+
}
37+
38+
/// Duplicates are handled in a way very similar to the way they are handled
39+
/// by Node.js. Which is to say that duplicates in the raw headers are handled as follows.
40+
///
41+
/// * Duplicates of age, authorization, content-length, content-type, etag, expires, from,
42+
/// host, if-modified-since, if-unmodified-since, last-modified, location, max-forwards,
43+
/// proxy-authorization, referer, retry-after, or user-agent are discarded.
44+
/// * set-cookie is *excluded* from the formatted headers are handled by the request and
45+
/// response. The cookies field on the Request and Response objects can be users to get
46+
/// and set the cookies.
47+
/// * For all other headers, the values are joined together with ', '.
48+
///
49+
/// The rawHeaders are processed from the 0th index forward.
50+
var headers: [String:String] {
51+
get {
52+
var headers: [String:String] = [:]
53+
let discardable = Set([
54+
"age",
55+
"authorization",
56+
"content-length",
57+
"content-type",
58+
"etag",
59+
"expires",
60+
"from",
61+
"host",
62+
"if-modified-since",
63+
"if-unmodified-since",
64+
"last-modified",
65+
"location",
66+
"max-forwards",
67+
"proxy-authorization",
68+
"referer",
69+
"retry-after",
70+
"user-agent"
71+
])
72+
let cookies = Set([
73+
"set-cookie",
74+
"cookie"
75+
])
76+
for (key, value) in lowercasedRawHeaderPairs {
77+
guard !cookies.contains(key) else {
78+
continue
79+
}
80+
if let currentValue = headers[key] {
81+
if discardable.contains(key) {
82+
headers[key] = value
83+
} else {
84+
headers[key] = [currentValue, value].joined(separator: ", ")
85+
}
86+
} else {
87+
headers[key] = value
88+
}
89+
}
90+
return headers
91+
}
92+
}
93+
94+
}

Sources/HTTP/Request.swift

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import Foundation
1010

11-
public struct Request: Serializable {
11+
public struct Request: Serializable, HTTPMessage {
1212
public var method: Method
1313
public var uri: URL
1414
public var version: Version
@@ -19,21 +19,22 @@ public struct Request: Serializable {
1919
public var serialized: [UInt8] {
2020
var headerString = ""
2121
headerString += "\(method) \(uri) HTTP/\(version.major).\(version.minor)"
22-
headerString += "\n"
22+
headerString += "\r\n"
2323

24-
let headerPairs: [(String, String)] = stride(from: 0, to: rawHeaders.count, by: 2).map {
25-
let chunk = rawHeaders[$0..<min($0 + 2, rawHeaders.count)]
26-
return (chunk.first!, chunk.last!)
27-
}
28-
29-
for (name, value) in headerPairs {
24+
for (name, value) in rawHeaderPairs {
3025
headerString += "\(name): \(value)"
31-
headerString += "\n"
26+
headerString += "\r\n"
3227
}
3328

34-
headerString += "\n"
29+
headerString += "\r\n"
3530
return headerString.utf8 + body
3631
}
32+
33+
public var cookies: [String] {
34+
return lowercasedRawHeaderPairs.filter { (key, value) in
35+
key == "cookie"
36+
}.map { $0.1 }
37+
}
3738

3839
public init(
3940
method: Method,

Sources/HTTP/Response.swift

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
//
77
//
88

9-
public struct Response: Serializable {
9+
public struct Response: Serializable, HTTPMessage {
1010

1111
public var version: Version
1212
public var status: Status
@@ -18,22 +18,23 @@ public struct Response: Serializable {
1818
var headerString = ""
1919
headerString += "HTTP/\(version.major).\(version.minor)"
2020
headerString += " \(status.statusCode) \(status.reasonPhrase)"
21-
headerString += "\n"
21+
headerString += "\r\n"
2222

23-
let headerPairs: [(String, String)] = stride(from: 0, to: rawHeaders.count, by: 2).map {
24-
let chunk = rawHeaders[$0..<min($0 + 2, rawHeaders.count)]
25-
return (chunk.first!, chunk.last!)
26-
}
27-
28-
for (name, value) in headerPairs {
23+
for (name, value) in rawHeaderPairs {
2924
headerString += "\(name): \(value)"
30-
headerString += "\n"
25+
headerString += "\r\n"
3126
}
3227

33-
headerString += "\n"
28+
headerString += "\r\n"
3429
return headerString.utf8 + body
3530
}
3631

32+
public var cookies: [String] {
33+
return lowercasedRawHeaderPairs.filter { (key, value) in
34+
key == "set-cookie"
35+
}.map { $0.1 }
36+
}
37+
3738
public init(
3839
version: Version = Version(major: 1, minor: 1),
3940
status: Status,
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//
2+
// HTTPMessageTests.swift
3+
// Edge
4+
//
5+
// Created by Tyler Fleming Cloutier on 10/30/16.
6+
//
7+
//
8+
9+
import Foundation
10+
import XCTest
11+
@testable import HTTP
12+
13+
class HTTPMessageTests: XCTestCase {
14+
15+
16+
struct TestMessageType: HTTPMessage {
17+
var version = Version(major: 1, minor: 1)
18+
var rawHeaders: [String] = []
19+
var cookies: [String] {
20+
return lowercasedRawHeaderPairs.filter { (key, value) in
21+
key == "set-cookie"
22+
}.map { $0.1 }
23+
}
24+
var body: [UInt8] = []
25+
}
26+
27+
func testHeaders() {
28+
var testMessage = TestMessageType()
29+
testMessage.rawHeaders = [
30+
"Date", "Sun, 30 Oct 2016 09:06:40 GMT",
31+
"Expires", "-1",
32+
"Cache-Control", "private, max-age=0",
33+
"Content-Type", "application/json",
34+
"Content-Type", "text/html; charset=ISO-8859-1",
35+
"P3P","CP=\"See https://www.google.com/support/accounts/answer/151657?hl=en for more info.\"",
36+
"Server", "gws",
37+
"Server", "gws", // Duplicate servers for test purposes.
38+
"X-XSS-Protection", "1; mode=block",
39+
"X-Frame-Options", "SAMEORIGIN",
40+
"Set-Cookie", "NID=89=c6V5PAWCEOXgvA6TQrNSR8Pnih2iX3Aa3rIQS005IG6WS8RHH" +
41+
"_3YTmymtEk5yMxLkz19C_qr2zBNspy7zwubAVo38-kIdjbArSJcXCBbjCcn_hJ" +
42+
"TEi9grq_ZgHxZTZ5V2YLnH3uxx6U4EA; expires=Mon, 01-May-2017 09:06:40 GMT;" +
43+
" path=/; domain=.google.com; HttpOnly",
44+
"Accept-Ranges", "none",
45+
"Vary", "Accept-Encoding",
46+
"Transfer-Encoding", "chunked"
47+
]
48+
let expectedHeaders = [
49+
"date": "Sun, 30 Oct 2016 09:06:40 GMT",
50+
"expires": "-1",
51+
"cache-control": "private, max-age=0",
52+
"content-type": "text/html; charset=ISO-8859-1",
53+
"p3p": "CP=\"See https://www.google.com/support/accounts/answer/151657?hl=en for more info.\"",
54+
"server": "gws, gws",
55+
"x-xss-protection": "1; mode=block",
56+
"x-frame-options": "SAMEORIGIN",
57+
"accept-ranges": "none",
58+
"vary": "Accept-Encoding",
59+
"transfer-encoding": "chunked"
60+
]
61+
XCTAssert(testMessage.headers == expectedHeaders, "Actual headers, \(testMessage.headers), did not match expected.")
62+
let expectedCookies = [
63+
"NID=89=c6V5PAWCEOXgvA6TQrNSR8Pnih2iX3Aa3rIQS005IG6WS8RHH" +
64+
"_3YTmymtEk5yMxLkz19C_qr2zBNspy7zwubAVo38-kIdjbArSJcXCBbjCcn_hJ" +
65+
"TEi9grq_ZgHxZTZ5V2YLnH3uxx6U4EA; expires=Mon, 01-May-2017 09:06:40 GMT;" +
66+
" path=/; domain=.google.com; HttpOnly"
67+
]
68+
XCTAssert(testMessage.cookies == expectedCookies, "Actual cookies, \(testMessage.cookies), did not match expected.")
69+
70+
}
71+
72+
}
73+
74+
extension HTTPMessageTests {
75+
static var allTests = [
76+
("testHeaders", testHeaders),
77+
]
78+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//
2+
// RequestSerializationTests.swift
3+
// Edge
4+
//
5+
// Created by Tyler Fleming Cloutier on 10/30/16.
6+
//
7+
//
8+
9+
import Foundation
10+
import XCTest
11+
@testable import HTTP
12+
13+
class RequestSerializationTests: XCTestCase {
14+
15+
func testBasicSerialization() {
16+
let expected = "GET / HTTP/1.1\r\n\r\n"
17+
let request = Request(
18+
method: .get,
19+
uri: URL(string: "/")!,
20+
version: Version(major: 1, minor: 1),
21+
rawHeaders: [],
22+
body: []
23+
)
24+
let actual = String(bytes: request.serialized, encoding: .utf8)
25+
XCTAssert(expected == actual, "Actual request, \(actual), did not match expected.")
26+
}
27+
28+
func testHeaderSerialization() {
29+
let expected = "GET / HTTP/1.1\r\nAccept: */*\r\nHost: www.google.com\r\nConnection: Keep-Alive\r\n\r\n"
30+
let request = Request(
31+
method: .get,
32+
uri: URL(string: "/")!,
33+
version: Version(major: 1, minor: 1),
34+
rawHeaders: ["Accept", "*/*", "Host", "www.google.com", "Connection", "Keep-Alive"],
35+
body: []
36+
)
37+
let actual = String(bytes: request.serialized, encoding: .utf8)
38+
XCTAssert(expected == actual, "Actual request, \(actual), did not match expected.")
39+
}
40+
41+
func testDefaultParameters() {
42+
let expected = "GET / HTTP/1.1\r\n\r\n"
43+
let request = Request(
44+
method: .get,
45+
uri: URL(string: "/")!
46+
)
47+
let actual = String(bytes: request.serialized, encoding: .utf8)
48+
XCTAssert(expected == actual, "Actual request, \(actual), did not match expected.")
49+
}
50+
51+
}
52+
53+
extension RequestSerializationTests {
54+
static var allTests = [
55+
("testBasicSerialization", testBasicSerialization),
56+
("testHeaderSerialization", testHeaderSerialization),
57+
("testDefaultParameters", testDefaultParameters),
58+
]
59+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// ResponseSerializationTests.swift
3+
// Edge
4+
//
5+
// Created by Tyler Fleming Cloutier on 10/30/16.
6+
//
7+
//
8+
9+
import Foundation
10+
import XCTest
11+
@testable import HTTP
12+
13+
class ResponseSerializationTests: XCTestCase {
14+
15+
func testBasicSerialization() {
16+
let expected = "HTTP/1.1 200 OK\r\n\r\n"
17+
let response = Response(
18+
version: Version(major: 1, minor: 1),
19+
status: .ok,
20+
rawHeaders: [],
21+
body: []
22+
)
23+
let actual = String(bytes: response.serialized, encoding: .utf8)
24+
XCTAssert(expected == actual, "Actual response, \(actual), did not match expected.")
25+
}
26+
27+
func testHeaderSerialization() {
28+
let expected =
29+
"HTTP/1.1 200 OK\r\n" +
30+
"Date: Sun, 30 Oct 2016 09:06:40 GMT\r\n" +
31+
"Content-Type: text/html; charset=ISO-8859-1\r\n" +
32+
"\r\n"
33+
let response = Response(
34+
version: Version(major: 1, minor: 1),
35+
status: .ok,
36+
rawHeaders: [
37+
"Date", "Sun, 30 Oct 2016 09:06:40 GMT",
38+
"Content-Type", "text/html; charset=ISO-8859-1"
39+
],
40+
body: []
41+
)
42+
let actual = String(bytes: response.serialized, encoding: .utf8)
43+
XCTAssert(expected == actual, "Actual request, \(actual), did not match expected.")
44+
}
45+
46+
func testDefaultParameters() {
47+
let expected = "HTTP/1.1 200 OK\r\n\r\n"
48+
let response = Response(status: .ok)
49+
let actual = String(bytes: response.serialized, encoding: .utf8)
50+
XCTAssert(expected == actual, "Actual response, \(actual), did not match expected.")
51+
}
52+
53+
}
54+
55+
extension ResponseSerializationTests {
56+
static var allTests = [
57+
("testBasicSerialization", testBasicSerialization),
58+
("testHeaderSerialization", testHeaderSerialization),
59+
("testDefaultParameters", testDefaultParameters),
60+
]
61+
}

Tests/HTTPTests/XCTestManifests.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import XCTest
44
public func allTests() -> [XCTestCaseEntry] {
55
return [
66
testCase(RequestParserTests.allTests),
7+
testCase(RequestSerializationTests.allTests),
8+
testCase(ResponseSerializationTests.allTests),
9+
testCase(HTTPMessageTests.allTests),
710
]
811
}
912
#endif

0 commit comments

Comments
 (0)