tryphon 0.2.0

Type-safe configuration loading from environment variables using derive macros
Documentation
use tryphon::{Config, ConfigFieldError, ConfigValueDecoder, env_vars};

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

impl ConfigValueDecoder for Point {
    fn decode(raw: String) -> Result<Self, String> {
        let parts = raw.split('/').collect::<Vec<_>>();
        if parts.len() != 2 {
            return Err(format!("Invalid format for Point: {}", raw));
        }
        let x = parts[0].trim().parse::<i32>().map_err(|e| e.to_string())?;
        let y = parts[1].trim().parse::<i32>().map_err(|e| e.to_string())?;
        Ok(Point { x, y })
    }
}

#[derive(Config, Debug)]
struct Rectangle {
    #[env("TOP_LEFT")]
    #[default(Point { x: 0, y: 0 })]
    top_left: Point,
    #[env("BOTTOM_RIGHT")]
    bottom_right: Point,
}

#[derive(ConfigValueDecoder, Debug)]
struct Flag(bool);

#[derive(ConfigValueDecoder, Debug)]
struct SystemPort(u16);

#[derive(ConfigValueDecoder, Debug, PartialEq)]
enum SwitchState {
    On,
    Off,
}

#[derive(Debug, Config)]
struct AppConfig {
    #[env("FLAG")]
    #[default(Flag(false))]
    flag: Flag,
    #[env("SYSTEM_PORT")]
    #[default(SystemPort(80))]
    port: SystemPort,
    #[env("SWITCH_STATE")]
    #[default(SwitchState::Off)]
    switch: SwitchState,
}

#[test]
#[env_vars(FLAG = "true", SYSTEM_PORT = "8080", SWITCH_STATE = "on")]
fn test_derived_decoders() {
    let config = AppConfig::load().expect("Failed to load config with custom decoders");

    assert!(config.flag.0);
    assert_eq!(config.port.0, 8080);
    assert_eq!(config.switch, SwitchState::On);
}

#[test]
#[env_vars(FLAG = "not_bool", SYSTEM_PORT = "not_a_number", SWITCH_STATE = "bad")]
fn test_derived_decoders_failure() {
    let error = AppConfig::load().expect_err("Should have failed to load config with bad values");

    assert_eq!(error.field_errors.len(), 3);

    if let [
        ConfigFieldError::ParsingError { raw: raw_flag, .. },
        ConfigFieldError::ParsingError { raw: raw_port, .. },
        ConfigFieldError::ParsingError {
            raw: raw_switch, ..
        },
    ] = &error.field_errors[..]
    {
        assert_eq!(raw_flag, "not_bool");
        assert_eq!(raw_port, "not_a_number");
        assert_eq!(raw_switch, "bad");
    } else {
        panic!("Expected parsing errors for all fields");
    }
}

#[test]
fn test_derived_decoders_defaults() {
    let config = AppConfig::load().expect("Failed to load config with custom decoders");

    assert!(!config.flag.0);
    assert_eq!(config.port.0, 80);
    assert_eq!(config.switch, SwitchState::Off);
}

#[test]
#[env_vars(BOTTOM_RIGHT = "30/40")]
fn test_custom_decoder() {
    let config = Rectangle::load().expect("Failed to load config with custom decoders");

    assert_eq!(config.top_left.x, 0);
    assert_eq!(config.top_left.y, 0);
    assert_eq!(config.bottom_right.x, 30);
    assert_eq!(config.bottom_right.y, 40);
}

#[test]
#[env_vars(BOTTOM_RIGHT = "30x40")]
fn test_custom_decoder_fail() {
    let error = Rectangle::load().expect_err("Should have failed to load config with bad values");

    matches!(&error.field_errors[..], [ConfigFieldError::ParsingError { message, .. }] if message.contains("Invalid format for Point"));
}