Skip to content
Snippets Groups Projects
config.rs 4.53 KiB
Newer Older
Eduardo Trujillo's avatar
Eduardo Trujillo committed
use std::{
Eduardo Trujillo's avatar
Eduardo Trujillo committed
    path::{Path, PathBuf},
};

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;

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,
    #[error("Unable to extract config from providers: {source:?}")]
    ExtractConfig {
        #[from]
        source: figment::Error,
    },
Eduardo Trujillo's avatar
Eduardo Trujillo committed
}

/// 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 })
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>) -> Result<C, ConfigError> {
    match get_first_valid_path(paths) {
        Some(path) => from_file(path),
        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 TOML file named `config.toml`.
pub fn from_current_dir<C: DeserializeOwned>() -> Result<C, ConfigError> {
    let current_path = get_current_dir_config_path()?;
Eduardo Trujillo's avatar
Eduardo Trujillo committed

    from_file(&current_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);
Eduardo Trujillo's avatar
Eduardo Trujillo committed

    paths.push(get_current_dir_config_path()?);
    paths.push(get_name_config_path(application_name));
Eduardo Trujillo's avatar
Eduardo Trujillo committed

    let figment = match get_first_valid_path(paths) {
        Some(path) => {
            info!("Reading config file from {}", path.display());
Eduardo Trujillo's avatar
Eduardo Trujillo committed

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

    None
}

fn as_paths<P: AsRef<Path>>(path_refs: &[P]) -> Vec<PathBuf> {
Eduardo Trujillo's avatar
Eduardo Trujillo committed
    let mut paths: Vec<PathBuf> = vec![];

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

Eduardo Trujillo's avatar
Eduardo Trujillo committed
}