commit 9ac6c7476c974f26fc5faeb7548bfa3eecb9ac33
parent aec8d755c2b3e5b2a8fd7d719dd005617058e8a6
Author: FIGBERT <figbert@figbert.com>
Date: Tue, 30 Aug 2022 18:19:51 -0700
Manage synchronicity with async/await
Diffstat:
2 files changed, 64 insertions(+), 43 deletions(-)
diff --git a/Sources/SwiftGemini/SwiftGemini.swift b/Sources/SwiftGemini/SwiftGemini.swift
@@ -1,56 +1,63 @@
import Foundation
import Network
-class GeminiRequest {
+class GeminiRequestor {
static let queue = DispatchQueue(label: "gemini", qos: .default)
fileprivate var conn: NWConnection?
- fileprivate var response: Data?
- let url: URL
- var status: StatusCode?
- var meta: MetaHeader?
- var body: Data?
- var text: String?
- var gemtext: String?
+ enum GeminiRequestorError: Error {
+ case invalidResponseHeader
+ }
- init?(_ url: URL) throws {
- guard url.user == nil && url.password == nil,
- url.host != nil && url.absoluteString.data(using: .utf8)!.count <= 1024 else { return nil }
- self.url = url
- request()
+ func request(url: URL) async throws -> GeminiResponse {
+ let response: GeminiResponse = try await withCheckedThrowingContinuation({ continuation in
+ requestWithCompletion(url: url, completion: { result in
+ switch result {
+ case .success(let response):
+ continuation.resume(returning: response)
+ case .failure(let error):
+ continuation.resume(throwing: error)
+ }
+ })
+ })
+ return response
}
- fileprivate func request() {
- let host = NWEndpoint.Host(self.url.host!)
- let port = NWEndpoint.Port(integerLiteral: NWEndpoint.Port.IntegerLiteralType(self.url.port ?? 1965))
+ func requestWithCompletion(url: URL, completion: @escaping (Result<GeminiResponse, Error>) -> Void) {
+ guard url.user == nil && url.password == nil,
+ url.host != nil && url.absoluteString.data(using: .utf8)!.count <= 1024 else { return }
+
+ let host = NWEndpoint.Host(url.host!)
+ let port = NWEndpoint.Port(integerLiteral: NWEndpoint.Port.IntegerLiteralType(url.port ?? 1965))
let opts = NWProtocolTLS.Options()
sec_protocol_options_set_min_tls_protocol_version(opts.securityProtocolOptions, .TLSv12)
sec_protocol_options_set_verify_block(opts.securityProtocolOptions, { (sec_protocol_metadata, sec_trust, sec_protocol_verify_complete) in
sec_protocol_metadata_copy_peer_public_key(sec_protocol_metadata)
sec_protocol_verify_complete(true)
- }, GeminiRequest.queue)
+ }, GeminiRequestor.queue)
- let request = "\(self.url.absoluteString)\r\n"
+ let request = "\(url.absoluteString)\r\n"
let data = request.data(using: .utf8)
let conn = NWConnection(host: host, port: port, using: NWParameters(tls: opts))
self.conn = conn
- conn.start(queue: GeminiRequest.queue)
+ conn.start(queue: GeminiRequestor.queue)
conn.send(content: data, contentContext: .defaultMessage, isComplete: true, completion: .contentProcessed({ (err) in
conn.receiveMessage { (data, ctx, isComplete, err) in
conn.cancel()
- self.parseResponse(data: data!)
+ completion(self.parseResponse(url: url, data: data!))
}
}))
}
- fileprivate func parseResponse(data: Data) {
+ fileprivate func parseResponse(url: URL, data: Data) -> Result<GeminiResponse, Error> {
guard let ix = data.firstIndex(of: 13),
- data[ix+1] == 10, ix < (1024+3) else { return }
+ data[ix+1] == 10, ix < (1024+3) else { return .failure(GeminiRequestorError.invalidResponseHeader) }
var header = String(data: data.prefix(upTo: ix), encoding: .utf8)!
let status = StatusCode(rawValue: Int(header.components(separatedBy: " ").first!)!)!
let meta: MetaHeader
+ var body: GeminiBody?
header = String(header.dropFirst(3))
switch status {
@@ -65,26 +72,40 @@ class GeminiRequest {
.ClientCertRequired: meta = MetaHeader.CertInfo(header)
}
- self.status = status
- self.meta = meta
- self.response = data
-
- if self.status == StatusCode.Success {
- self.parseBody()
+ if status == StatusCode.Success {
+ body = parseBody(header: meta, data: data)
}
+
+ return .success(GeminiResponse(
+ url: url, status: status,
+ header: meta, body: body
+ ))
}
- fileprivate func parseBody() {
- let ix = self.response!.firstIndex(of: 13)! + 2
- let body = self.response!.suffix(from: ix)
+ fileprivate func parseBody(header: MetaHeader, data: Data) -> GeminiBody {
+ let ix = data.firstIndex(of: 13)! + 2
+ let body = data.suffix(from: ix)
+ var gemtext, text: String?
- if case let .MIME(mime) = self.meta, mime.hasPrefix("text/") || mime.isEmpty {
+ if case let .MIME(mime) = header, mime.hasPrefix("text/") || mime.isEmpty {
if mime.hasPrefix("text/gemini") || mime.isEmpty {
- self.gemtext = String(data: body, encoding: .utf8)
+ gemtext = String(data: body, encoding: .utf8)
}
- self.text = String(data: body, encoding: .utf8)
- } else {
- self.body = body
+ text = String(data: body, encoding: .utf8)
}
+
+ return GeminiBody(raw: body, text: text, gemtext: gemtext)
}
}
+
+struct GeminiResponse {
+ let url: URL
+ let status: StatusCode
+ let header: MetaHeader
+ let body: GeminiBody?
+}
+
+struct GeminiBody {
+ let raw: Data
+ let text, gemtext: String?
+}
diff --git a/Tests/SwiftGeminiTests/SwiftGeminiTests.swift b/Tests/SwiftGeminiTests/SwiftGeminiTests.swift
@@ -2,15 +2,15 @@ import XCTest
@testable import SwiftGemini
final class SwiftGeminiTests: XCTestCase {
- func testExample() {
- // This is an example of a functional test case.
- // Use XCTAssert and related functions to verify your tests produce the correct
- // results.
- let _ = try! GeminiRequest(URL(string: "gemini://figbert.com")!)
- sleep(1)
+ func testFigbertDotCom() async {
+ let engine = GeminiRequestor()
+ let test = try! await engine.request(url: URL(string: "gemini://figbert.com")!)
+ XCTAssertEqual(URL(string: "gemini://figbert.com"), test.url)
+ XCTAssertEqual(test.status, StatusCode.Success)
+ XCTAssertEqual(test.body!.gemtext!.prefix(13), "# Hello World")
}
static var allTests = [
- ("testExample", testExample),
+ ("testFigbertDotCom", testFigbertDotCom),
]
}