confroid 0.0.1

The n+1-st config reader for your environment-based configs.
Documentation
//! Custom scalar (enum) support via the `from_str_scalar!` macro.

use confroid::{ConfroidError, from_pairs};
use std::fmt;
use std::str::FromStr;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Mode {
    Standalone,
    Cluster,
}

impl FromStr for Mode {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_ascii_lowercase().as_str() {
            "standalone" => Ok(Mode::Standalone),
            "cluster" => Ok(Mode::Cluster),
            other => Err(format!("unknown mode `{other}`")),
        }
    }
}

// Needed so the `default` can be rendered when the `docs` feature is on.
impl fmt::Display for Mode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            Mode::Standalone => "standalone",
            Mode::Cluster => "cluster",
        })
    }
}

confroid::from_str_scalar!(Mode);

#[derive(confroid::Config, Debug)]
struct Config {
    mode: Mode,
    #[confroid(default = Mode::Standalone)]
    fallback: Mode,
    maybe: Option<Mode>,
    tags: Vec<Mode>,
}

#[test]
fn reads_enum_fields() {
    let config: Config = from_pairs([
        ("MODE", "cluster"),
        ("FALLBACK", "cluster"),
        ("MAYBE", "standalone"),
        ("TAGS__0", "standalone"),
        ("TAGS__1", "cluster"),
    ])
    .unwrap();
    assert_eq!(config.mode, Mode::Cluster);
    assert_eq!(config.fallback, Mode::Cluster);
    assert_eq!(config.maybe, Some(Mode::Standalone));
    assert_eq!(config.tags, vec![Mode::Standalone, Mode::Cluster]);
}

#[test]
fn case_insensitive_via_from_str() {
    let config: Config = from_pairs([("MODE", "CLUSTER"), ("TAGS__0", "Standalone")]).unwrap();
    assert_eq!(config.mode, Mode::Cluster);
    assert_eq!(config.tags, vec![Mode::Standalone]);
}

#[test]
fn default_applies_when_unset() {
    let config: Config = from_pairs([("MODE", "standalone"), ("TAGS__0", "cluster")]).unwrap();
    assert_eq!(config.fallback, Mode::Standalone);
    assert_eq!(config.maybe, None);
}

#[test]
fn invalid_enum_value_errors() {
    let err =
        from_pairs::<Config, _, _, _>([("MODE", "bogus"), ("TAGS__0", "cluster")]).unwrap_err();
    match err {
        ConfroidError::EnvVarInvalid { field, value, .. } => {
            assert_eq!(field, "mode");
            assert_eq!(value, "bogus");
        }
        other => panic!("expected EnvVarInvalid, got {other:?}"),
    }
}

#[test]
fn missing_required_enum_errors() {
    let err = from_pairs::<Config, _, _, _>([("TAGS__0", "cluster")]).unwrap_err();
    assert_eq!(
        err,
        ConfroidError::EnvVarNotFound {
            var_name: "MODE".to_string(),
            field: "mode".to_string(),
        }
    );
}