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# \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    /// Whether to copy input files to the output folder.
56    pub copy_input_files: bool,
57}
58
59impl Default for Settings {
60    fn default() -> Self {
61        Self {
62            log_level: DEFAULT_LOG_LEVEL.to_string(),
63            overwrite: false,
64            debug_model: false,
65            results_root: PathBuf::from("muse2_results"),
66            graph_results_root: PathBuf::from("muse2_graphs"),
67            copy_input_files: true,
68        }
69    }
70}
71
72impl Settings {
73    /// Read the contents of a settings file from the global MUSE2 configuration directory.
74    ///
75    /// If the file is not present or the user has set the `MUSE2_USE_DEFAULT_SETTINGS` environment
76    /// variable to 1, then the default settings will be used.
77    ///
78    /// # Returns
79    ///
80    /// The program settings as a `Settings` struct or an error if loading fails.
81    pub fn load_or_default() -> Result<Settings> {
82        if env::var("MUSE2_USE_DEFAULT_SETTINGS").is_ok_and(|v| v == "1") {
83            Ok(Settings::default())
84        } else {
85            Self::from_path_or_default(&get_settings_file_path())
86        }
87    }
88
89    /// Try to read settings from the specified path, returning `Settings::default()` if it doesn't
90    /// exist
91    fn from_path_or_default(file_path: &Path) -> Result<Settings> {
92        if !file_path.is_file() {
93            return Ok(Settings::default());
94        }
95
96        read_toml(file_path)
97    }
98
99    /// The contents of the default settings file.
100    pub fn default_file_contents() -> String {
101        // Settings object with default values for params
102        let settings = Settings::default();
103
104        // Convert to TOML
105        let settings_raw = toml::to_string(&settings).expect("Could not convert settings to TOML");
106
107        // Iterate through the generated TOML, commenting out parameter lines and inserting
108        // their documentation comments
109        let mut out = DEFAULT_SETTINGS_FILE_HEADER.to_string();
110        for line in settings_raw.split('\n') {
111            if let Some((field, _)) = line.split_once('=') {
112                // Add documentation from doc comments
113                let field = field.trim();
114
115                // Use doc comment to document parameter. All fields should have doc comments.
116                let docs = Settings::get_field_docs(field).expect("Missing doc comment for field");
117                for line in docs.split('\n') {
118                    write!(&mut out, "\n# # {}\n", line.trim()).unwrap();
119                }
120
121                writeln!(&mut out, "# {}", line.trim()).unwrap();
122            }
123        }
124
125        out
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use std::fs::File;
133    use std::io::Write;
134    use tempfile::tempdir;
135
136    #[test]
137    fn settings_from_path_or_default_no_file() {
138        let dir = tempdir().unwrap();
139        let file_path = dir.path().join(SETTINGS_FILE_NAME); // NB: doesn't exist
140        assert_eq!(
141            Settings::from_path_or_default(&file_path).unwrap(),
142            Settings::default()
143        );
144    }
145
146    #[test]
147    fn settings_from_path_or_default() {
148        let dir = tempdir().unwrap();
149        let file_path = dir.path().join(SETTINGS_FILE_NAME);
150
151        {
152            let mut file = File::create(&file_path).unwrap();
153            writeln!(file, "log_level = \"warn\"").unwrap();
154        }
155
156        assert_eq!(
157            Settings::from_path_or_default(&file_path).unwrap(),
158            Settings {
159                log_level: "warn".to_string(),
160                ..Settings::default()
161            }
162        );
163    }
164
165    #[test]
166    fn default_file_contents() {
167        assert!(!Settings::default_file_contents().is_empty());
168    }
169}