#![forbid(unsafe_code)]
#![warn(rust_2018_idioms)]
#![doc = include_str!("../README.md")]
#[macro_export]
macro_rules! required {
($key:literal $(,)?) => {
$crate::__private::required_string($key, None)
};
($key:literal, $msg:expr $(,)?) => {
$crate::__private::required_string($key, Some(($msg).to_string()))
};
($key:literal => $t:ty $(,)?) => {
$crate::__private::required_parse::<$t>($key, None)
};
($key:literal => $t:ty, $msg:expr $(,)?) => {
$crate::__private::required_parse::<$t>($key, Some(($msg).to_string()))
};
([$($key:literal),+ $(,)?] $(,)?) => {
$crate::__private::validate_required(&[$($key),+], None)
};
([$($key:literal),+ $(,)?], $msg:expr $(,)?) => {
$crate::__private::validate_required(&[$($key),+], Some(($msg).to_string()))
};
() => {
::core::compile_error!(
"env-required: expected input. Example: required!(\"PORT\") or required!([\"A\", \"B\"])"
)
};
($($anything:tt)+) => {
::core::compile_error!(
"env-required: invalid syntax. Use string literals, e.g. required!(\"PORT\"), required!(\"PORT\" => u16), required!([\"A\", \"B\"])."
)
};
}
#[doc(hidden)]
pub mod __private {
#![allow(missing_docs)]
use core::fmt;
use core::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VarIssueKind {
Missing,
Empty,
NotUnicode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VarIssue {
pub key: &'static str,
pub kind: VarIssueKind,
}
pub fn required_string(key: &'static str, msg: Option<String>) -> String {
match raw_required(key) {
Ok(v) => v,
Err(issue) => panic_issues(&[issue], msg.as_deref()),
}
}
pub fn required_parse<T>(key: &'static str, msg: Option<String>) -> T
where
T: FromStr,
T::Err: fmt::Display,
{
let raw = match raw_required(key) {
Ok(v) => v,
Err(issue) => panic_issues(&[issue], msg.as_deref()),
};
match raw.parse::<T>() {
Ok(v) => v,
Err(err) => panic_parse::<T>(key, &err, msg.as_deref()),
}
}
pub fn validate_required(keys: &[&'static str], msg: Option<String>) {
let mut issues = Vec::new();
for &key in keys {
if let Err(issue) = raw_required(key) {
issues.push(issue);
}
}
if !issues.is_empty() {
panic_issues(&issues, msg.as_deref());
}
}
fn raw_required(key: &'static str) -> Result<String, VarIssue> {
let os = match std::env::var_os(key) {
Some(v) => v,
None => {
return Err(VarIssue {
key,
kind: VarIssueKind::Missing,
})
}
};
let s = match os.into_string() {
Ok(v) => v,
Err(_) => {
return Err(VarIssue {
key,
kind: VarIssueKind::NotUnicode,
})
}
};
#[cfg(not(feature = "allow-empty"))]
if s.is_empty() {
return Err(VarIssue {
key,
kind: VarIssueKind::Empty,
});
}
Ok(s)
}
fn panic_parse<T>(key: &'static str, err: &T::Err, msg: Option<&str>) -> !
where
T: FromStr,
T::Err: fmt::Display,
{
let mut out = String::new();
if let Some(msg) = msg {
out.push_str("env-required: ");
out.push_str(msg);
out.push_str("\n\n");
}
out.push_str("env-required: failed to parse required environment variable\n");
out.push_str("\n");
out.push_str("Key: ");
out.push_str(key);
out.push_str("\n");
out.push_str("Expected type: ");
out.push_str(core::any::type_name::<T>());
out.push_str("\n");
out.push_str("Parse error: ");
out.push_str(&err.to_string());
out.push_str("\n\n");
out.push_str("How to fix:\n");
out.push_str("- Ensure the value matches the expected type (see the message above).\n");
out.push_str("- Tip: print the env var before parsing to inspect its contents.\n");
panic!("{}", out);
}
fn panic_issues(issues: &[VarIssue], msg: Option<&str>) -> ! {
let mut out = String::new();
if let Some(msg) = msg {
out.push_str("env-required: ");
out.push_str(msg);
out.push_str("\n\n");
}
if issues.len() == 1 {
let i = issues[0];
out.push_str("env-required: missing required environment variable\n\n");
out.push_str("Key: ");
out.push_str(i.key);
out.push_str("\n");
out.push_str("Problem: ");
out.push_str(kind_human(i.kind));
out.push_str("\n\n");
} else {
out.push_str("env-required: missing required environment variables\n\n");
out.push_str("Missing count: ");
out.push_str(&issues.len().to_string());
out.push_str("\n\n");
for i in issues {
out.push_str("- ");
out.push_str(i.key);
out.push_str(": ");
out.push_str(kind_human(i.kind));
out.push_str("\n");
}
out.push_str("\n");
}
out.push_str("How to fix:\n");
out.push_str("- Set the env var(s) before running this program.\n");
out.push_str("- Example (bash/zsh): export KEY=\"value\"\n");
out.push_str("- Example (PowerShell): $Env:KEY = \"value\"\n");
#[cfg(not(feature = "allow-empty"))]
{
out.push_str("- Note: empty strings (KEY=\"\") are treated as missing by default.\n");
out.push_str(" Enable feature `allow-empty` if you want to accept empty values.\n");
}
panic!("{}", out);
}
fn kind_human(kind: VarIssueKind) -> &'static str {
match kind {
VarIssueKind::Missing => "not set",
VarIssueKind::Empty => "set but empty",
VarIssueKind::NotUnicode => "set but not valid UTF-8",
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn lock_env() -> std::sync::MutexGuard<'static, ()> {
ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
#[test]
fn reads_string_env_var() {
let _guard = lock_env();
std::env::set_var("ENV_REQUIRED_TEST_URL", "postgres://localhost/db");
let v = required!("ENV_REQUIRED_TEST_URL");
assert_eq!(v, "postgres://localhost/db");
std::env::remove_var("ENV_REQUIRED_TEST_URL");
}
#[test]
fn parses_fromstr_type() {
let _guard = lock_env();
std::env::set_var("ENV_REQUIRED_TEST_PORT", "5432");
let port: u16 = required!("ENV_REQUIRED_TEST_PORT" => u16);
assert_eq!(port, 5432);
std::env::remove_var("ENV_REQUIRED_TEST_PORT");
}
#[test]
fn validates_many() {
let _guard = lock_env();
std::env::set_var("ENV_REQUIRED_TEST_A", "a");
std::env::set_var("ENV_REQUIRED_TEST_B", "b");
required!(["ENV_REQUIRED_TEST_A", "ENV_REQUIRED_TEST_B"]);
std::env::remove_var("ENV_REQUIRED_TEST_A");
std::env::remove_var("ENV_REQUIRED_TEST_B");
}
#[test]
fn missing_env_panics_with_key_name() {
let _guard = lock_env();
std::env::remove_var("ENV_REQUIRED_TEST_MISSING");
let panic_msg = std::panic::catch_unwind(|| {
let _ = required!("ENV_REQUIRED_TEST_MISSING");
})
.expect_err("expected panic");
let msg = panic_to_string(panic_msg);
assert!(msg.contains("ENV_REQUIRED_TEST_MISSING"));
assert!(msg.contains("missing required environment variable"));
}
#[test]
fn parse_error_includes_type_name() {
let _guard = lock_env();
std::env::set_var("ENV_REQUIRED_TEST_BAD_U16", "not-a-number");
let panic_msg = std::panic::catch_unwind(|| {
let _: u16 = required!("ENV_REQUIRED_TEST_BAD_U16" => u16);
})
.expect_err("expected panic");
let msg = panic_to_string(panic_msg);
assert!(msg.contains("ENV_REQUIRED_TEST_BAD_U16"));
assert!(msg.contains("Expected type"));
assert!(msg.contains("u16"));
std::env::remove_var("ENV_REQUIRED_TEST_BAD_U16");
}
#[test]
fn validate_many_reports_all_missing_keys() {
let _guard = lock_env();
std::env::set_var("ENV_REQUIRED_TEST_PRESENT", "x");
std::env::remove_var("ENV_REQUIRED_TEST_MISSING_1");
std::env::remove_var("ENV_REQUIRED_TEST_MISSING_2");
let panic_msg = std::panic::catch_unwind(|| {
required!([
"ENV_REQUIRED_TEST_PRESENT",
"ENV_REQUIRED_TEST_MISSING_1",
"ENV_REQUIRED_TEST_MISSING_2",
]);
})
.expect_err("expected panic");
let msg = panic_to_string(panic_msg);
assert!(msg.contains("ENV_REQUIRED_TEST_MISSING_1"));
assert!(msg.contains("ENV_REQUIRED_TEST_MISSING_2"));
std::env::remove_var("ENV_REQUIRED_TEST_PRESENT");
}
fn panic_to_string(p: Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = p.downcast_ref::<&'static str>() {
s.to_string()
} else if let Some(s) = p.downcast_ref::<String>() {
s.clone()
} else {
"<non-string panic payload>".to_string()
}
}
}