Skip to main content

muse2/
settings.rs

1//! Code for loading program settings.
2use crate::get_muse2_config_dir;
3use crate::input::read_toml;
4use crate::log::DEFAULT_LOG_LEVEL;
5use anyhow::Result;
6use documented::DocumentedFields;
7use serde::{Deserialize, Serialize};
8use std::env;
9use std::fmt::Write;
10use std::path::{Path, PathBuf};
11
12const SETTINGS_FILE_NAME: &str = "settings.toml";
13
14const DEFAULT_SETTINGS_FILE_HEADER: &str = concat!(
15    "# This file contains the program settings for MUSE2.
16#
17# The default options for MUSE2 v",
18    env!("CARGO_PKG_VERSION"),
19    " are shown below, commented out. To change an option, uncomment it and set the value
20# appropriately.
21#
22# To show the default options for the current version of MUSE2, run:
23# \tmuse2 settings show-default
24#
25# For information about the possible settings, visit:
26# \t",
27    crate::docs_url!("file_formats/program_settings.html"),
28    "\n"
29);
30
31/// Get the path to where the settings file will be read from
32pub fn get_settings_file_path() -> PathBuf {
33    let mut path = get_muse2_config_dir();
34    path.push(SETTINGS_FILE_NAME);
35
36    path
37}
38
39/// Program settings from config file
40///
41/// NOTE: If you add or change a field in this struct, you must also update the schema in
42/// `schemas/settings.yaml`.
43#[derive(Debug, DocumentedFields, Serialize, Deserialize, PartialEq)]
44#[serde(default)]
45pub struct Settings {
46    /// The default program log level
47    pub log_level: String,
48    /// Whether to overwrite output files by default
49    pub overwrite: bool,
50    /// Whether to write additional information to CSV files
51    pub debug_model: bool,
52    /// Results root path to save MUSE2 results. Defaults to `muse2_results`.
53    pub results_root: PathBuf,
54    /// Results root path to save MUSE2 graph outputs. Defaults to `muse2_graphs`.
55    pub graph_results_root: PathBuf,
56    /// Whether to copy input files to the output folder.
57    pub copy_input_files: bool,
58}
59
60impl Default for Settings {
61    fn default() -> Self {
62        Self {
63            log_level: DEFAULT_LOG_LEVEL.to_string(),
64            overwrite: false,
65            debug_model: false,
66            results_root: PathBuf::from("muse2_results"),
67            graph_results_root: PathBuf::from("muse2_graphs"),
68            copy_input_files: true,
69        }
70    }
71}
72
73impl Settings {
74    /// Read the contents of a settings file from the global MUSE2 configuration directory.
75    ///
76    /// If the file is not present or the user has set the `MUSE2_USE_DEFAULT_SETTINGS` environment
77    /// variable to 1, then the default settings will be used.
78    ///
79    /// # Returns
80    ///
81    /// The program settings as a `Settings` struct or an error if loading fails.
82    pub fn load_or_default() -> Result<Settings> {
83        if env::var("MUSE2_USE_DEFAULT_SETTINGS").is_ok_and(|v| v == "1") {
84            Ok(Settings::default())
85        } else {
86            Self::from_path_or_default(&get_settings_file_path())
87        }
88    }
89
90    /// Try to read settings from the specified path, returning `Settings::default()` if it doesn't
91    /// exist
92    fn from_path_or_default(file_path: &Path) -> Result<Settings> {
93        if !file_path.is_file() {
94            return Ok(Settings::default());
95        }
96
97        read_toml(file_path)
98    }
99
100    /// The contents of the default settings file.
101    pub fn default_file_contents() -> String {
102        // Settings object with default values for params
103        let settings = Settings::default();
104
105        // Convert to TOML
106        let settings_raw = toml::to_string(&settings).expect("Could not convert settings to TOML");
107
108        // Iterate through the generated TOML, commenting out parameter lines and inserting
109        // their documentation comments
110        let mut out = DEFAULT_SETTINGS_FILE_HEADER.to_string();
111        for line in settings_raw.split('\n') {
112            if let Some((field, _)) = line.split_once('=') {
113                // Add documentation from doc comments
114                let field = field.trim();
115
116                // Use doc comment to document parameter. All fields should have doc comments.
117                let docs = Settings::get_field_docs(field).expect("Missing doc comment for field");
118                for line in docs.split('\n') {
119                    write!(&mut out, "\n# # {}\n", line.trim()).unwrap();
120                }
121
122                writeln!(&mut out, "# {}", line.trim()).unwrap();
123            }
124        }
125
126        out
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use std::fs::File;
134    use std::io::Write;
135    use tempfile::tempdir;
136
137    #[test]
138    fn settings_from_path_or_default_no_file() {
139        let dir = tempdir().unwrap();
140        let file_path = dir.path().join(SETTINGS_FILE_NAME); // NB: doesn't exist
141        assert_eq!(
142            Settings::from_path_or_default(&file_path).unwrap(),
143            Settings::default()
144        );
145    }
146
147    #[test]
148    fn settings_from_path_or_default() {
149        let dir = tempdir().unwrap();
150        let file_path = dir.path().join(SETTINGS_FILE_NAME);
151
152        {
153            let mut file = File::create(&file_path).unwrap();
154            writeln!(file, "log_level = \"warn\"").unwrap();
155        }
156
157        assert_eq!(
158            Settings::from_path_or_default(&file_path).unwrap(),
159            Settings {
160                log_level: "warn".to_string(),
161                ..Settings::default()
162            }
163        );
164    }
165
166    #[test]
167    fn default_file_contents() {
168        assert!(!Settings::default_file_contents().is_empty());
169    }
170}