import Foundation /// Zero-backend DSClient: loads the 14 bundled fixtures (Bundle.module) with the SAME decoder the /// live client will use, so "it previews" == "the DTOs decode the real shapes". Powers SwiftUI /// previews and the contract acceptance test. Write-side methods return a plausible echo. /// /// Holds no non-Sendable stored state (uses Bundle.module locally) → Sendable. public struct FixtureDSClient: DSClient { public init() {} private func load(_ name: String, as type: T.Type) throws -> T { guard let url = Bundle.module.url(forResource: name, withExtension: "json") else { throw DSError.notFound(message: "fixture \(name).json") } let data = try Data(contentsOf: url) do { return try DSDecoder.make().decode(T.self, from: data) } catch { throw DSError.decoding("\(name): \(error)") } } // Auth public func login(username: String, password: String, totpCode: String?) async throws -> AccessTokenResponse { try load("auth_login", as: AccessTokenResponse.self) } public func me() async throws -> UserResponse { try load("auth_me", as: UserResponse.self) } public func refresh() async throws -> AccessTokenResponse { try load("auth_login", as: AccessTokenResponse.self) } public func logout() async throws {} // Documents public func documents(_ query: DocumentListQuery) async throws -> DocumentListResponse { try load("documents_list", as: DocumentListResponse.self) } public func document(id: Int) async throws -> DocumentDetailResponse { // id 5301 = the pending-MD fixture (extracted_text fallback); otherwise the completed MD-first fixture. try load(id == 5301 ? "document_detail_pending_md" : "document_detail", as: DocumentDetailResponse.self) } public func documentContent(id: Int) async throws -> DocumentContentResponse { try load("document_content", as: DocumentContentResponse.self) } public func documentTree() async throws -> [DomainTreeNode] { try load("documents_tree", as: [DomainTreeNode].self) } public func categoryCounts() async throws -> CategoryCounts { try load("documents_stats", as: CategoryCounts.self) } public func duplicates() async throws -> DuplicatesResponse { try load("documents_duplicates", as: DuplicatesResponse.self) } public func patchDocument(id: Int, _ update: DocumentUpdate) async throws -> DocumentResponse { try load("document_detail", as: DocumentDetailResponse.self).base } public func putContent(id: Int, content: String) async throws {} public func deleteDocument(id: Int) async throws {} // Search / Ask public func search(q: String, mode: SearchMode?, page: Int?, debug: Bool?) async throws -> SearchResponse { try load("search", as: SearchResponse.self) } public func ask(q: String, limit: Int?, backend: String?, debug: Bool?) async throws -> AskResponse { try load("ask", as: AskResponse.self) } // Memos public func memos(_ query: MemoListQuery) async throws -> MemoListResponse { try load("memos_list", as: MemoListResponse.self) } public func memo(id: Int) async throws -> MemoResponse { try load("memo_detail", as: MemoResponse.self) } public func createMemo(_ create: MemoCreate) async throws -> MemoResponse { try load("memo_detail", as: MemoResponse.self) } public func patchMemo(id: Int, _ update: MemoUpdate) async throws -> MemoResponse { try load("memo_detail", as: MemoResponse.self) } public func pinMemo(id: Int, pinned: Bool) async throws -> MemoResponse { try load("memo_detail", as: MemoResponse.self) } public func archiveMemo(id: Int, archived: Bool) async throws -> MemoResponse { try load("memo_detail", as: MemoResponse.self) } public func toggleMemoTask(id: Int, taskIndex: Int, checked: Bool) async throws -> MemoResponse { try load("memo_detail", as: MemoResponse.self) } public func deleteMemo(id: Int) async throws {} // Digest public func digest(date: String?, country: String?) async throws -> DigestResponse { try load("digest", as: DigestResponse.self) } }