1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
use std::collections::HashMap;
use thiserror::Error;

/// Shared error type for configuration-related errors.
#[derive(Error, Debug)]
pub enum ConfgrError {
    #[error("Config File IO Error: {0}")]
    File(#[from] std::io::Error),
    #[error("Configured filepath does not exist.")]
    NoFilePath,
    #[error("Config Error: {0}")]
    Config(#[from] config::ConfigError),
}

/// Merges configuration layers. Self takes precedence over other.
pub trait Merge {
    fn merge(self, other: Self) -> Self;
}

/// Creates an empty configuration layer, used to initialize all [`None`]'s, instead of [`Default`].
pub trait Empty {
    fn empty() -> Self;
}

/// Deserializes a configuration layer from environment variables.
pub trait FromEnv {
    fn from_env() -> Self;
    fn get_env_keys() -> HashMap<String, String>;
}

/// Deserializes a configuration layer from a file.
pub trait FromFile: Sized {
    fn from_file() -> Result<Self, ConfgrError>;
    fn check_file() -> Result<(), ConfgrError>;
    fn get_file_path() -> Option<String>;
}

/// Provides a unified approach to load configurations from environment variables,
/// files, and default settings. This trait is typically derived using a macro to automate
/// implementations based on struct field names and annotations.
pub trait Confgr
where
    Self: Sized,
{
    type Layer: Default + FromEnv + Merge + FromFile + From<Self> + Into<Self>;

    /// Loads and merges configurations from files, environment variables, and default values.
    /// Order of precedence: Environment variables, file configurations, default values.
    ///
    /// # Examples
    ///
    /// ```rust no_run
    /// let config = AppConfig::load_config();
    /// assert_eq!(config.port, 8080);
    /// ```
    fn load_config() -> Self {
        let file_layer = match Self::deserialize_from_file() {
            Ok(file_layer) => file_layer,
            Err(_e) => Self::Layer::default(),
        };

        let default_layer = Self::Layer::default();
        let env_layer = Self::Layer::from_env();

        env_layer.merge(file_layer.merge(default_layer)).into()
    }

    /// Attempts to deserialize configuration from a file.
    /// This method is a part of the file loading phase of the configuration process.
    ///
    /// # Errors
    ///
    /// Returns [`ConfgrError`] if the file cannot be read.
    ///
    /// # Examples
    ///
    /// ``` rust no_run
    /// let file_layer = AppConfig::deserialize_from_file();
    /// match file_layer {
    ///     Ok(layer) => println!("Configuration loaded from file."),
    ///     Err(e) => eprintln!("Failed to load configuration: {}", e),
    /// }
    /// ```
    fn deserialize_from_file() -> Result<Self::Layer, ConfgrError> {
        Self::Layer::from_file()
    }

    /// Checks the accessibility of the specified configuration file.
    ///
    /// # Returns
    ///
    /// [`Ok`] if the file is accessible, otherwise an [`Err`]\([`ConfgrError`]) if the file cannot be found or opened.
    ///
    /// # Examples
    ///
    /// ```
    /// if AppConfig::check_file().is_ok() {
    ///     println!("Configuration file is accessible.");
    /// } else {
    ///     println!("Cannot access configuration file.");
    /// }
    /// ```
    fn check_file() -> Result<(), ConfgrError> {
        Self::Layer::check_file()
    }

    /// Retrieves the map of environment variable keys associated with the configuration properties.
    ///
    /// # Returns
    ///
    /// A [`HashMap`] where the keys are property names and the values are the corresponding environment variable names.
    ///
    /// # Examples
    ///
    /// ```
    /// let env_keys = AppConfig::get_env_keys();
    /// assert_eq!(env_keys["port"], "APP_PORT");
    /// ```
    fn get_env_keys() -> HashMap<String, String> {
        Self::Layer::get_env_keys()
    }

    /// Gets the file path used for loading the configuration, if specified.
    ///
    /// # Returns
    ///
    /// An [`Option<String>`] which is `Some(path)` if a path is set, otherwise `None`.
    ///
    /// # Examples
    ///
    /// ```
    /// if let Some(path) = AppConfig::get_file_path() {
    ///     println!("Configuration file used: {}", path);
    /// } else {
    ///     println!("No specific configuration file used.");
    /// }
    /// ```
    fn get_file_path() -> Option<String> {
        Self::Layer::get_file_path()
    }
}