zitare-native/Sources/Core/API/ZitareAPI.swift
Till JS f5a26b2392 chore(format): disable redundantSelf + redundantSendable in swiftformat
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>
2026-05-22 12:37:46 +02:00

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)"
}
}