Skip to main content

claude_agent/config/
file.rs

1//! File-based Configuration Provider
2//!
3//! Loads configuration from JSON files (CLI compatible).
4
5use std::collections::HashMap;
6use std::path::PathBuf;
7use std::sync::Arc;
8
9use tokio::sync::RwLock;
10
11use super::ConfigResult;
12use super::provider::ConfigProvider;
13
14/// File-based configuration provider
15pub struct FileConfigProvider {
16    /// Path to the configuration file
17    path: PathBuf,
18    /// Cached data
19    data: Arc<RwLock<Option<HashMap<String, serde_json::Value>>>>,
20    /// Whether to auto-reload on get
21    auto_reload: bool,
22}
23
24impl FileConfigProvider {
25    /// Create a new file provider
26    pub fn new(path: PathBuf) -> Self {
27        Self {
28            path,
29            data: Arc::new(RwLock::new(None)),
30            auto_reload: false,
31        }
32    }
33
34    /// Create a file provider with auto-reload enabled
35    pub fn auto_reload(path: PathBuf) -> Self {
36        Self {
37            path,
38            data: Arc::new(RwLock::new(None)),
39            auto_reload: true,
40        }
41    }
42
43    /// Load configuration from file
44    async fn load(&self) -> ConfigResult<HashMap<String, serde_json::Value>> {
45        if !self.path.exists() {
46            return Ok(HashMap::new());
47        }
48
49        let content = tokio::fs::read_to_string(&self.path).await?;
50        let data: HashMap<String, serde_json::Value> = serde_json::from_str(&content)?;
51        Ok(data)
52    }
53
54    /// Ensure data is loaded
55    async fn ensure_loaded(&self) -> ConfigResult<()> {
56        let mut data = self.data.write().await;
57        if data.is_none() || self.auto_reload {
58            *data = Some(self.load().await?);
59        }
60        Ok(())
61    }
62
63    /// Save configuration to file
64    async fn save(&self, data: &HashMap<String, serde_json::Value>) -> ConfigResult<()> {
65        if let Some(parent) = self.path.parent() {
66            tokio::fs::create_dir_all(parent).await?;
67        }
68
69        let content = serde_json::to_string_pretty(data)?;
70        tokio::fs::write(&self.path, content).await?;
71        Ok(())
72    }
73
74    /// Reload configuration from file
75    pub async fn reload(&self) -> ConfigResult<()> {
76        let mut data = self.data.write().await;
77        *data = Some(self.load().await?);
78        Ok(())
79    }
80
81    /// Get the file path
82    pub fn path(&self) -> &PathBuf {
83        &self.path
84    }
85}
86
87#[async_trait::async_trait]
88impl ConfigProvider for FileConfigProvider {
89    fn name(&self) -> &str {
90        "file"
91    }
92
93    async fn get_raw(&self, key: &str) -> ConfigResult<Option<String>> {
94        self.ensure_loaded().await?;
95
96        let data = self.data.read().await;
97        if let Some(ref map) = *data {
98            // Support nested keys with dot notation
99            let parts: Vec<&str> = key.split('.').collect();
100            let mut current: Option<&serde_json::Value> = None;
101
102            for (i, part) in parts.iter().enumerate() {
103                if i == 0 {
104                    current = map.get(*part);
105                } else {
106                    current = current.and_then(|v| v.get(*part));
107                }
108            }
109
110            match current {
111                Some(serde_json::Value::String(s)) => Ok(Some(s.clone())),
112                Some(v) => Ok(Some(v.to_string())),
113                None => Ok(None),
114            }
115        } else {
116            Ok(None)
117        }
118    }
119
120    async fn set_raw(&self, key: &str, value: &str) -> ConfigResult<()> {
121        self.ensure_loaded().await?;
122
123        let mut data = self.data.write().await;
124        let map = data.get_or_insert_with(HashMap::new);
125
126        // Parse value as JSON if possible, otherwise store as string
127        let json_value: serde_json::Value = serde_json::from_str(value)
128            .unwrap_or_else(|_| serde_json::Value::String(value.to_string()));
129
130        // Simple top-level set (doesn't support nested paths for writes)
131        map.insert(key.to_string(), json_value);
132
133        self.save(map).await?;
134        Ok(())
135    }
136
137    async fn delete(&self, key: &str) -> ConfigResult<bool> {
138        self.ensure_loaded().await?;
139
140        let mut data = self.data.write().await;
141        if let Some(ref mut map) = *data {
142            let existed = map.remove(key).is_some();
143            if existed {
144                self.save(map).await?;
145            }
146            Ok(existed)
147        } else {
148            Ok(false)
149        }
150    }
151
152    async fn list_keys(&self, prefix: &str) -> ConfigResult<Vec<String>> {
153        self.ensure_loaded().await?;
154
155        let data = self.data.read().await;
156        if let Some(ref map) = *data {
157            let keys: Vec<String> = map
158                .keys()
159                .filter(|k| k.starts_with(prefix))
160                .cloned()
161                .collect();
162            Ok(keys)
163        } else {
164            Ok(Vec::new())
165        }
166    }
167}
168
169impl std::fmt::Debug for FileConfigProvider {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        f.debug_struct("FileConfigProvider")
172            .field("path", &self.path)
173            .field("auto_reload", &self.auto_reload)
174            .finish()
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use tempfile::TempDir;
182
183    #[tokio::test]
184    async fn test_file_provider_create_and_read() {
185        let temp_dir = TempDir::new().unwrap();
186        let config_path = temp_dir.path().join("config.json");
187
188        // Create config file
189        let config = serde_json::json!({
190            "api_key": "sk-test-123",
191            "model": "claude-sonnet-4-5",
192            "nested": {
193                "value": "inner"
194            }
195        });
196        tokio::fs::write(&config_path, config.to_string())
197            .await
198            .unwrap();
199
200        let provider = FileConfigProvider::new(config_path);
201
202        // Read values
203        assert_eq!(
204            provider.get_raw("api_key").await.unwrap(),
205            Some("sk-test-123".to_string())
206        );
207        assert_eq!(
208            provider.get_raw("model").await.unwrap(),
209            Some("claude-sonnet-4-5".to_string())
210        );
211
212        // Read nested - string values from nested objects
213        assert_eq!(
214            provider.get_raw("nested.value").await.unwrap(),
215            Some("inner".to_string())
216        );
217    }
218
219    #[tokio::test]
220    async fn test_file_provider_nonexistent() {
221        let temp_dir = TempDir::new().unwrap();
222        let config_path = temp_dir.path().join("nonexistent.json");
223
224        let provider = FileConfigProvider::new(config_path);
225
226        // Should return None for non-existent file
227        assert_eq!(provider.get_raw("key").await.unwrap(), None);
228    }
229
230    #[tokio::test]
231    async fn test_file_provider_write() {
232        let temp_dir = TempDir::new().unwrap();
233        let config_path = temp_dir.path().join("new_config.json");
234
235        let provider = FileConfigProvider::new(config_path.clone());
236
237        // Write
238        provider.set_raw("key1", "value1").await.unwrap();
239        provider.set_raw("key2", "42").await.unwrap();
240
241        // Verify file was created
242        assert!(config_path.exists());
243
244        // Read back
245        assert_eq!(
246            provider.get_raw("key1").await.unwrap(),
247            Some("value1".to_string())
248        );
249    }
250
251    #[tokio::test]
252    async fn test_file_provider_delete() {
253        let temp_dir = TempDir::new().unwrap();
254        let config_path = temp_dir.path().join("delete_config.json");
255
256        let provider = FileConfigProvider::new(config_path);
257
258        // Set then delete
259        provider.set_raw("temp", "value").await.unwrap();
260        assert!(provider.delete("temp").await.unwrap());
261        assert_eq!(provider.get_raw("temp").await.unwrap(), None);
262    }
263
264    #[tokio::test]
265    async fn test_file_provider_list_keys() {
266        let temp_dir = TempDir::new().unwrap();
267        let config_path = temp_dir.path().join("list_config.json");
268
269        let provider = FileConfigProvider::new(config_path);
270
271        provider.set_raw("app.name", "\"test\"").await.unwrap();
272        provider.set_raw("app.version", "\"1.0\"").await.unwrap();
273        provider.set_raw("other", "\"value\"").await.unwrap();
274
275        let keys = provider.list_keys("app.").await.unwrap();
276        assert_eq!(keys.len(), 2);
277    }
278}