cbtop/profile_persistence/
mod.rs1mod config;
18mod overlay;
19
20pub use config::{BackendConfig, ProfileConfig, WorkloadConfig};
21pub use overlay::{templates, ProfileOverlay};
22
23use config::validate_profile_name;
24use std::collections::HashMap;
25use std::path::{Path, PathBuf};
26
27#[derive(Debug, Clone, PartialEq)]
29pub enum ProfileError {
30 NotFound(String),
32 InvalidName(String),
34 IoError(String),
36 ParseError(String),
38 DirectoryError(String),
40}
41
42impl std::fmt::Display for ProfileError {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 Self::NotFound(name) => write!(f, "Profile not found: {}", name),
46 Self::InvalidName(msg) => write!(f, "Invalid profile name: {}", msg),
47 Self::IoError(msg) => write!(f, "IO error: {}", msg),
48 Self::ParseError(msg) => write!(f, "Parse error: {}", msg),
49 Self::DirectoryError(msg) => write!(f, "Directory error: {}", msg),
50 }
51 }
52}
53
54impl std::error::Error for ProfileError {}
55
56pub type ProfileResult<T> = Result<T, ProfileError>;
58
59#[derive(Debug, Clone)]
61pub struct ProfileManager {
62 profile_dir: PathBuf,
64 cache: HashMap<String, ProfileConfig>,
66 default_profile: Option<String>,
68}
69
70impl ProfileManager {
71 pub fn new(profile_dir: PathBuf) -> Self {
73 Self {
74 profile_dir,
75 cache: HashMap::new(),
76 default_profile: None,
77 }
78 }
79
80 pub fn with_default_dir() -> Self {
82 let profile_dir = dirs::config_dir()
83 .unwrap_or_else(|| PathBuf::from("."))
84 .join("cbtop")
85 .join("profiles");
86 Self::new(profile_dir)
87 }
88
89 pub fn profile_dir(&self) -> &Path {
91 &self.profile_dir
92 }
93
94 pub fn set_default(&mut self, name: &str) {
96 self.default_profile = Some(name.to_string());
97 }
98
99 pub fn default_profile(&self) -> Option<&str> {
101 self.default_profile.as_deref()
102 }
103
104 pub fn ensure_directory(&self) -> ProfileResult<()> {
106 if !self.profile_dir.exists() {
107 std::fs::create_dir_all(&self.profile_dir)
108 .map_err(|e| ProfileError::DirectoryError(e.to_string()))?;
109 }
110 Ok(())
111 }
112
113 pub fn save_profile(&mut self, profile: &ProfileConfig) -> ProfileResult<PathBuf> {
115 self.ensure_directory()?;
116
117 let filename = format!("{}.toml", profile.name);
118 let path = self.profile_dir.join(&filename);
119
120 let toml_content = profile.to_toml()?;
121 std::fs::write(&path, toml_content).map_err(|e| ProfileError::IoError(e.to_string()))?;
122
123 self.cache.insert(profile.name.clone(), profile.clone());
125
126 Ok(path)
127 }
128
129 pub fn load_profile(&mut self, name: &str) -> ProfileResult<ProfileConfig> {
131 validate_profile_name(name)?;
132
133 if let Some(profile) = self.cache.get(name) {
135 return Ok(profile.clone());
136 }
137
138 let filename = format!("{}.toml", name);
139 let path = self.profile_dir.join(&filename);
140
141 if !path.exists() {
142 return Err(ProfileError::NotFound(name.to_string()));
143 }
144
145 let content =
146 std::fs::read_to_string(&path).map_err(|e| ProfileError::IoError(e.to_string()))?;
147
148 let profile = ProfileConfig::from_toml(&content)?;
149
150 self.cache.insert(name.to_string(), profile.clone());
152
153 Ok(profile)
154 }
155
156 pub fn load_default(&mut self) -> ProfileConfig {
158 if let Some(name) = &self.default_profile.clone() {
159 if let Ok(profile) = self.load_profile(name) {
160 return profile;
161 }
162 }
163 ProfileConfig::default()
164 }
165
166 pub fn list_profiles(&self) -> ProfileResult<Vec<String>> {
168 if !self.profile_dir.exists() {
169 return Ok(vec![]);
170 }
171
172 let entries = std::fs::read_dir(&self.profile_dir)
173 .map_err(|e| ProfileError::IoError(e.to_string()))?;
174
175 let mut profiles = Vec::new();
176 for entry in entries.flatten() {
177 let path = entry.path();
178 if path.extension().map_or(false, |ext| ext == "toml") {
179 if let Some(stem) = path.file_stem() {
180 if let Some(name) = stem.to_str() {
181 profiles.push(name.to_string());
182 }
183 }
184 }
185 }
186
187 profiles.sort();
188 Ok(profiles)
189 }
190
191 pub fn delete_profile(&mut self, name: &str) -> ProfileResult<()> {
193 validate_profile_name(name)?;
194
195 let filename = format!("{}.toml", name);
196 let path = self.profile_dir.join(&filename);
197
198 if !path.exists() {
199 return Err(ProfileError::NotFound(name.to_string()));
200 }
201
202 std::fs::remove_file(&path).map_err(|e| ProfileError::IoError(e.to_string()))?;
203
204 self.cache.remove(name);
206
207 Ok(())
208 }
209
210 pub fn export_profile(&self, name: &str, export_path: &Path) -> ProfileResult<()> {
212 let profile = if let Some(cached) = self.cache.get(name) {
213 cached.clone()
214 } else {
215 let filename = format!("{}.toml", name);
216 let path = self.profile_dir.join(&filename);
217
218 if !path.exists() {
219 return Err(ProfileError::NotFound(name.to_string()));
220 }
221
222 let content =
223 std::fs::read_to_string(&path).map_err(|e| ProfileError::IoError(e.to_string()))?;
224
225 ProfileConfig::from_toml(&content)?
226 };
227
228 let toml_content = profile.to_toml()?;
229 std::fs::write(export_path, toml_content).map_err(|e| ProfileError::IoError(e.to_string()))
230 }
231
232 pub fn import_profile(&mut self, import_path: &Path) -> ProfileResult<ProfileConfig> {
234 if !import_path.exists() {
235 return Err(ProfileError::NotFound(import_path.display().to_string()));
236 }
237
238 let content = std::fs::read_to_string(import_path)
239 .map_err(|e| ProfileError::IoError(e.to_string()))?;
240
241 let profile = ProfileConfig::from_toml(&content)?;
242
243 self.save_profile(&profile)?;
245
246 Ok(profile)
247 }
248
249 pub fn profile_exists(&self, name: &str) -> bool {
251 if self.cache.contains_key(name) {
252 return true;
253 }
254
255 let filename = format!("{}.toml", name);
256 let path = self.profile_dir.join(&filename);
257 path.exists()
258 }
259
260 pub fn profile_count(&self) -> ProfileResult<usize> {
262 self.list_profiles().map(|p| p.len())
263 }
264
265 pub fn clear_cache(&mut self) {
267 self.cache.clear();
268 }
269}
270
271#[cfg(test)]
272mod tests;