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}