lockedenv
Ergonomic, type-safe, freeze-on-load environment variable management for Rust 🦀.
Read once, parse immediately, freeze forever.
v0.3:
env_struct!(named structs),check!/try_check!(collect all errors), decimalDuration("1.5h"),with_hinton 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:
[]
= "0.3"
Use the load! macro to define and parse your configuration:
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::varcalls. Validates everything on startup. - Type-safe: Built-in parsers for standard library types (
u16,bool,IpAddr,std::time::Duration, etc.) and seamlessly extensible viaFromEnvStr. - Zero-Boilerplate Default & Optional values: Naturally handles
fallback = defaultsandOption<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 ofstd::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 broken
let config = load! ;
// 2. Returns Result<_, EnvLockError> to handle or propagate failures
let config = try_load! ?;
// 3. Named struct — the config type has a real name and can be used in signatures
env_struct!
// 4. Collect ALL errors before failing — show every missing/bad var at once
match try_check!
// Or panic with the full list in one shot:
let config = check! ;
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:
env_struct!
// Can appear in function signatures, struct fields, type aliases:
// In tests, inject a HashMap instead of touching the real environment:
let m = from;
let cfg = from_map;
assert_eq!;
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 = check! ;
// Panic message: "3 configuration error(s):
// - EnvLockError: missing required variable
// variable: HOST …
// - EnvLockError: missing required variable
// variable: PORT …
// …"
// Or handle the error list yourself:
if let Err = try_check!
Thread-Safe Testing
In tests, mutating the global environment is an anti-pattern. All macros accept map: for HashMap injection:
Supported Types (Zero extra dependencies)
| Rust Type | Syntax Example | Notes |
|---|---|---|
String, char |
"value", 'a' |
|
| Integer primitives | 8080, -20 |
Native 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 empty |
An 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 FromEnvStr;
;
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 = load! ;
println!;
When wrapping in a typed application struct, map at the boundary:
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 ;
;
// Attach a hint to a parse error:
let e = parse_error
.with_hint;
// → "expected type: unknown unit\n hint: use 5m or 5s instead"
// Attach a hint to a missing-variable error:
let e = missing
.with_hint;
// → "missing required variable\n …\n hint: see .env.example for the expected format"
Optional Features
Extend lockedenv by enabling features in Cargo.toml.
| Feature | Description |
|---|---|
dotenv |
Unlocks load_dotenv!("path", { ... }) macros using dotenvy. |
serde |
Automatically derives Serialize and Deserialize on your generated configuration struct. Great for debug logging / dumping config state. |
watch |
Provides lockedenv::watch! for async, background-thread interval drift detection. Generates a listener delta without heavy file watchers. |
url-type |
Connects directly to the url crate for strong url::Url typing. |
tracing |
Automatically 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 environment
let config = load! ;
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!; // { 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: Secret = Stringfrom.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 = load! ;
// Checks every 5 seconds; only reads the listed keys on each tick.
let _handle = watch!;
License
MIT OR Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.
made with Rust 🦀