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