import Foundation enum RestAPIError: Error, LocalizedError { case invalidURL(String) case badStatus(Int, String?) case decoding(Error) var errorDescription: String? { switch self { case .invalidURL(let s): return "Ungültige URL: \(s)" case .badStatus(let code, let body): return "HTTP \(code): \(body ?? "")" case .decoding(let e): return "JSON: \(e.localizedDescription)" } } } /// REST-Schicht analog `RestApi.kt`. final class RestAPIClient: @unchecked Sendable { private let baseURLString: String private let session: URLSession private let decoder: JSONDecoder private let encoder: JSONEncoder init(baseURLString: String, session: URLSession) { self.baseURLString = baseURLString.trimmingCharacters(in: CharacterSet(charactersIn: "/")) self.session = session self.decoder = JSONDecoder() self.encoder = JSONEncoder() } func sessionStatus() async throws -> SessionResponse { try await request(path: "api/session", method: "GET", body: nil) } func logout() async throws -> LogoutResponse { let url = try url(for: "api/logout") var req = URLRequest(url: url) req.httpMethod = "POST" req.setValue("application/json", forHTTPHeaderField: "Accept") let (data, response) = try await session.data(for: req) guard let http = response as? HTTPURLResponse else { throw RestAPIError.badStatus(-1, nil) } guard (200 ... 299).contains(http.statusCode) else { let text = String(data: data, encoding: .utf8) throw RestAPIError.badStatus(http.statusCode, text) } if data.isEmpty { return LogoutResponse(success: true) } do { return try decoder.decode(LogoutResponse.self, from: data) } catch { throw RestAPIError.decoding(error) } } func countries() async throws -> [String: String] { try await request(path: "api/countries", method: "GET", body: nil) } func feedback() async throws -> FeedbackResponse { try await request(path: "api/feedback", method: "GET", body: nil) } func feedbackAdminStatus() async throws -> FeedbackAdminStatusResponse { try await request(path: "api/feedback/admin-status", method: "GET", body: nil) } func submitFeedback(_ requestBody: FeedbackRequest) async throws { let data = try encoder.encode(requestBody) try await requestVoid(path: "api/feedback", method: "POST", body: data) } func feedbackAdminLogin(_ requestBody: FeedbackAdminLoginRequest) async throws -> FeedbackAdminStatusResponse { let data = try encoder.encode(requestBody) return try await request(path: "api/feedback/admin-login", method: "POST", body: data) } func feedbackAdminLogout() async throws { try await requestVoid(path: "api/feedback/admin-logout", method: "POST", body: Data()) } func deleteFeedback(id: String) async throws { let encoded = id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? id try await requestVoid(path: "api/feedback/\(encoded)", method: "DELETE", body: nil) } func partners() async throws -> [PartnerLinkDto] { try await request(path: "api/partners", method: "GET", body: nil) } /// Multipart-Feld `image` wie OkHttp `MultipartBody.Part`. func uploadImage(data: Data, fileName: String, mimeType: String) async throws -> (response: ImageUploadResponse, httpStatus: Int) { let url = try url(for: "api/upload-image") let boundary = "Boundary-\(UUID().uuidString)" var req = URLRequest(url: url) req.httpMethod = "POST" req.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") var body = Data() body.append("--\(boundary)\r\n".data(using: .utf8)!) body.append( "Content-Disposition: form-data; name=\"image\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)! ) body.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!) body.append(data) body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!) req.httpBody = body let (respData, response) = try await session.data(for: req) guard let http = response as? HTTPURLResponse else { throw RestAPIError.badStatus(-1, nil) } guard (200 ... 299).contains(http.statusCode) else { let text = String(data: respData, encoding: .utf8) throw RestAPIError.badStatus(http.statusCode, text) } do { let decoded = try decoder.decode(ImageUploadResponse.self, from: respData) return (decoded, http.statusCode) } catch { throw RestAPIError.decoding(error) } } // MARK: - Request private func url(for path: String) throws -> URL { let trimmed = path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) guard let url = URL(string: "\(baseURLString)/\(trimmed)") else { throw RestAPIError.invalidURL("\(baseURLString)/\(trimmed)") } return url } private func requestVoid(path: String, method: String, body: Data?) async throws { let url = try url(for: path) var req = URLRequest(url: url) req.httpMethod = method req.httpBody = body if let body, !body.isEmpty { req.setValue("application/json", forHTTPHeaderField: "Content-Type") } req.setValue("application/json", forHTTPHeaderField: "Accept") let (data, response) = try await session.data(for: req) guard let http = response as? HTTPURLResponse else { throw RestAPIError.badStatus(-1, nil) } guard (200 ... 299).contains(http.statusCode) else { let text = String(data: data, encoding: .utf8) throw RestAPIError.badStatus(http.statusCode, text) } } private func request(path: String, method: String, body: Data?) async throws -> T { let url = try url(for: path) var req = URLRequest(url: url) req.httpMethod = method req.httpBody = body if let body, !body.isEmpty { req.setValue("application/json", forHTTPHeaderField: "Content-Type") } req.setValue("application/json", forHTTPHeaderField: "Accept") let (data, response) = try await session.data(for: req) guard let http = response as? HTTPURLResponse else { throw RestAPIError.badStatus(-1, nil) } guard (200 ... 299).contains(http.statusCode) else { let text = String(data: data, encoding: .utf8) throw RestAPIError.badStatus(http.statusCode, text) } do { return try decoder.decode(T.self, from: data) } catch { throw RestAPIError.decoding(error) } } }