vanguard_plugin/
config.rs1use serde::{de::DeserializeOwned, Serialize};
2use std::path::PathBuf;
3use thiserror::Error;
4use tokio::fs;
5use tokio::sync::RwLock;
6
7#[derive(Error, Debug)]
9pub enum ConfigError {
10 #[error("Config file not found: {0}")]
12 NotFound(PathBuf),
13
14 #[error("Failed to parse config: {0}")]
16 ParseError(String),
17
18 #[error("Config validation failed: {0}")]
20 ValidationError(String),
21
22 #[error("Failed to save config: {0}")]
24 SaveError(String),
25
26 #[error("IO error: {0}")]
28 IoError(#[from] std::io::Error),
29}
30
31pub type ConfigResult<T> = Result<T, ConfigError>;
33
34#[derive(Debug)]
36pub struct ConfigManager {
37 config_dir: PathBuf,
39 cache: RwLock<serde_json::Value>,
41}
42
43impl ConfigManager {
44 pub fn new(config_dir: PathBuf) -> Self {
46 Self {
47 config_dir,
48 cache: RwLock::new(serde_json::Value::Null),
49 }
50 }
51
52 pub async fn load_config<T: DeserializeOwned>(&self, plugin_name: &str) -> ConfigResult<T> {
54 let config_path = self.get_config_path(plugin_name);
55
56 if !config_path.exists() {
58 return Err(ConfigError::NotFound(config_path));
59 }
60
61 let content = fs::read_to_string(&config_path).await?;
63 let config: serde_json::Value =
64 serde_json::from_str(&content).map_err(|e| ConfigError::ParseError(e.to_string()))?;
65
66 *self.cache.write().await = config.clone();
68
69 serde_json::from_value(config).map_err(|e| ConfigError::ParseError(e.to_string()))
71 }
72
73 pub async fn save_config<T: Serialize>(
75 &self,
76 plugin_name: &str,
77 config: &T,
78 ) -> ConfigResult<()> {
79 let config_path = self.get_config_path(plugin_name);
80
81 if let Some(parent) = config_path.parent() {
83 fs::create_dir_all(parent).await?;
84 }
85
86 let content = serde_json::to_string_pretty(config)
88 .map_err(|e| ConfigError::SaveError(e.to_string()))?;
89
90 fs::write(&config_path, content).await?;
92
93 *self.cache.write().await =
95 serde_json::to_value(config).map_err(|e| ConfigError::SaveError(e.to_string()))?;
96
97 Ok(())
98 }
99
100 pub async fn validate_config(
102 &self,
103 config: &serde_json::Value,
104 schema: &serde_json::Value,
105 ) -> ConfigResult<()> {
106 if schema.get("type") == Some(&serde_json::json!("object")) && !config.is_object() {
109 return Err(ConfigError::ValidationError(
110 "Config must be an object".into(),
111 ));
112 }
113 Ok(())
114 }
115
116 fn get_config_path(&self, plugin_name: &str) -> PathBuf {
118 self.config_dir.join(plugin_name).with_extension("json")
119 }
120
121 pub async fn has_config(&self, plugin_name: &str) -> bool {
123 self.get_config_path(plugin_name).exists()
124 }
125
126 pub async fn delete_config(&self, plugin_name: &str) -> ConfigResult<()> {
128 let config_path = self.get_config_path(plugin_name);
129 if config_path.exists() {
130 fs::remove_file(&config_path).await?;
131 *self.cache.write().await = serde_json::Value::Null;
132 }
133 Ok(())
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use serde::{Deserialize, Serialize};
141 use tempfile::TempDir;
142
143 #[derive(Debug, Serialize, Deserialize, PartialEq)]
144 struct TestConfig {
145 name: String,
146 value: i32,
147 }
148
149 #[tokio::test]
150 async fn test_config_lifecycle() {
151 let temp_dir = TempDir::new().unwrap();
152 let config_manager = ConfigManager::new(temp_dir.path().to_path_buf());
153
154 assert!(!config_manager.has_config("test-plugin").await);
156
157 let config = TestConfig {
159 name: "test".to_string(),
160 value: 42,
161 };
162 config_manager
163 .save_config("test-plugin", &config)
164 .await
165 .unwrap();
166
167 assert!(config_manager.has_config("test-plugin").await);
169
170 let loaded: TestConfig = config_manager.load_config("test-plugin").await.unwrap();
172 assert_eq!(loaded, config);
173
174 config_manager.delete_config("test-plugin").await.unwrap();
176 assert!(!config_manager.has_config("test-plugin").await);
177 }
178
179 #[tokio::test]
180 async fn test_config_validation() {
181 let temp_dir = TempDir::new().unwrap();
182 let config_manager = ConfigManager::new(temp_dir.path().to_path_buf());
183
184 let valid_config = serde_json::json!({
186 "name": "test",
187 "value": 42
188 });
189
190 let schema = serde_json::json!({
191 "type": "object",
192 "properties": {
193 "name": { "type": "string" },
194 "value": { "type": "integer" }
195 }
196 });
197
198 assert!(config_manager
199 .validate_config(&valid_config, &schema)
200 .await
201 .is_ok());
202
203 let invalid_config = serde_json::json!(42);
205 assert!(matches!(
206 config_manager
207 .validate_config(&invalid_config, &schema)
208 .await,
209 Err(ConfigError::ValidationError(_))
210 ));
211 }
212
213 #[tokio::test]
214 async fn test_config_errors() {
215 let temp_dir = TempDir::new().unwrap();
216 let config_manager = ConfigManager::new(temp_dir.path().to_path_buf());
217
218 let result: ConfigResult<TestConfig> = config_manager.load_config("nonexistent").await;
220 assert!(matches!(result, Err(ConfigError::NotFound(_))));
221
222 let config_path = config_manager.get_config_path("invalid");
224 fs::create_dir_all(config_path.parent().unwrap())
225 .await
226 .unwrap();
227 fs::write(&config_path, "invalid json").await.unwrap();
228
229 let result: ConfigResult<TestConfig> = config_manager.load_config("invalid").await;
230 assert!(matches!(result, Err(ConfigError::ParseError(_))));
231 }
232}