use std::{ env, path::{Path, PathBuf}, }; #[cfg(feature = "config-json")] use figment::providers::Json; #[cfg(feature = "config-yaml")] use figment::providers::Yaml; use figment::{ providers::{Env, Format, Serialized, Toml}, Figment, Profile, }; use log::{debug, info}; use serde::{de::DeserializeOwned, Serialize}; use thiserror::Error; #[cfg(feature = "xdg")] use xdg; use crate::str::to_train_case; #[allow(clippy::large_enum_variant)] #[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, /// A config file format was not speicfied and we were unable to infer one /// from the path. #[error("Unable to derive config file format from extension.")] UnknownExtension, #[error("Unable to extract config from providers: {source}")] ExtractConfig { #[from] source: figment::Error, }, } #[derive(Clone)] pub enum ConfigFileFormat { #[cfg(feature = "config-json")] Json, Toml, #[cfg(feature = "config-yaml")] Yaml, } /// Attempts to read a Config object from the specified path. /// /// The expected file format can be optionally specified. If a format is not /// specified, the library will attempt to infer it from the file extension. pub fn figment_from_file<P: AsRef<Path>>( path: P, format: Option<ConfigFileFormat>, ) -> Result<Figment, ConfigError> { let path = path.as_ref(); let format = match format { Some(format) => format, None => infer_format_from_path(path)?, }; info!("Reading config file from {}", path.display()); let figment = Figment::new(); let figment = match format { #[cfg(feature = "config-json")] ConfigFileFormat::Json => figment.merge(Json::file(path)), ConfigFileFormat::Toml => figment.merge(Toml::file(path)), #[cfg(feature = "config-yaml")] ConfigFileFormat::Yaml => figment.merge(Yaml::file(path)), }; Ok(figment) } /// Attempts to read a Config object from the specified path. /// /// The expected file format can be optionally specified. If a format is not /// specified, the library will attempt to infer it from the file extension. pub fn from_file<P: AsRef<Path>, C: DeserializeOwned>( path: P, format: Option<ConfigFileFormat>, ) -> Result<C, ConfigError> { extract(figment_from_file(path, format)?) } /// Attempts to read a Config object from the specified paths. pub fn figment_from_paths<P: AsRef<Path>>( paths: Vec<(P, Option<ConfigFileFormat>)>, ) -> Result<Option<Figment>, ConfigError> { for (path, format) in paths { if path.as_ref().exists() { return Ok(Some(figment_from_file(path, format)?)); } } Ok(None) } /// Attempts to read a Config object from the specified paths. pub fn from_paths<P: AsRef<Path>, C: DeserializeOwned>( paths: Vec<(P, Option<ConfigFileFormat>)>, ) -> Result<C, ConfigError> { match figment_from_paths(paths)? { Some(figment) => extract(figment), None => Err(ConfigError::NoValidPath), } } /// Attempts to read a Config object from the current directory. /// /// The configuration file is expected to be a file named `config.ext` where /// `ext` is one of `json`, `toml`, or `yaml`. pub fn from_current_dir<C: DeserializeOwned>(format: ConfigFileFormat) -> Result<C, ConfigError> { let current_path = get_current_dir_config_path(&format)?; from_file(current_path, Some(format)) } /// 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, Option<ConfigFileFormat>)], format: ConfigFileFormat, ) -> Result<C, ConfigError> { let mut paths = as_paths(additional_paths); paths.push((get_current_dir_config_path(&format)?, Some(format.clone()))); paths.push(( get_name_config_path(application_name, &format), Some(format), )); from_paths(paths) } /// 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, Option<ConfigFileFormat>)], format: ConfigFileFormat, ) -> Result<C, ConfigError> { let mut paths = as_paths(additional_paths); paths.push((get_current_dir_config_path(&format)?, Some(format.clone()))); paths.push(( get_name_config_path(application_name, &format), Some(format), )); let default_config: C = Default::default(); let figment = Figment::from(Serialized::from(default_config, Profile::default())); let figment = match figment_from_paths(paths)? { Some(file_figment) => figment.merge(file_figment), None => figment, }; let env_prefix = format!("{}_", to_train_case(application_name)); debug!("Using env prefix: {}", &env_prefix); extract(figment.merge(Env::prefixed(&env_prefix))) } fn as_paths<P: AsRef<Path>>( path_refs: &[(P, Option<ConfigFileFormat>)], ) -> Vec<(PathBuf, Option<ConfigFileFormat>)> { let mut paths: Vec<(PathBuf, Option<ConfigFileFormat>)> = vec![]; for (path, format) in path_refs { let mut pathbuf = PathBuf::new(); pathbuf.push(path); paths.push((pathbuf, format.clone())); } paths } fn get_current_dir_config_path(format: &ConfigFileFormat) -> Result<PathBuf, ConfigError> { let mut current_path = env::current_dir().map_err(|source| ConfigError::GetCurrentDir { source })?; current_path.push(get_default_filename_from_format(format)); Ok(current_path) } fn get_name_config_path(application_name: &str, format: &ConfigFileFormat) -> PathBuf { let mut config_path = PathBuf::from("/etc/"); config_path.push(application_name); config_path.push(get_default_filename_from_format(format)); config_path } #[cfg(feature = "xdg")] pub fn get_name_xdg_config_path( application_name: &str, format: &ConfigFileFormat, ) -> Option<PathBuf> { let xdg_dirs = xdg::BaseDirectories::with_prefix(application_name).ok(); if let Some(xdg_dirs) = xdg_dirs { xdg_dirs.find_config_file(get_default_filename_from_format(format)) } else { None } } fn infer_format_from_path<P: AsRef<Path>>(path: P) -> Result<ConfigFileFormat, ConfigError> { match path.as_ref().extension() { Some(extension) => match extension.to_str() { Some("yaml") => Ok(ConfigFileFormat::Yaml), Some("yml") => Ok(ConfigFileFormat::Yaml), Some("json") => Ok(ConfigFileFormat::Json), Some("toml") => Ok(ConfigFileFormat::Toml), _ => Err(ConfigError::UnknownExtension), }, _ => Err(ConfigError::UnknownExtension), } } fn get_default_filename_from_format(format: &ConfigFileFormat) -> &'static str { match format { #[cfg(feature = "config-json")] ConfigFileFormat::Json => "config.json", ConfigFileFormat::Toml => "config.toml", #[cfg(feature = "config-yaml")] ConfigFileFormat::Yaml => "config.yaml", } } fn extract<C: DeserializeOwned>(figment: Figment) -> Result<C, ConfigError> { figment .extract() .map_err(|source| ConfigError::ExtractConfig { source }) }