mirror of
https://github.com/yattee/yattee.git
synced 2026-02-19 17:29:45 +00:00
164 lines
5.2 KiB
Swift
164 lines
5.2 KiB
Swift
//
|
|
// APIError.swift
|
|
// Yattee
|
|
//
|
|
// Typed errors for network operations.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// Errors that can occur during API operations.
|
|
enum APIError: Error, LocalizedError, Equatable, Sendable {
|
|
/// The URL could not be constructed.
|
|
case invalidURL
|
|
|
|
/// The request failed with an HTTP status code.
|
|
case httpError(statusCode: Int, message: String?)
|
|
|
|
/// The response could not be decoded.
|
|
case decodingError(String)
|
|
|
|
/// The request timed out.
|
|
case timeout
|
|
|
|
/// No network connection available.
|
|
case noConnection
|
|
|
|
/// The request was cancelled.
|
|
case cancelled
|
|
|
|
/// Server returned an error message.
|
|
case serverError(String)
|
|
|
|
/// Rate limited by the server.
|
|
case rateLimited(retryAfter: TimeInterval?)
|
|
|
|
/// Authentication required or failed.
|
|
case unauthorized
|
|
|
|
/// Resource not found, with optional server-provided detail message.
|
|
case notFound(String?)
|
|
|
|
/// Comments are disabled for this video.
|
|
case commentsDisabled
|
|
|
|
/// No suitable instance available.
|
|
case noInstance
|
|
|
|
/// No playable streams available.
|
|
case noStreams
|
|
|
|
/// The request parameters were invalid.
|
|
case invalidRequest
|
|
|
|
/// Operation not supported by this instance type.
|
|
case notSupported
|
|
|
|
/// An unknown error occurred.
|
|
case unknown(String)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .invalidURL:
|
|
return "Invalid URL"
|
|
case .httpError(let statusCode, let message):
|
|
if let message {
|
|
return message
|
|
}
|
|
return "HTTP error: \(statusCode)"
|
|
case .decodingError(let message):
|
|
return "Failed to decode response: \(message)"
|
|
case .timeout:
|
|
return "Request timed out"
|
|
case .noConnection:
|
|
return "No network connection"
|
|
case .cancelled:
|
|
return "Request was cancelled"
|
|
case .serverError(let message):
|
|
return "Server error: \(message)"
|
|
case .rateLimited(let retryAfter):
|
|
if let retryAfter {
|
|
return "Rate limited. Retry after \(Int(retryAfter)) seconds"
|
|
}
|
|
return "Rate limited"
|
|
case .unauthorized:
|
|
return "Authentication required"
|
|
case .notFound(let detail):
|
|
if let detail {
|
|
return detail
|
|
}
|
|
return "Resource not found"
|
|
case .commentsDisabled:
|
|
return "Comments are disabled"
|
|
case .noInstance:
|
|
return "No suitable instance available"
|
|
case .noStreams:
|
|
return "No playable streams available"
|
|
case .invalidRequest:
|
|
return "Invalid request"
|
|
case .notSupported:
|
|
return "Operation not supported"
|
|
case .unknown(let message):
|
|
return message
|
|
}
|
|
}
|
|
|
|
/// Whether this error is likely recoverable by retrying.
|
|
var isRetryable: Bool {
|
|
switch self {
|
|
case .timeout, .noConnection, .rateLimited:
|
|
return true
|
|
case .httpError(let statusCode, _):
|
|
return statusCode >= 500 || statusCode == 429
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
static func == (lhs: APIError, rhs: APIError) -> Bool {
|
|
switch (lhs, rhs) {
|
|
case (.invalidURL, .invalidURL),
|
|
(.timeout, .timeout),
|
|
(.noConnection, .noConnection),
|
|
(.cancelled, .cancelled),
|
|
(.unauthorized, .unauthorized),
|
|
(.commentsDisabled, .commentsDisabled),
|
|
(.noInstance, .noInstance),
|
|
(.noStreams, .noStreams),
|
|
(.invalidRequest, .invalidRequest),
|
|
(.notSupported, .notSupported):
|
|
return true
|
|
case (.notFound(let lDetail), .notFound(let rDetail)):
|
|
return lDetail == rDetail
|
|
case (.httpError(let lCode, let lMsg), .httpError(let rCode, let rMsg)):
|
|
return lCode == rCode && lMsg == rMsg
|
|
case (.decodingError(let lMsg), .decodingError(let rMsg)):
|
|
return lMsg == rMsg
|
|
case (.serverError(let lMsg), .serverError(let rMsg)):
|
|
return lMsg == rMsg
|
|
case (.rateLimited(let lRetry), .rateLimited(let rRetry)):
|
|
return lRetry == rRetry
|
|
case (.unknown(let lMsg), .unknown(let rMsg)):
|
|
return lMsg == rMsg
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// Creates a decoding error from a Swift DecodingError.
|
|
static func decodingError(_ error: DecodingError) -> APIError {
|
|
switch error {
|
|
case .typeMismatch(let type, let context):
|
|
return .decodingError("Type mismatch for \(type): \(context.debugDescription)")
|
|
case .valueNotFound(let type, let context):
|
|
return .decodingError("Value not found for \(type): \(context.debugDescription)")
|
|
case .keyNotFound(let key, let context):
|
|
return .decodingError("Key '\(key.stringValue)' not found: \(context.debugDescription)")
|
|
case .dataCorrupted(let context):
|
|
return .decodingError("Data corrupted: \(context.debugDescription)")
|
|
@unknown default:
|
|
return .decodingError(error.localizedDescription)
|
|
}
|
|
}
|
|
}
|