1use std::path::Path;
8
9use serde::{Deserialize, Serialize};
10
11use crate::error::{Error, Result};
12
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15#[serde(default)]
16pub struct Config {
17 pub idle_timeout_secs: Option<u64>,
20 pub max_concurrent_downloads: usize,
22}
23
24impl Default for Config {
25 fn default() -> Self {
26 Self {
27 idle_timeout_secs: None,
28 max_concurrent_downloads: 2,
29 }
30 }
31}
32
33impl Config {
34 pub fn load() -> Result<Self> {
36 Self::load_from(&crate::paths::config_file()?)
37 }
38
39 pub fn load_from(path: &Path) -> Result<Self> {
41 let text = match std::fs::read_to_string(path) {
42 Ok(text) => text,
43 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
44 return Ok(Self::default());
45 }
46 Err(error) => return Err(error.into()),
47 };
48
49 toml::from_str(&text).map_err(|error| {
51 Error::InvalidParams(format!("{} is not valid: {error}", path.display()))
52 })
53 }
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 #[test]
61 fn a_missing_file_yields_defaults() {
62 let root = tempfile::tempdir().unwrap();
63 let config = Config::load_from(&root.path().join("config.toml")).unwrap();
64 assert_eq!(config, Config::default());
65 }
66
67 #[test]
68 fn the_idle_timeout_defaults_to_disabled() {
69 assert_eq!(Config::default().idle_timeout_secs, None);
70 }
71
72 #[test]
73 fn an_empty_file_yields_defaults() {
74 let root = tempfile::tempdir().unwrap();
75 let path = root.path().join("config.toml");
76 std::fs::write(&path, "").unwrap();
77 assert_eq!(Config::load_from(&path).unwrap(), Config::default());
78 }
79
80 #[test]
81 fn a_partial_file_leaves_the_rest_at_their_defaults() {
82 let root = tempfile::tempdir().unwrap();
83 let path = root.path().join("config.toml");
84 std::fs::write(&path, "idle_timeout_secs = 600\n").unwrap();
85
86 let config = Config::load_from(&path).unwrap();
87
88 assert_eq!(config.idle_timeout_secs, Some(600));
89 assert_eq!(
90 config.max_concurrent_downloads,
91 Config::default().max_concurrent_downloads
92 );
93 }
94
95 #[test]
99 fn a_file_naming_a_setting_that_no_longer_exists_still_loads() {
100 let root = tempfile::tempdir().unwrap();
101 let path = root.path().join("config.toml");
102 std::fs::write(
103 &path,
104 "volume = 0.3\nmuted = true\ncache_max_bytes = 1024\nidle_timeout_secs = 600\n",
105 )
106 .unwrap();
107
108 let config = Config::load_from(&path).unwrap();
109
110 assert_eq!(config.idle_timeout_secs, Some(600));
111 }
112
113 #[test]
114 fn an_unparseable_file_is_reported_rather_than_ignored() {
115 let root = tempfile::tempdir().unwrap();
116 let path = root.path().join("config.toml");
117 std::fs::write(&path, "idle_timeout_secs = = 3\n").unwrap();
118
119 let error = Config::load_from(&path).unwrap_err();
120
121 assert_eq!(error.code(), crate::error::ErrorCode::InvalidParams);
122 assert!(error.to_string().contains("is not valid"));
123 }
124}