use confroid::{ConfroidError, from_pairs};
use std::collections::HashMap;
#[derive(confroid::Config, Debug)]
struct Defaults {
#[confroid(default = 8080)]
port: u16,
#[confroid(default)] retries: u16,
#[confroid(default = "Hello")]
greeting: String,
#[confroid(default)] note: String,
}
#[test]
fn explicit_and_bare_defaults_apply_when_unset() {
let config: Defaults = from_pairs(Vec::<(String, String)>::new()).unwrap();
assert_eq!(config.port, 8080);
assert_eq!(config.retries, 0);
assert_eq!(config.greeting, "Hello");
assert_eq!(config.note, "");
}
#[test]
fn defaults_are_overridden_when_set() {
let config: Defaults = from_pairs([
("PORT", "1"),
("RETRIES", "5"),
("GREETING", "Hi"),
("NOTE", "n"),
])
.unwrap();
assert_eq!(config.port, 1);
assert_eq!(config.retries, 5);
assert_eq!(config.greeting, "Hi");
assert_eq!(config.note, "n");
}
#[test]
fn default_does_not_mask_a_parse_error() {
let err = from_pairs::<Defaults, _, _, _>([("PORT", "not-a-number")]).unwrap_err();
assert!(matches!(err, ConfroidError::EnvVarInvalid { .. }));
}
#[derive(confroid::Config, Debug)]
struct WithTls {
tls: Option<TlsConfig>,
}
#[derive(confroid::Config, Debug)]
struct TlsConfig {
cert: String,
key: String,
}
#[test]
fn optional_struct_absent_is_none() {
let config: WithTls = from_pairs(Vec::<(String, String)>::new()).unwrap();
assert!(config.tls.is_none());
}
#[test]
fn optional_struct_fully_set_is_some() {
let config: WithTls = from_pairs([("TLS__CERT", "c.pem"), ("TLS__KEY", "k.pem")]).unwrap();
let tls = config.tls.unwrap();
assert_eq!(tls.cert, "c.pem");
assert_eq!(tls.key, "k.pem");
}
#[test]
fn optional_struct_partially_set_errors() {
let err = from_pairs::<WithTls, _, _, _>([("TLS__CERT", "c.pem")]).unwrap_err();
assert_eq!(
err,
ConfroidError::EnvVarNotFound {
var_name: "TLS__KEY".to_string(),
field: "tls.key".to_string(),
}
);
}
#[derive(confroid::Config, Debug)]
#[allow(dead_code)] struct ReqVec {
names: Vec<String>,
}
#[derive(confroid::Config, Debug)]
struct OptVec {
#[confroid(default)]
names: Vec<String>,
}
#[derive(confroid::Config, Debug)]
struct AutoVec {
#[confroid(auto_vec)]
names: Vec<String>,
}
#[test]
fn missing_vector_without_default_is_missing_error() {
let err = from_pairs::<ReqVec, _, _, _>(Vec::<(String, String)>::new()).unwrap_err();
assert_eq!(
err,
ConfroidError::EnvVarNotFound {
var_name: "NAMES".to_string(),
field: "names".to_string(),
}
);
}
#[test]
fn missing_vector_with_bare_default_is_empty() {
let config: OptVec = from_pairs(Vec::<(String, String)>::new()).unwrap();
assert!(config.names.is_empty());
}
#[test]
fn vector_index_gap_is_an_error() {
let err = from_pairs::<ReqVec, _, _, _>([("NAMES__0", "a"), ("NAMES__2", "c")]).unwrap_err();
assert_eq!(
err,
ConfroidError::VecIndexGap {
var_name: "NAMES".to_string(),
field: "names".to_string(),
missing: 1,
}
);
}
#[test]
fn empty_delimited_vector_is_empty() {
let config: AutoVec = from_pairs([("NAMES", "")]).unwrap();
assert!(config.names.is_empty());
}
#[test]
fn delimited_vector_of_numbers() {
#[derive(confroid::Config, Debug)]
struct Ports {
#[confroid(auto_vec)]
ports: Vec<u16>,
}
let config: Ports = from_pairs([("PORTS", "80,443,8080")]).unwrap();
assert_eq!(config.ports, vec![80, 443, 8080]);
}
#[test]
fn delimited_vector_element_parse_error() {
#[derive(confroid::Config, Debug)]
#[allow(dead_code)] struct Ports {
#[confroid(auto_vec)]
ports: Vec<u16>,
}
let err = from_pairs::<Ports, _, _, _>([("PORTS", "80,nope")]).unwrap_err();
match err {
ConfroidError::EnvVarInvalid { value, field, .. } => {
assert_eq!(value, "nope");
assert_eq!(field, "ports");
}
other => panic!("expected EnvVarInvalid, got {other:?}"),
}
}
#[derive(confroid::Config, Debug)]
struct Renamed {
#[confroid(name = "LISTEN_PORT")]
port: u16,
#[confroid(name = "SERVER")]
http: Inner,
}
#[derive(confroid::Config, Debug)]
struct Inner {
port: u16,
}
#[test]
fn leaf_rename() {
let config: Renamed = from_pairs([("LISTEN_PORT", "1234"), ("SERVER__PORT", "80")]).unwrap();
assert_eq!(config.port, 1234);
}
#[test]
fn nested_rename_propagates_to_children() {
let config: Renamed = from_pairs([("LISTEN_PORT", "1234"), ("SERVER__PORT", "8443")]).unwrap();
assert_eq!(config.http.port, 8443);
}
#[derive(confroid::Config, Debug)]
struct Scalars {
a: i64,
b: f64,
c: bool,
d: char,
e: std::net::IpAddr,
f: std::path::PathBuf,
}
#[test]
fn many_scalar_types() {
let config: Scalars = from_pairs([
("A", "-42"),
("B", "3.5"),
("C", "true"),
("D", "x"),
("E", "127.0.0.1"),
("F", "/etc/config"),
])
.unwrap();
assert_eq!(config.a, -42);
assert_eq!(config.b, 3.5);
assert!(config.c);
assert_eq!(config.d, 'x');
assert_eq!(config.e, "127.0.0.1".parse::<std::net::IpAddr>().unwrap());
assert_eq!(config.f, std::path::PathBuf::from("/etc/config"));
}
#[test]
fn hashmap_of_scalars_preserves_key_case() {
#[derive(confroid::Config, Debug)]
struct Weights {
weights: HashMap<String, u32>,
}
let config: Weights = from_pairs([("WEIGHTS__Alice", "3"), ("WEIGHTS__bob", "7")]).unwrap();
assert_eq!(config.weights.len(), 2);
assert_eq!(config.weights.get("Alice"), Some(&3));
assert_eq!(config.weights.get("bob"), Some(&7));
}