claude_agent/config/
file.rs1use 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
14pub struct FileConfigProvider {
16 path: PathBuf,
18 data: Arc<RwLock<Option<HashMap<String, serde_json::Value>>>>,
20 auto_reload: bool,
22}
23
24impl FileConfigProvider {
25 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 pub fn with_auto_reload(path: PathBuf) -> Self {
36 Self {
37 path,
38 data: Arc::new(RwLock::new(None)),
39 auto_reload: true,
40 }
41 }
42
43 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 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 async fn save(&self, data: &HashMap<String, serde_json::Value>) -> ConfigResult<()> {
65 if let Some(parent) = self.path.parent() {
67 tokio::fs::create_dir_all(parent).await?;
68 }
69
70 let content = serde_json::to_string_pretty(data)?;
71 tokio::fs::write(&self.path, content).await?;
72 Ok(())
73 }
74
75 pub async fn reload(&self) -> ConfigResult<()> {
77 let mut data = self.data.write().await;
78 *data = Some(self.load().await?);
79 Ok(())
80 }
81
82 pub fn path(&self) -> &PathBuf {
84 &self.path
85 }
86}
87
88#[async_trait::async_trait]
89impl ConfigProvider for FileConfigProvider {
90 fn name(&self) -> &str {
91 "file"
92 }
93
94 async fn get_raw(&self, key: &str) -> ConfigResult<Option<String>> {
95 self.ensure_loaded().await?;
96
97 let data = self.data.read().await;
98 if let Some(ref map) = *data {
99 let parts: Vec<&str> = key.split('.').collect();
101 let mut current: Option<&serde_json::Value> = None;
102
103 for (i, part) in parts.iter().enumerate() {
104 if i == 0 {
105 current = map.get(*part);
106 } else {
107 current = current.and_then(|v| v.get(*part));
108 }
109 }
110
111 match current {
112 Some(serde_json::Value::String(s)) => Ok(Some(s.clone())),
113 Some(v) => Ok(Some(v.to_string())),
114 None => Ok(None),
115 }
116 } else {
117 Ok(None)
118 }
119 }
120
121 async fn set_raw(&self, key: &str, value: &str) -> ConfigResult<()> {
122 self.ensure_loaded().await?;
123
124 let mut data = self.data.write().await;
125 let map = data.get_or_insert_with(HashMap::new);
126
127 let json_value: serde_json::Value = serde_json::from_str(value)
129 .unwrap_or_else(|_| serde_json::Value::String(value.to_string()));
130
131 map.insert(key.to_string(), json_value);
133
134 self.save(map).await?;
135 Ok(())
136 }
137
138 async fn delete(&self, key: &str) -> ConfigResult<bool> {
139 self.ensure_loaded().await?;
140
141 let mut data = self.data.write().await;
142 if let Some(ref mut map) = *data {
143 let existed = map.remove(key).is_some();
144 if existed {
145 self.save(map).await?;
146 }
147 Ok(existed)
148 } else {
149 Ok(false)
150 }
151 }
152
153 async fn list_keys(&self, prefix: &str) -> ConfigResult<Vec<String>> {
154 self.ensure_loaded().await?;
155
156 let data = self.data.read().await;
157 if let Some(ref map) = *data {
158 let keys: Vec<String> = map
159 .keys()
160 .filter(|k| k.starts_with(prefix))
161 .cloned()
162 .collect();
163 Ok(keys)
164 } else {
165 Ok(Vec::new())
166 }
167 }
168}
169
170impl std::fmt::Debug for FileConfigProvider {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 f.debug_struct("FileConfigProvider")
173 .field("path", &self.path)
174 .field("auto_reload", &self.auto_reload)
175 .finish()
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use tempfile::TempDir;
183
184 #[tokio::test]
185 async fn test_file_provider_create_and_read() {
186 let temp_dir = TempDir::new().unwrap();
187 let config_path = temp_dir.path().join("config.json");
188
189 let config = serde_json::json!({
191 "api_key": "sk-test-123",
192 "model": "claude-sonnet-4-5",
193 "nested": {
194 "value": "inner"
195 }
196 });
197 tokio::fs::write(&config_path, config.to_string())
198 .await
199 .unwrap();
200
201 let provider = FileConfigProvider::new(config_path);
202
203 assert_eq!(
205 provider.get_raw("api_key").await.unwrap(),
206 Some("sk-test-123".to_string())
207 );
208 assert_eq!(
209 provider.get_raw("model").await.unwrap(),
210 Some("claude-sonnet-4-5".to_string())
211 );
212
213 assert_eq!(
215 provider.get_raw("nested.value").await.unwrap(),
216 Some("inner".to_string())
217 );
218 }
219
220 #[tokio::test]
221 async fn test_file_provider_nonexistent() {
222 let temp_dir = TempDir::new().unwrap();
223 let config_path = temp_dir.path().join("nonexistent.json");
224
225 let provider = FileConfigProvider::new(config_path);
226
227 assert_eq!(provider.get_raw("key").await.unwrap(), None);
229 }
230
231 #[tokio::test]
232 async fn test_file_provider_write() {
233 let temp_dir = TempDir::new().unwrap();
234 let config_path = temp_dir.path().join("new_config.json");
235
236 let provider = FileConfigProvider::new(config_path.clone());
237
238 provider.set_raw("key1", "value1").await.unwrap();
240 provider.set_raw("key2", "42").await.unwrap();
241
242 assert!(config_path.exists());
244
245 assert_eq!(
247 provider.get_raw("key1").await.unwrap(),
248 Some("value1".to_string())
249 );
250 }
251
252 #[tokio::test]
253 async fn test_file_provider_delete() {
254 let temp_dir = TempDir::new().unwrap();
255 let config_path = temp_dir.path().join("delete_config.json");
256
257 let provider = FileConfigProvider::new(config_path);
258
259 provider.set_raw("temp", "value").await.unwrap();
261 assert!(provider.delete("temp").await.unwrap());
262 assert_eq!(provider.get_raw("temp").await.unwrap(), None);
263 }
264
265 #[tokio::test]
266 async fn test_file_provider_list_keys() {
267 let temp_dir = TempDir::new().unwrap();
268 let config_path = temp_dir.path().join("list_config.json");
269
270 let provider = FileConfigProvider::new(config_path);
271
272 provider.set_raw("app.name", "\"test\"").await.unwrap();
273 provider.set_raw("app.version", "\"1.0\"").await.unwrap();
274 provider.set_raw("other", "\"value\"").await.unwrap();
275
276 let keys = provider.list_keys("app.").await.unwrap();
277 assert_eq!(keys.len(), 2);
278 }
279}