use std::env;
use std::num::ParseIntError;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConfigError {
InvalidUnicode {
var: String,
},
InvalidValue {
var: String,
raw: String,
message: String,
},
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigError::InvalidUnicode { var } => {
write!(f, "environment variable {var:?} is not valid UTF-8")
}
ConfigError::InvalidValue { var, raw, message } => {
write!(
f,
"environment variable {var:?} has an invalid value {raw:?}: {message}"
)
}
}
}
}
impl std::error::Error for ConfigError {}
pub fn seed_from_env_or_time(var: &str) -> Result<u64, ConfigError> {
match env::var(var) {
Ok(raw) => parse_number(var, &raw),
Err(env::VarError::NotPresent) => Ok(time_seed()),
Err(env::VarError::NotUnicode(_)) => Err(ConfigError::InvalidUnicode {
var: var.to_string(),
}),
}
}
pub fn iterations_from_env(var: &str, default: usize) -> Result<usize, ConfigError> {
match env::var(var) {
Ok(raw) => parse_number(var, &raw),
Err(env::VarError::NotPresent) => Ok(default),
Err(env::VarError::NotUnicode(_)) => Err(ConfigError::InvalidUnicode {
var: var.to_string(),
}),
}
}
fn parse_number<T>(var: &str, raw: &str) -> Result<T, ConfigError>
where
T: std::str::FromStr<Err = ParseIntError>,
{
raw.parse::<T>().map_err(|err| ConfigError::InvalidValue {
var: var.to_string(),
raw: raw.to_string(),
message: err.to_string(),
})
}
fn time_seed() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_number_accepts_valid_u64() {
let v: u64 = parse_number("SEED", "42").unwrap();
assert_eq!(v, 42);
}
#[test]
fn parse_number_accepts_valid_usize() {
let v: usize = parse_number("ITER", "128").unwrap();
assert_eq!(v, 128);
}
#[test]
fn parse_number_reports_invalid_value_with_context() {
let err = parse_number::<u64>("SEED", "not-a-number").unwrap_err();
match err {
ConfigError::InvalidValue { var, raw, message } => {
assert_eq!(var, "SEED");
assert_eq!(raw, "not-a-number");
assert!(!message.is_empty(), "parse message should be populated");
}
other => panic!("expected InvalidValue, got {other:?}"),
}
}
#[test]
fn parse_number_reports_negative_for_usize() {
let err = parse_number::<usize>("ITER", "-1").unwrap_err();
assert!(matches!(err, ConfigError::InvalidValue { .. }));
}
#[test]
fn time_seed_returns_recent_nanos() {
let a = time_seed();
let b = time_seed();
assert!(a != 0 || b != 0);
}
#[test]
fn config_error_display_mentions_the_variable() {
let err = ConfigError::InvalidUnicode {
var: "MYAPP_SEED".into(),
};
assert!(err.to_string().contains("MYAPP_SEED"));
let err = ConfigError::InvalidValue {
var: "MYAPP_ITERATIONS".into(),
raw: "abc".into(),
message: "invalid digit found in string".into(),
};
let text = err.to_string();
assert!(text.contains("MYAPP_ITERATIONS"));
assert!(text.contains("abc"));
}
#[test]
fn seed_from_env_or_time_falls_back_when_variable_unset() {
let name = "NOPROP_CONFIG_TESTS_ABSOLUTELY_UNSET_SEED_9F3A_2E7B";
let a = seed_from_env_or_time(name).expect("unset var must fall back");
let b = seed_from_env_or_time(name).expect("unset var must fall back");
assert!(a != 0 || b != 0);
}
#[test]
fn iterations_from_env_uses_default_when_variable_unset() {
let name = "NOPROP_CONFIG_TESTS_ABSOLUTELY_UNSET_ITER_9F3A_2E7B";
let v = iterations_from_env(name, 512).expect("unset var must use default");
assert_eq!(v, 512);
}
}