functora-cfg 0.1.0

A Rust library that merges configuration values from multiple sources into a single typed value.
Documentation
#![doc = include_str!("../README.md")]
pub use config::ConfigError;
use config::{Config, Environment, File, Value};
use serde::{Serialize, de::DeserializeOwned};
use std::path::Path;
use toml::Value as TomlValue;

pub struct Cfg<'a, Src: Serialize> {
    pub default: &'a Src,
    pub file_path: fn(&Src) -> Option<&str>,
    pub env_prefix: &'a str,
    pub command_line: &'a Src,
}

impl<'a, Src: Serialize> Cfg<'a, Src> {
    pub fn eval<Dst: Serialize + DeserializeOwned>(
        &self,
    ) -> Result<Dst, ConfigError> {
        eval(self)
    }
}

pub fn eval<Src, Dst>(
    cfg: &Cfg<Src>,
) -> Result<Dst, ConfigError>
where
    Src: Serialize,
    Dst: Serialize + DeserializeOwned,
{
    let mut builder = Config::builder();

    builder =
        builder.add_source(term_to_config(cfg.default)?);

    let getter = cfg.file_path;
    if let Some(path) =
        getter(cfg.command_line).or(getter(cfg.default))
    {
        let path = Path::new(&path);
        if path.exists() && path.is_file() {
            builder = builder.add_source(File::from(path));
        } else {
            return Err(ConfigError::Message(
                "Config file path does not exist!".into(),
            ));
        }
    }

    builder = builder.add_source(
        Environment::with_prefix(
            &cfg.env_prefix.to_uppercase(),
        )
        .separator("_"),
    );

    builder = builder
        .add_source(term_to_config(cfg.command_line)?);

    builder.build()?.try_deserialize()
}

fn term_to_config<T: Serialize>(
    value: &T,
) -> Result<Config, ConfigError> {
    let toml = toml::Value::try_from(value)
        .map_err(|e| ConfigError::Message(e.to_string()))?;

    if let TomlValue::Table(kv) = toml {
        kv.into_iter()
            .try_fold(Config::builder(), |acc, (k, v)| {
                acc.set_override(k, toml_to_config(v))
            })?
            .build()
    } else {
        Err(ConfigError::Message(
            "Expected table at root".into(),
        ))
    }
}

fn toml_to_config(toml: TomlValue) -> Value {
    match toml {
        TomlValue::String(x) => Value::from(x),
        TomlValue::Integer(x) => Value::from(x),
        TomlValue::Float(x) => Value::from(x),
        TomlValue::Boolean(x) => Value::from(x),
        TomlValue::Datetime(x) => {
            Value::from(x.to_string())
        }
        TomlValue::Array(xs) => Value::from(
            xs.into_iter()
                .map(toml_to_config)
                .collect::<Vec<_>>(),
        ),
        TomlValue::Table(kv) => Value::from(
            kv.into_iter()
                .map(|(k, v)| (k, toml_to_config(v)))
                .collect::<config::Map<String, Value>>(),
        ),
    }
}