Skip to main content

mfp_core/
config.rs

1//! The optional configuration file.
2//!
3//! Every setting has a default, so a missing file yields those defaults rather than an error
4//! and the player runs unconfigured. A file that exists but cannot be parsed is an error:
5//! ignoring a typo would leave a user's setting quietly unapplied.
6
7use std::path::Path;
8
9use serde::{Deserialize, Serialize};
10
11use crate::error::{Error, Result};
12
13/// Player settings, as read from `config.toml`.
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15#[serde(default)]
16pub struct Config {
17    /// Seconds the daemon may sit with nothing playing, nothing downloading, and no client
18    /// connected before it exits. `None`, the default, disables it - it idles indefinitely.
19    pub idle_timeout_secs: Option<u64>,
20    /// How many downloads may transfer at once. Further requests queue.
21    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    /// Loads the configuration from [`crate::paths::config_file`].
35    pub fn load() -> Result<Self> {
36        Self::load_from(&crate::paths::config_file()?)
37    }
38
39    /// Loads the configuration from `path`, returning the defaults when it does not exist.
40    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        // a typo in a user's own file is their mistake to see, not an internal one
50        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    /// Loudness belongs to the operating system's mixer and the cache limit was never
96    /// implemented, so both settings are gone - but a file naming them must still load the
97    /// settings beside them.
98    #[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}