use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use fslite_core::WorkspaceId;
use serde::{Deserialize, Serialize};
use crate::paths::config_dir;
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct Registry {
filesystems: BTreeMap<String, PathBuf>,
workspaces: BTreeMap<String, BTreeMap<String, WorkspaceId>>,
}
impl Registry {
fn path() -> Result<PathBuf, Box<dyn std::error::Error>> {
Ok(config_dir()?.join("registry.json"))
}
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
let path = Self::path()?;
match std::fs::read_to_string(&path) {
Ok(contents) => Ok(serde_json::from_str(&contents)?),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Registry::default()),
Err(err) => Err(err.into()),
}
}
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
let path = Self::path()?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(self)?;
std::fs::write(path, json)?;
Ok(())
}
pub fn filesystem_exists(&self, name: &str) -> bool {
self.filesystems.contains_key(name)
}
pub fn filesystem_path(&self, name: &str) -> Option<&Path> {
self.filesystems.get(name).map(PathBuf::as_path)
}
pub fn register_filesystem(&mut self, name: String, path: PathBuf) {
self.filesystems.insert(name, path);
}
pub fn remove_filesystem(&mut self, name: &str) {
self.filesystems.remove(name);
self.workspaces.remove(name);
}
pub fn workspace_exists(&self, filesystem: &str, workspace_name: &str) -> bool {
self.workspaces
.get(filesystem)
.is_some_and(|workspaces| workspaces.contains_key(workspace_name))
}
pub fn workspace_names(&self, filesystem: &str) -> Vec<&str> {
self.workspaces
.get(filesystem)
.map(|workspaces| workspaces.keys().map(String::as_str).collect())
.unwrap_or_default()
}
pub fn register_workspace(
&mut self,
filesystem: &str,
workspace_name: String,
id: WorkspaceId,
) {
self.workspaces
.entry(filesystem.to_string())
.or_default()
.insert(workspace_name, id);
}
pub fn resolve_workspace_name(
&self,
filesystem: &str,
workspace_name: &str,
) -> Option<WorkspaceId> {
self.workspaces
.get(filesystem)?
.get(workspace_name)
.copied()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::with_temp_config_dir;
#[test]
fn round_trips_through_save_and_load() {
with_temp_config_dir(|| {
let mut registry = Registry::load().unwrap();
assert!(!registry.filesystem_exists("main"));
registry.register_filesystem("main".to_string(), PathBuf::from("/tmp/main.db"));
let id = WorkspaceId::new();
registry.register_workspace("main", "primary".to_string(), id);
registry.save().unwrap();
let reloaded = Registry::load().unwrap();
assert_eq!(
reloaded.filesystem_path("main"),
Some(Path::new("/tmp/main.db"))
);
assert_eq!(reloaded.resolve_workspace_name("main", "primary"), Some(id));
assert!(!reloaded.workspace_exists("main", "missing"));
});
}
#[test]
fn remove_filesystem_forgets_its_workspaces_too() {
with_temp_config_dir(|| {
let mut registry = Registry::load().unwrap();
registry.register_filesystem("main".to_string(), PathBuf::from("/tmp/main.db"));
registry.register_workspace("main", "primary".to_string(), WorkspaceId::new());
registry.remove_filesystem("main");
assert!(!registry.filesystem_exists("main"));
assert!(!registry.workspace_exists("main", "primary"));
});
}
#[test]
fn missing_registry_file_loads_as_empty_default() {
with_temp_config_dir(|| {
let registry = Registry::load().unwrap();
assert!(!registry.filesystem_exists("anything"));
});
}
}