Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
||||
/// An error with a message ready to show; `offline` when the server could not be reached.
|
||||
/// An error with a message ready to show. `code` is the server's machine-readable reason
|
||||
/// (server/errors.js); `offline` means the data could not be fetched — no network, or the
|
||||
/// university's catalog is down — so the saved copy stays and "Повторить" makes sense.
|
||||
struct APIError: LocalizedError {
|
||||
var message: String
|
||||
var offline = false
|
||||
var code: String?
|
||||
var errorDescription: String? { message }
|
||||
}
|
||||
|
||||
@@ -25,6 +28,8 @@ struct API: Sendable {
|
||||
|
||||
private static let log = Logger(subsystem: "org.xtimms.psu.schedule", category: "API")
|
||||
private static let offline = "Нет связи с сервером расписания."
|
||||
/// Server codes after which trying again later can help.
|
||||
private static let sourceDown: Set<String> = ["source_unavailable", "source_timeout", "busy"]
|
||||
private static let retries = 3
|
||||
|
||||
// The server answers "no-cache" with an ETag: URLSession keeps the answer and asks again
|
||||
@@ -43,14 +48,21 @@ struct API: Sendable {
|
||||
if !items.isEmpty { components.queryItems = items }
|
||||
let url = components.url!
|
||||
let (data, response) = try await withRetry(url)
|
||||
guard let http = response as? HTTPURLResponse else { throw APIError(message: Self.offline, offline: true) }
|
||||
guard let http = response as? HTTPURLResponse else { throw APIError(message: Self.offline, offline: true, code: "offline") }
|
||||
guard (200..<300).contains(http.statusCode) else {
|
||||
let message = (try? JSONSerialization.jsonObject(with: data) as? [String: Any])?["error"] as? String
|
||||
// The server's catch-all 404 means it runs an older version without this endpoint.
|
||||
if http.statusCode == 404 && message == "Адрес не найден." {
|
||||
throw APIError(message: "Сервер расписания не знает запрос /\(path) — на нём старая версия. Обновите сервер.")
|
||||
let body = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
|
||||
let message = body?["error"] as? String
|
||||
// Servers before API version 5 send no code; their catch-all 404 has this text.
|
||||
let code = body?["code"] as? String
|
||||
?? (http.statusCode == 404 && message == "Адрес не найден." ? "unknown_endpoint" : nil)
|
||||
if code == "unknown_endpoint" {
|
||||
throw APIError(message: "Сервер расписания не знает запрос /\(path) — на нём старая версия. Обновите сервер.", code: code)
|
||||
}
|
||||
throw APIError(message: message ?? "Сервер ответил ошибкой \(http.statusCode).")
|
||||
throw APIError(
|
||||
message: message ?? "Сервер ответил ошибкой \(http.statusCode).",
|
||||
offline: code.map(Self.sourceDown.contains) ?? false,
|
||||
code: code
|
||||
)
|
||||
}
|
||||
do {
|
||||
return try JSONDecoder().decode(T.self, from: data)
|
||||
@@ -74,7 +86,7 @@ struct API: Sendable {
|
||||
wait *= 3
|
||||
}
|
||||
}
|
||||
throw APIError(message: Self.offline, offline: true)
|
||||
throw APIError(message: Self.offline, offline: true, code: "offline")
|
||||
}
|
||||
|
||||
/// The whole current semester; a server without range=semester answers the from/days window.
|
||||
|
||||
Reference in New Issue
Block a user