1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Instance {
8 pub name: String,
9 pub data_dir: PathBuf,
10 #[serde(default)]
11 pub is_default: bool,
12}
13
14#[derive(Debug, Clone, Default, Serialize, Deserialize)]
16pub struct InstanceRegistry {
17 #[serde(default)]
18 pub instances: Vec<Instance>,
19 #[serde(default)]
20 pub active: Option<String>,
21}
22
23impl InstanceRegistry {
24 #[must_use]
26 pub fn load() -> Self {
27 let path = registry_path();
28 if !path.exists() {
29 return Self::default();
30 }
31 std::fs::read_to_string(&path)
32 .ok()
33 .and_then(|s| toml::from_str(&s).ok())
34 .unwrap_or_default()
35 }
36
37 pub fn save(&self) -> crate::error::Result<()> {
39 let path = registry_path();
40 if let Some(parent) = path.parent() {
41 std::fs::create_dir_all(parent)?;
42 }
43 let content = toml::to_string_pretty(self)?;
44 std::fs::write(&path, content)?;
45 Ok(())
46 }
47
48 pub fn create(&mut self, name: &str, data_dir: PathBuf) -> crate::error::Result<()> {
50 if self.instances.iter().any(|i| i.name == name) {
51 return Err(crate::error::CoreError::Validation(
52 format!("instance '{name}' already exists").into(),
53 ));
54 }
55 std::fs::create_dir_all(&data_dir)?;
56 self.instances.push(Instance {
57 name: name.to_string(),
58 data_dir,
59 is_default: self.instances.is_empty(),
60 });
61 if self.active.is_none() {
62 self.active = Some(name.to_string());
63 }
64 self.save()?;
65 Ok(())
66 }
67
68 pub fn switch(&mut self, name: &str) -> crate::error::Result<()> {
70 if !self.instances.iter().any(|i| i.name == name) {
71 return Err(crate::error::CoreError::Validation(
72 format!("instance '{name}' not found").into(),
73 ));
74 }
75 self.active = Some(name.to_string());
76 self.save()?;
77 Ok(())
78 }
79
80 #[must_use]
82 pub fn active_data_dir(&self) -> Option<&Path> {
83 let name = self.active.as_ref()?;
84 self.instances
85 .iter()
86 .find(|i| &i.name == name)
87 .map(|i| i.data_dir.as_path())
88 }
89
90 #[must_use]
92 pub fn list(&self) -> &[Instance] {
93 &self.instances
94 }
95
96 #[must_use]
98 pub fn load_from(path: &Path) -> Self {
99 if !path.exists() {
100 return Self::default();
101 }
102 std::fs::read_to_string(path)
103 .ok()
104 .and_then(|s| toml::from_str(&s).ok())
105 .unwrap_or_default()
106 }
107
108 pub fn save_to(&self, path: &Path) -> crate::error::Result<()> {
110 if let Some(parent) = path.parent() {
111 std::fs::create_dir_all(parent)?;
112 }
113 let content = toml::to_string_pretty(self)?;
114 std::fs::write(path, content)?;
115 Ok(())
116 }
117
118 pub fn create_with_path(
120 &mut self,
121 name: &str,
122 data_dir: PathBuf,
123 registry_path: &Path,
124 ) -> crate::error::Result<()> {
125 if self.instances.iter().any(|i| i.name == name) {
126 return Err(crate::error::CoreError::Validation(
127 format!("instance '{name}' already exists").into(),
128 ));
129 }
130 std::fs::create_dir_all(&data_dir)?;
131 self.instances.push(Instance {
132 name: name.to_string(),
133 data_dir,
134 is_default: self.instances.is_empty(),
135 });
136 if self.active.is_none() {
137 self.active = Some(name.to_string());
138 }
139 self.save_to(registry_path)?;
140 Ok(())
141 }
142
143 pub fn switch_with_path(
145 &mut self,
146 name: &str,
147 registry_path: &Path,
148 ) -> crate::error::Result<()> {
149 if !self.instances.iter().any(|i| i.name == name) {
150 return Err(crate::error::CoreError::Validation(
151 format!("instance '{name}' not found").into(),
152 ));
153 }
154 self.active = Some(name.to_string());
155 self.save_to(registry_path)?;
156 Ok(())
157 }
158}
159
160fn registry_path() -> PathBuf {
161 crate::paths::modde_config_dir().join("instances.toml")
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_create_and_list() {
170 let tmp = tempfile::tempdir().unwrap();
171 let registry_path = tmp.path().join("instances.toml");
172 let data_dir = tmp.path().join("instance-data");
173
174 let mut reg = InstanceRegistry::default();
175 reg.create_with_path("default", data_dir.clone(), ®istry_path)
176 .unwrap();
177
178 assert_eq!(reg.list().len(), 1);
179 assert_eq!(reg.list()[0].name, "default");
180 assert_eq!(reg.list()[0].data_dir, data_dir);
181 assert!(reg.list()[0].is_default);
182 }
183
184 #[test]
185 fn test_switch() {
186 let tmp = tempfile::tempdir().unwrap();
187 let registry_path = tmp.path().join("instances.toml");
188
189 let mut reg = InstanceRegistry::default();
190 reg.create_with_path("first", tmp.path().join("first"), ®istry_path)
191 .unwrap();
192 reg.create_with_path("second", tmp.path().join("second"), ®istry_path)
193 .unwrap();
194
195 assert_eq!(reg.active.as_deref(), Some("first"));
196
197 reg.switch_with_path("second", ®istry_path).unwrap();
198 assert_eq!(reg.active.as_deref(), Some("second"));
199 }
200
201 #[test]
202 fn test_duplicate_name_errors() {
203 let tmp = tempfile::tempdir().unwrap();
204 let registry_path = tmp.path().join("instances.toml");
205
206 let mut reg = InstanceRegistry::default();
207 reg.create_with_path("myinstance", tmp.path().join("data1"), ®istry_path)
208 .unwrap();
209
210 let result = reg.create_with_path("myinstance", tmp.path().join("data2"), ®istry_path);
211 assert!(result.is_err());
212 let err = result.unwrap_err();
213 assert!(
214 err.to_string().contains("already exists"),
215 "expected 'already exists' error, got: {err}"
216 );
217 }
218
219 #[test]
220 fn test_active_data_dir() {
221 let tmp = tempfile::tempdir().unwrap();
222 let registry_path = tmp.path().join("instances.toml");
223 let first_dir = tmp.path().join("first");
224 let second_dir = tmp.path().join("second");
225
226 let mut reg = InstanceRegistry::default();
227 reg.create_with_path("first", first_dir.clone(), ®istry_path)
228 .unwrap();
229 reg.create_with_path("second", second_dir.clone(), ®istry_path)
230 .unwrap();
231
232 assert_eq!(reg.active_data_dir(), Some(first_dir.as_path()));
234
235 reg.switch_with_path("second", ®istry_path).unwrap();
237 assert_eq!(reg.active_data_dir(), Some(second_dir.as_path()));
238 }
239}