import Foundation /// A failure from either API surface, already mapped off the exit code so /// callers switch on meaning rather than on numbers. /// /// exit http case /// 2 400 usage a bug in the app /// 3 404 notFound an empty state, not an error banner /// 4 403 denied the message explains the rule; show it verbatim /// 1, 5 500 failure retried once by the client, then surfaced /// — 401 unauthorized the token is invalid, expired or revoked /// — 429 rateLimited Retry-After was longer than the client will wait nonisolated enum GitbayError: LocalizedError, Sendable { case usage(String) case notFound(String) case denied(String) case failure(String) case unauthorized(String) case rateLimited(retryAfter: TimeInterval) /// The command exists but is not read-only, so it cannot be reached /// with GET. Also an app bug: it means we routed a write through `read`. case notReadable(String) case transport(any Error) case decoding(any Error) /// A major protocol version the app does not understand. case protocolMismatch(Int) /// The client refused to send the token somewhere other than the instance. case unexpectedHost(URL) /// Text for a person. `denied` and `notFound` come from the server /// verbatim — those messages exist to explain a rule, and rewording /// them loses the explanation. var userFacingMessage: String { switch self { case .denied(let message), .notFound(let message): message case .unauthorized: "Your token is no longer valid. Sign in again." case .rateLimited(let seconds): "Too many requests. Try again in \(Int(seconds.rounded()))s." case .failure(let message): message.isEmpty ? "The server could not complete that." : message case .transport: "Check your connection and try again." // A usage or protocol failure is ours, not the user's. argv never // reaches the screen. case .usage, .notReadable, .decoding, .protocolMismatch, .unexpectedHost: "Something went wrong. Please try again." } } var errorDescription: String? { userFacingMessage } /// True when the right response is an empty screen rather than a banner. var isEmptyState: Bool { if case .notFound = self { return true } return false } /// True when the token needs replacing and the app should sign out. var requiresReauthentication: Bool { if case .unauthorized = self { return true } return false } }