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 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() {
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 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 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 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 let json_value: serde_json::Value = serde_json::from_str(value)
128 .unwrap_or_else(|_| serde_json::Value::String(value.to_string()));
129
130 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 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 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 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 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 provider.set_raw("key1", "value1").await.unwrap();
239 provider.set_raw("key2", "42").await.unwrap();
240
241 assert!(config_path.exists());
243
244 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 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}