import Foundation protocol APIClientProtocol { func get(path: String, queryItems: [URLQueryItem], token: String?) async throws -> T func post(path: String, body: Body, token: String?) async throws -> T } enum APIError: LocalizedError { case invalidURL case invalidResponse case unauthorized case server(status: Int, message: String) case decoding(Error) case transport(Error) var errorDescription: String? { switch self { case .invalidURL: return "Geçersiz URL" case .invalidResponse: return "Geçersiz sunucu yanıtı" case .unauthorized: return "Oturum süresi doldu" case .server(_, let message): return message case .decoding: return "Sunucu verisi çözümlenemedi" case .transport(let error): return error.localizedDescription } } } final class APIClient: APIClientProtocol { private let baseURL: URL private let session: URLSession private let decoder: JSONDecoder private let encoder: JSONEncoder init(baseURL: URL, session: URLSession = .shared) { self.baseURL = baseURL self.session = session self.decoder = JSONDecoder() self.encoder = JSONEncoder() } func get(path: String, queryItems: [URLQueryItem] = [], token: String? = nil) async throws -> T { var request = try buildRequest(path: path, method: "GET", queryItems: queryItems, token: token) request.httpBody = nil return try await perform(request) } func post(path: String, body: Body, token: String? = nil) async throws -> T { var request = try buildRequest(path: path, method: "POST", queryItems: [], token: token) request.httpBody = try encoder.encode(body) request.setValue("application/json", forHTTPHeaderField: "Content-Type") return try await perform(request) } private func buildRequest(path: String, method: String, queryItems: [URLQueryItem], token: String?) throws -> URLRequest { guard var components = URLComponents(url: baseURL.appendingPathComponent(path), resolvingAgainstBaseURL: false) else { throw APIError.invalidURL } if !queryItems.isEmpty { components.queryItems = queryItems } guard let url = components.url else { throw APIError.invalidURL } var request = URLRequest(url: url) request.httpMethod = method request.timeoutInterval = 20 if let token { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } return request } private func perform(_ request: URLRequest) async throws -> T { #if DEBUG print("[API] \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") #endif do { let (data, response) = try await session.data(for: request) guard let http = response as? HTTPURLResponse else { throw APIError.invalidResponse } if http.statusCode == 401 { throw APIError.unauthorized } guard (200...299).contains(http.statusCode) else { let message = (try? JSONSerialization.jsonObject(with: data) as? [String: Any])?["message"] as? String ?? "Sunucu hatası" throw APIError.server(status: http.statusCode, message: message) } do { return try decoder.decode(T.self, from: data) } catch { throw APIError.decoding(error) } } catch let error as APIError { throw error } catch { throw APIError.transport(error) } } } extension Bundle { var apiBaseURL: URL { let raw = (object(forInfoDictionaryKey: "API_BASE_URL") as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" // 1) Normal case: full URL in Info.plist / xcconfig. if let url = URL(string: raw), let host = url.host, !host.isEmpty { return url } // 2) If scheme is missing (e.g. "192.168.1.124:8080"), prepend http://. if !raw.isEmpty, !raw.contains("://"), let url = URL(string: "http://\(raw)"), let host = url.host, !host.isEmpty { return url } // 3) Device-local fallback for current dev network. if let fallback = URL(string: "http://192.168.1.124:8080") { #if DEBUG print("[API] Invalid API_BASE_URL='\(raw)'. Falling back to \(fallback.absoluteString)") #endif return fallback } // 4) Last resort. return URL(string: "http://localhost:8080")! } }