import Foundation /// Was beim Reden mit dem Certain-Backend schiefgehen kann. /// /// Die Fälle sind nach dem geschnitten, was die App damit *tun* muss – nicht /// nach HTTP-Status. `pflichtOffen` führt auf einen anderen Bildschirm, /// `konflikt` ist eine Rückfrage, die sich mit „trotzdem" wiederholen lässt, /// alles übrige ist eine Meldung. enum APIFehler: Error, LocalizedError, Sendable, Equatable { /// Netz weg, Server nicht erreichbar, Zeitüberschreitung. case keineVerbindung(String) /// Unter der Adresse antwortet etwas, aber kein Certain. case keinCertainServer /// 401 – Marke ungültig oder widerrufen, Sitzung abgelaufen. case nichtAngemeldet /// 403 mit maschinenlesbarem Schlüssel: erst muss etwas erledigt werden. case pflichtOffen(Pflicht) /// 403 aus einem anderen Grund. case verboten(String) /// 404 – gibt es nicht, oder gehört jemand anderem. case nichtGefunden /// 409 – meistens eine Rückfrage, die sich mit `trotzdem = true` /// wiederholen lässt. Der Text ist für den Nutzer geschrieben. case konflikt(String) /// 400 oder 422. case ungueltig(String) /// Alles übrige, was der Server sagt. case server(status: Int, text: String) /// Die Antwort passt nicht zu dem, was wir erwarten. case antwortUnlesbar(String) /// Was der Benutzer erledigen muss, bevor die API ihn durchlässt. /// Die Werte sind die Schlüssel aus `backend/app/deps.py`. enum Pflicht: String, Sendable { case passwortWechseln = "password_change_required" case zweiterFaktor = "mfa_setup_required" } var errorDescription: String? { switch self { case .keineVerbindung(let grund): return String(format: String(localized: "fehler.keineVerbindung"), grund) case .keinCertainServer: return String(localized: "fehler.keinCertainServer") case .nichtAngemeldet: return String(localized: "fehler.nichtAngemeldet") case .pflichtOffen(.passwortWechseln): return String(localized: "fehler.passwortWechseln") case .pflichtOffen(.zweiterFaktor): return String(localized: "fehler.zweiterFaktor") case .verboten(let text), .konflikt(let text), .ungueltig(let text): return text case .nichtGefunden: return String(localized: "fehler.nichtGefunden") case .server(let status, let text): return text.isEmpty ? String(format: String(localized: "fehler.server"), status) : text case .antwortUnlesbar(let grund): return String(format: String(localized: "fehler.antwortUnlesbar"), grund) } } /// Ob ein erneuter Versuch überhaupt Sinn ergibt. var lohntWiederholung: Bool { switch self { case .keineVerbindung, .server: return true default: return false } } } /// Der Fehlerkörper des Backends. /// /// `detail` ist dort mal eine Zeichenkette (`{"detail": "Nicht gefunden"}`), /// mal eine Liste von Pydantic-Meldungen (bei 422). Beides muss hier durch, /// sonst verliert die App bei jedem Eingabefehler die Meldung. struct Fehlerantwort: Decodable, Sendable { let detail: String private enum Schluessel: String, CodingKey { case detail } private struct Einzelfehler: Decodable { let msg: String? } init(from decoder: Decoder) throws { let behaelter = try decoder.container(keyedBy: Schluessel.self) if let text = try? behaelter.decode(String.self, forKey: .detail) { detail = text } else if let liste = try? behaelter.decode([Einzelfehler].self, forKey: .detail) { detail = liste.compactMap(\.msg).joined(separator: ", ") } else { detail = "" } } /// Versucht, aus Antwortdaten eine Meldung zu ziehen. Schlägt das fehl, /// kommt eine leere Zeichenkette – nie ein Fehler, denn wir sind hier /// bereits im Fehlerfall. static func ausDaten(_ daten: Data) -> String { (try? JSONDecoder().decode(Fehlerantwort.self, from: daten))?.detail ?? "" } }