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