Refactor code structure for improved readability and maintainability

This commit is contained in:
2026-10-19 19:36:19 +03:00
parent 170a44c377
commit 5f7bffd6e4
17 changed files with 454 additions and 85 deletions

View File

@@ -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.