Skip to content

Repository files navigation

lockedenv

Ergonomic, type-safe, freeze-on-load environment variable management for Rust 🦀.

Read once, parse immediately, freeze forever.

crates.iodocs.rslicense-MITlicense-Apache-2.0

v0.3: env_struct! (named structs), check!/try_check! (collect all errors), decimal Duration ("1.5h"), with_hint on all error variants.

Environment variables are often a source of subtle bugs: they are read multiple times across the codebase, treated as untyped Strings, and can silently fail if mutated at runtime. Testing them natively with std::env::set_var is unsafe in parallel contexts.

lockedenv solves this cleanly: define a struct layout via a macro, enforce type-safe parsing at startup, and pass the generated, immutable struct to your application.

Quickstart

Add lockedenv to your Cargo.toml:

[dependencies]
lockedenv = "0.3"

Use the load! macro to define and parse your configuration:

fnmain(){let config = lockedenv::load! {PORT:u16,DATABASE_URL:String,DEBUG:bool = false,TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30),SENTRY_DSN:Option<String>,};// The generated 'config' struct implements `Clone` and `Debug`println!("Listening on port {} in debug mode: {}", config.PORT, config.DEBUG);}

If a required variable is missing or cannot be parsed, load!panics with a clear message describing the variable name, the value found, and a hint on how to fix it. Your application cannot boot into an invalid state.

Core Features

  • Safe: Eliminates repeated std::env::var calls. Validates everything on startup.
  • Type-safe: Built-in parsers for standard library types (u16, bool, IpAddr, std::time::Duration, etc.) and seamlessly extensible via FromEnvStr.
  • Zero-Boilerplate Default & Optional values: Naturally handles fallback = defaults and Option<T> for transparent absences.
  • Thread-safe testing: The from_map! macro allows you to inject HashMaps into the parser, avoiding the deprecation and threading issues of std::env::set_var.
  • Hygienic: Generates an isolated, anonymous struct ensuring no namespace pollution.

The Macro Family

lockedenv provides variants for every need:

// 1. Panics on missing/bad config — recommended for services that must not boot brokenlet config = lockedenv::load! {PORT:u16,DS_URL:String};// 2. Returns Result<_, EnvLockError> to handle or propagate failureslet config = lockedenv::try_load! {PORT:u16}?;// 3. Named struct — the config type has a real name and can be used in signatures
lockedenv::env_struct! {pubstructAppConfig{PORT:u16,HOST:String,DEBUG:bool = false,}}fnstart(cfg:AppConfig){/* ... */}fnmain(){start(AppConfig::load());}// 4. Collect ALL errors before failing — show every missing/bad var at oncematch lockedenv::try_check! {PORT:u16,HOST:String,DB:String}{Ok(cfg) => {/* use cfg */}Err(errs) => {for e in&errs {eprintln!("{e}");}
std::process::exit(1);}}// Or panic with the full list in one shot:let config = lockedenv::check! {PORT:u16,HOST:String,DB:String};

Named Structs with env_struct!

When your config struct needs to outlive a single expression — to be stored, returned from a function, or named as a type — use env_struct!. It generates a named struct with load(), try_load(), from_map(), and try_from_map() associated functions:

lockedenv::env_struct! {pubstructServiceConfig{
prefix = "SVC_",// reads SVC_HOST, SVC_PORT, …HOST:String,PORT:u16 = 8080,TOKEN: lockedenv::Secret<String>,LABEL:Option<String>,}}// Can appear in function signatures, struct fields, type aliases:fnload_config() -> ServiceConfig{ServiceConfig::load()}// In tests, inject a HashMap instead of touching the real environment:let m = std::collections::HashMap::from([("SVC_HOST".into(),"localhost".into()),("SVC_TOKEN".into(),"secret".into()),]);let cfg = ServiceConfig::from_map(&m);assert_eq!(cfg.HOST,"localhost");

Collect All Errors with check! / try_check!

load! stops at the first error. check! and try_check! try every field and report all problems:

// Panics with a list of ALL errors, not just the first:let config = lockedenv::check! {HOST:String,PORT:u16,DB:String};// Panic message: "3 configuration error(s):// - EnvLockError: missing required variable// variable: HOST …// - EnvLockError: missing required variable// variable: PORT …// …"// Or handle the error list yourself:ifletErr(errors) = lockedenv::try_check! {HOST:String,PORT:u16,DB:String}{for e in&errors {eprintln!("{e}");}
std::process::exit(1);}

Thread-Safe Testing

In tests, mutating the global environment is an anti-pattern. All macros accept map: for HashMap injection:

#[test]fntest_config_parsing(){let map = std::collections::HashMap::from([("PORT".into(),"8080".into())]);let config = lockedenv::from_map! { map: map,PORT:u16};assert_eq!(config.PORT,8080);// Same works for check! and env_struct!:let result = lockedenv::try_check! { map: map,PORT:u16};assert!(result.is_ok());}

Supported Types (Zero extra dependencies)

Rust TypeSyntax ExampleNotes
String, char"value", 'a'
Integer primitives8080, -20Native bounds checked
Floating point"3.14"
bool"true", "1", "yes", "false"Case-insensitive
std::path::PathBuf"/etc/hosts"Does not check disk presence
IpAddr, SocketAddr"127.0.0.1", "0.0.0.0:8080"
std::time::Duration"30s", "1.5h", "0.5s", "1h30m"Units: h, m, s, ms; integer and decimal segments; compound allowed
Vec<T>"a,b,c", "80,443"Comma-separated; empty segments (leading/trailing/double commas) are silently ignored
lockedenv::Secret<T>"password"Redacts value in Debug and Serialize logs
Option<T>None if absent or emptyAn absent key and an empty string (VAR="") both produce None

You can add support for your own types by simply implementing lockedenv::parse::FromEnvStr.

use lockedenv::parse::FromEnvStr;structRetries(u8);implFromEnvStrforRetries{typeErr = String;fnfrom_env_str(s:&str) -> Result<Self,Self::Err>{let n:u8 = s.parse().map_err(|e| format!("{}", e))?;if n > 10{returnErr("max 10 retries".into());}Ok(Retries(n))}}

Naming Convention

Field names in the macro match the real environment variable name exactly, including case. By convention variables are UPPER_SNAKE_CASE and are accessed the same way on the generated struct:

let config = lockedenv::load! {DATABASE_URL:String,MAX_CONN:u32};println!("{} (max {})", config.DATABASE_URL, config.MAX_CONN);

When wrapping in a typed application struct, map at the boundary:

structAppConfig{db_url:String,max_conn:u32}implAppConfig{fnfrom_env() -> Self{let raw = lockedenv::load! {DATABASE_URL:String,MAX_CONN:u32 = 10};Self{db_url: raw.DATABASE_URL,max_conn: raw.MAX_CONN}}}

Custom Error Hints

EnvLockError::with_hint attaches a human-readable hint to any error variant — Missing, Parse, or Dotenv. The hint is shown in Display output after the primary message:

use lockedenv::{parse::FromEnvStr,EnvLockError};structPort(u16);implFromEnvStrforPort{typeErr = String;fnfrom_env_str(s:&str) -> Result<Self,Self::Err>{
s.parse::<u16>().map(Port).map_err(|_| "not a valid port (0–65535)".into())}}// Attach a hint to a parse error:let e = EnvLockError::parse_error("TIMEOUT".into(),"5min".into(),"unknown unit").with_hint("use 5m or 5s instead");// → "expected type: unknown unit\n hint: use 5m or 5s instead"// Attach a hint to a missing-variable error:let e = EnvLockError::missing("DATABASE_URL".into()).with_hint("see .env.example for the expected format");// → "missing required variable\n …\n hint: see .env.example for the expected format"

Optional Features

Extend lockedenv by enabling features in Cargo.toml.

FeatureDescription
dotenvUnlocks load_dotenv!("path", { ... }) macros using dotenvy.
serdeAutomatically derives Serialize and Deserialize on your generated configuration struct. Great for debug logging / dumping config state.
watchProvides lockedenv::watch! for async, background-thread interval drift detection. Generates a listener delta without heavy file watchers.
url-typeConnects directly to the url crate for strong url::Url typing.
tracingAutomatically logs the loaded configuration struct (with redacted secrets) at INFO level using the tracing crate upon successful load.

Prefixes & Secrets

If your environment variables share a common prefix, declare it once at the macro level:

// Reads APP_PORT and APP_TOKEN from the environmentlet config = lockedenv::load! {
prefix = "APP_",PORT:u16,TOKEN: lockedenv::Secret<String>,};

Secret<T> wraps any type and redacts its value in Debug output and serde serialization — useful when logging the config state at startup:

// Printing the config is always safe:println!("{:?}", config);// { PORT: 8080, TOKEN: Secret([REDACTED]) }// Access the real value when needed:let token:&str = config.TOKEN.as_ref();// AsRef<String>let owned:String = config.TOKEN.clone().into_inner();let s: lockedenv::Secret<String> = String::from("raw").into();// From<T>

Feature Showcase: Watcher

Ideal for environments (like K8s or Docker) where external factors could unexpectedly orchestrate config shifts at runtime. Note that dropping the handle stops the watcher cleanly.

// Requires: lockedenv = { version = "0.1", features = ["watch"] }let config = lockedenv::load! {TARGET_URL:String};// Checks every 5 seconds; only reads the listed keys on each tick.let _handle = lockedenv::watch!(
keys = ["TARGET_URL"],
interval_secs = 5,
on_drift = |key, old, new| {
eprintln!("Drift Alert: {} shifted from {} to {}", key, old, new);});

License

MIT OR Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.


made with Rust 🦀

About

Ergonomic, type-safe, freeze-on-load environment variable management for Rust 🦀.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages