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};
pub trait Loader: Any + Send + Sync {
fn load_env(&self, env: Env) -> Env;
}
pub trait Config:
Any + Clone + Debug + Default + Serialize + Send + Sync + for<'a> Deserialize<'a>
{
}
pub struct LoadAll<C: Config> {
loaders: Vec<Arc<dyn Loader>>,
_phantom: PhantomData<C>,
}
impl<C: Config> LoadAll<C> {
pub fn new(loaders: Vec<Arc<dyn Loader>>) -> Self {
Self {
loaders,
_phantom: Default::default(),
}
}
pub fn load(&self, custom_path: Option<PathBuf>) -> figment::error::Result<C> {
let mut config = Figment::new()
.merge(Serialized::defaults(C::default()))
.merge(Toml::file("config.toml"))
.merge(Yaml::file("config.yml"))
.merge(Yaml::file("config.yaml"))
.merge(Json::file("config.json"));
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));
}
}
}
let mut env = Env::raw();
for loader in &self.loaders {
env = loader.load_env(env);
}
config = config.merge(env);
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(())
}
}