summaryrefslogtreecommitdiff
path: root/Hutch/Networking/SRHTError.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-13 14:20:27 -0500
committerChristian Cleberg <[email protected]>2026-04-13 14:20:27 -0500
commit5e63048ab6a741ae947f7fe42efcb5206a3f3963 (patch)
treecf8eb2e3bc3b2edb20ef96a2599e684378120e5b /Hutch/Networking/SRHTError.swift
parent0b84ecaf7ba0fb2cd3f3867da8dc732450b55b50 (diff)
downloadhutch-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.swift88
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