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
use std::{any::Any, fmt::Debug, marker::PhantomData, path::PathBuf, sync::Arc};

use figment::{
    providers::{Env, Format, Json, Serialized, Toml, Yaml},
    Figment,
};
use serde::{Deserialize, Serialize};

/// A Loader uses hooks to augment the Config loaded for the application
///
/// TODO: Add more hooks! ๐Ÿ™‚
pub trait Loader: Any + Send + Sync {
    /// Apply transformations to the environment variables loaded by Figment
    fn load_env(&self, env: Env) -> Env;
}

/// Config is the final loaded result
pub trait Config:
    Any + Clone + Debug + Default + Serialize + Send + Sync + for<'a> Deserialize<'a>
{
}

/// An extensible Config loader based on Figment
pub struct LoadAll<C: Config> {
    loaders: Vec<Arc<dyn Loader>>,
    _phantom: PhantomData<C>,
}

impl<C: Config> LoadAll<C> {
    /// Create a new Config instance with the given loaders
    pub fn new(loaders: Vec<Arc<dyn Loader>>) -> Self {
        Self {
            loaders,
            _phantom: Default::default(),
        }
    }

    /// Create a new Config by merging in various sources
    pub fn load(&self, custom_path: Option<PathBuf>) -> figment::error::Result<C> {
        let mut config = Figment::new()
            // Load defaults
            .merge(Serialized::defaults(C::default()))
            // Load local overrides
            .merge(Toml::file("config.toml"))
            .merge(Yaml::file("config.yml"))
            .merge(Yaml::file("config.yaml"))
            .merge(Json::file("config.json"));

        // Load the custom config file if provided
        if let Some(path) = custom_path {
            if let Some(path_str) = path.to_str() {
                if path_str.ends_with(".toml") {
                    config = config.merge(Toml::file(path_str));
                } else if path_str.ends_with(".yml") || path_str.ends_with(".yaml") {
                    config = config.merge(Yaml::file(path_str));
                } else if path_str.ends_with(".json") {
                    config = config.merge(Json::file(path_str));
                }
            }
        }

        // Environment Variables
        // ---------------------

        let mut env = Env::raw();

        for loader in &self.loaders {
            env = loader.load_env(env);
        }

        config = config.merge(env);

        // Serialize and freeze
        config.extract()
    }
}

#[cfg(test)]
pub(crate) mod test {
    use anyhow::Result;

    use super::*;

    #[derive(Default, Debug, Serialize, Deserialize, Clone)]
    pub struct Config {}

    impl crate::Config for Config {}

    #[tokio::test]
    async fn test_load_all_success() -> Result<()> {
        let loader = LoadAll::<Config>::new(vec![]);

        loader.load(None)?;

        Ok(())
    }

    #[tokio::test]
    async fn test_load_all_custom_path() -> Result<()> {
        let loader = LoadAll::<Config>::new(vec![]);

        let custom_path = PathBuf::from("config.toml");

        loader.load(Some(custom_path))?;

        Ok(())
    }
}