Default `--self remove` strippt `self.` aus @autoclosure-Calls (Logger.info) und Closure-Captures, was Swift-6-strict-concurrency dann als "implicit use of self in closure" rejected. Default `redundantSendable` strippt `Sendable` von Codable-DTOs, die über actor-Grenzen wandern müssen. Beide Regeln aus. Zusätzlich Lauf über alle Files: harmlose Whitespace-/ Trailing-Comma-/Optional-Init-Normalisierung in 5 Files. `self.` und `Sendable` bleiben überall erhalten. Build grün. Hintergrund: η-0-Lauf hat das aktiv gemacht und Submit-DTOs zerschossen, die ich dann von Hand revertieren musste. Dieser Commit verhindert die Wiederholung in η-1+. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
108 lines
3.8 KiB
Swift
108 lines
3.8 KiB
Swift
import Foundation
|
|
import ManaCore
|
|
|
|
/// Zitare-spezifischer API-Client. Wrapper um `AuthenticatedTransport`
|
|
/// aus ManaCore, der die zitare-api-Endpoints kennt.
|
|
actor ZitareAPI {
|
|
let transport: AuthenticatedTransport
|
|
let decoder: JSONDecoder
|
|
let encoder: JSONEncoder
|
|
|
|
init(auth: AuthClient) {
|
|
transport = AuthenticatedTransport(baseURL: AppConfig.apiBaseURL, auth: auth)
|
|
decoder = JSONDecoder()
|
|
encoder = JSONEncoder()
|
|
}
|
|
|
|
/// `GET /healthz` — verifiziert dass zitare-api erreichbar ist.
|
|
/// Öffentlicher Endpoint, läuft direkt via `URLSession` (nicht
|
|
/// `AuthenticatedTransport`), damit auch nicht-eingeloggte Apps
|
|
/// die API-Erreichbarkeit prüfen können.
|
|
func healthCheck() async throws -> Bool {
|
|
let url = AppConfig.apiBaseURL.appendingPathComponent("healthz")
|
|
let (_, response) = try await URLSession.shared.data(from: url)
|
|
guard let http = response as? HTTPURLResponse else { return false }
|
|
return http.statusCode == 200
|
|
}
|
|
|
|
/// `POST /api/v1/quotes` — Quote-Draft einreichen. Server-Schema:
|
|
/// `zitare/apps/api/src/routes/quotes.ts:submissionSchema`. Pflicht:
|
|
/// text + acceptedTos:true + (authorName ODER authorSlug). Optionale
|
|
/// Source-Felder kommen mit, wenn der User sie ausgefüllt hat.
|
|
func submitQuote(_ draft: QuoteDraft) async throws -> SubmittedQuote {
|
|
let payload = try encoder.encode(draft)
|
|
let (data, http) = try await transport.request(
|
|
path: "/api/v1/quotes",
|
|
method: "POST",
|
|
body: payload
|
|
)
|
|
try ensureOK(http, data: data)
|
|
return try decoder.decode(SubmittedQuote.self, from: data)
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private func ensureOK(_ http: HTTPURLResponse, data: Data) throws {
|
|
guard http.statusCode >= 400 else { return }
|
|
let body = String(data: data, encoding: .utf8) ?? ""
|
|
let code = Self.parseErrorCode(from: body)
|
|
throw ZitareAPIError(status: http.statusCode, code: code, body: body)
|
|
}
|
|
|
|
private static func parseErrorCode(from body: String) -> String? {
|
|
guard let data = body.data(using: .utf8),
|
|
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
let code = obj["error"] as? String
|
|
else { return nil }
|
|
return code
|
|
}
|
|
}
|
|
|
|
// MARK: - DTOs
|
|
|
|
/// Submit-Payload für `POST /api/v1/quotes`. JSON-Keys matchen
|
|
/// `submissionSchema` aus `zitare/apps/api/src/routes/quotes.ts`.
|
|
struct QuoteDraft: Codable, Sendable, Equatable {
|
|
var text: String
|
|
var language: String
|
|
var authorName: String?
|
|
var authorSlug: String?
|
|
var sourceTitle: String?
|
|
var sourceKind: SourceKind?
|
|
var sourceYear: Int?
|
|
var editReason: String?
|
|
var acceptedTos: Bool
|
|
|
|
enum SourceKind: String, Codable, Sendable, CaseIterable, Identifiable {
|
|
case book, article, talk, film, other
|
|
var id: String {
|
|
rawValue
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Response von `POST /api/v1/quotes`. Server gibt das volle
|
|
/// QuoteRow zurück — wir brauchen für die UI nur slug + status.
|
|
struct SubmittedQuote: Codable, Sendable {
|
|
let slug: String
|
|
let status: String
|
|
}
|
|
|
|
/// Lokalisierter API-Fehler. `code` ist der server-seitige Code aus
|
|
/// `{ "error": "<code>" }`, der via `api.error.<code>` im
|
|
/// Localizable.xcstrings nachgeschlagen wird.
|
|
struct ZitareAPIError: LocalizedError {
|
|
let status: Int
|
|
let code: String?
|
|
let body: String
|
|
|
|
var errorDescription: String? {
|
|
if let code, !code.isEmpty {
|
|
let key = "api.error.\(code)"
|
|
let localized = Bundle.main.localizedString(forKey: key, value: nil, table: nil)
|
|
if localized != key { return localized }
|
|
return "Fehler \(status): \(code)"
|
|
}
|
|
return "HTTP \(status)"
|
|
}
|
|
}
|