krz/rune
an ios client for njalla
clone: git clone https://gitbay.org/krz/rune.git
v1.0.0: Rune/API/Models.swift · raw
1import Foundation
2
3struct EmptyResult: Codable {}
4
5struct WalletBalance: Codable, Equatable {
6 let balance: Int
7}
8
9struct DomainListResponse: Codable {
10 let domains: [Domain]
11}
12
13struct RecordListResponse: Codable {
14 let records: [DNSRecord]
15}
16
17struct TokenListResponse: Codable {
18 let tokens: [APIToken]
19}
20
21struct Domain: Codable, Identifiable, Hashable {
22 var id: String { name }
23
24 let name: String
25 let status: String?
26 let expiry: String?
27 let autorenew: Bool?
28 let mailforwarding: Bool?
29 let dnssec: Bool?
30 let lock: Bool?
31 let nameservers: [String]?
32}
33
34struct DNSRecord: Codable, Identifiable, Hashable {
35 let id: String
36 let domain: String
37 let type: String
38 let name: String
39 let content: String?
40 let ttl: Int?
41 let prio: Int?
42 let weight: Int?
43 let port: Int?
44 let target: String?
45 let sshAlgorithm: Int?
46 let sshType: Int?
47
48 enum CodingKeys: String, CodingKey {
49 case id
50 case domain
51 case type
52 case name
53 case content
54 case ttl
55 case prio
56 case weight
57 case port
58 case target
59 case sshAlgorithm = "ssh_algorithm"
60 case sshType = "ssh_type"
61 }
62
63 init(
64 id: String,
65 domain: String,
66 type: String,
67 name: String,
68 content: String?,
69 ttl: Int?,
70 prio: Int?,
71 weight: Int?,
72 port: Int?,
73 target: String?,
74 sshAlgorithm: Int?,
75 sshType: Int?
76 ) {
77 self.id = id
78 self.domain = domain
79 self.type = type
80 self.name = name
81 self.content = content
82 self.ttl = ttl
83 self.prio = prio
84 self.weight = weight
85 self.port = port
86 self.target = target
87 self.sshAlgorithm = sshAlgorithm
88 self.sshType = sshType
89 }
90
91 init(from decoder: Decoder) throws {
92 let container = try decoder.container(keyedBy: CodingKeys.self)
93
94 id = try container.decodeLossyString(forKey: .id)
95 domain = try container.decodeIfPresent(String.self, forKey: .domain) ?? ""
96 type = try container.decode(String.self, forKey: .type)
97 name = try container.decode(String.self, forKey: .name)
98 content = try container.decodeIfPresent(String.self, forKey: .content)
99 ttl = try container.decodeLossyIntIfPresent(forKey: .ttl)
100 prio = try container.decodeLossyIntIfPresent(forKey: .prio)
101 weight = try container.decodeLossyIntIfPresent(forKey: .weight)
102 port = try container.decodeLossyIntIfPresent(forKey: .port)
103 target = try container.decodeIfPresent(String.self, forKey: .target)
104 sshAlgorithm = try container.decodeLossyIntIfPresent(forKey: .sshAlgorithm)
105 sshType = try container.decodeLossyIntIfPresent(forKey: .sshType)
106 }
107
108 func withDomain(_ domain: String) -> DNSRecord {
109 DNSRecord(
110 id: id,
111 domain: domain,
112 type: type,
113 name: name,
114 content: content,
115 ttl: ttl,
116 prio: prio,
117 weight: weight,
118 port: port,
119 target: target,
120 sshAlgorithm: sshAlgorithm,
121 sshType: sshType
122 )
123 }
124}
125
126struct APIToken: Codable, Identifiable, Hashable {
127 var id: String { key }
128
129 let key: String
130 let comment: String?
131 let from: [String]?
132 let allowedDomains: [String]?
133 let allowedServers: [String]?
134 let allowedMethods: [String]?
135 let allowedPrefixes: [String]?
136 let allowedTypes: [String]?
137
138 enum CodingKeys: String, CodingKey {
139 case key
140 case comment
141 case from
142 case allowedDomains = "allowed_domains"
143 case allowedServers = "allowed_servers"
144 case allowedMethods = "allowed_methods"
145 case allowedPrefixes = "allowed_prefixes"
146 case allowedTypes = "allowed_types"
147 }
148}
149
150enum DNSRecordType: String, CaseIterable, Identifiable, Codable {
151 case a = "A"
152 case aaaa = "AAAA"
153 case aname = "ANAME"
154 case caa = "CAA"
155 case cname = "CNAME"
156 case ds = "DS"
157 case dynamic = "Dynamic"
158 case https = "HTTPS"
159 case mx = "MX"
160 case naptr = "NAPTR"
161 case ns = "NS"
162 case ptr = "PTR"
163 case srv = "SRV"
164 case sshfp = "SSHFP"
165 case svcb = "SVCB"
166 case tlsa = "TLSA"
167 case txt = "TXT"
168
169 var id: String { rawValue }
170
171 var usesContent: Bool {
172 switch self {
173 case .dynamic, .https, .svcb:
174 return false
175 default:
176 return true
177 }
178 }
179
180 var usesTTL: Bool {
181 usesContent
182 }
183
184 var usesPriority: Bool {
185 switch self {
186 case .https, .mx, .srv, .svcb:
187 return true
188 default:
189 return false
190 }
191 }
192
193 var usesWeight: Bool {
194 self == .srv
195 }
196
197 var usesPort: Bool {
198 self == .srv
199 }
200
201 var usesTarget: Bool {
202 self == .https || self == .svcb
203 }
204
205 var usesSSHFields: Bool {
206 self == .sshfp
207 }
208}
209
210struct DNSRecordDraft: Equatable {
211 var type: DNSRecordType = .a
212 var name = ""
213 var content = ""
214 var ttl = ""
215 var prio = ""
216 var weight = ""
217 var port = ""
218 var target = ""
219 var sshAlgorithm = ""
220 var sshType = ""
221
222 init() {}
223
224 init(record: DNSRecord) {
225 type = DNSRecordType(rawValue: record.type) ?? .a
226 name = record.name
227 content = record.content ?? ""
228 ttl = record.ttl.map(String.init) ?? ""
229 prio = record.prio.map(String.init) ?? ""
230 weight = record.weight.map(String.init) ?? ""
231 port = record.port.map(String.init) ?? ""
232 target = record.target ?? ""
233 sshAlgorithm = record.sshAlgorithm.map(String.init) ?? ""
234 sshType = record.sshType.map(String.init) ?? ""
235 }
236
237 mutating func resetTypeSpecificFields() {
238 content = ""
239 ttl = ""
240 prio = ""
241 weight = ""
242 port = ""
243 target = ""
244 sshAlgorithm = ""
245 sshType = ""
246 }
247
248 var trimmedName: String {
249 name.trimmingCharacters(in: .whitespacesAndNewlines)
250 }
251
252 var canSubmit: Bool {
253 !trimmedName.isEmpty
254 }
255
256 func params(domain: String, id: String? = nil) -> [String: Any] {
257 var params: [String: Any] = [
258 "domain": domain,
259 "type": type.rawValue,
260 "name": trimmedName
261 ]
262
263 if let id {
264 params["id"] = id
265 }
266
267 if type.usesContent {
268 params["content"] = content.trimmingCharacters(in: .whitespacesAndNewlines)
269 }
270
271 if type.usesTTL, let value = Int(ttl.trimmingCharacters(in: .whitespacesAndNewlines)) {
272 params["ttl"] = value
273 }
274
275 if type.usesPriority, let value = Int(prio.trimmingCharacters(in: .whitespacesAndNewlines)) {
276 params["prio"] = value
277 }
278
279 if type.usesWeight, let value = Int(weight.trimmingCharacters(in: .whitespacesAndNewlines)) {
280 params["weight"] = value
281 }
282
283 if type.usesPort, let value = Int(port.trimmingCharacters(in: .whitespacesAndNewlines)) {
284 params["port"] = value
285 }
286
287 if type.usesTarget {
288 params["target"] = target.trimmingCharacters(in: .whitespacesAndNewlines)
289 }
290
291 if type.usesSSHFields {
292 if let value = Int(sshAlgorithm.trimmingCharacters(in: .whitespacesAndNewlines)) {
293 params["ssh_algorithm"] = value
294 }
295 if let value = Int(sshType.trimmingCharacters(in: .whitespacesAndNewlines)) {
296 params["ssh_type"] = value
297 }
298 }
299
300 return params
301 }
302}
303
304struct DomainUpdateRequest {
305 var autorenew: Bool
306 var mailforwarding: Bool
307 var dnssec: Bool
308 var lock: Bool
309 var nameservers: [String]
310
311 var params: [String: Any] {
312 [
313 "autorenew": autorenew,
314 "mailforwarding": mailforwarding,
315 "dnssec": dnssec,
316 "lock": lock,
317 "nameservers": nameservers
318 ]
319 }
320}
321
322struct TokenCreateRequest {
323 var comment: String
324 var from: [String]
325 var allowedMethods: [String]
326
327 var params: [String: Any] {
328 var params: [String: Any] = [:]
329
330 let trimmedComment = comment.trimmingCharacters(in: .whitespacesAndNewlines)
331 if !trimmedComment.isEmpty {
332 params["comment"] = trimmedComment
333 }
334
335 if !from.isEmpty {
336 params["from"] = from
337 }
338
339 if !allowedMethods.isEmpty {
340 params["allowed_methods"] = allowedMethods
341 }
342
343 return params
344 }
345}
346
347extension String {
348 func formattedExpiry() -> String {
349 let parser = ISO8601DateFormatter()
350 guard let date = parser.date(from: self) else { return self }
351 let formatter = DateFormatter()
352 formatter.dateStyle = .medium
353 formatter.timeStyle = .none
354 return formatter.string(from: date)
355 }
356}
357
358private extension KeyedDecodingContainer where K == DNSRecord.CodingKeys {
359 func decodeLossyString(forKey key: K) throws -> String {
360 if let stringValue = try decodeIfPresent(String.self, forKey: key) {
361 return stringValue
362 }
363
364 if let intValue = try decodeIfPresent(Int.self, forKey: key) {
365 return String(intValue)
366 }
367
368 throw DecodingError.keyNotFound(
369 key,
370 DecodingError.Context(codingPath: codingPath, debugDescription: "Missing required value for \(key.stringValue).")
371 )
372 }
373
374 func decodeLossyIntIfPresent(forKey key: K) throws -> Int? {
375 if let intValue = try decodeIfPresent(Int.self, forKey: key) {
376 return intValue
377 }
378
379 if let stringValue = try decodeIfPresent(String.self, forKey: key) {
380 return Int(stringValue)
381 }
382
383 return nil
384 }
385}