Files
iKeyMon/Sources/Model/API/BaseAPI.swift

114 lines
3.3 KiB
Swift

//
// BaseAPI.swift
// iKeyMon
//
// Created by tracer on 13.11.25.
//
import Foundation
protocol ServerAPIProtocol {
associatedtype LoadType: Codable
associatedtype MemoryType: Codable
associatedtype UtilizationType: Codable
func fetchSystemInfo() async throws -> SystemInfo
func fetchLoad() async throws -> LoadType
func fetchMemory() async throws -> MemoryType
func fetchUtilization() async throws -> UtilizationType
func fetchServerSummary(apiKey: String) async throws -> ServerInfo
func restartServer(apiKey: String) async throws
}
struct SystemInfo: Codable {
let version: String
let timestamp: Date
let hostname: String
}
class BaseAPIClient {
let baseURL: URL
let session: URLSession
init(baseURL: URL, session: URLSession = .shared) {
self.baseURL = baseURL
self.session = session
}
func performRequest<T: Codable>(_ request: URLRequest, responseType: T.Type) async throws -> T {
let (data, _) = try await performDataRequest(request)
return try JSONDecoder().decode(T.self, from: data)
}
func performRequestWithoutBody(_ request: URLRequest) async throws {
_ = try await performDataRequest(request)
}
private func performDataRequest(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) {
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
guard 200...299 ~= httpResponse.statusCode else {
throw APIError.httpError(
httpResponse.statusCode,
BaseAPIClient.extractErrorMessage(from: data)
)
}
return (data, httpResponse)
}
private static func extractErrorMessage(from data: Data) -> String? {
guard !data.isEmpty else { return nil }
if let envelope = try? JSONDecoder().decode(APIErrorEnvelope.self, from: data) {
let parts = [envelope.code, envelope.message]
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
if !parts.isEmpty {
return parts.joined(separator: " ")
}
}
if let text = String(data: data, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines),
!text.isEmpty {
return text
}
return nil
}
}
enum APIError: Error, LocalizedError {
case invalidURL
case invalidResponse
case httpError(Int, String?)
case decodingError(Error)
case unsupportedFeature(String)
var errorDescription: String? {
switch self {
case .invalidURL: return "Invalid URL"
case .invalidResponse: return "Invalid response"
case .httpError(let code, let message):
if let message, !message.isEmpty {
return "HTTP Error: \(code)\n\(message)"
}
return "HTTP Error: \(code)"
case .decodingError(let error): return "Decoding error: \(error.localizedDescription)"
case .unsupportedFeature(let feature): return "\(feature) is not supported by this host"
}
}
}
private struct APIErrorEnvelope: Decodable {
let code: String?
let message: String?
}