use std::ffi::OsStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EnvKind {
OwnedFlag,
ForeignFlag,
OptOutFlag,
ExactValue(&'static str),
Path,
Text,
Number { zero_selects_default: bool },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Owner {
Crate,
Foreign,
}
#[derive(Debug, Clone, Copy)]
pub struct EnvVar {
pub name: &'static str,
pub kind: EnvKind,
pub owner: Owner,
pub default: &'static str,
pub summary: &'static str,
}
impl EnvVar {
pub fn is_set(&self) -> bool {
match self.kind {
EnvKind::OwnedFlag => flag_owned(self.name),
EnvKind::ForeignFlag => flag_foreign(self.name),
EnvKind::OptOutFlag => flag_opt_out(self.name),
EnvKind::ExactValue(expected) => {
std::env::var_os(self.name).is_some_and(|value| value == OsStr::new(expected))
}
other => panic!("{} is declared as {other:?}, not a flag", self.name),
}
}
}
impl EnvVar {
pub fn count_or(&self, default: usize) -> usize {
self.parsed::<usize>().unwrap_or(default)
}
pub fn millis_or(&self, default: std::time::Duration) -> std::time::Duration {
self.parsed::<u64>()
.map(std::time::Duration::from_millis)
.unwrap_or(default)
}
pub fn port(&self) -> Option<u16> {
self.parsed::<u16>()
}
pub fn is_present(&self) -> bool {
std::env::var_os(self.name).is_some()
}
pub fn text(&self) -> Option<String> {
std::env::var(self.name)
.ok()
.filter(|value| !value.is_empty())
}
pub fn os(&self) -> Option<std::ffi::OsString> {
std::env::var_os(self.name)
}
pub fn string(&self) -> Option<String> {
std::env::var(self.name).ok()
}
pub fn path(&self) -> Option<std::path::PathBuf> {
std::env::var_os(self.name)
.filter(|value| !value.is_empty())
.map(std::path::PathBuf::from)
}
fn parsed<T>(&self) -> Option<T>
where
T: std::str::FromStr + Default + PartialEq,
{
let EnvKind::Number {
zero_selects_default,
} = self.kind
else {
panic!("{} is declared as {:?}, not a number", self.name, self.kind);
};
let parsed: T = std::env::var(self.name)
.ok()?
.trim()
.parse()
.ok()
.filter(|value: &T| !(zero_selects_default && *value == T::default()))?;
Some(parsed)
}
}
const AFFIRMATIVE: &[&str] = &["1", "true", "yes", "on"];
const NEGATIVE: &[&str] = &["", "0", "false", "no", "off"];
pub fn flag_owned(name: &str) -> bool {
match std::env::var_os(name) {
Some(value) => AFFIRMATIVE.contains(&normalize(&value).as_str()),
None => false,
}
}
pub fn flag_foreign(name: &str) -> bool {
match std::env::var_os(name) {
Some(value) => !NEGATIVE.contains(&normalize(&value).as_str()),
None => false,
}
}
pub fn flag_opt_out(name: &str) -> bool {
match std::env::var_os(name) {
Some(value) => !NEGATIVE.contains(&normalize(&value).as_str()),
None => true,
}
}
pub fn os_named(name: &str) -> Option<std::ffi::OsString> {
std::env::var_os(name)
}
pub fn string_named(name: &str) -> Option<String> {
std::env::var(name).ok()
}
pub fn value_is_affirmative_foreign(value: &str) -> bool {
!NEGATIVE.contains(&value.trim().to_ascii_lowercase().as_str())
}
#[macro_export]
macro_rules! declare_env_vars {
(
$(#[$table_meta:meta])*
pub const $table:ident;
$($ident:ident => $name:literal, $kind:expr, $owner:expr, $default:literal, $summary:literal;)*
) => {
$(
#[doc = $summary]
#[doc = concat!("Environment variable `", $name, "`. Unset: ", $default, ".")]
pub const $ident: $crate::env::EnvVar = $crate::env::EnvVar {
name: $name,
kind: $kind,
owner: $owner,
default: $default,
summary: $summary,
};
)*
$(#[$table_meta])*
pub const $table: &[$crate::env::EnvVar] = &[$($ident),*];
};
}
fn normalize(value: &OsStr) -> String {
value.to_string_lossy().trim().to_ascii_lowercase()
}
#[cfg(test)]
mod tests {
use super::*;
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn with_var<T>(name: &str, value: Option<&str>, body: impl FnOnce() -> T) -> T {
let _guard = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let previous = std::env::var_os(name);
match value {
Some(value) => std::env::set_var(name, value),
None => std::env::remove_var(name),
}
let outcome = body();
match previous {
Some(previous) => std::env::set_var(name, previous),
None => std::env::remove_var(name),
}
outcome
}
const PROBE: &str = "PLATFORM_INTERNAL_ENV_MECHANISM_PROBE";
fn var(kind: EnvKind) -> EnvVar {
EnvVar {
name: PROBE,
kind,
owner: Owner::Crate,
default: "unset",
summary: "test probe",
}
}
#[test]
fn the_three_flag_readers_disagree_only_on_unrecognised_values_and_unset() {
for (value, owned, foreign, opt_out) in [
(Some("1"), true, true, true),
(Some(" TRUE "), true, true, true),
(Some("0"), false, false, false),
(Some("off"), false, false, false),
(Some("maybe"), false, true, true),
(None, false, false, true),
] {
assert_eq!(
with_var(PROBE, value, || flag_owned(PROBE)),
owned,
"{value:?}"
);
assert_eq!(
with_var(PROBE, value, || flag_foreign(PROBE)),
foreign,
"{value:?}"
);
assert_eq!(
with_var(PROBE, value, || flag_opt_out(PROBE)),
opt_out,
"{value:?}"
);
}
}
#[test]
fn a_declared_flag_reads_through_its_own_semantics() {
assert!(with_var(PROBE, Some("yes"), || var(EnvKind::OwnedFlag).is_set()));
assert!(!with_var(PROBE, None, || var(EnvKind::OwnedFlag).is_set()));
assert!(with_var(PROBE, None, || var(EnvKind::OptOutFlag).is_set()));
assert!(with_var(PROBE, Some("exactly"), || {
var(EnvKind::ExactValue("exactly")).is_set()
}));
assert!(!with_var(PROBE, Some("Exactly"), || {
var(EnvKind::ExactValue("exactly")).is_set()
}));
}
#[test]
fn numbers_honour_the_declared_meaning_of_zero_and_reject_garbage() {
let selects = var(EnvKind::Number {
zero_selects_default: true,
});
let honours = var(EnvKind::Number {
zero_selects_default: false,
});
assert_eq!(with_var(PROBE, Some("0"), || selects.count_or(9)), 9);
assert_eq!(with_var(PROBE, Some("0"), || honours.count_or(9)), 0);
assert_eq!(with_var(PROBE, Some(" 42 "), || honours.count_or(9)), 42);
assert_eq!(with_var(PROBE, Some("many"), || honours.count_or(9)), 9);
assert_eq!(with_var(PROBE, Some("8080"), || honours.port()), Some(8080));
assert_eq!(
with_var(PROBE, Some("250"), || {
honours.millis_or(std::time::Duration::from_secs(1))
}),
std::time::Duration::from_millis(250)
);
}
#[test]
fn text_and_path_treat_empty_as_absent_and_keep_the_value_verbatim() {
let text = var(EnvKind::Text);
assert_eq!(with_var(PROBE, Some(""), || text.text()), None);
assert_eq!(
with_var(PROBE, Some("a b"), || text.text()),
Some("a b".into())
);
assert_eq!(with_var(PROBE, Some(""), || text.path()), None);
assert_eq!(
with_var(PROBE, Some("/x/y"), || text.path()),
Some(std::path::PathBuf::from("/x/y"))
);
}
#[test]
fn os_and_string_keep_an_empty_value_as_set() {
let text = var(EnvKind::Text);
assert_eq!(
with_var(PROBE, Some(""), || text.os()),
Some(std::ffi::OsString::new())
);
assert_eq!(
with_var(PROBE, Some(""), || text.string()),
Some(String::new())
);
assert_eq!(with_var(PROBE, None, || text.os()), None);
assert_eq!(with_var(PROBE, None, || text.string()), None);
}
#[test]
fn a_presence_switch_is_on_for_any_value_even_empty_or_zero() {
let presence = var(EnvKind::Text);
for value in ["", "0", "off", "1"] {
assert!(
with_var(PROBE, Some(value), || presence.is_present()),
"{value:?}"
);
}
assert!(!with_var(PROBE, None, || presence.is_present()));
}
#[test]
#[should_panic(expected = "not a flag")]
fn reading_a_non_flag_as_a_flag_is_a_programming_error() {
with_var(PROBE, Some("1"), || var(EnvKind::Text).is_set());
}
#[test]
fn a_value_already_in_hand_reads_like_the_foreign_flag() {
for value in ["1", "true", "maybe"] {
assert!(value_is_affirmative_foreign(value), "{value}");
}
for value in ["", "0", " OFF "] {
assert!(!value_is_affirmative_foreign(value), "{value}");
}
}
}