Describe the requirements of environment variables in a type-safe and ergonomic way.
- Type Safety: Parse environment variables directly into Rust types
- Flexible Loading: Choose between on-demand and startup loading strategies
- Globals:
LazyLock-like globals for easy access to environment variables. - Rich Types: Support for primitives, booleans, strings, lists and your custom types.
- Default Values: Support for factory-based fallback/default values.
- Error Handling: Comprehensive error types with helpful messages
- Specialized Support:
- List: Parse comma-separated (or custom delimiter) lists with filtering options.
- Boolean: Flexible boolean parsing with multiple accepted formats:
true,1,yes,y,on,enabled(case insensitive) for true, andfalse,0,no,n,off,disabled(case insensitive) for false. - Custom Types: Parse custom types with custom parsing and error handling.
Add to your Cargo.toml:
[dependencies]
typed-env = "0.2"use typed_env::{Envar, EnvarDef};
// Define typed environment variables
static PORT: Envar<u16> = Envar::on_demand("PORT", || EnvarDef::Default(8080));
static DEBUG: Envar<bool> = Envar::on_demand("DEBUG", || EnvarDef::Default(false));
static DATABASE_URL: Envar<String> = Envar::on_demand("DATABASE_URL", || EnvarDef::Unset);
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Use the values - they're parsed automatically
let port = PORT.value()?; // u16
let debug = DEBUG.value()?; // bool
let db_url = DATABASE_URL.value()?; // String
println!("Server running on port {} (debug: {})", port, debug);
println!("Database: {}", db_url);
Ok(())
}Variables are parsed when first accessed and cached thereafter. If the environment variable is changed, the new value will be loaded on the next access.
static API_KEY: Envar<String> = Envar::on_demand("API_KEY", || EnvarDef::Unset);
// Parsed and cached on first call
let key1 = API_KEY.value()?;
// Returns cached value
let key2 = API_KEY.value()?;
// ...
unsafe { std::env::set_var("API_KEY", "1234567890"); }
// ...
let key3 = API_KEY.value()?; // Loads the new value
// ...Variables are parsed once at startup and never re-read:
static MAX_CONNECTIONS: Envar<u32> = Envar::on_startup("MAX_CONNECTIONS", || EnvarDef::Default(100));
// Always returns the same value, even if env var changes
let max_conn = MAX_CONNECTIONS.value()?;WARNING: Envar::on_startup does not load the environment variables at the actual startup time, but at the time of the first access.
All standard integer and float types are supported:
static TIMEOUT_MS: Envar<u64> = Envar::on_demand("TIMEOUT_MS", || EnvarDef::Default(5000));
static RATE_LIMIT: Envar<f64> = Envar::on_demand("RATE_LIMIT", || EnvarDef::Default(10.5));Flexible boolean parsing with multiple accepted formats:
static ENABLE_LOGS: Envar<bool> = Envar::on_demand("ENABLE_LOGS", || EnvarDef::Default(true));Accepted values:
- True:
true,1,yes,y,on,enabled(case insensitive) - False:
false,0,no,n,off,disabled(case insensitive) - Empty string: treated as
false
Parse delimited lists with configurable separators and filtering:
use typed_env::{ListEnvar, ListEnvarConfig};
// Define a custom list configuration
struct CommaList;
impl ListEnvarConfig for CommaList {
const SEP: &'static str = ",";
const FILTER_EMPTY_STR: bool = true;
const FILTER_WHITESPACE: bool = true;
}
static ALLOWED_ORIGINS: Envar<ListEnvar<String, CommaList>> =
Envar::on_demand("ALLOWED_ORIGINS", || EnvarDef::Unset);
// ALLOWED_ORIGINS="localhost,127.0.0.1,example.com"
let origins = ALLOWED_ORIGINS.value()?;
for origin in origins.iter() {
println!("Allowed origin: {}", origin);
}use typed_env::{
Envar,
EnvarDef,
EnvarError,
EnvarParse,
EnvarParser,
ErrorReason
};
// accept: LEVEL="vvv"
static LEVEL: Envar<Level> = Envar::on_demand("LEVEL", || EnvarDef::Unset);
#[derive(Clone, Debug)]
pub struct Level(pub usize);
impl EnvarParse<Level> for EnvarParser<Level> {
fn parse(varname: std::borrow::Cow<'static, str>, value: &str) -> Result<Level, typed_env::EnvarError> {
let value = value.trim();
let mut count = 0;
for c in value.chars() {
if c == 'v' {
count += 1;
}
else {
return Err(EnvarError::ParseError {
varname,
typename: std::any::type_name::<Level>(),
value: value.to_string(),
reason: ErrorReason::new(move || format!("invalid character: {}", c)),
});
}
}
Ok(Level(count))
}
}Provide a fallback value when the environment variable is not set:
static WORKER_THREADS: Envar<usize> = Envar::on_demand("WORKER_THREADS", || EnvarDef::Default(4));
// Returns 4 if WORKER_THREADS is not set
let threads = WORKER_THREADS.value()?;Require the environment variable to be present:
static SECRET_KEY: Envar<String> = Envar::on_demand("SECRET_KEY", || EnvarDef::Unset);
// Returns Err(EnvarError::NotSet) if SECRET_KEY is not set
let secret = SECRET_KEY.value()?;The library provides detailed error information:
use typed_env::EnvarError;
match DATABASE_URL.value() {
Ok(url) => println!("Database URL: {}", url),
Err(EnvarError::NotSet(name)) => {
eprintln!("Required environment variable {} is not set", name);
}
Err(EnvarError::ParseError { varname, typename, value, .. }) => {
eprintln!("Failed to parse {} as {}: {:?}", varname, typename, value);
}
}Envar<T>: The main environment variable containerEnvarDef<T>: Defines default behavior (Default(value)orUnset)ListEnvar<T, C>: Container for list-type environment variablesListEnvarConfig: Trait for configuring list parsing behavior
Envar::on_demand(name, default_factory): Create an on-demand loaded variableEnvar::on_startup(name, default_factory): Create a startup-loaded variableenvar.value(): Get the parsed value, returnsResult<T, EnvarError>envar.name(): Get the environment variable name
EnvarError::NotSet(name): Environment variable is not set and no default providedEnvarError::ParseError { varname, typename, value, reason }: Failed to parse the valueEnvarError::TryDefault(varname): A "soft" error, indicating that the environment variable could fallback to the default value, if specified withEnvarDef::Default.
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.