Skip to main content

babyrite/
config.rs

1//! Configuration module for Babyrite.
2//!
3//! This module handles both environment variables and file-based configuration.
4
5use serde::Deserialize;
6
7/// Global configuration instance.
8pub static CONFIG: once_cell::sync::OnceCell<BabyriteConfig> = once_cell::sync::OnceCell::new();
9
10/// Environment variable configuration.
11#[derive(Deserialize, Debug)]
12pub struct EnvConfig {
13    /// Discord API token for bot authentication.
14    pub discord_api_token: String,
15    /// Optional path to the configuration file.
16    #[serde(default)]
17    #[serde(deserialize_with = "crate::config::empty_string_as_none")]
18    pub config_file_path: Option<String>,
19}
20
21impl EnvConfig {
22    /// Returns a reference to the environment configuration.
23    ///
24    /// Initializes the configuration from environment variables on first call.
25    pub fn get() -> &'static EnvConfig {
26        static ENV_CONFIG: std::sync::OnceLock<EnvConfig> = std::sync::OnceLock::new();
27        ENV_CONFIG
28            .get_or_init(|| envy::from_env().expect("Failed to load environment configuration."))
29    }
30}
31
32/// Babyrite configuration.
33///
34/// All configuration default values are `false`.
35#[derive(Deserialize, Debug, Default)]
36pub struct BabyriteConfig {
37    /// If enabled, logs are output in JSON format.
38    #[serde(default)]
39    pub json_logging: bool,
40}
41
42/// Errors that can occur when loading configuration.
43#[derive(thiserror::Error, Debug)]
44pub enum BabyriteConfigError {
45    /// Failed to read the configuration file from disk.
46    #[error("Failed to read configuration file.")]
47    Read,
48    /// Failed to parse the configuration file contents.
49    #[error("Failed to parse configuration file.")]
50    Parse,
51    /// Failed to set the global configuration.
52    #[error("Failed to set configuration file.")]
53    Set,
54}
55
56impl BabyriteConfig {
57    /// Initializes the global configuration.
58    ///
59    /// Loads configuration from a file if `CONFIG_FILE_PATH` is set,
60    /// otherwise uses default values.
61    pub fn init() -> anyhow::Result<(), BabyriteConfigError> {
62        let envs = EnvConfig::get();
63        match &envs.config_file_path {
64            Some(p) => {
65                let buffer = &std::fs::read_to_string(p).map_err(|_| BabyriteConfigError::Read)?;
66                let config: BabyriteConfig =
67                    toml::from_str(buffer).map_err(|_| BabyriteConfigError::Parse)?;
68                Ok(CONFIG.set(config).map_err(|_| BabyriteConfigError::Parse)?)
69            }
70            None => Ok(CONFIG
71                .set(BabyriteConfig::default())
72                .map_err(|_| BabyriteConfigError::Set)?),
73        }
74    }
75
76    /// Returns a reference to the global configuration.
77    ///
78    /// # Panics
79    ///
80    /// Panics if [`BabyriteConfig::init`] has not been called.
81    pub fn get() -> &'static BabyriteConfig {
82        CONFIG.get().expect("Failed to get configuration.")
83    }
84}
85
86/// Deserialize a string as an `Option<String>`, treating empty strings as `None`.
87pub fn empty_string_as_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
88where
89    D: serde::Deserializer<'de>,
90{
91    let opt = Option::<String>::deserialize(deserializer)?;
92    Ok(opt.filter(|s| !s.is_empty()))
93}