├── .gitignore ├── .swift-version ├── .travis.yml ├── LICENSE ├── Makefile ├── Package.swift ├── README.md ├── Sources ├── Convenience.swift ├── Data.swift ├── HTTPParser.swift ├── Requests.swift ├── Socket.swift ├── String.swift └── URL.swift └── Tests ├── HTTPParserSpec.swift ├── RequestSpec.swift ├── SendResponseSpec.swift ├── URLSpec.swift └── main.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | Packages/ 3 | /run-tests 4 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | DEVELOPMENT-SNAPSHOT-2016-02-08-a 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | - osx 4 | language: generic 5 | sudo: required 6 | dist: trusty 7 | osx_image: xcode7.2 8 | install: 9 | - eval "$(curl -sL https://gist.githubusercontent.com/kylef/5c0475ff02b7c7671d2a/raw/02090c7ede5a637b76e6df1710e83cd0bbe7dcdf/swiftenv-install.sh)" 10 | script: 11 | - make test 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Kyle Fuller 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | requests: 2 | @echo "Building Requests" 3 | @swift build 4 | 5 | test: requests 6 | @.build/debug/spectre-build 7 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | 4 | let package = Package( 5 | name: "Requests", 6 | dependencies: [ 7 | .Package(url: "https://github.com/nestproject/Inquiline.git", majorVersion: 0, minor: 3), 8 | ], 9 | testDependencies: [ 10 | .Package(url: "https://github.com/kylef/spectre-build.git", majorVersion: 0), 11 | .Package(url: "https://github.com/kylef/Spectre.git", majorVersion: 0), 12 | ] 13 | ) 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Requests 2 | 3 | Simple synchronous HTTP client in Swift. 4 | 5 | ## Usage 6 | 7 | ```swift 8 | let response = try get("http://httpbin.org/get") 9 | 10 | print(response.headers) 11 | print(response.body) 12 | ``` 13 | 14 | ```swift 15 | let response = try post("http://httpbin.org/post", content: "Hello World") 16 | 17 | print(response.headers) 18 | print(response.body) 19 | ``` 20 | 21 | ### Missing Features 22 | 23 | I wouldn't recommend using this for anything serious, it misses many 24 | features. When considering the correct approach, this library 25 | probably takes a shortcut. 26 | 27 | It misses at least the following: 28 | 29 | - HTTPS (SSL/TLS) 30 | - IPv6 31 | - Handling of redirects 32 | - Dozens of other things 33 | 34 | Pull requests are however very welcome if you are interested in adding any of 35 | these missing features. 36 | -------------------------------------------------------------------------------- /Sources/Convenience.swift: -------------------------------------------------------------------------------- 1 | import Nest 2 | import Inquiline 3 | 4 | 5 | public func head(url: String, headers: [Header]? = nil) throws -> Response { 6 | return try request(method: "HEAD", url: url, headers: headers) 7 | } 8 | 9 | 10 | public func get(url: String, headers: [Header]? = nil) throws -> Response { 11 | return try request(method: "GET", url: url, headers: headers) 12 | } 13 | 14 | 15 | public func delete(url: String, headers: [Header]? = nil) throws -> Response { 16 | return try request(method: "DELETE", url: url, headers: headers) 17 | } 18 | 19 | 20 | public func post(url: String, headers: [Header]? = nil, content: String) throws -> Response { 21 | return try request(method: "POST", url: url, headers: headers, body: content) 22 | } 23 | 24 | 25 | public func put(url: String, headers: [Header]? = nil, content: String) throws -> Response { 26 | return try request(method: "PUT", url: url, headers: headers, body: content) 27 | } 28 | 29 | 30 | public func patch(url: String, headers: [Header]? = nil, content: String) throws -> Response { 31 | return try request(method: "PATCH", url: url, headers: headers, body: content) 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Data.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Glibc 3 | #else 4 | import Darwin.C 5 | #endif 6 | 7 | class Data { 8 | let bytes: UnsafeMutablePointer 9 | let capacity: Int 10 | 11 | init(capacity: Int) { 12 | bytes = UnsafeMutablePointer(malloc(capacity + 1)) 13 | self.capacity = capacity 14 | } 15 | 16 | deinit { 17 | free(bytes) 18 | } 19 | 20 | var characters: [CChar] { 21 | var data = [CChar](count: capacity, repeatedValue: 0) 22 | memcpy(&data, bytes, data.count) 23 | return data 24 | } 25 | 26 | var string: String? { 27 | return String.fromCString(bytes) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/HTTPParser.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Glibc 3 | #else 4 | import Darwin.C 5 | #endif 6 | import Nest 7 | import Inquiline 8 | 9 | 10 | enum HTTPParserError : ErrorType { 11 | case Unknown 12 | } 13 | 14 | class HTTPParser { 15 | let socket: Socket 16 | 17 | init(socket: Socket) { 18 | self.socket = socket 19 | } 20 | 21 | // Read the socket until we find \r\n\r\n 22 | // returning string before and chars after 23 | func readUntil() throws -> (String, [CChar])? { 24 | var buffer: [CChar] = [] 25 | 26 | while true { 27 | if let bytes = try? socket.read(512) { 28 | if bytes.isEmpty { 29 | return nil 30 | } 31 | 32 | buffer += bytes 33 | 34 | let crln: [CChar] = [13, 10, 13, 10] 35 | if let (top, bottom) = buffer.find(crln) { 36 | if let headers = String.fromCString(top + [0]) { 37 | return (headers, bottom) 38 | } 39 | 40 | return nil 41 | } 42 | } 43 | } 44 | } 45 | 46 | func parse() throws -> Response { 47 | guard let (top, bodyPart) = try readUntil() else { 48 | throw HTTPParserError.Unknown 49 | } 50 | 51 | var components = top.split("\r\n") 52 | let requestLine = components.removeFirst() 53 | components.removeLast() 54 | let responseComponents = requestLine.split(" ") 55 | if responseComponents.count < 3 { 56 | throw HTTPParserError.Unknown 57 | } 58 | 59 | let version = responseComponents[0] 60 | guard let statusCode = Int(responseComponents[1]) else { 61 | throw HTTPParserError.Unknown 62 | } 63 | //let statusReason = responseComponents[2] 64 | 65 | guard let status = Status(rawValue: statusCode) else { 66 | throw HTTPParserError.Unknown 67 | } 68 | 69 | if !version.hasPrefix("HTTP/1") { 70 | throw HTTPParserError.Unknown 71 | } 72 | 73 | let headers = parseHeaders(components) 74 | let contentLength = Int(headers.filter { $0.0 == "Content-Length" }.first?.1 ?? "0") ?? 0 75 | 76 | var body: String? = nil 77 | 78 | if contentLength > 0 { 79 | var buffer = bodyPart 80 | var readLength = bodyPart.count 81 | 82 | while contentLength > readLength { 83 | let bytes = try socket.read(2048) 84 | if bytes.isEmpty { 85 | throw HTTPParserError.Unknown // Server closed before sending complete body 86 | } 87 | buffer += bytes 88 | readLength += bytes.count 89 | } 90 | 91 | body = String.fromCString(buffer + [0]) 92 | } 93 | 94 | return Response(status, headers: headers, content: body) 95 | } 96 | 97 | func parseHeaders(headers: [String]) -> [Header] { 98 | return headers.map { $0.split(":", maxSplit: 1) }.flatMap { 99 | if $0.count == 2 { 100 | if $0[1].characters.first == " " { 101 | let value = String($0[1].characters[$0[1].startIndex.successor()..<$0[1].endIndex]) 102 | return ($0[0], value) 103 | } 104 | return ($0[0], $0[1]) 105 | } 106 | 107 | return nil 108 | } 109 | } 110 | } 111 | 112 | 113 | extension CollectionType where Generator.Element == CChar { 114 | func find(characters: [CChar]) -> ([CChar], [CChar])? { 115 | var lhs: [CChar] = [] 116 | var rhs = Array(self) 117 | 118 | while !rhs.isEmpty { 119 | let character = rhs.removeAtIndex(0) 120 | lhs.append(character) 121 | if lhs.hasSuffix(characters) { 122 | return (lhs, rhs) 123 | } 124 | } 125 | 126 | return nil 127 | } 128 | 129 | func hasSuffix(characters: [CChar]) -> Bool { 130 | let chars = Array(self) 131 | if chars.count >= characters.count { 132 | let index = chars.count - characters.count 133 | return Array(chars[index.. Bool { 142 | let characters = utf16 143 | let prefixCharacters = prefix.utf16 144 | let start = characters.startIndex 145 | let prefixStart = prefixCharacters.startIndex 146 | 147 | if characters.count < prefixCharacters.count { 148 | return false 149 | } 150 | 151 | for var idx = 0; idx < prefixCharacters.count; idx++ { 152 | if characters[start.advancedBy(idx)] != prefixCharacters[prefixStart.advancedBy(idx)] { 153 | return false 154 | } 155 | } 156 | 157 | return true 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Sources/Requests.swift: -------------------------------------------------------------------------------- 1 | import Nest 2 | import Inquiline 3 | 4 | 5 | public typealias Header = (String, String) 6 | 7 | 8 | public enum RequestError : ErrorType { 9 | case InvalidURL 10 | case UnsupportedScheme(String) 11 | } 12 | 13 | 14 | func createRequest(method: String, path: String, hostname: String, headers: [Header], body: String? = nil) -> RequestType { 15 | var requestsHeaders: [Header] = [("Host", hostname), ("Connection", "close")] 16 | 17 | if let body = body { 18 | requestsHeaders.append(("Content-Length", "\(body.utf8.count)")) 19 | } 20 | 21 | return Request(method: method, path: path, headers: requestsHeaders + headers, content: body) 22 | } 23 | 24 | 25 | func sendRequest(socket: Socket, request: RequestType) { 26 | socket.write("\(request.method) \(request.path) HTTP/1.1\r\n") 27 | for (key, value) in request.headers { 28 | socket.write("\(key): \(value)\r\n") 29 | } 30 | socket.write("\r\n") 31 | 32 | if var body = request.body { 33 | while let chunk = body.next() { 34 | socket.write(chunk) 35 | } 36 | } 37 | } 38 | 39 | 40 | public func request(method method: String, url: String, headers: [Header]? = nil, body: String? = nil) throws -> Response { 41 | guard let url = URL(string: url) else { 42 | throw RequestError.InvalidURL 43 | } 44 | 45 | if url.scheme != "http" { 46 | throw RequestError.UnsupportedScheme(url.scheme) 47 | } 48 | 49 | let socket = try Socket() 50 | try socket.connect(url.hostname, port: url.port) 51 | let request = createRequest(method, path: url.path, hostname: url.hostname, headers: headers ?? [], body: body) 52 | sendRequest(socket, request: request) 53 | 54 | let parser = HTTPParser(socket: socket) 55 | let response = try parser.parse() 56 | 57 | socket.close() 58 | 59 | return response 60 | } 61 | -------------------------------------------------------------------------------- /Sources/Socket.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Glibc 3 | 4 | private let sock_stream = Int32(SOCK_STREAM.rawValue) 5 | 6 | private let system_accept = Glibc.accept 7 | private let system_bind = Glibc.bind 8 | private let system_close = Glibc.close 9 | private let system_listen = Glibc.listen 10 | private let system_read = Glibc.read 11 | private let system_send = Glibc.send 12 | private let system_write = Glibc.write 13 | private let system_shutdown = Glibc.shutdown 14 | private let system_select = Glibc.select 15 | private let system_pipe = Glibc.pipe 16 | private let system_connect = Glibc.connect 17 | #else 18 | import Darwin.C 19 | 20 | private let sock_stream = SOCK_STREAM 21 | 22 | private let system_accept = Darwin.accept 23 | private let system_bind = Darwin.bind 24 | private let system_close = Darwin.close 25 | private let system_listen = Darwin.listen 26 | private let system_read = Darwin.read 27 | private let system_send = Darwin.send 28 | private let system_write = Darwin.write 29 | private let system_shutdown = Darwin.shutdown 30 | private let system_select = Darwin.select 31 | private let system_pipe = Darwin.pipe 32 | private let system_connect = Darwin.connect 33 | #endif 34 | 35 | 36 | @_silgen_name("fcntl") private func fcntl(descriptor: Int32, _ command: Int32, _ flags: Int32) -> Int32 37 | 38 | 39 | struct SocketError : ErrorType, CustomStringConvertible { 40 | let function: String 41 | let number: Int32 42 | 43 | init(function: String = __FUNCTION__) { 44 | self.function = function 45 | self.number = errno 46 | } 47 | 48 | var description: String { 49 | return "Socket.\(function) failed [\(number)]" 50 | } 51 | } 52 | 53 | 54 | /// Represents a TCP AF_INET socket 55 | class Socket { 56 | typealias Descriptor = Int32 57 | typealias Port = UInt16 58 | 59 | let descriptor: Descriptor 60 | 61 | class func pipe() throws -> [Socket] { 62 | var fds: [Int32] = [0, 0] 63 | if system_pipe(&fds) == -1 { 64 | throw SocketError() 65 | } 66 | return [Socket(descriptor: fds[0]), Socket(descriptor: fds[1])] 67 | } 68 | 69 | init() throws { 70 | #if os(Linux) 71 | descriptor = socket(AF_INET, sock_stream, 0) 72 | #else 73 | descriptor = socket(AF_INET, sock_stream, IPPROTO_TCP) 74 | #endif 75 | assert(descriptor > 0) 76 | 77 | var value: Int32 = 1 78 | guard setsockopt(descriptor, SOL_SOCKET, SO_REUSEADDR, &value, socklen_t(sizeof(Int32))) != -1 else { 79 | throw SocketError(function: "setsockopt()") 80 | } 81 | } 82 | 83 | init(descriptor: Descriptor) { 84 | self.descriptor = descriptor 85 | } 86 | 87 | func connect(hostname: String, port: Int16) throws { 88 | let host = hostname.withCString { gethostbyname($0) } 89 | if host == nil { 90 | throw SocketError() 91 | } 92 | 93 | var addr = sockaddr_in() 94 | addr.sin_family = sa_family_t(AF_INET) 95 | addr.sin_port = in_port_t(htons(in_port_t(port))) 96 | addr.sin_zero = (0, 0, 0, 0, 0, 0, 0, 0) 97 | memcpy(&addr.sin_addr, host.memory.h_addr_list[0], Int(host.memory.h_length)) 98 | 99 | let len = socklen_t(UInt8(sizeof(sockaddr_in))) 100 | guard system_connect(descriptor, sockaddr_cast(&addr), len) != -1 else { 101 | throw SocketError() 102 | } 103 | } 104 | 105 | func close() { 106 | system_close(descriptor) 107 | } 108 | 109 | func shutdown() { 110 | system_shutdown(descriptor, Int32(SHUT_RDWR)) 111 | } 112 | 113 | func send(output: String) { 114 | output.withCString { bytes in 115 | system_send(descriptor, bytes, Int(strlen(bytes)), 0) 116 | } 117 | } 118 | 119 | func write(output: String) { 120 | output.withCString { bytes in 121 | system_write(descriptor, bytes, Int(strlen(bytes))) 122 | } 123 | } 124 | 125 | func write(output: [UInt8]) { 126 | output.withUnsafeBufferPointer { bytes in 127 | system_write(descriptor, bytes.baseAddress, bytes.count) 128 | } 129 | } 130 | 131 | func read(bytes: Int) throws -> [CChar] { 132 | let data = Data(capacity: bytes) 133 | let bytes = system_read(descriptor, data.bytes, data.capacity) 134 | if bytes > 0 { 135 | return Array(data.characters[0.. CUnsignedShort { 142 | return (value << 8) + (value >> 8) 143 | } 144 | 145 | private func sockaddr_cast(p: UnsafeMutablePointer) -> UnsafeMutablePointer { 146 | return UnsafeMutablePointer(p) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Sources/String.swift: -------------------------------------------------------------------------------- 1 | class Scanner { 2 | var content: String 3 | 4 | init(_ content: String) { 5 | self.content = content 6 | } 7 | 8 | var isEmpty: Bool { 9 | return content.characters.count == 0 10 | } 11 | 12 | func scan(until until: String) -> String { 13 | if until.isEmpty { 14 | return "" 15 | } 16 | 17 | var characters: [Character] = [] 18 | 19 | while !content.isEmpty { 20 | let character = content.characters.first! 21 | content = String(content.characters.dropFirst()) 22 | 23 | characters.append(character) 24 | 25 | if content.hasPrefix(until) { 26 | let index = content.characters.startIndex.advancedBy(until.characters.count) 27 | content = String(content.characters[index.. [String] { 39 | var components: [String] = [] 40 | let scanner = Scanner(self) 41 | var count = 0 42 | 43 | while !scanner.isEmpty || count == maxSplit { 44 | let scanned = scanner.scan(until: separator) 45 | if scanned.isEmpty && scanner.isEmpty { 46 | break 47 | } 48 | components.append(scanned) 49 | ++count 50 | } 51 | 52 | return components 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/URL.swift: -------------------------------------------------------------------------------- 1 | struct URL { 2 | let scheme: String 3 | let hostname: String 4 | let port: Int16 5 | let path: String 6 | 7 | init?(string: String) { 8 | let parts = string.split("://", maxSplit: 1) 9 | if parts.count != 2 { 10 | scheme = "" 11 | hostname = "" 12 | port = 80 13 | path = "" 14 | return nil 15 | } 16 | 17 | scheme = parts[0] 18 | let parts1 = parts[1].split("/") 19 | if parts1.count < 2 { 20 | hostname = "" 21 | port = 80 22 | path = "" 23 | return nil 24 | } 25 | 26 | path = "/" + parts1[1.. 1 { 32 | hostname = parts2[0] 33 | if let port = Int16(parts2[1]) { 34 | self.port = port 35 | } else { 36 | port = 80 37 | return nil 38 | } 39 | } else { 40 | hostname = address 41 | port = 80 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/HTTPParserSpec.swift: -------------------------------------------------------------------------------- 1 | import Spectre 2 | @testable import Requests 3 | 4 | 5 | func describeHTTPParser() { 6 | describe("HTTP Parser") { 7 | var parser: HTTPParser! 8 | var outSocket: Socket! 9 | var inSocket: Socket! 10 | 11 | $0.before { 12 | let pipe = try! Socket.pipe() 13 | inSocket = pipe[0] 14 | outSocket = pipe[1] 15 | parser = HTTPParser(socket: inSocket) 16 | } 17 | 18 | $0.it("can parse a HTTP response") { 19 | outSocket.write("HTTP/1.1 200 OK\r\nContent-Length: 12\r\nConnection: close\r\nContent-Type: text/plain\r\n\r\nHello World!") 20 | 21 | let response = try parser.parse() 22 | try expect(response.status.rawValue) == 200 23 | try expect(response["Content-Length"]) == "12" 24 | try expect(response["Content-Type"]) == "text/plain" 25 | 26 | guard var payload = response.body else { 27 | throw failure("response has no payload") 28 | } 29 | var body = [CChar]() 30 | while let chunk = payload.next() { 31 | body.appendContentsOf(chunk.map({ return CChar($0) })) 32 | } 33 | body.append(0) 34 | try body.withUnsafeBufferPointer { buffer in 35 | guard let string = String.fromCString(buffer.baseAddress) else { 36 | throw failure("response payload is broken") 37 | } 38 | try expect(string) == "Hello World!" 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/RequestSpec.swift: -------------------------------------------------------------------------------- 1 | import Inquiline 2 | import Nest 3 | import Spectre 4 | @testable import Requests 5 | 6 | 7 | func describeHTTPRequest() { 8 | describe("HTTP Request") { 9 | var outSocket: Socket! 10 | var inSocket: Socket! 11 | 12 | $0.before { 13 | let pipe = try! Socket.pipe() 14 | inSocket = pipe[0] 15 | outSocket = pipe[1] 16 | } 17 | 18 | $0.it("can send a request") { 19 | let request = Request(method: "GET", path: "/test", headers: [("Test-Header", "Test Value")], content: "testing") 20 | sendRequest(outSocket, request: request) 21 | 22 | let requestString = String.fromCString(try inSocket.read(2048) + [0]) 23 | try expect(requestString) == "GET /test HTTP/1.1\r\nTest-Header: Test Value\r\n\r\ntesting" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/SendResponseSpec.swift: -------------------------------------------------------------------------------- 1 | import Spectre 2 | import Inquiline 3 | @testable import Requests 4 | 5 | 6 | func describeSendRequest() { 7 | describe("Sending a request") { 8 | var outSocket: Socket! 9 | var inSocket: Socket! 10 | 11 | $0.before { 12 | let pipe = try! Socket.pipe() 13 | inSocket = pipe[0] 14 | outSocket = pipe[1] 15 | } 16 | 17 | $0.it("sends the HTTP request to the server") { 18 | let request = Request(method: "GET", path: "/path", headers: [], content: "Hello World") 19 | sendRequest(outSocket, request: request) 20 | 21 | let message = String.fromCString((try inSocket.read(512)) + [0]) 22 | try expect(message) == "GET /path HTTP/1.1\r\n\r\nHello World" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/URLSpec.swift: -------------------------------------------------------------------------------- 1 | import Spectre 2 | @testable import Requests 3 | 4 | 5 | func describeURL() { 6 | describe("URL") { 7 | $0.describe("when parsing from a string") { 8 | let url = URL(string: "http://fuller.li/posts") 9 | 10 | $0.it("can parse the scheme") { 11 | try expect(url?.scheme) == "http" 12 | } 13 | 14 | $0.it("can parse the hostname") { 15 | try expect(url?.hostname) == "fuller.li" 16 | } 17 | 18 | $0.it("can parse the path") { 19 | try expect(url?.path) == "/posts" 20 | } 21 | 22 | $0.it("defaults HTTP port to 80") { 23 | try expect(url?.port) == 80 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/main.swift: -------------------------------------------------------------------------------- 1 | import Spectre 2 | 3 | 4 | describeURL() 5 | describeHTTPParser() 6 | describeSendRequest() 7 | --------------------------------------------------------------------------------