krz/rune

an ios client for njalla

clone: git clone https://gitbay.org/krz/rune.git

v1.0.0: Rune/API/NjallaClient.swift · raw

  1import Foundation
  2
  3struct NjallaClient: Sendable, Equatable {
  4    private let token: String
  5    private let endpoint = URL(string: "https://njal.la/api/1/")!
  6
  7    init(token: String) {
  8        self.token = token
  9    }
 10
 11    func call<T: Decodable>(_ method: String, params: [String: Any] = [:]) async throws -> T {
 12        var request = URLRequest(url: endpoint)
 13        request.httpMethod = "POST"
 14        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
 15        request.setValue("application/json", forHTTPHeaderField: "Accept")
 16        request.setValue("Njalla \(token)", forHTTPHeaderField: "Authorization")
 17
 18        let body: [String: Any] = [
 19            "jsonrpc": "2.0",
 20            "method": method,
 21            "params": params,
 22            "id": "1"
 23        ]
 24
 25        request.httpBody = try JSONSerialization.data(withJSONObject: body)
 26
 27        let (data, response) = try await URLSession.shared.data(for: request)
 28        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
 29
 30        #if DEBUG
 31        debugPrint("Njalla method:", method)
 32        debugPrint("Njalla status:", statusCode)
 33        #endif
 34
 35        let decoder = JSONDecoder()
 36        let envelope = try decoder.decode(RPCResponse<T>.self, from: data)
 37
 38        if let error = envelope.error {
 39            throw NjallaError.api(message: error.message)
 40        }
 41
 42        guard let result = envelope.result else {
 43            throw NjallaError.missingResult
 44        }
 45
 46        return result
 47    }
 48
 49    func listDomains() async throws -> [Domain] {
 50        let response: DomainListResponse = try await call("list-domains")
 51        return response.domains
 52    }
 53
 54    func getDomain(named domain: String) async throws -> Domain {
 55        try await call("get-domain", params: ["domain": domain])
 56    }
 57
 58    func editDomain(named domain: String, request: DomainUpdateRequest) async throws -> Domain {
 59        var params = request.params
 60        params["domain"] = domain
 61        return try await call("edit-domain", params: params)
 62    }
 63
 64    func listRecords(for domain: String) async throws -> [DNSRecord] {
 65        let response: RecordListResponse = try await call("list-records", params: ["domain": domain])
 66        return response.records.map { record in
 67            record.domain.isEmpty ? record.withDomain(domain) : record
 68        }
 69    }
 70
 71    func addRecord(for domain: String, draft: DNSRecordDraft) async throws -> DNSRecord {
 72        try await call("add-record", params: draft.params(domain: domain))
 73    }
 74
 75    func editRecord(for domain: String, id: String, draft: DNSRecordDraft) async throws -> DNSRecord {
 76        try await call("edit-record", params: draft.params(domain: domain, id: id))
 77    }
 78
 79    func removeRecord(_ record: DNSRecord) async throws {
 80        let params: [String: Any] = [
 81            "domain": record.domain,
 82            "id": record.id,
 83            "name": record.name,
 84            "type": record.type
 85        ]
 86        let _: RecordListResponse = try await call("remove-record", params: params)
 87    }
 88
 89    func listTokens() async throws -> [APIToken] {
 90        let response: TokenListResponse = try await call("list-tokens")
 91        return response.tokens
 92    }
 93
 94    func addToken(request: TokenCreateRequest) async throws {
 95        let _: EmptyResult = try await call("add-token", params: request.params)
 96    }
 97
 98    func removeToken(key: String) async throws {
 99        let _: EmptyResult = try await call("remove-token", params: ["key": key])
100    }
101
102    func getBalance() async throws -> WalletBalance {
103        try await call("get-balance")
104    }
105}
106
107enum NjallaError: LocalizedError {
108    case api(message: String)
109    case missingResult
110
111    var errorDescription: String? {
112        switch self {
113        case .api(let message):
114            return message
115        case .missingResult:
116            return "The API response did not include a result."
117        }
118    }
119}
120
121private struct RPCResponse<Result: Decodable>: Decodable {
122    let result: Result?
123    let error: RPCError?
124}
125
126private struct RPCError: Decodable {
127    let code: Int
128    let message: String
129}