use std::str::FromStr;
use anyhow::{Context, Result, anyhow};
pub fn var<T: FromStr>(name: &str) -> Result<T>
where
T::Err: std::error::Error + Send + Sync + 'static,
{
let value =
std::env::var(name).with_context(|| format!("Environment variable {name} not set"))?;
value
.parse()
.with_context(|| format!("Failed to parse environment variable {name}"))
}
pub fn var_or<T: FromStr>(name: &str, default: T) -> T
where
T::Err: std::error::Error + Send + Sync + 'static,
{
var(name).unwrap_or(default)
}
pub fn dotenv() -> Result<()> {
match dotenvy::dotenv() {
Ok(_) => Ok(()),
Err(dotenvy::Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(anyhow!("Failed to load .env file: {e}")),
}
}
pub fn require_vars(names: &[&str]) -> Result<()> {
let missing: Vec<&str> = names
.iter()
.filter(|name| std::env::var(name).is_err())
.copied()
.collect();
if missing.is_empty() {
Ok(())
} else {
Err(anyhow!(
"Missing required environment variables: {}",
missing.join(", ")
))
}
}
#[cfg(test)]
#[allow(unsafe_code)]
mod tests {
use super::*;
#[test]
fn var_reads_and_parses_existing_env_var() {
unsafe { std::env::set_var("SCRIPTKIT_TEST_INT", "42") };
let value: i32 = var("SCRIPTKIT_TEST_INT").unwrap();
assert_eq!(value, 42);
unsafe { std::env::remove_var("SCRIPTKIT_TEST_INT") };
}
#[test]
fn var_reads_string() {
unsafe { std::env::set_var("SCRIPTKIT_TEST_STR", "hello") };
let value: String = var("SCRIPTKIT_TEST_STR").unwrap();
assert_eq!(value, "hello");
unsafe { std::env::remove_var("SCRIPTKIT_TEST_STR") };
}
#[test]
fn var_fails_on_missing() {
unsafe { std::env::remove_var("SCRIPTKIT_MISSING_VAR") };
let result: Result<String> = var("SCRIPTKIT_MISSING_VAR");
assert!(result.is_err());
}
#[test]
fn var_fails_on_parse_error() {
unsafe { std::env::set_var("SCRIPTKIT_BAD_INT", "not_a_number") };
let result: Result<i32> = var("SCRIPTKIT_BAD_INT");
assert!(result.is_err());
unsafe { std::env::remove_var("SCRIPTKIT_BAD_INT") };
}
#[test]
fn var_or_returns_value_when_set() {
unsafe { std::env::set_var("SCRIPTKIT_TEST_OR", "100") };
let value: i32 = var_or("SCRIPTKIT_TEST_OR", 50);
assert_eq!(value, 100);
unsafe { std::env::remove_var("SCRIPTKIT_TEST_OR") };
}
#[test]
fn var_or_returns_default_when_missing() {
unsafe { std::env::remove_var("SCRIPTKIT_MISSING_OR") };
let value: i32 = var_or("SCRIPTKIT_MISSING_OR", 50);
assert_eq!(value, 50);
}
#[test]
fn var_or_returns_default_on_parse_error() {
unsafe { std::env::set_var("SCRIPTKIT_BAD_OR", "not_a_number") };
let value: i32 = var_or("SCRIPTKIT_BAD_OR", 99);
assert_eq!(value, 99);
unsafe { std::env::remove_var("SCRIPTKIT_BAD_OR") };
}
#[test]
fn require_vars_succeeds_when_all_present() {
unsafe {
std::env::set_var("SCRIPTKIT_REQ_A", "a");
std::env::set_var("SCRIPTKIT_REQ_B", "b");
}
let result = require_vars(&["SCRIPTKIT_REQ_A", "SCRIPTKIT_REQ_B"]);
assert!(result.is_ok());
unsafe {
std::env::remove_var("SCRIPTKIT_REQ_A");
std::env::remove_var("SCRIPTKIT_REQ_B");
}
}
#[test]
fn require_vars_fails_when_any_missing() {
unsafe {
std::env::set_var("SCRIPTKIT_REQ_C", "c");
std::env::remove_var("SCRIPTKIT_MISSING_REQ");
}
let result = require_vars(&["SCRIPTKIT_REQ_C", "SCRIPTKIT_MISSING_REQ"]);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("SCRIPTKIT_MISSING_REQ"));
unsafe { std::env::remove_var("SCRIPTKIT_REQ_C") };
}
#[test]
fn dotenv_succeeds_when_no_file() {
let result = dotenv();
assert!(result.is_ok());
}
}