183 lines
6.9 KiB
Swift
Executable File
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)
|
|
}
|
|
}
|
|
}
|