diff options
Diffstat (limited to 'Sources/Cepis/Errors.swift')
| -rw-r--r-- | Sources/Cepis/Errors.swift | 82 |
1 files changed, 82 insertions, 0 deletions
diff --git a/Sources/Cepis/Errors.swift b/Sources/Cepis/Errors.swift new file mode 100644 index 0000000..1472f81 --- /dev/null +++ b/Sources/Cepis/Errors.swift @@ -0,0 +1,82 @@ +import Foundation + +/// Every failure this library produces. +/// +/// One type, so `catch let error as CepisError` handles anything that can go +/// wrong. The cases split by **the decision the caller has to make**, not by +/// the HTTP status that caused them: +/// +/// - ``invalidCEP(_:)`` and ``cepNotFound(_:)`` are problems with the +/// *request*. Retrying is pointless — the same CEP will fail again. +/// - ``networkError(message:)``, ``serverError(message:status:)`` and +/// ``rateLimited(message:retryAfter:)`` are problems with *transport*. +/// Retrying may help, and the client already retried on its own before +/// throwing any of them. +/// +/// That split is why ``cepNotFound(_:)`` is not "the HTTP 404 case": nothing +/// about it is tied to a status line. +public enum CepisError: Error, Sendable { + /// The CEP is not 8 digits. + /// + /// Usually thrown **without** a request ever being made — the client + /// checks the format before going to the network. Also thrown when the + /// API itself answers 400, in which case the payload carries the API's own + /// message. + case invalidCEP(String) + + /// The CEP is well-formed but does not exist in the DNE database. + case cepNotFound(String) + + /// The API (or the edge in front of it) answered 429 on every attempt. + /// + /// `retryAfter` is what the server asked for, in seconds, or `nil` if it + /// did not say. The client already honoured that value across the attempts + /// it made; it is exposed here for callers who want to reschedule the work. + case rateLimited(message: String, retryAfter: TimeInterval?) + + /// The API answered 5xx on every attempt. `status` is the last one seen. + case serverError(message: String, status: Int) + + /// The request never completed: DNS, refused connection, timeout, TLS. + /// + /// Thrown only after every configured attempt has failed. + case networkError(message: String) + + /// A success status came back with a body that cannot be parsed. + /// + /// In practice this means something between you and the API is answering + /// instead of it — a captive Wi-Fi portal, a corporate proxy, an HTML + /// error page. Useful as a diagnosis: the problem is not in your code. + case invalidResponse(String) + + /// The human-readable message carried by the case. + public var message: String { + switch self { + case .invalidCEP(let message): return message + case .cepNotFound(let message): return message + case .rateLimited(let message, _): return message + case .serverError(let message, _): return message + case .networkError(let message): return message + case .invalidResponse(let message): return message + } + } + + /// Whether making the same call again later stands a chance of succeeding. + /// + /// `true` for rate limiting, server errors and network failures; `false` + /// for a malformed or absent CEP, which will fail identically forever. + public var isRetriable: Bool { + switch self { + case .rateLimited, .serverError, .networkError: return true + case .invalidCEP, .cepNotFound, .invalidResponse: return false + } + } +} + +extension CepisError: CustomStringConvertible { + public var description: String { message } +} + +extension CepisError: LocalizedError { + public var errorDescription: String? { message } +} |