1pub mod netrc;
2pub mod option;
3pub mod option_definitions;
4pub mod parser;
5pub mod uri_list;
6
7use std::collections::HashMap;
8use std::sync::Arc;
9
10pub use netrc::{NetRcEntry, NetRcError, NetRcFile};
11pub use option::{
12 ChoiceValidator, DependencyChecker, OptionCategory, OptionDef, OptionDefinition, OptionError,
13 OptionRegistry, OptionType, OptionValidator, OptionValue, PathValidator, RangeValidator,
14 RegexValidator, UrlValidator,
15};
16pub use parser::{ConfigError, ConfigParser, ConfigSource};
17pub use uri_list::{UriListEntry, UriListError, UriListFile};
18
19#[derive(Debug, Clone)]
23pub struct ConfigChangeEvent {
24 pub key: String,
26 pub old_value: OptionValue,
28 pub new_value: OptionValue,
30}
31
32pub struct ConfigManager {
58 global_opts: Arc<tokio::sync::RwLock<HashMap<String, OptionValue>>>,
59 task_defaults: Arc<tokio::sync::RwLock<HashMap<String, HashMap<String, OptionValue>>>>,
60 parser: ConfigParser,
61 registry: OptionRegistry,
62 change_tx: tokio::sync::broadcast::Sender<ConfigChangeEvent>,
63}
64
65impl ConfigManager {
66 pub fn new() -> Self {
69 let (change_tx, _) = tokio::sync::broadcast::channel(64);
70 let registry = OptionRegistry::new();
71 let mut parser = ConfigParser::with_registry(registry.clone());
72 parser.load_defaults_first();
73 Self {
74 global_opts: Arc::new(tokio::sync::RwLock::new(parser.options().clone())),
75 task_defaults: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
76 parser,
77 registry,
78 change_tx,
79 }
80 }
81
82 pub fn new_with_registry(registry: OptionRegistry) -> Self {
87 let (change_tx, _) = tokio::sync::broadcast::channel(64);
88 let mut parser = ConfigParser::with_registry(registry.clone());
89 parser.load_defaults_first();
90 Self {
91 global_opts: Arc::new(tokio::sync::RwLock::new(parser.options().clone())),
92 task_defaults: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
93 parser,
94 registry,
95 change_tx,
96 }
97 }
98
99 pub async fn load_cli(&mut self, args: &[String]) {
104 let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
105 self.parser.parse_cli_args(&args_ref);
106 self.sync_global().await;
107 }
108
109 pub async fn load_file(&mut self, path: &str) {
111 self.parser.parse_file(path);
112 self.sync_global().await;
113 }
114
115 pub async fn load_env(&mut self) {
119 self.parser.parse_env_vars();
120 self.sync_global().await;
121 }
122
123 async fn sync_global(&self) {
124 let mut opts = self.global_opts.write().await;
125 for (k, v) in self.parser.options() {
126 opts.insert(k.clone(), v.clone());
127 }
128 }
129
130 pub async fn get_global_option(&self, name: &str) -> Option<OptionValue> {
132 self.global_opts.read().await.get(name).cloned()
133 }
134
135 pub async fn get_global_str(&self, name: &str) -> Option<String> {
139 self.global_opts
140 .read()
141 .await
142 .get(name)
143 .and_then(|v| match v {
144 OptionValue::Str(s) => Some(s.clone()),
145 _ => None,
146 })
147 }
148
149 pub async fn get_global_i64(&self, name: &str) -> Option<i64> {
153 self.global_opts
154 .read()
155 .await
156 .get(name)
157 .and_then(|v| match v {
158 OptionValue::Int(n) => Some(*n),
159 _ => None,
160 })
161 }
162
163 pub async fn get_global_bool(&self, name: &str) -> Option<bool> {
167 self.global_opts
168 .read()
169 .await
170 .get(name)
171 .and_then(|v| match v {
172 OptionValue::Bool(b) => Some(*b),
173 _ => None,
174 })
175 }
176
177 pub async fn set_global_option(
183 &mut self,
184 name: &str,
185 value: OptionValue,
186 ) -> Result<(), String> {
187 if !self.registry.contains(name) {
188 return Err(format!("unknown option '{}'", name));
189 }
190 let def = self.registry.get(name).unwrap();
191 let parsed = def.parse_value(&value.to_string())?;
192 let old = self.global_opts.read().await.get(name).cloned();
193 {
194 let mut opts = self.global_opts.write().await;
195 opts.insert(name.to_string(), parsed.clone());
196 }
197 self.parser.set(name, value);
198 let _ = self.change_tx.send(ConfigChangeEvent {
199 key: name.to_string(),
200 old_value: old.unwrap_or(OptionValue::None),
201 new_value: parsed,
202 });
203 Ok(())
204 }
205
206 pub async fn change_global_options(&mut self, options: HashMap<String, String>) -> Vec<String> {
211 let mut errors = Vec::new();
212 for (key, value) in options {
213 if let Err(e) = self.set_global_option(&key, OptionValue::Str(value)).await {
214 errors.push(e);
215 }
216 }
217 errors
218 }
219
220 pub async fn get_all_global_options(&self) -> HashMap<String, OptionValue> {
221 self.global_opts.read().await.clone()
222 }
223
224 pub async fn get_all_global_options_json(&self) -> serde_json::Value {
225 let opts = self.global_opts.read().await;
226 let mut map = serde_json::Map::new();
227 for (k, v) in opts.iter() {
228 map.insert(
229 k.clone(),
230 <&OptionValue as Into<serde_json::Value>>::into(v),
231 );
232 }
233 serde_json::Value::Object(map)
234 }
235
236 pub async fn get_task_default(&self, gid: &str, name: &str) -> Option<OptionValue> {
237 let tasks = self.task_defaults.read().await;
238 let task_val = tasks.get(gid).and_then(|m| m.get(name)).cloned();
239 if task_val.is_some() {
240 return task_val;
241 }
242 drop(tasks);
243 self.global_opts.read().await.get(name).cloned()
244 }
245
246 pub async fn set_task_option(
247 &mut self,
248 gid: &str,
249 name: &str,
250 value: OptionValue,
251 ) -> Result<(), String> {
252 if !self.registry.contains(name) {
253 return Err(format!("unknown option '{}'", name));
254 }
255 let def = self.registry.get(name).unwrap();
256 let parsed = def.parse_value(&value.to_string())?;
257 let mut tasks = self.task_defaults.write().await;
258 let entry = tasks.entry(gid.to_string()).or_insert_with(HashMap::new);
259 entry.insert(name.to_string(), parsed);
260 Ok(())
261 }
262
263 pub async fn change_task_options(
264 &mut self,
265 gid: &str,
266 options: HashMap<String, String>,
267 ) -> Vec<String> {
268 let mut errors = Vec::new();
269 for (key, value) in options {
270 if let Err(e) = self
271 .set_task_option(gid, &key, OptionValue::Str(value))
272 .await
273 {
274 errors.push(e);
275 }
276 }
277 errors
278 }
279
280 pub async fn get_task_options(&self, gid: &str) -> HashMap<String, OptionValue> {
281 let tasks = self.task_defaults.read().await;
282 let global = self.global_opts.read().await;
283 if let Some(task_opts) = tasks.get(gid) {
284 let mut merged = global.clone();
285 for (k, v) in task_opts {
286 merged.insert(k.clone(), v.clone());
287 }
288 merged
289 } else {
290 global.clone()
291 }
292 }
293
294 pub async fn remove_task(&mut self, gid: &str) {
295 let mut tasks = self.task_defaults.write().await;
296 tasks.remove(gid);
297 }
298
299 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ConfigChangeEvent> {
304 self.change_tx.subscribe()
305 }
306
307 pub fn registry(&self) -> &OptionRegistry {
308 &self.registry
309 }
310 pub fn parser(&self) -> &ConfigParser {
311 &self.parser
312 }
313 pub fn has_errors(&self) -> bool {
314 self.parser.has_errors()
315 }
316 pub fn errors(&self) -> &[ConfigError] {
317 self.parser.errors()
318 }
319
320 pub async fn save_session(&self, path: &str) -> Result<(), String> {
321 let opts = self.global_opts.read().await;
322 let content = opts
323 .iter()
324 .filter_map(|(k, v)| {
325 if matches!(v, OptionValue::None) {
326 None
327 } else {
328 Some(format!("{}={}", k, v))
329 }
330 })
331 .collect::<Vec<_>>()
332 .join("\n");
333 std::fs::write(path, content).map_err(|e| format!("failed to save session: {}", e))
334 }
335
336 pub async fn load_session(&mut self, path: &str) -> Result<(), String> {
337 self.parser.parse_file(path);
338 self.sync_global().await;
339 Ok(())
340 }
341
342 pub async fn create_task_config(
343 &self,
344 overrides: HashMap<String, OptionValue>,
345 ) -> HashMap<String, OptionValue> {
346 let global = self.global_opts.read().await;
347 let mut config = global.clone();
348 for (k, v) in overrides {
349 config.insert(k, v);
350 }
351 config
352 }
353}
354
355impl Default for ConfigManager {
356 fn default() -> Self {
357 Self::new()
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[tokio::test]
366 async fn test_manager_creation() {
367 let mgr = ConfigManager::new();
368 assert!(!mgr.has_errors());
369 let dir = mgr.get_global_str("dir").await;
370 assert!(dir.is_some());
371 }
372
373 #[tokio::test]
374 async fn test_get_and_set_global() {
375 let mut mgr = ConfigManager::new();
376 let result = mgr.set_global_option("split", OptionValue::Int(8)).await;
377 assert!(result.is_ok());
378 let val = mgr.get_global_i64("split").await;
379 assert_eq!(val, Some(8));
380 }
381
382 #[tokio::test]
383 async fn test_set_unknown_option_fails() {
384 let mut mgr = ConfigManager::new();
385 let result = mgr
386 .set_global_option("nonexistent-option", OptionValue::Str("value".into()))
387 .await;
388 assert!(result.is_err());
389 }
390
391 #[tokio::test]
392 async fn test_change_global_options_batch() {
393 let mut mgr = ConfigManager::new();
394 let mut opts = HashMap::new();
395 opts.insert("split".to_string(), "10".to_string());
396 opts.insert("quiet".to_string(), "true".to_string());
397 let errors = mgr.change_global_options(opts).await;
398 assert!(errors.is_empty());
399 assert_eq!(mgr.get_global_i64("split").await, Some(10));
400 assert_eq!(mgr.get_global_bool("quiet").await, Some(true));
401 }
402
403 #[tokio::test]
404 async fn test_get_all_global_options() {
405 let mgr = ConfigManager::new();
406 let all = mgr.get_all_global_options().await;
407 assert!(!all.is_empty());
408 assert!(all.contains_key("dir"));
409 }
410
411 #[tokio::test]
412 async fn test_get_all_global_options_json() {
413 let mgr = ConfigManager::new();
414 let json = mgr.get_all_global_options_json().await;
415 assert!(json.is_object());
416 let obj = json.as_object().unwrap();
417 assert!(obj.contains_key("dir"));
418 }
419
420 #[tokio::test]
421 async fn test_task_options_inherit_global() {
422 let mut mgr = ConfigManager::new();
423 mgr.set_global_option("split", OptionValue::Int(8))
424 .await
425 .unwrap();
426 let task_val = mgr.get_task_default("gid-001", "split").await;
427 assert_eq!(task_val.as_ref().and_then(|v| v.as_i64()), Some(8));
428 }
429
430 #[tokio::test]
431 async fn test_task_options_override_global() {
432 let mut mgr = ConfigManager::new();
433 mgr.set_global_option("split", OptionValue::Int(5))
434 .await
435 .unwrap();
436 mgr.set_task_option("gid-001", "split", OptionValue::Int(12))
437 .await
438 .unwrap();
439 let task_val = mgr.get_task_default("gid-001", "split").await;
440 assert_eq!(task_val.and_then(|v| v.as_i64()), Some(12));
441 }
442
443 #[tokio::test]
444 async fn test_change_task_options_batch() {
445 let mut mgr = ConfigManager::new();
446 let mut opts = HashMap::new();
447 opts.insert("out".to_string(), "special.txt".to_string());
448 let errors = mgr.change_task_options("gid-002", opts).await;
449 assert!(errors.is_empty());
450 }
451
452 #[tokio::test]
453 async fn test_remove_task() {
454 let mut mgr = ConfigManager::new();
455 mgr.set_task_option("gid-003", "out", OptionValue::Str("file.txt".into()))
456 .await
457 .unwrap();
458 mgr.remove_task("gid-003").await;
459 let val = mgr.get_task_default("gid-003", "out").await;
460 assert_eq!(val.map(|v| v.as_str().map(|s| s.to_string())), None);
461 }
462
463 #[tokio::test]
464 async fn test_change_event_broadcast() {
465 let mut mgr = ConfigManager::new();
466 let mut rx = mgr.subscribe_changes();
467 mgr.set_global_option("quiet", OptionValue::Bool(true))
468 .await
469 .unwrap();
470 let event = rx.recv().await;
471 assert!(event.is_ok());
472 let evt = event.unwrap();
473 assert_eq!(evt.key, "quiet");
474 }
475
476 #[tokio::test]
477 async fn test_create_task_config_merges_overrides() {
478 let mut mgr = ConfigManager::new();
479 mgr.set_global_option("dir", OptionValue::Str("/global".into()))
480 .await
481 .unwrap();
482 let mut overrides = HashMap::new();
483 overrides.insert("dir".into(), OptionValue::Str("/local".into()));
484 overrides.insert("out".into(), OptionValue::Str("file.txt".into()));
485 let config = mgr.create_task_config(overrides).await;
486 assert_eq!(config.get("dir").and_then(|v| v.as_str()), Some("/local"));
487 assert_eq!(config.get("out").and_then(|v| v.as_str()), Some("file.txt"));
488 }
489
490 #[tokio::test]
491 async fn test_save_and_load_session() {
492 let mut mgr = ConfigManager::new();
493 mgr.set_global_option("split", OptionValue::Int(7))
494 .await
495 .unwrap();
496 let tmp_dir = std::env::temp_dir();
497 let session_path = format!(
498 "{}/aria2_test_session_{}.txt",
499 tmp_dir.display(),
500 std::process::id()
501 );
502 mgr.save_session(&session_path).await.unwrap();
503
504 let mut mgr2 = ConfigManager::new();
505 mgr2.load_session(&session_path).await.unwrap();
506 let val = mgr2.get_global_i64("split").await;
507 assert_eq!(val, Some(7));
508
509 let _ = std::fs::remove_file(&session_path);
510 }
511
512 #[tokio::test]
513 async fn test_load_cli_args() {
514 let mut mgr = ConfigManager::new();
515 mgr.load_cli(&["--dir=/custom/path".to_string(), "--split=12".to_string()])
516 .await;
517 assert_eq!(mgr.get_global_str("dir").await, Some("/custom/path".into()));
518 assert_eq!(mgr.get_global_i64("split").await, Some(12));
519 }
520
521 #[tokio::test]
522 async fn test_registry_access() {
523 let mgr = ConfigManager::new();
524 assert!(mgr.registry().contains("split"));
525 assert!(mgr.registry().get("dir").is_some());
526 assert!(mgr.registry().count() >= 60);
527 }
528}