confgr_core/
lib.rs

1use std::collections::HashMap;
2use thiserror::Error;
3
4/// Shared error type for configuration-related errors.
5#[derive(Error, Debug)]
6pub enum ConfgrError {
7    #[error("Config File IO Error: {0}")]
8    File(#[from] std::io::Error),
9    #[error("Configured filepath does not exist.")]
10    NoFilePath,
11    #[error("Config Error: {0}")]
12    Config(#[from] config::ConfigError),
13}
14
15/// Merges configuration layers. Self takes precedence over other.
16pub trait Merge {
17    fn merge(self, other: Self) -> Self;
18}
19
20/// Creates an empty configuration layer, used to initialize all [`None`]'s, instead of [`Default`].
21pub trait Empty {
22    fn empty() -> Self;
23}
24
25/// Deserializes a configuration layer from environment variables.
26pub trait FromEnv {
27    fn from_env() -> Self;
28    fn get_env_keys() -> HashMap<String, String>;
29}
30
31/// Deserializes a configuration layer from a file.
32pub trait FromFile: Sized {
33    fn from_file() -> Result<Self, ConfgrError>;
34    fn check_file() -> Result<(), ConfgrError>;
35    fn get_file_path() -> Option<String>;
36}
37
38/// Provides a unified approach to load configurations from environment variables,
39/// files, and default settings. This trait is typically derived using a macro to automate
40/// implementations based on struct field names and annotations.
41pub trait Confgr
42where
43    Self: Sized,
44{
45    type Layer: Default + FromEnv + Merge + FromFile + From<Self> + Into<Self>;
46
47    /// Loads and merges configurations from files, environment variables, and default values.
48    /// Order of precedence: Environment variables, file configurations, default values.
49    ///
50    /// # Examples
51    ///
52    /// ```rust no_run
53    /// let config = AppConfig::load_config();
54    /// assert_eq!(config.port, 8080);
55    /// ```
56    fn load_config() -> Self {
57        let file_layer = match Self::deserialize_from_file() {
58            Ok(file_layer) => file_layer,
59            Err(_e) => Self::Layer::default(),
60        };
61
62        let default_layer = Self::Layer::default();
63        let env_layer = Self::Layer::from_env();
64
65        env_layer.merge(file_layer.merge(default_layer)).into()
66    }
67
68    /// Attempts to deserialize configuration from a file.
69    /// This method is a part of the file loading phase of the configuration process.
70    ///
71    /// # Errors
72    ///
73    /// Returns [`ConfgrError`] if the file cannot be read.
74    ///
75    /// # Examples
76    ///
77    /// ``` rust no_run
78    /// let file_layer = AppConfig::deserialize_from_file();
79    /// match file_layer {
80    ///     Ok(layer) => println!("Configuration loaded from file."),
81    ///     Err(e) => eprintln!("Failed to load configuration: {}", e),
82    /// }
83    /// ```
84    fn deserialize_from_file() -> Result<Self::Layer, ConfgrError> {
85        Self::Layer::from_file()
86    }
87
88    /// Checks the accessibility of the specified configuration file.
89    ///
90    /// # Returns
91    ///
92    /// [`Ok`] if the file is accessible, otherwise an [`Err`]\([`ConfgrError`]) if the file cannot be found or opened.
93    ///
94    /// # Examples
95    ///
96    /// ```
97    /// if AppConfig::check_file().is_ok() {
98    ///     println!("Configuration file is accessible.");
99    /// } else {
100    ///     println!("Cannot access configuration file.");
101    /// }
102    /// ```
103    fn check_file() -> Result<(), ConfgrError> {
104        Self::Layer::check_file()
105    }
106
107    /// Retrieves the map of environment variable keys associated with the configuration properties.
108    ///
109    /// # Returns
110    ///
111    /// A [`HashMap`] where the keys are property names and the values are the corresponding environment variable names.
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// let env_keys = AppConfig::get_env_keys();
117    /// assert_eq!(env_keys["port"], "APP_PORT");
118    /// ```
119    fn get_env_keys() -> HashMap<String, String> {
120        Self::Layer::get_env_keys()
121    }
122
123    /// Gets the file path used for loading the configuration, if specified.
124    ///
125    /// # Returns
126    ///
127    /// An [`Option<String>`] which is `Some(path)` if a path is set, otherwise `None`.
128    ///
129    /// # Examples
130    ///
131    /// ```
132    /// if let Some(path) = AppConfig::get_file_path() {
133    ///     println!("Configuration file used: {}", path);
134    /// } else {
135    ///     println!("No specific configuration file used.");
136    /// }
137    /// ```
138    fn get_file_path() -> Option<String> {
139        Self::Layer::get_file_path()
140    }
141}