confroid 0.0.4

The n+1-st config reader for your environment-based configs.
Documentation
//! End-to-end tests mirroring the examples in the README, so the documented
//! behaviour stays true.

use confroid::{ConfroidError, from_pairs};

// ---------------------------------------------------------------------------
// Main example
// ---------------------------------------------------------------------------

#[derive(confroid::Config, Debug)]
struct HttpConfig {
    #[confroid(default = 8080)]
    port: u16,
    #[confroid(example = "localhost")]
    host: String,
}

const DEFAULT_NAME: &str = "World";

#[derive(confroid::Config, Debug)]
struct Config {
    http: HttpConfig,
    #[confroid(default = "Hello")]
    greeting: String,
    #[confroid(default = DEFAULT_NAME, example = "John")]
    name: String,
}

#[test]
fn all_defaults_with_required_host() {
    // `host` is required (no default); everything else falls back.
    let config: Config = from_pairs([("HTTP__HOST", "localhost")]).unwrap();
    assert_eq!(config.http.port, 8080);
    assert_eq!(config.http.host, "localhost");
    assert_eq!(config.greeting, "Hello");
    assert_eq!(config.name, "World");
}

#[test]
fn everything_overridden() {
    let config: Config = from_pairs([
        ("HTTP__PORT", "9090"),
        ("HTTP__HOST", "example.com"),
        ("GREETING", "Hi"),
        ("NAME", "Bob"),
    ])
    .unwrap();
    assert_eq!(config.http.port, 9090);
    assert_eq!(config.http.host, "example.com");
    assert_eq!(config.greeting, "Hi");
    assert_eq!(config.name, "Bob");
}

#[test]
fn missing_required_field_reports_var_and_path() {
    let err = from_pairs::<Config, _, _, _>([("GREETING", "Hi")]).unwrap_err();
    assert!(matches!(
        err,
        ConfroidError::EnvVarNotFound { var_name, field }
            if var_name == "HTTP__HOST" && field == "http.host"
    ));
}

#[test]
fn invalid_value_reports_var_path_and_value() {
    let err =
        from_pairs::<Config, _, _, _>([("HTTP__HOST", "h"), ("HTTP__PORT", "abc")]).unwrap_err();
    match err {
        ConfroidError::EnvVarInvalid {
            var_name,
            field,
            value,
            ..
        } => {
            assert_eq!(var_name, "HTTP__PORT");
            assert_eq!(field, "http.port");
            assert_eq!(value, "abc");
        }
        other => panic!("expected EnvVarInvalid, got {other:?}"),
    }
}

// ---------------------------------------------------------------------------
// HashMap support
// ---------------------------------------------------------------------------

#[derive(confroid::Config, Debug)]
struct People {
    people: std::collections::HashMap<String, Person>,
}

#[derive(confroid::Config, Debug)]
struct Person {
    name: String,
    age: u8,
}

#[test]
fn hashmap_of_structs() {
    let config: People = from_pairs([
        ("PEOPLE__alice__NAME", "Alice"),
        ("PEOPLE__alice__AGE", "30"),
        ("PEOPLE__bob__NAME", "Bob"),
        ("PEOPLE__bob__AGE", "25"),
    ])
    .unwrap();

    assert_eq!(config.people.len(), 2);
    assert_eq!(config.people.get("alice").unwrap().name, "Alice");
    assert_eq!(config.people.get("alice").unwrap().age, 30);
    assert_eq!(config.people.get("bob").unwrap().name, "Bob");
    assert_eq!(config.people.get("bob").unwrap().age, 25);
}

// ---------------------------------------------------------------------------
// Smart vectors — indexed
// ---------------------------------------------------------------------------

#[derive(confroid::Config, Debug)]
struct Names {
    names: Vec<String>,
}

#[test]
fn indexed_vector() {
    let config: Names = from_pairs([("NAMES__0", "Alice"), ("NAMES__1", "Bob")]).unwrap();
    assert_eq!(config.names.len(), 2);
    assert_eq!(config.names[0], "Alice");
    assert_eq!(config.names[1], "Bob");
}

// ---------------------------------------------------------------------------
// Smart vectors — delimited (auto_vec)
// ---------------------------------------------------------------------------

#[derive(confroid::Config, Debug)]
struct AutoNames {
    #[confroid(auto_vec)]
    names: Vec<String>,
}

#[derive(confroid::Config, Debug)]
struct AutoNamesSemi {
    #[confroid(auto_vec, auto_vec_delimiter = ";")]
    names: Vec<String>,
}

#[test]
fn delimited_vector_default_comma() {
    let config: AutoNames = from_pairs([("NAMES", "Alice,Bob")]).unwrap();
    assert_eq!(config.names, vec!["Alice".to_string(), "Bob".to_string()]);
}

#[test]
fn delimited_vector_custom_delimiter() {
    let config: AutoNamesSemi = from_pairs([("NAMES", "Alice;Bob")]).unwrap();
    assert_eq!(config.names, vec!["Alice".to_string(), "Bob".to_string()]);
}

// ---------------------------------------------------------------------------
// Vectors of structs
// ---------------------------------------------------------------------------

#[derive(confroid::Config, Debug)]
struct Servers {
    servers: Vec<Server>,
}

#[derive(confroid::Config, Debug)]
struct Server {
    host: String,
    port: u16,
}

#[test]
fn vector_of_structs() {
    let config: Servers = from_pairs([
        ("SERVERS__0__HOST", "a.example.com"),
        ("SERVERS__0__PORT", "80"),
        ("SERVERS__1__HOST", "b.example.com"),
        ("SERVERS__1__PORT", "443"),
    ])
    .unwrap();
    assert_eq!(config.servers.len(), 2);
    assert_eq!(config.servers[0].host, "a.example.com");
    assert_eq!(config.servers[1].port, 443);
}

// ---------------------------------------------------------------------------
// Options
// ---------------------------------------------------------------------------

#[derive(confroid::Config, Debug)]
struct Optional {
    name: Option<String>,
}

#[test]
fn option_absent_is_none() {
    let config: Optional = from_pairs::<Optional, _, _, _>(Vec::<(String, String)>::new()).unwrap();
    assert_eq!(config.name, None);
}

#[test]
fn option_empty_is_none() {
    let config: Optional = from_pairs([("NAME", "")]).unwrap();
    assert_eq!(config.name, None);
}

#[test]
fn required_string_preserves_an_empty_value() {
    #[derive(confroid::Config, Debug)]
    struct Required {
        name: String,
    }

    let config: Required = from_pairs([("NAME", "")]).unwrap();
    assert_eq!(config.name, "");
}

#[test]
fn option_set_is_some_value() {
    let config: Optional = from_pairs([("NAME", "Alice")]).unwrap();
    assert_eq!(config.name, Some("Alice".to_string()));
}

#[test]
fn option_empty_uses_its_default() {
    #[derive(confroid::Config, Debug)]
    struct WithDefault {
        #[confroid(default = Some(String::from("anonymous")))]
        name: Option<String>,
    }

    let config: WithDefault = from_pairs([("NAME", "")]).unwrap();
    assert_eq!(config.name.as_deref(), Some("anonymous"));
}