use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Instance {
pub name: String,
pub data_dir: PathBuf,
#[serde(default)]
pub is_default: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct InstanceRegistry {
#[serde(default)]
pub instances: Vec<Instance>,
#[serde(default)]
pub active: Option<String>,
}
impl InstanceRegistry {
#[must_use]
pub fn load() -> Self {
let path = registry_path();
if !path.exists() {
return Self::default();
}
std::fs::read_to_string(&path)
.ok()
.and_then(|s| toml::from_str(&s).ok())
.unwrap_or_default()
}
pub fn save(&self) -> crate::error::Result<()> {
let path = registry_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = toml::to_string_pretty(self)?;
std::fs::write(&path, content)?;
Ok(())
}
pub fn create(&mut self, name: &str, data_dir: PathBuf) -> crate::error::Result<()> {
if self.instances.iter().any(|i| i.name == name) {
return Err(crate::error::CoreError::Validation(
format!("instance '{name}' already exists").into(),
));
}
std::fs::create_dir_all(&data_dir)?;
self.instances.push(Instance {
name: name.to_string(),
data_dir,
is_default: self.instances.is_empty(),
});
if self.active.is_none() {
self.active = Some(name.to_string());
}
self.save()?;
Ok(())
}
pub fn switch(&mut self, name: &str) -> crate::error::Result<()> {
if !self.instances.iter().any(|i| i.name == name) {
return Err(crate::error::CoreError::Validation(
format!("instance '{name}' not found").into(),
));
}
self.active = Some(name.to_string());
self.save()?;
Ok(())
}
#[must_use]
pub fn active_data_dir(&self) -> Option<&Path> {
let name = self.active.as_ref()?;
self.instances
.iter()
.find(|i| &i.name == name)
.map(|i| i.data_dir.as_path())
}
#[must_use]
pub fn list(&self) -> &[Instance] {
&self.instances
}
#[must_use]
pub fn load_from(path: &Path) -> Self {
if !path.exists() {
return Self::default();
}
std::fs::read_to_string(path)
.ok()
.and_then(|s| toml::from_str(&s).ok())
.unwrap_or_default()
}
pub fn save_to(&self, path: &Path) -> crate::error::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = toml::to_string_pretty(self)?;
std::fs::write(path, content)?;
Ok(())
}
pub fn create_with_path(
&mut self,
name: &str,
data_dir: PathBuf,
registry_path: &Path,
) -> crate::error::Result<()> {
if self.instances.iter().any(|i| i.name == name) {
return Err(crate::error::CoreError::Validation(
format!("instance '{name}' already exists").into(),
));
}
std::fs::create_dir_all(&data_dir)?;
self.instances.push(Instance {
name: name.to_string(),
data_dir,
is_default: self.instances.is_empty(),
});
if self.active.is_none() {
self.active = Some(name.to_string());
}
self.save_to(registry_path)?;
Ok(())
}
pub fn switch_with_path(
&mut self,
name: &str,
registry_path: &Path,
) -> crate::error::Result<()> {
if !self.instances.iter().any(|i| i.name == name) {
return Err(crate::error::CoreError::Validation(
format!("instance '{name}' not found").into(),
));
}
self.active = Some(name.to_string());
self.save_to(registry_path)?;
Ok(())
}
}
fn registry_path() -> PathBuf {
crate::paths::modde_config_dir().join("instances.toml")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_and_list() {
let tmp = tempfile::tempdir().unwrap();
let registry_path = tmp.path().join("instances.toml");
let data_dir = tmp.path().join("instance-data");
let mut reg = InstanceRegistry::default();
reg.create_with_path("default", data_dir.clone(), ®istry_path)
.unwrap();
assert_eq!(reg.list().len(), 1);
assert_eq!(reg.list()[0].name, "default");
assert_eq!(reg.list()[0].data_dir, data_dir);
assert!(reg.list()[0].is_default);
}
#[test]
fn test_switch() {
let tmp = tempfile::tempdir().unwrap();
let registry_path = tmp.path().join("instances.toml");
let mut reg = InstanceRegistry::default();
reg.create_with_path("first", tmp.path().join("first"), ®istry_path)
.unwrap();
reg.create_with_path("second", tmp.path().join("second"), ®istry_path)
.unwrap();
assert_eq!(reg.active.as_deref(), Some("first"));
reg.switch_with_path("second", ®istry_path).unwrap();
assert_eq!(reg.active.as_deref(), Some("second"));
}
#[test]
fn test_duplicate_name_errors() {
let tmp = tempfile::tempdir().unwrap();
let registry_path = tmp.path().join("instances.toml");
let mut reg = InstanceRegistry::default();
reg.create_with_path("myinstance", tmp.path().join("data1"), ®istry_path)
.unwrap();
let result = reg.create_with_path("myinstance", tmp.path().join("data2"), ®istry_path);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("already exists"),
"expected 'already exists' error, got: {err}"
);
}
#[test]
fn test_active_data_dir() {
let tmp = tempfile::tempdir().unwrap();
let registry_path = tmp.path().join("instances.toml");
let first_dir = tmp.path().join("first");
let second_dir = tmp.path().join("second");
let mut reg = InstanceRegistry::default();
reg.create_with_path("first", first_dir.clone(), ®istry_path)
.unwrap();
reg.create_with_path("second", second_dir.clone(), ®istry_path)
.unwrap();
assert_eq!(reg.active_data_dir(), Some(first_dir.as_path()));
reg.switch_with_path("second", ®istry_path).unwrap();
assert_eq!(reg.active_data_dir(), Some(second_dir.as_path()));
}
}