Skip to content
Snippets Groups Projects
config.rs 7.2 KiB
Newer Older
Eduardo Trujillo's avatar
Eduardo Trujillo committed
use std::{
Eduardo Trujillo's avatar
Eduardo Trujillo committed
    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};
Eduardo Trujillo's avatar
Eduardo Trujillo committed
use thiserror::Error;

use crate::str::to_train_case;

#[allow(clippy::large_enum_variant)]
Eduardo Trujillo's avatar
Eduardo Trujillo committed
#[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,
}

Eduardo Trujillo's avatar
Eduardo Trujillo committed
/// 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> {
Eduardo Trujillo's avatar
Eduardo Trujillo committed
    let path = path.as_ref();

    let format = match format {
        Some(format) => format,
        None => infer_format_from_path(path)?,
    };

Eduardo Trujillo's avatar
Eduardo Trujillo committed
    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)
Eduardo Trujillo's avatar
Eduardo Trujillo committed
}

/// 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),
Eduardo Trujillo's avatar
Eduardo Trujillo committed
    }
}

/// 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)?;
Eduardo Trujillo's avatar
Eduardo Trujillo committed

    from_file(&current_path, Some(format))
Eduardo Trujillo's avatar
Eduardo Trujillo committed
}

/// 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,
Eduardo Trujillo's avatar
Eduardo Trujillo committed
) -> Result<C, ConfigError> {
    let mut paths = as_paths(additional_paths);
Eduardo Trujillo's avatar
Eduardo Trujillo committed

    paths.push((get_current_dir_config_path(&format)?, Some(format.clone())));
    paths.push((
        get_name_config_path(application_name, &format),
        Some(format),
    ));
}

/// 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![];
Eduardo Trujillo's avatar
Eduardo Trujillo committed

    for (path, format) in path_refs {
Eduardo Trujillo's avatar
Eduardo Trujillo committed
        let mut pathbuf = PathBuf::new();

        pathbuf.push(path);

        paths.push((pathbuf, format.clone()));
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));
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));
Eduardo Trujillo's avatar
Eduardo Trujillo committed

Eduardo Trujillo's avatar
Eduardo Trujillo committed
}

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 })