vanguard_plugin/
config.rs

1use serde::{de::DeserializeOwned, Serialize};
2use std::path::PathBuf;
3use thiserror::Error;
4use tokio::fs;
5use tokio::sync::RwLock;
6
7/// Errors that can occur during plugin configuration operations
8#[derive(Error, Debug)]
9pub enum ConfigError {
10    /// Configuration file not found
11    #[error("Config file not found: {0}")]
12    NotFound(PathBuf),
13
14    /// Failed to parse configuration
15    #[error("Failed to parse config: {0}")]
16    ParseError(String),
17
18    /// Failed to validate configuration
19    #[error("Config validation failed: {0}")]
20    ValidationError(String),
21
22    /// Failed to save configuration
23    #[error("Failed to save config: {0}")]
24    SaveError(String),
25
26    /// IO error occurred
27    #[error("IO error: {0}")]
28    IoError(#[from] std::io::Error),
29}
30
31/// Result type for configuration operations
32pub type ConfigResult<T> = Result<T, ConfigError>;
33
34/// Manages plugin configuration storage and validation
35#[derive(Debug)]
36pub struct ConfigManager {
37    /// Base directory for plugin configs
38    config_dir: PathBuf,
39    /// In-memory config cache
40    cache: RwLock<serde_json::Value>,
41}
42
43impl ConfigManager {
44    /// Create a new config manager
45    pub fn new(config_dir: PathBuf) -> Self {
46        Self {
47            config_dir,
48            cache: RwLock::new(serde_json::Value::Null),
49        }
50    }
51
52    /// Load configuration for a plugin
53    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        // Check if config file exists
57        if !config_path.exists() {
58            return Err(ConfigError::NotFound(config_path));
59        }
60
61        // Read and parse config file
62        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        // Update cache
67        *self.cache.write().await = config.clone();
68
69        // Convert to requested type
70        serde_json::from_value(config).map_err(|e| ConfigError::ParseError(e.to_string()))
71    }
72
73    /// Save configuration for a plugin
74    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        // Create parent directories if they don't exist
82        if let Some(parent) = config_path.parent() {
83            fs::create_dir_all(parent).await?;
84        }
85
86        // Serialize config
87        let content = serde_json::to_string_pretty(config)
88            .map_err(|e| ConfigError::SaveError(e.to_string()))?;
89
90        // Write to file
91        fs::write(&config_path, content).await?;
92
93        // Update cache
94        *self.cache.write().await =
95            serde_json::to_value(config).map_err(|e| ConfigError::SaveError(e.to_string()))?;
96
97        Ok(())
98    }
99
100    /// Validate configuration against a JSON schema
101    pub async fn validate_config(
102        &self,
103        config: &serde_json::Value,
104        schema: &serde_json::Value,
105    ) -> ConfigResult<()> {
106        // TODO: Implement JSON Schema validation
107        // For now just check that config is an object if schema requires it
108        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    /// Get the path to a plugin's config file
117    fn get_config_path(&self, plugin_name: &str) -> PathBuf {
118        self.config_dir.join(plugin_name).with_extension("json")
119    }
120
121    /// Check if configuration exists for a plugin
122    pub async fn has_config(&self, plugin_name: &str) -> bool {
123        self.get_config_path(plugin_name).exists()
124    }
125
126    /// Delete configuration for a plugin
127    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        // Initially no config exists
155        assert!(!config_manager.has_config("test-plugin").await);
156
157        // Save new config
158        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        // Config should now exist
168        assert!(config_manager.has_config("test-plugin").await);
169
170        // Load and verify config
171        let loaded: TestConfig = config_manager.load_config("test-plugin").await.unwrap();
172        assert_eq!(loaded, config);
173
174        // Delete config
175        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        // Valid object config
185        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        // Invalid non-object config
204        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        // Test loading non-existent config
219        let result: ConfigResult<TestConfig> = config_manager.load_config("nonexistent").await;
220        assert!(matches!(result, Err(ConfigError::NotFound(_))));
221
222        // Test loading invalid JSON
223        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}