Typed Config Binding
Derive a load() method that reads environment variables into a strongly-typed config struct at startup.
Setup
Enable the macros feature in Cargo.toml:
[dependencies]
rust-web-server = { version = "18", features = ["macros"] }Quick start
use rust_web_server::Config;
#[derive(rust_web_server::Config)]
#[config(prefix = "APP_")]
struct AppConfig {
#[config(env = "PORT", default = "8080")]
port: u16,
#[config(env = "DATABASE_URL")]
database_url: String,
#[config(env = "DEBUG")]
debug: Option<bool>,
}
fn main() {
let cfg = AppConfig::load().expect("invalid config");
println!("listening on port {}", cfg.port);
}#[derive(Config)]
The derive macro generates a single method on the struct:
impl AppConfig {
pub fn load() -> Result<Self, String> { ... }
}load() reads every field from environment variables and returns Err(String) with a descriptive message on the first parse failure.
Struct-level attribute
#[config(prefix = "APP_")]When set, every field's environment variable key is prefix + key. With prefix = "APP_" and a field named port, the default env var is APP_PORT.
Field-level attribute
#[config(env = "MY_PORT", default = "8080")]| Option | Meaning |
|---|---|
env = "KEY" |
Explicit env var name. The struct prefix is still prepended. |
default = "v" |
Fallback string when the env var is absent. Parsed with the same FromEnvStr impl as a present value. |
If env is omitted the field name is uppercased (pool_size → POOL_SIZE), then the prefix is prepended.
Field derivation rules
| Field declaration | Env var absent | Env var present |
|---|---|---|
field: T with default |
parse default value |
parse env var |
field: T without default |
Err — required |
parse env var |
field: Option<T> |
Ok(None) (also when empty) |
parse env var, wrap in Some |
FromEnvStr trait
All primitive Rust scalar types implement FromEnvStr out of the box:
String, bool, u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64
bool accepts "true", "1", "yes" → true; "false", "0", "no" → false.
Implement FromEnvStr on your own type to use it as a config field:
use rust_web_server::config_binding::FromEnvStr;
#[derive(Debug)]
enum LogLevel { Error, Warn, Info, Debug }
impl FromEnvStr for LogLevel {
fn from_env_str(s: &str) -> Result<Self, String> {
match s.trim().to_ascii_lowercase().as_str() {
"error" => Ok(LogLevel::Error),
"warn" => Ok(LogLevel::Warn),
"info" => Ok(LogLevel::Info),
"debug" => Ok(LogLevel::Debug),
other => Err(format!("unknown log level: {other}")),
}
}
}Low-level helpers
The functions the derive expands to are public, for reading a single value without a struct (no #[derive] needed):
use rust_web_server::config_binding::{load_optional, load_required, load_with_default};
let port: u16 = load_with_default("APP_PORT", "8080")?;
let db: String = load_required("DATABASE_URL")?;
let debug: Option<bool> = load_optional("APP_DEBUG")?;Each returns Result<_, String> with the same error messages as load().
Complete example
use rust_web_server::config_binding::FromEnvStr;
use rust_web_server::Config;
#[derive(Debug)]
enum LogLevel { Error, Warn, Info, Debug }
impl FromEnvStr for LogLevel {
fn from_env_str(s: &str) -> Result<Self, String> {
match s.trim().to_ascii_lowercase().as_str() {
"error" => Ok(LogLevel::Error),
"warn" => Ok(LogLevel::Warn),
"info" => Ok(LogLevel::Info),
"debug" => Ok(LogLevel::Debug),
other => Err(format!("unknown log level: {other}")),
}
}
}
#[derive(Debug, rust_web_server::Config)]
#[config(prefix = "APP_")]
struct AppConfig {
/// APP_HOST — defaults to "0.0.0.0"
#[config(env = "HOST", default = "0.0.0.0")]
host: String,
/// APP_PORT — defaults to 8080
#[config(env = "PORT", default = "8080")]
port: u16,
/// APP_DB_HOST — required, no default
#[config(env = "DB_HOST")]
db_host: String,
/// APP_DB_PORT — defaults to 5432
#[config(env = "DB_PORT", default = "5432")]
db_port: u16,
/// APP_DB_POOL_SIZE — defaults to 10
#[config(env = "DB_POOL_SIZE", default = "10")]
db_pool_size: u32,
/// APP_LOG_LEVEL — optional; None if not set
#[config(env = "LOG_LEVEL")]
log_level: Option<LogLevel>,
}
fn main() {
let cfg = AppConfig::load().unwrap_or_else(|e| {
eprintln!("Config error: {e}");
std::process::exit(1);
});
println!("starting on {}:{}", cfg.host, cfg.port);
println!("db: {}:{} pool={}", cfg.db_host, cfg.db_port, cfg.db_pool_size);
if let Some(level) = cfg.log_level {
println!("log level: {:?}", level);
}
}Set environment variables before running:
export APP_DB_HOST=postgres.internal
export APP_LOG_LEVEL=info
cargo runload() returns Err on the first failure with a message like required env var `APP_DB_HOST` is not set or `APP_PORT`: invalid digit found in string. Check the message to identify which variable caused the problem.
#[derive(Config)] reads only from environment variables. To support .env files or rws.config.toml, set those variables in the environment before calling load(), or use a crate like dotenvy to load a .env file at the top of main().