confroid 0.0.3

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

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

#[derive(Debug)]
struct ParseModeError(String);

impl fmt::Display for ParseModeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "unknown mode `{}`", self.0)
    }
}

impl std::error::Error for ParseModeError {}

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

impl FromStr for Mode {
    type Err = ParseModeError;
    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(ParseModeError(other.to_string())),
        }
    }
}

// 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",
        })
    }
}

#[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!(matches!(
        err,
        ConfroidError::EnvVarNotFound { var_name, field }
            if var_name == "MODE" && field == "mode"
    ));
}