use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Registry {
#[serde(default)]
pub current: Option<String>,
#[serde(default)]
pub libraries: BTreeMap<String, PathBuf>,
}
impl Registry {
pub fn path() -> Result<PathBuf> {
Ok(crate::config::base_dir()?.join("libraries.toml"))
}
pub fn load() -> Result<Self> {
Self::load_from(&Self::path()?)
}
pub fn load_from(path: &Path) -> Result<Self> {
if !path.exists() {
return Ok(Self::default());
}
Ok(toml::from_str(&std::fs::read_to_string(path)?)?)
}
pub fn save(&self) -> Result<()> {
self.save_to(&Self::path()?)
}
pub fn save_to(&self, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content =
toml::to_string_pretty(self).map_err(|e| Error::InvalidTaskFile(e.to_string()))?;
std::fs::write(path, content)?;
Ok(())
}
pub fn add(&mut self, name: &str, path: PathBuf) -> Result<()> {
if self.libraries.contains_key(name) {
return Err(Error::LibraryExists(name.to_string()));
}
std::fs::create_dir_all(&path)?;
crate::storage::LibraryMeta::default_init(&path)?;
self.libraries.insert(name.to_string(), path);
if self.current.is_none() {
self.current = Some(name.to_string());
}
Ok(())
}
pub fn remove(&mut self, name: &str) -> Result<()> {
if self.libraries.remove(name).is_none() {
return Err(Error::LibraryNotFound(name.to_string()));
}
if self.current.as_deref() == Some(name) {
self.current = self.libraries.keys().next().cloned();
}
Ok(())
}
pub fn use_library(&mut self, name: &str) -> Result<()> {
if !self.libraries.contains_key(name) {
return Err(Error::LibraryNotFound(name.to_string()));
}
self.current = Some(name.to_string());
Ok(())
}
pub fn current_library(&self) -> Result<(&str, &Path)> {
let name = self.current.as_deref().ok_or(Error::NoActiveLibrary)?;
let path = self
.libraries
.get(name)
.ok_or_else(|| Error::LibraryNotFound(name.to_string()))?;
Ok((name, path))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_dir() -> PathBuf {
let dir = std::env::temp_dir().join(format!("tasks-lib-test-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn add_use_remove_flow() {
let dir = temp_dir();
let mut reg = Registry::default();
reg.add("personal", dir.join("personal")).unwrap();
reg.add("work", dir.join("work")).unwrap();
assert_eq!(reg.current.as_deref(), Some("personal"));
assert!(dir.join("personal/.tasks-meta.toml").exists());
reg.use_library("work").unwrap();
assert_eq!(reg.current_library().unwrap().0, "work");
assert!(reg.use_library("nope").is_err());
assert!(reg.add("work", dir.join("dup")).is_err());
reg.remove("work").unwrap();
assert_eq!(reg.current.as_deref(), Some("personal"));
assert!(dir.join("work").exists());
reg.remove("personal").unwrap();
assert!(reg.current.is_none());
assert!(reg.current_library().is_err());
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn save_load_round_trip() {
let dir = temp_dir();
let mut reg = Registry::default();
reg.add("a", dir.join("a")).unwrap();
let file = dir.join("libraries.toml");
reg.save_to(&file).unwrap();
let loaded = Registry::load_from(&file).unwrap();
assert_eq!(loaded, reg);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn missing_registry_file_is_empty() {
let reg = Registry::load_from(Path::new("/nonexistent/libraries.toml")).unwrap();
assert!(reg.libraries.is_empty());
assert!(reg.current.is_none());
}
}