130 lines
4.7 KiB
Swift
130 lines
4.7 KiB
Swift
import Foundation
|
||
|
||
protocol APIClientProtocol {
|
||
func get<T: Decodable>(path: String, queryItems: [URLQueryItem], token: String?) async throws -> T
|
||
func post<T: Decodable, Body: Encodable>(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<T: Decodable>(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<T: Decodable, Body: Encodable>(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<T: Decodable>(_ 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")!
|
||
}
|
||
}
|