Files
singlechat/ios/YpChat/Data/RestAPIClient.swift
Torsten Schulz (local) bfe28a25fa
All checks were successful
Deploy SingleChat / deploy (push) Successful in 40s
android app, extension of stats
2026-08-10 14:03:34 +02:00

183 lines
6.9 KiB
Swift
Executable File

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<T: Decodable>(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)
}
}
}