api-swift / Sources / IrdAPI / Core / IrdClient.swift
527 words
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import Foundation

/// The entry point. Immutable and `Sendable`; module accessors (`client.vps`,
/// `client.registrations`, ...) are generated from the spec, `client.raw` is the
/// untyped escape hatch.
public final class IrdClient: Sendable {
    public let configuration: Configuration
    /// The domain gate, present only on clients built with `gated`.
    public let gate: DomainGate?

    public init(configuration: Configuration = Configuration()) {
        self.configuration = configuration
        self.gate = nil
    }

    init(configuration: Configuration, gate: DomainGate?) {
        self.configuration = configuration
        self.gate = gate
    }

    public convenience init(apiKey: String) {
        self.init(configuration: Configuration(apiKey: apiKey))
    }

    public convenience init(credential: any CredentialProvider) {
        var c = Configuration()
        c.credential = credential
        self.init(configuration: c)
    }

    /// A client whose domain-touching mutations are checked against `rules`. Builds the
    /// registration index up front, so the credential must resolve to a token.
    public static func gated(_ configuration: Configuration, rules: GateRules) async throws -> IrdClient {
        let plain = IrdClient(configuration: configuration)
        guard let token = try await configuration.credential.token(),
              !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
            throw IrdError.config("Domain gate options require an authenticated client (apiKey)")
        }
        let index = try await plain.fetchRegistrationIndex()
        return IrdClient(configuration: configuration, gate: DomainGate(rules: rules, index: index))
    }

    public var isGated: Bool { gate != nil }

    /// Re-reads the registration index (after registering a domain, say); no-op when ungated.
    public func refreshGate() async throws {
        guard let gate else { return }
        await gate.replace(try await fetchRegistrationIndex())
    }

    public var raw: RawAPI { RawAPI(client: self) }

    /// The one call every generated operation makes: fills the path, checks the gate,
    /// attaches the credential, sends, applies the decode table, and decodes `Response`
    /// from the original bytes.
    public func request<Response: Decodable & Sendable>(
        _ op: IrdOperation, params: [String: String] = [:], query: [QueryItem] = [],
        body: (any Encodable & Sendable)? = nil
    ) async throws -> Response {
        let (data, _) = try await perform(op, params: params, query: query, body: body, gated: true)
        do {
            return try JSONDecoder().decode(Response.self, from: data)
        } catch {
            throw IrdError.transport("decoding \(Response.self): \(error)")
        }
    }

    /// Same pipeline for operations whose success carries no body.
    public func requestNoContent(
        _ op: IrdOperation, params: [String: String] = [:], query: [QueryItem] = [],
        body: (any Encodable & Sendable)? = nil
    ) async throws {
        _ = try await perform(op, params: params, query: query, body: body, gated: true)
    }

    func perform(_ op: IrdOperation, params: [String: String], query: [QueryItem],
                 body: (any Encodable & Sendable)?, gated: Bool) async throws -> (data: Data, json: JSONValue) {
        var bodyData: Data? = nil
        if let body {
            do { bodyData = try JSONEncoder().encode(AnyEncodable(value: body)) } catch {
                throw IrdError.config("encoding body for \(op.key): \(error)")
            }
        }
        if gated, gate != nil, let source = GatedOperations.table[op.key] {
            let bodyJSON = try bodyData.map { try JSONValue.parse($0) }
            let target = try await gateTarget(for: source, params: params, body: bodyJSON)
            try await checkGate(op: op.key, target: target)
        }
        let token: String? = op.security == .anonymous ? nil : try await configuration.credential.token()
        let path = try RequestBuilder.fillPath(op.pathTemplate, params: params)
        guard let url = URL(string: RequestBuilder.base(configuration.apiURL) + path + RequestBuilder.queryString(query)) else {
            throw IrdError.config("invalid URL for \(op.key)")
        }
        let request = RequestBuilder.build(url: url, method: op.method, token: token, body: bodyData,
                                           headers: configuration.headers, timeout: configuration.timeout)
        let response = try await configuration.transport.send(request)
        return try ResponseDecoder.decode(status: response.status, body: response.body)
    }
}

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/api-swift.git