diff options
| author | Christian Cleberg <[email protected]> | 2026-04-13 14:20:27 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-13 14:20:27 -0500 |
| commit | 5e63048ab6a741ae947f7fe42efcb5206a3f3963 (patch) | |
| tree | cf8eb2e3bc3b2edb20ef96a2599e684378120e5b /Hutch/Networking/SRHTError.swift | |
| parent | 0b84ecaf7ba0fb2cd3f3867da8dc732450b55b50 (diff) | |
| download | hutch-3.0.4.tar.gz hutch-3.0.4.tar.bz2 hutch-3.0.4.zip | |
fix: mercurial parity and graphql handlingv3.0.4
- implements consistent UX between repo types
- ensures centralized services and routes are used between repo types
- graphql error handling is centralized
- error messages are consistent
Implements: https://todo.sr.ht/~ccleberg/hutch/58
Implements: https://todo.sr.ht/~ccleberg/hutch/59
Diffstat (limited to 'Hutch/Networking/SRHTError.swift')
| -rw-r--r-- | Hutch/Networking/SRHTError.swift | 88 |
1 files changed, 80 insertions, 8 deletions
diff --git a/Hutch/Networking/SRHTError.swift b/Hutch/Networking/SRHTError.swift index b9fc4c8..e946774 100644 --- a/Hutch/Networking/SRHTError.swift +++ b/Hutch/Networking/SRHTError.swift @@ -18,7 +18,7 @@ enum SRHTError: LocalizedError, Sendable { var errorDescription: String? { switch self { case .graphQLErrors(let errors): - let messages = errors.map(\.message).joined(separator: "\n") + let messages = errors.diagnosticSummary return "GraphQL error: \(messages)" case .httpError(let code): return "Server returned HTTP \(code)." @@ -33,17 +33,21 @@ enum SRHTError: LocalizedError, Sendable { } } - var userFacingMessage: String { + nonisolated var userFacingMessage: String { switch self { case .graphQLErrors(let errors): - let firstMessage = errors.first?.message.lowercased() ?? "" - if firstMessage.contains("unauthorized") || firstMessage.contains("forbidden") { + switch errors.classification { + case .unauthorized, .forbidden: return "You do not have permission to do that." - } - if firstMessage.contains("not found") || firstMessage.contains("no rows in result set") { + case .notFound, .noRows, .missingReference, .unknownRevision: return "That content is no longer available." + case .serviceNotProvisioned: + return "That account needs to activate this SourceHut service before this action can succeed." + case .validation: + return errors.primaryMessage ?? "Please review your changes and try again." + case .other: + return "Something went wrong. Please try again." } - return "Something went wrong. Please try again." case .httpError(let code): if code == 401 { return "Please sign in again." @@ -105,7 +109,7 @@ enum SRHTError: LocalizedError, Sendable { } extension Error { - var userFacingMessage: String { + nonisolated var userFacingMessage: String { if let error = self as? SRHTError { return error.userFacingMessage } @@ -125,6 +129,22 @@ extension Error { return "Something went wrong. Please try again." } } + + nonisolated var graphQLErrors: [GraphQLError]? { + guard let srhtError = self as? SRHTError, + case let SRHTError.graphQLErrors(errors) = srhtError else { + return nil + } + return errors + } + + nonisolated func matchesGraphQLErrorClassification(_ classification: GraphQLErrorClassification) -> Bool { + graphQLErrors?.classification == classification + } + + nonisolated func containsGraphQLErrorMessage(_ fragment: String) -> Bool { + graphQLErrors?.containsMessage(fragment) == true + } } /// A single error entry from the GraphQL `errors` array. @@ -133,6 +153,58 @@ struct GraphQLError: Decodable, Sendable { let locations: [GraphQLErrorLocation]? } +enum GraphQLErrorClassification: Sendable { + case unauthorized + case forbidden + case notFound + case noRows + case missingReference + case unknownRevision + case serviceNotProvisioned + case validation + case other +} + +extension Array where Element == GraphQLError { + nonisolated var classification: GraphQLErrorClassification { + if containsMessage("unauthorized") { return .unauthorized } + if containsMessage("forbidden") { return .forbidden } + if containsMessage("reference not found") { return .missingReference } + if containsMessage("no rows in result set") { return .noRows } + if containsMessage("unknown revision") || containsMessage("path not in the working tree") { + return .unknownRevision + } + if containsMessage("not found") || containsMessage("no such") || containsMessage("missing revision") { + return .notFound + } + if containsMessage("no such repository or user found") { + return .serviceNotProvisioned + } + if let primaryMessage, !primaryMessage.isEmpty { + return .validation + } + return .other + } + + nonisolated var primaryMessage: String? { + let candidates = map(\.message) + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + return candidates.first + } + + nonisolated var diagnosticSummary: String { + map(\.message) + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + .joined(separator: "\n") + } + + nonisolated func containsMessage(_ fragment: String) -> Bool { + contains { $0.message.localizedCaseInsensitiveContains(fragment) } + } +} + struct GraphQLErrorLocation: Decodable, Sendable { let line: Int let column: Int |
