summaryrefslogtreecommitdiffstats
path: root/Sources/Cepis/Errors.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Sources/Cepis/Errors.swift')
-rw-r--r--Sources/Cepis/Errors.swift82
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 }
+}