0becf7829e
- Package.swift: AI (S2-owned) + DSKit (models/client/fixtures) + DSKitTests, tools 6.2, .swiftLanguageMode(.v6), .macOS(.v26) - JSONValue (Sendable AnyCodable), DSDate (value-type ISO8601FormatStyle cascade, date-only UTC), explicit-CodingKeys decoder - Models: Auth/Document(+Detail flat-compose, MD-first)/Catalog/Search+Ask/Memo/Digest; non-optional limited to id/file_type/created+updated_at/total - DSClient protocol + FixtureDSClient (Bundle.module, zero backend) + DSError + DSConfig + DownloadURL (?token= query) - Tests: 14-fixture contract acceptance (value asserts) + JSONValue number trap + Ask round-trip + AI router fallback/explicit-unavailable swift build + swift test green (19 tests). Sources/AI untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
82 lines
4.1 KiB
Swift
82 lines
4.1 KiB
Swift
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<T: Decodable>(_ 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)
|
|
}
|
|
}
|