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}