use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(export, export_to = "bindings/"))]
pub struct WorkspaceEntry {
pub id: String,
pub name: String,
#[cfg_attr(feature = "typescript", ts(optional))]
pub path: Option<PathBuf>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(export, export_to = "bindings/"))]
pub struct WorkspaceRegistry {
pub entries: Vec<WorkspaceEntry>,
#[cfg_attr(feature = "typescript", ts(optional))]
pub default_id: Option<String>,
}
impl WorkspaceRegistry {
pub fn find_by_id(&self, id: &str) -> Option<&WorkspaceEntry> {
self.entries.iter().find(|e| e.id == id)
}
pub fn find_by_name(&self, name: &str) -> Option<&WorkspaceEntry> {
self.entries.iter().find(|e| e.name == name)
}
pub fn find_by_path(&self, path: &Path) -> Option<&WorkspaceEntry> {
self.entries
.iter()
.find(|e| e.path.as_deref() == Some(path))
}
#[cfg(feature = "uuid")]
pub fn register(&mut self, name: String, path: Option<PathBuf>) -> &WorkspaceEntry {
if let Some(ref p) = path
&& let Some(idx) = self
.entries
.iter()
.position(|e| e.path.as_deref() == Some(p))
{
return &self.entries[idx];
}
let entry = WorkspaceEntry {
id: format!("local-{}", uuid::Uuid::new_v4()),
name,
path,
};
self.entries.push(entry);
self.entries.last().unwrap()
}
pub fn unregister(&mut self, id: &str) -> Option<WorkspaceEntry> {
if let Some(idx) = self.entries.iter().position(|e| e.id == id) {
let entry = self.entries.remove(idx);
if self.default_id.as_deref() == Some(id) {
self.default_id = None;
}
Some(entry)
} else {
None
}
}
pub fn rename(&mut self, id: &str, new_name: String) -> bool {
if let Some(entry) = self.entries.iter_mut().find(|e| e.id == id) {
entry.name = new_name;
true
} else {
false
}
}
pub fn set_default(&mut self, id: &str) -> bool {
if self.entries.iter().any(|e| e.id == id) {
self.default_id = Some(id.to_string());
true
} else {
false
}
}
pub fn default_entry(&self) -> Option<&WorkspaceEntry> {
self.default_id
.as_deref()
.and_then(|id| self.find_by_id(id))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn register_generates_unique_ids() {
let mut reg = WorkspaceRegistry::default();
let id1 = reg.register("a".into(), None).id.clone();
let id2 = reg.register("b".into(), None).id.clone();
assert_ne!(id1, id2);
assert!(id1.starts_with("local-"));
assert!(id2.starts_with("local-"));
}
#[test]
fn register_deduplicates_by_path() {
let mut reg = WorkspaceRegistry::default();
let path = PathBuf::from("/home/user/journal");
let id1 = reg
.register("journal".into(), Some(path.clone()))
.id
.clone();
let id2 = reg.register("journal-dup".into(), Some(path)).id.clone();
assert_eq!(id1, id2);
assert_eq!(reg.entries.len(), 1);
}
#[test]
fn find_by_id_name_path() {
let mut reg = WorkspaceRegistry::default();
let path = PathBuf::from("/ws");
let id = reg.register("my-ws".into(), Some(path.clone())).id.clone();
assert!(reg.find_by_id(&id).is_some());
assert!(reg.find_by_name("my-ws").is_some());
assert!(reg.find_by_path(&path).is_some());
assert!(reg.find_by_name("nonexistent").is_none());
}
#[test]
fn unregister_removes_and_clears_default() {
let mut reg = WorkspaceRegistry::default();
let id = reg.register("ws".into(), None).id.clone();
reg.set_default(&id);
assert!(reg.default_entry().is_some());
let removed = reg.unregister(&id);
assert!(removed.is_some());
assert!(reg.default_entry().is_none());
assert_eq!(reg.entries.len(), 0);
}
#[test]
fn unregister_nonexistent_returns_none() {
let mut reg = WorkspaceRegistry::default();
assert!(reg.unregister("nope").is_none());
}
#[test]
fn rename_entry() {
let mut reg = WorkspaceRegistry::default();
let id = reg.register("old".into(), None).id.clone();
assert!(reg.rename(&id, "new".into()));
assert_eq!(reg.find_by_id(&id).unwrap().name, "new");
assert!(!reg.rename("bad-id", "x".into()));
}
#[test]
fn set_default_and_default_entry() {
let mut reg = WorkspaceRegistry::default();
let id = reg.register("ws".into(), None).id.clone();
assert!(reg.default_entry().is_none());
assert!(reg.set_default(&id));
assert_eq!(reg.default_entry().unwrap().id, id);
assert!(!reg.set_default("nonexistent"));
}
}