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