gitbay/Networking/GitbayError.swift

v1.13.1
gitbay-ios/gitbay/Networking/GitbayError.swift history · blame · raw

77 lines · 3411 bytes

 1import Foundation
 2
 3/// A failure from either API surface, already mapped off the exit code so
 4/// callers switch on meaning rather than on numbers.
 5///
 6///     exit  http  case
 7///        2   400  usage         a bug in the app
 8///        3   404  notFound      an empty state, not an error banner
 9///        4   403  denied        the message explains the rule; show it verbatim
10///     1, 5   500  failure       retried once by the client, then surfaced
11///        —   401  unauthorized  the token is invalid, expired or revoked
12///        —   429  rateLimited   Retry-After was longer than the client will wait
13nonisolated enum GitbayError: LocalizedError, Sendable {
14    case usage(String)
15    case notFound(String)
16    case denied(String)
17    case failure(String)
18    case unauthorized(String)
19    case rateLimited(retryAfter: TimeInterval)
20    /// The command exists but is not read-only, so it cannot be reached
21    /// with GET. Also an app bug: it means we routed a write through `read`.
22    case notReadable(String)
23    case transport(any Error)
24    case decoding(any Error)
25    /// A major protocol version the app does not understand.
26    case protocolMismatch(Int)
27    /// The client refused to send the token somewhere other than the instance.
28    case unexpectedHost(URL)
29
30    /// Text for a person. `denied` and `notFound` come from the server
31    /// verbatim — those messages exist to explain a rule, and rewording
32    /// them loses the explanation.
33    var userFacingMessage: String {
34        switch self {
35        // Since krz/gitbay#257 an expiring token cannot create credentials
36        // (SSH keys, email addresses), and the sign-in hint mints one.
37        case .denied(let message) where message.contains("creates a credential"):
38            "This needs a token without an expiry. Add it on the web under Settings, or over SSH."
39        case .denied(let message), .notFound(let message):
40            message
41        case .unauthorized:
42            "Your token is no longer valid. Sign in again."
43        case .rateLimited(let seconds):
44            "Too many requests. Try again in \(Int(seconds.rounded()))s."
45        case .failure(let message):
46            message.isEmpty ? "The server could not complete that." : message
47        case .transport:
48            "Check your connection and try again."
49        // A usage or protocol failure is ours, not the user's. argv never
50        // reaches the screen.
51        case .usage, .notReadable, .decoding, .protocolMismatch, .unexpectedHost:
52            "Something went wrong. Please try again."
53        }
54    }
55
56    var errorDescription: String? { userFacingMessage }
57
58    /// `userFacingMessage` for a command whose argument the user typed: a
59    /// query, a name, a check's name. A usage refusal is then about what
60    /// they wrote, not the app's argv, and its message says what to fix.
61    var messageForTypedInput: String {
62        if case .usage(let message) = self, !message.isEmpty { return message }
63        return userFacingMessage
64    }
65
66    /// True when the right response is an empty screen rather than a banner.
67    var isEmptyState: Bool {
68        if case .notFound = self { return true }
69        return false
70    }
71
72    /// True when the token needs replacing and the app should sign out.
73    var requiresReauthentication: Bool {
74        if case .unauthorized = self { return true }
75        return false
76    }
77}