use std::path::PathBuf;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use ts_rs::TS;
use crate::export::ExportPlan;
use crate::link_parser::LinkFormat;
use crate::search::SearchResults;
use crate::validate::{FixResult, ValidationResult, ValidationResultWithMeta};
use crate::workspace::{TreeNode, WorkspaceConfig};
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "bindings/")]
#[serde(tag = "type", content = "params")]
pub enum Command {
GetEntry {
path: String,
},
SaveEntry {
path: String,
content: String,
},
CreateEntry {
path: String,
#[serde(default)]
options: CreateEntryOptions,
},
DeleteEntry {
path: String,
#[serde(default)]
hard_delete: bool,
},
MoveEntry {
from: String,
to: String,
},
RenameEntry {
path: String,
new_filename: String,
},
DuplicateEntry {
path: String,
},
ConvertToIndex {
path: String,
},
ConvertToLeaf {
path: String,
},
CreateChildEntry {
parent_path: String,
},
AttachEntryToParent {
entry_path: String,
parent_path: String,
},
EnsureDailyEntry {
workspace_path: String,
#[serde(default)]
daily_entry_folder: Option<String>,
#[serde(default)]
template: Option<String>,
},
GetAdjacentDailyEntry {
path: String,
direction: String,
},
IsDailyEntry {
path: String,
},
FindRootIndex {
directory: String,
},
GetWorkspaceTree {
path: Option<String>,
depth: Option<u32>,
},
GetFilesystemTree {
path: Option<String>,
#[serde(default)]
show_hidden: bool,
depth: Option<u32>,
},
CreateWorkspace {
path: Option<String>,
name: Option<String>,
},
GetFrontmatter {
path: String,
},
SetFrontmatterProperty {
path: String,
key: String,
value: JsonValue,
},
RemoveFrontmatterProperty {
path: String,
key: String,
},
SearchWorkspace {
pattern: String,
#[serde(default)]
options: SearchOptions,
},
ValidateWorkspace {
path: Option<String>,
},
ValidateFile {
path: String,
},
FixBrokenPartOf {
path: String,
},
FixBrokenContentsRef {
index_path: String,
target: String,
},
FixBrokenAttachment {
path: String,
attachment: String,
},
FixNonPortablePath {
path: String,
property: String,
old_value: String,
new_value: String,
},
FixUnlistedFile {
index_path: String,
file_path: String,
},
FixOrphanBinaryFile {
index_path: String,
file_path: String,
},
FixMissingPartOf {
file_path: String,
index_path: String,
},
FixAll {
validation_result: ValidationResult,
},
FixCircularReference {
file_path: String,
part_of_value: String,
},
GetAvailableParentIndexes {
file_path: String,
workspace_root: String,
},
GetAvailableAudiences {
root_path: String,
},
PlanExport {
root_path: String,
audience: String,
},
ExportToMemory {
root_path: String,
audience: String,
},
ExportToHtml {
root_path: String,
audience: String,
},
ExportBinaryAttachments {
root_path: String,
audience: String,
},
ListTemplates {
workspace_path: Option<String>,
},
GetTemplate {
name: String,
workspace_path: Option<String>,
},
SaveTemplate {
name: String,
content: String,
workspace_path: String,
},
DeleteTemplate {
name: String,
workspace_path: String,
},
GetAttachments {
path: String,
},
UploadAttachment {
entry_path: String,
filename: String,
data_base64: String,
},
DeleteAttachment {
entry_path: String,
attachment_path: String,
},
GetAttachmentData {
entry_path: String,
attachment_path: String,
},
MoveAttachment {
source_entry_path: String,
target_entry_path: String,
attachment_path: String,
new_filename: Option<String>,
},
GetAncestorAttachments {
path: String,
},
FileExists {
path: String,
},
ReadFile {
path: String,
},
WriteFile {
path: String,
content: String,
},
DeleteFile {
path: String,
},
WriteFileWithMetadata {
path: String,
metadata: serde_json::Value,
body: String,
},
UpdateFileMetadata {
path: String,
metadata: serde_json::Value,
body: Option<String>,
},
GetStorageUsage,
#[cfg(feature = "crdt")]
InitializeWorkspaceCrdt {
workspace_path: String,
audience: Option<String>,
},
#[cfg(feature = "crdt")]
GetSyncState {
doc_name: String,
},
#[cfg(feature = "crdt")]
ApplyRemoteUpdate {
doc_name: String,
update: Vec<u8>,
},
#[cfg(feature = "crdt")]
GetMissingUpdates {
doc_name: String,
remote_state_vector: Vec<u8>,
},
#[cfg(feature = "crdt")]
GetFullState {
doc_name: String,
},
#[cfg(feature = "crdt")]
GetHistory {
doc_name: String,
limit: Option<usize>,
},
#[cfg(feature = "crdt")]
GetFileHistory {
file_path: String,
limit: Option<usize>,
},
#[cfg(feature = "crdt")]
RestoreVersion {
doc_name: String,
update_id: i64,
},
#[cfg(feature = "crdt")]
GetVersionDiff {
doc_name: String,
from_id: i64,
to_id: i64,
},
#[cfg(feature = "crdt")]
GetStateAt {
doc_name: String,
update_id: i64,
},
#[cfg(feature = "crdt")]
GetCrdtFile {
path: String,
},
#[cfg(feature = "crdt")]
SetCrdtFile {
path: String,
metadata: serde_json::Value,
},
#[cfg(feature = "crdt")]
ListCrdtFiles {
#[serde(default)]
include_deleted: bool,
},
#[cfg(feature = "crdt")]
SaveCrdtState {
doc_name: String,
},
#[cfg(feature = "crdt")]
GetBodyContent {
doc_name: String,
},
#[cfg(feature = "crdt")]
SetBodyContent {
doc_name: String,
content: String,
},
#[cfg(feature = "crdt")]
ResetBodyDoc {
doc_name: String,
},
#[cfg(feature = "crdt")]
GetBodySyncState {
doc_name: String,
},
#[cfg(feature = "crdt")]
GetBodyFullState {
doc_name: String,
},
#[cfg(feature = "crdt")]
ApplyBodyUpdate {
doc_name: String,
update: Vec<u8>,
},
#[cfg(feature = "crdt")]
GetBodyMissingUpdates {
doc_name: String,
remote_state_vector: Vec<u8>,
},
#[cfg(feature = "crdt")]
SaveBodyDoc {
doc_name: String,
},
#[cfg(feature = "crdt")]
SaveAllBodyDocs,
#[cfg(feature = "crdt")]
ListLoadedBodyDocs,
#[cfg(feature = "crdt")]
UnloadBodyDoc {
doc_name: String,
},
#[cfg(feature = "crdt")]
CreateSyncStep1 {
doc_name: String,
},
#[cfg(feature = "crdt")]
HandleSyncMessage {
doc_name: String,
message: Vec<u8>,
#[serde(default)]
write_to_disk: bool,
},
#[cfg(feature = "crdt")]
CreateUpdateMessage {
doc_name: String,
update: Vec<u8>,
},
#[cfg(feature = "crdt")]
ConfigureSyncHandler {
guest_join_code: Option<String>,
#[serde(default)]
uses_opfs: bool,
},
#[cfg(feature = "crdt")]
ApplyRemoteWorkspaceUpdateWithEffects {
update: Vec<u8>,
#[serde(default)]
write_to_disk: bool,
},
#[cfg(feature = "crdt")]
ApplyRemoteBodyUpdateWithEffects {
doc_name: String,
update: Vec<u8>,
#[serde(default)]
write_to_disk: bool,
},
#[cfg(feature = "crdt")]
GetStoragePath {
canonical_path: String,
},
#[cfg(feature = "crdt")]
GetCanonicalPath {
storage_path: String,
},
#[cfg(feature = "crdt")]
HandleWorkspaceSyncMessage {
message: Vec<u8>,
#[serde(default)]
write_to_disk: bool,
},
#[cfg(feature = "crdt")]
HandleCrdtState {
state: Vec<u8>,
},
#[cfg(feature = "crdt")]
CreateWorkspaceSyncStep1,
#[cfg(feature = "crdt")]
CreateWorkspaceUpdate {
since_state_vector: Option<Vec<u8>>,
},
#[cfg(feature = "crdt")]
InitBodySync {
doc_name: String,
},
#[cfg(feature = "crdt")]
CloseBodySync {
doc_name: String,
},
#[cfg(feature = "crdt")]
HandleBodySyncMessage {
doc_name: String,
message: Vec<u8>,
#[serde(default)]
write_to_disk: bool,
},
#[cfg(feature = "crdt")]
CreateBodySyncStep1 {
doc_name: String,
},
#[cfg(feature = "crdt")]
CreateBodyUpdate {
doc_name: String,
content: String,
},
#[cfg(feature = "crdt")]
IsSyncComplete,
#[cfg(feature = "crdt")]
IsWorkspaceSynced,
#[cfg(feature = "crdt")]
IsBodySynced {
doc_name: String,
},
#[cfg(feature = "crdt")]
MarkSyncComplete,
#[cfg(feature = "crdt")]
GetActiveSyncs,
#[cfg(feature = "crdt")]
TrackContent {
path: String,
content: String,
},
#[cfg(feature = "crdt")]
IsEcho {
path: String,
content: String,
},
#[cfg(feature = "crdt")]
ClearTrackedContent {
path: String,
},
#[cfg(feature = "crdt")]
ResetSyncState,
#[cfg(feature = "crdt")]
TriggerWorkspaceSync,
GetLinkFormat {
root_index_path: String,
},
SetLinkFormat {
root_index_path: String,
format: String,
},
GetWorkspaceConfig {
root_index_path: String,
},
ConvertLinks {
root_index_path: String,
format: String,
path: Option<String>,
#[serde(default)]
dry_run: bool,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "bindings/")]
pub struct CreateChildResult {
pub child_path: String,
pub parent_path: String,
pub parent_converted: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub original_parent_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "bindings/")]
#[serde(tag = "type", content = "data")]
pub enum Response {
Ok,
String(String),
Bool(bool),
Entry(EntryData),
Tree(TreeNode),
Frontmatter(IndexMap<String, JsonValue>),
SearchResults(SearchResults),
ValidationResult(ValidationResultWithMeta),
FixResult(FixResult),
FixSummary(FixSummary),
ExportPlan(ExportPlan),
ExportedFiles(Vec<ExportedFile>),
BinaryFiles(Vec<BinaryExportFile>),
BinaryFilePaths(Vec<BinaryFileInfo>),
Templates(Vec<TemplateInfo>),
Strings(Vec<String>),
Bytes(Vec<u8>),
StorageInfo(StorageInfo),
AncestorAttachments(AncestorAttachmentsResult),
LinkFormat(LinkFormat),
WorkspaceConfig(WorkspaceConfig),
ConvertLinksResult(ConvertLinksResult),
CreateChildResult(CreateChildResult),
#[cfg(feature = "crdt")]
Binary(Vec<u8>),
#[cfg(feature = "crdt")]
CrdtFile(Option<crate::crdt::FileMetadata>),
#[cfg(feature = "crdt")]
CrdtFiles(Vec<(String, crate::crdt::FileMetadata)>),
#[cfg(feature = "crdt")]
CrdtHistory(Vec<CrdtHistoryEntry>),
#[cfg(feature = "crdt")]
UpdateId(Option<i64>),
#[cfg(feature = "crdt")]
VersionDiff(Vec<crate::crdt::FileDiff>),
#[cfg(feature = "crdt")]
HistoryEntries(Vec<crate::crdt::HistoryEntry>),
#[cfg(feature = "crdt")]
WorkspaceSyncResult {
response: Option<Vec<u8>>,
changed_files: Vec<String>,
sync_complete: bool,
},
#[cfg(feature = "crdt")]
BodySyncResult {
response: Option<Vec<u8>>,
content: Option<String>,
is_echo: bool,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "bindings/")]
pub struct EntryData {
pub path: PathBuf,
pub title: Option<String>,
pub frontmatter: IndexMap<String, JsonValue>,
pub content: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[ts(export, export_to = "bindings/")]
pub struct CreateEntryOptions {
pub title: Option<String>,
pub part_of: Option<String>,
pub template: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[ts(export, export_to = "bindings/")]
pub struct SearchOptions {
pub workspace_path: Option<String>,
#[serde(default)]
pub search_frontmatter: bool,
pub property: Option<String>,
#[serde(default)]
pub case_sensitive: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "bindings/")]
pub struct ExportedFile {
pub path: String,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "bindings/")]
pub struct BinaryExportFile {
pub path: String,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "bindings/")]
pub struct BinaryFileInfo {
pub source_path: String,
pub relative_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "bindings/")]
pub struct TemplateInfo {
pub name: String,
pub path: Option<PathBuf>,
pub source: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "bindings/")]
pub struct StorageInfo {
pub used: u64,
pub limit: Option<u64>,
pub attachment_limit: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "bindings/")]
pub struct FixSummary {
pub error_fixes: Vec<FixResult>,
pub warning_fixes: Vec<FixResult>,
pub total_fixed: usize,
pub total_failed: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "bindings/")]
pub struct AncestorAttachmentEntry {
pub entry_path: String,
pub entry_title: Option<String>,
pub attachments: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "bindings/")]
pub struct AncestorAttachmentsResult {
pub entries: Vec<AncestorAttachmentEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "bindings/")]
pub struct ConvertLinksResult {
pub files_modified: usize,
pub links_converted: usize,
pub modified_files: Vec<String>,
pub dry_run: bool,
}
#[cfg(feature = "crdt")]
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "bindings/")]
pub struct CrdtHistoryEntry {
pub update_id: i64,
pub timestamp: i64,
pub origin: String,
pub files_changed: Vec<String>,
pub device_id: Option<String>,
pub device_name: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_command_serialization() {
let cmd = Command::GetEntry {
path: "notes/hello.md".to_string(),
};
let json = serde_json::to_string(&cmd).unwrap();
assert!(json.contains("GetEntry"));
assert!(json.contains("notes/hello.md"));
let cmd2: Command = serde_json::from_str(&json).unwrap();
if let Command::GetEntry { path } = cmd2 {
assert_eq!(path, "notes/hello.md");
} else {
panic!("Wrong command type");
}
}
#[test]
fn test_response_serialization() {
let resp = Response::String("hello".to_string());
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("String"));
assert!(json.contains("hello"));
let resp2: Response = serde_json::from_str(&json).unwrap();
if let Response::String(s) = resp2 {
assert_eq!(s, "hello");
} else {
panic!("Wrong response type");
}
}
#[test]
fn test_create_entry_options_default() {
let opts = CreateEntryOptions::default();
assert!(opts.title.is_none());
assert!(opts.part_of.is_none());
assert!(opts.template.is_none());
}
#[test]
fn test_search_options_default() {
let opts = SearchOptions::default();
assert!(!opts.search_frontmatter);
assert!(!opts.case_sensitive);
assert!(opts.property.is_none());
}
}