rastrillo / native Public

Clone
git clone https://amadan.net/rastrillo/native

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2
3/// What a native client needs from the network. `NativeHTTP` is the real one;
4/// `RastrilloTestSupport.ScriptedTransport` answers from a script, so offline,
5/// lost responses and ended sessions can be tested without a fixture server.
6///
7/// Anything that can outlive a session should take a supplier,
8/// `@MainActor () -> NativeTransport?`, rather than capturing one transport:
9/// signing in again replaces the transport under it.
10///
11/// The requirements take every argument. The shorter forms, with defaults,
12/// are extensions below; they have different signatures from the
13/// requirements, so no conformer can pick one up as a default
14/// implementation that calls itself.
15public protocol NativeTransport: Sendable {
16 var origin: URL { get }
17 /// Returns any status. Throws `NativeClientError.offline` when the request
18 /// never reached the server or its answer never came back.
19 func responseDetails(_ path: String, method: String, body: Data?, contentType: String, maxBytes: Int) async throws -> NativeResponse
20 /// Throws the typed case for a refused stream: a 401 reads as `.unauthorized`.
21 func stream(_ path: String, onOpen: @Sendable () async throws -> Void, onLine: @Sendable (String) async throws -> Void) async throws
22 func download(_ path: String, to destination: URL) async throws
23}
24
25/// The default response bound, 4 MiB.
26public let nativeDefaultMaxBytes = 4 * 1024 * 1024
27
28public extension NativeTransport {
29 func responseDetails(_ path: String, method: String = "GET", body: Data? = nil,
30 contentType: String = "application/json") async throws -> NativeResponse {
31 try await responseDetails(path, method: method, body: body, contentType: contentType, maxBytes: nativeDefaultMaxBytes)
32 }
33
34 func responseDetails(_ path: String, method: String = "GET", body: Data? = nil, maxBytes: Int) async throws -> NativeResponse {
35 try await responseDetails(path, method: method, body: body, contentType: "application/json", maxBytes: maxBytes)
36 }
37
38 func response(_ path: String, method: String = "GET", body: Data? = nil,
39 contentType: String = "application/json", maxBytes: Int = nativeDefaultMaxBytes) async throws -> (Data, Int) {
40 let result = try await responseDetails(path, method: method, body: body, contentType: contentType, maxBytes: maxBytes)
41 return (result.data, result.status)
42 }
43
44 /// Decodes a 2xx body; any other status throws its typed case.
45 func request<T: Decodable>(_ path: String, method: String = "GET", body: Data? = nil,
46 contentType: String = "application/json", maxBytes: Int = nativeDefaultMaxBytes) async throws -> T {
47 let result = try await responseDetails(path, method: method, body: body, contentType: contentType, maxBytes: maxBytes)
48 try result.requireSuccess()
49 do { return try JSONDecoder().decode(T.self, from: result.data) }
50 catch { throw NativeClientError.message("The server returned an unsupported response.") }
51 }
52
53 func stream(_ path: String, onLine: @Sendable (String) async throws -> Void) async throws {
54 try await stream(path, onOpen: {}, onLine: onLine)
55 }
56}
57
58/// What `NativeHTTP` tells its outcome observer. A response of any status
59/// proves the server is reachable; an offline-class failure suggests it is not.
60public enum NativeTransportOutcome: Sendable, Equatable {
61 case responded(status: Int)
62 case offline(URLError.Code)
63}
64