Files

136 lines
5.6 KiB
Swift

import Foundation
/// Single source of truth mapping each DSClient call to HTTP method + path + query + body.
/// Trailing slashes are significant (e.g. `documents/`, `search/`) and are preserved by building the
/// URL from the base string rather than appendingPathComponent (which strips them).
enum DSEndpoint {
case login(String, String, String?)
case me
case refresh
case logout
case documents(DocumentListQuery)
case document(Int)
case documentContent(Int)
case documentTree
case categoryCounts
case duplicates
case patchDocument(Int, DocumentUpdate)
case putContent(Int, String)
case deleteDocument(Int)
case search(String, SearchMode?, Int?, Bool?)
case ask(String, Int?, String?, Bool?)
case memos(MemoListQuery)
case memo(Int)
case createMemo(MemoCreate)
case patchMemo(Int, MemoUpdate)
case pinMemo(Int, Bool)
case archiveMemo(Int, Bool)
case toggleMemoTask(Int, Int, Bool)
case deleteMemo(Int)
case digest(String?, String?)
var method: String {
switch self {
case .login, .refresh, .logout, .createMemo: return "POST"
case .patchDocument, .patchMemo, .pinMemo, .archiveMemo, .toggleMemoTask: return "PATCH"
case .putContent: return "PUT"
case .deleteDocument, .deleteMemo: return "DELETE"
default: return "GET"
}
}
var path: String {
switch self {
case .login: return "auth/login"
case .me: return "auth/me"
case .refresh: return "auth/refresh"
case .logout: return "auth/logout"
case .documents: return "documents/"
case .document(let id): return "documents/\(id)"
case .documentContent(let id): return "documents/\(id)/content"
case .documentTree: return "documents/tree"
case .categoryCounts: return "documents/stats/category-counts"
case .duplicates: return "documents/duplicates"
case .patchDocument(let id, _): return "documents/\(id)"
case .putContent(let id, _): return "documents/\(id)/content"
case .deleteDocument(let id): return "documents/\(id)"
case .search: return "search/"
case .ask: return "search/ask"
case .memos: return "memos/"
case .memo(let id): return "memos/\(id)"
case .createMemo: return "memos/"
case .patchMemo(let id, _): return "memos/\(id)"
case .pinMemo(let id, _): return "memos/\(id)/pin"
case .archiveMemo(let id, _): return "memos/\(id)/archive"
case .toggleMemoTask(let id, let idx, _): return "memos/\(id)/tasks/\(idx)"
case .deleteMemo(let id): return "memos/\(id)"
case .digest: return "digest"
}
}
/// Bearer header applies to everything except login/refresh (refresh rides the HttpOnly cookie).
var requiresBearer: Bool {
switch self {
case .login, .refresh: return false
default: return true
}
}
var queryItems: [URLQueryItem] {
var items: [URLQueryItem] = []
func add(_ name: String, _ value: String?) { if let value { items.append(URLQueryItem(name: name, value: value)) } }
switch self {
case .documents(let q):
add("page", String(q.page)); add("page_size", String(q.pageSize))
add("domain", q.domain); add("sub_group", q.subGroup); add("source", q.source)
add("format", q.format); add("review_status", q.reviewStatus); add("category", q.category)
case .search(let qq, let mode, let page, let debug):
add("q", qq); add("mode", mode?.rawValue); add("page", page.map(String.init)); add("debug", debug.map(String.init))
case .ask(let qq, let limit, let backend, let debug):
add("q", qq); add("limit", limit.map(String.init)); add("backend", backend); add("debug", debug.map(String.init))
case .memos(let q):
add("page", String(q.page)); add("page_size", String(q.pageSize))
add("pinned", q.pinned.map(String.init)); add("archived", q.archived.map(String.init))
case .digest(let date, let country):
add("date", date); add("country", country)
default:
break
}
return items
}
func httpBody(_ encoder: JSONEncoder) throws -> Data? {
switch self {
case .login(let u, let p, let totp):
return try encoder.encode(LoginBody(username: u, password: p, totpCode: totp))
case .patchDocument(_, let update):
return try encoder.encode(update)
case .putContent(_, let content):
return try encoder.encode(ContentBody(content: content))
case .createMemo(let create):
return try encoder.encode(create)
case .patchMemo(_, let update):
return try encoder.encode(update)
case .pinMemo(_, let pinned):
return try encoder.encode(PinnedBody(pinned: pinned))
case .archiveMemo(_, let archived):
return try encoder.encode(ArchivedBody(archived: archived))
case .toggleMemoTask(_, _, let checked):
return try encoder.encode(CheckedBody(checked: checked))
default:
return nil
}
}
}
private struct LoginBody: Encodable {
let username: String
let password: String
let totpCode: String?
enum CodingKeys: String, CodingKey { case username, password; case totpCode = "totp_code" }
}
private struct ContentBody: Encodable { let content: String }
private struct PinnedBody: Encodable { let pinned: Bool }
private struct ArchivedBody: Encodable { let archived: Bool }
private struct CheckedBody: Encodable { let checked: Bool }