use std::{ env, path::{Path, PathBuf}, }; use figment::{ providers::{Env, Format, Serialized, Toml}, Figment, Profile, }; use log::{debug, info}; use serde::{de::DeserializeOwned, Serialize}; use thiserror::Error; use crate::str::to_train_case; #[derive(Error, Debug)] pub enum ConfigError { /// The current directory could not be determined. #[error("Unable to determine the current directory.")] GetCurrentDir { #[from] source: std::io::Error, }, /// None of the provided paths were valid configuration files. #[error("None of the provided paths were valid configuration files.")] NoValidPath, #[error("Unable to extract config from providers: {source:?}")] ExtractConfig { #[from] source: figment::Error, }, } /// Attempts to read a Config object from the specified path. /// /// The configuration file is expected to be a TOML file. pub fn from_file<P: AsRef<Path>, C: DeserializeOwned>(path: P) -> Result<C, ConfigError> { let path = path.as_ref(); info!("Reading config file from {}", path.display()); Figment::new() .merge(Toml::file(path)) .extract() .map_err(|source| ConfigError::ExtractConfig { source }) } /// Attempts to read a Config object from the specified paths. pub fn from_paths<P: AsRef<Path>, C: DeserializeOwned>(paths: Vec<P>) -> Result<C, ConfigError> { match get_first_valid_path(paths) { Some(path) => from_file(path), None => Err(ConfigError::NoValidPath), } } /// Attempts to read a Config object from the current directory. /// /// The configuration file is expected to be a TOML file named `config.toml`. pub fn from_current_dir<C: DeserializeOwned>() -> Result<C, ConfigError> { let current_path = get_current_dir_config_path()?; from_file(¤t_path) } /// Similar to `from_paths`. Uses a default set of paths: /// /// - CURRENT_WORKING_DIRECTORY/config.toml /// - /etc/CRATE/config.toml pub fn from_default_paths<P: AsRef<Path>, C: DeserializeOwned>( application_name: &str, additional_paths: &[P], ) -> Result<C, ConfigError> { let mut paths = as_paths(additional_paths); paths.push(get_current_dir_config_path()?); paths.push(get_name_config_path(application_name)); let figment = match get_first_valid_path(paths) { Some(path) => { info!("Reading config file from {}", path.display()); Ok(Figment::from(Toml::file(path))) } None => Err(ConfigError::NoValidPath), }?; figment .extract() .map_err(|source| ConfigError::ExtractConfig { source }) } /// Similar to `from_paths`. Uses a default set of paths: /// /// - CURRENT_WORKING_DIRECTORY/config.toml /// - /etc/CRATE/config.toml pub fn from_defaults<P: AsRef<Path>, C: DeserializeOwned + Default + Serialize>( application_name: &str, additional_paths: &[P], ) -> Result<C, ConfigError> { let mut paths = as_paths(additional_paths); paths.push(get_current_dir_config_path()?); paths.push(get_name_config_path(application_name)); let default_config: C = Default::default(); let figment = Figment::from(Serialized::from(default_config, Profile::default())); let figment = match get_first_valid_path(paths) { Some(path) => { info!("Reading config file from {}", path.display()); figment.merge(Toml::file(path)) } None => figment, }; let env_prefix = format!("{}_", to_train_case(application_name)); debug!("Using env prefix: {}", &env_prefix); figment .merge(Env::prefixed(&env_prefix)) .extract() .map_err(|source| ConfigError::ExtractConfig { source }) } pub fn get_first_valid_path<P: AsRef<Path>>(paths: Vec<P>) -> Option<P> { for path in paths { if path.as_ref().exists() { return Some(path); } } None } fn as_paths<P: AsRef<Path>>(path_refs: &[P]) -> Vec<PathBuf> { let mut paths: Vec<PathBuf> = vec![]; for path in path_refs { let mut pathbuf = PathBuf::new(); pathbuf.push(path); paths.push(pathbuf); } paths } fn get_current_dir_config_path() -> Result<PathBuf, ConfigError> { let mut current_path = env::current_dir().map_err(|source| ConfigError::GetCurrentDir { source })?; current_path.push("config.toml"); Ok(current_path) } fn get_name_config_path(application_name: &str) -> PathBuf { let mut config_path = PathBuf::from("/etc/"); config_path.push(application_name); config_path.push("config.toml"); config_path }