// // 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(_ 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? }