rust_utils/
config.rs

1//! Basic config API
2//!
3//! Contains a special trait that allows a type to have an associated config file
4
5use serde::{Deserialize, Serialize};
6use std::{
7    env, fs,
8    io::Result as IoResult,
9    path::PathBuf
10};
11use ron::ser::PrettyConfig;
12
13/// The type of config file we are using
14#[derive(Clone, Copy)]
15pub enum ConfigType {
16    Toml,
17    Ron
18}
19
20/// The `Config` trait allows a type to have an associated config file
21///
22/// Requires the [`Default`], [`Serialize`], and [`Deserialize`] traits (can be derived) to be implemented
23pub trait Config: Serialize + Default + for <'de> Deserialize<'de> {
24    /// This constant must be set to the file name of the config file
25    const FILE_NAME: &'static str;
26
27    /// This constant is the type of config
28    ///
29    /// Defaults to TOML
30    const TYPE: ConfigType = ConfigType::Toml;
31
32    /// Returns the save directory of the config file.
33    ///
34    /// This method must be defined
35    fn get_save_dir() -> PathBuf;
36
37    /// Returns the full path of the config file
38    fn get_full_path() -> PathBuf {
39        Self::get_save_dir().join(Self::FILE_NAME)
40    }
41
42    /// Load from the config file
43    ///
44    /// Returns the defaults if the file does not exist or is corrupted
45    ///
46    /// Panics if the config directory cannot be created
47    #[track_caller]
48    fn load() -> Self {
49        let file_str = if let Ok(file) = fs::read_to_string(Self::get_full_path()) {
50            file
51        }
52        else {
53            let config = Self::default();
54            config.save().expect("Unable to load config!");
55            return config;
56        };
57
58        match Self::TYPE {
59            ConfigType::Toml => {
60                if let Ok(cfg) = toml::from_str(&file_str) {
61                    cfg
62                }
63                else { Self::default() }
64            }
65
66            ConfigType::Ron => {
67                if let Ok(cfg) = ron::from_str(&file_str) {
68                    cfg
69                }
70                else { Self::default() }
71            }
72        }
73    }
74
75    /// Save to the config file
76    ///
77    /// It is recommended to call this in every method that has &mut self as an argument!
78    #[track_caller]
79    fn save(&self) -> IoResult<()> {
80        let save_str = match Self::TYPE {
81            ConfigType::Toml => toml::to_string_pretty(&self).unwrap(),
82            ConfigType::Ron => {
83                let cfg = PrettyConfig::default();
84                ron::ser::to_string_pretty(&self, cfg).unwrap()
85            }
86        };
87
88        fs::create_dir_all(Self::get_save_dir())?;
89        fs::write(Self::get_full_path(), save_str)
90    }
91
92    /// Helper function to get the user's config root folder (normally `$HOME/.config` on most unix-like systems)
93    ///
94    /// Panics if your user does not have a home folder
95    #[track_caller]
96    fn get_config_root() -> PathBuf {
97        if let Ok(xdg_config_home) = env::var("XDG_CONFIG_HOME") {
98            xdg_config_home.into()
99        }
100        else {
101            PathBuf::from(
102                env::var("HOME").expect("Where is your home folder?!")
103            )
104                .join(".config")
105        }
106    }
107}