confroid
Your config on 'roids
confroid is the n+1-st config reader for your environment-based configs.
confroid derives an environment-variable reader from a plain Rust struct. It is type-driven: nesting, collections, and optionality all come from the field's type, so there is very little to annotate.
Installation
# for documentation generation:
Quick start
const DEFAULT_NAME: &str = "World";
With only HTTP__HOST=localhost set, this reads http.port as 8080, greeting as Hello, and name as World.
The entry points are:
confroid::from_env()— read from the process environment.confroid::from_pairs(pairs)— read from an explicit set of key/value pairs (handy if you have a custom config source).
Error reporting
Confroid reads every independent field before returning an error.
When multiple variables are missing or invalid,
ConfroidError::Multiple contains all of the failures in declaration order;
nested structs and collections are flattened into the same report.
Its Display output lists every problem, so a deployment can be fixed in one pass rather than one restart per variable.
Invalid-value errors retain the concrete FromStr error as their std::error::Error::source,
allowing reporters such as anyhow and color-eyre to show the parser's cause chain.
Use ConfroidError::errors() to iterate over either a single leaf error or all errors in an aggregate uniformly.
Naming
Every field maps to an environment variable named after the field in SCREAMING_SNAKE_CASE.
Nesting is detected from the field's type and nested fields prepend their own name as a prefix, joined with __,
so http.port becomes HTTP__PORT.
Add a prefix to every variable represented by a struct with a container-level prefix attribute.
This is useful for namespacing an application's environment:
Override an individual field's derived name with the name attribute.
Renaming a nested field renames the whole prefix, so its children follow along:
Defaults
Use default = <expr> to supply an explicit fallback, or the bare default attribute
(no value) to fall back to Default::default() for the field's type:
A default only applies when the variable is unset — a value that is present but fails to parse is always an error.
Optional fields
An Option<T> field is read from presence:
- if you don't set
NAME,nameisNone - if
NAMEis present but empty,nameis alsoNone - if you set
NAMEto a non-empty string,nameisSome(value)
Treating an empty optional leaf as absent matches .env examples and deployment tools such as Compose, Helm,
and CI templating, which commonly materialize unset variables as empty strings.
Required String fields still preserve an explicit empty value;
required parsed fields receive it and report a parse error when it is invalid.
Combining Option with default
An Option field with a default can never be None when the variable is unset, since the default takes over
(unless when the default itself resolves to None):
Optional structs
For an Option around a nested struct there is no single variable to test, so presence is decided by its children:
- if none of
TLS__*are set,tlsisNone - if any of
TLS__*are set, the struct is parsed normally — so a partially-set struct (e.g.TLS__CERTwithoutTLS__KEY) is anEnvVarNotFounderror rather than silently becomingNone
Collections
HashMap
Confroid supports HashMap fields out of the box.
Map keys are taken verbatim from the variable name (case preserved), while field names are still uppercased:
PEOPLE__alice__NAME=Alice
PEOPLE__alice__AGE=30
PEOPLE__bob__NAME=Bob
PEOPLE__bob__AGE=25
becomes
assert_eq!;
assert_eq!;
assert_eq!;
Vectors
By default a Vec is read from contiguous indexed variables, no configuration required:
NAMES__0=Alice
NAMES__1=Bob
becomes
assert_eq!;
assert_eq!;
assert_eq!;
Indices must be contiguous starting at 0.
A gap (e.g. NAMES__0 and NAMES__2 with no NAMES__1) is an error rather than a silently compacted list.
A Vec field is required: if no matching variables are set and the field has no default,
it is treated as missing and returns the same EnvVarNotFound error as any other required variable.
Use #[confroid(default)] to fall back to an empty vector instead:
Delimited values
For scalar elements you can opt into a single delimited variable with auto_vec (default delimiter ,):
#[derive(confroid::Config)]
struct Config {
+ #[confroid(auto_vec)]
names: Vec<String>,
}
so NAMES=Alice,Bob becomes names: vec!["Alice", "Bob"].
Choose a different delimiter with auto_vec_delimiter:
auto_vec only applies to scalar element types, since a delimited value cannot express a struct's fields.
Vectors of structs
The indexed form composes with nested structs — each index is its own prefix:
SERVERS__0__HOST=a.example.com
SERVERS__0__PORT=80
SERVERS__1__HOST=b.example.com
SERVERS__1__PORT=443
becomes
assert_eq!;
assert_eq!;
assert_eq!;
Supported types
Out of the box, confroid reads:
- integers (
u8–u128,usize,i8–i128,isize), floats (f32,f64),bool,char,String,Box<str>, andArc<str> NonZeroU16, useful for ports that must not be zerostd::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}andstd::path::PathBufOption<T>,Vec<T>, andHashMap<String, T>for any supportedT- any struct that derives
confroid::Config
Two optional features add external scalar types:
humantimeenables#[confroid(humantime)]onstd::time::DurationandOption<std::time::Duration>fields, accepting values such as30s,5m, or1h 15mvia thehumantimecrateurlparsesurl::Urlvalues using theurlcrate
[]
= { = "0.0.2", = ["humantime", "url"] }
Duration parsing is explicitly enabled per field:
Scalars are parsed via FromStr.
To read a custom scalar type — an enum, a newtype, anything with a FromStr —
implement FromStr for it and derive ConfigValue:
use FromStr;
/// How chatty the logs are.
so LOG_LEVEL=debug parses into LogLevel::Debug.
The FromStr error must implement std::error::Error + Send + Sync + 'static;
confroid preserves it as the source of an invalid-value error.
With the docs feature, a ConfigValue must also implement Display so defaults and examples can be rendered.
The existing confroid::from_str_scalar!(Type) macro remains available
when adding a derive at the type definition is not convenient.
A full runnable version is in examples/enum_config.rs.
(If you enable the docs feature, also implement Display for the type so its default and example can be rendered.)
Generating documentation
Enable the docs feature:
[]
= { = "0.0.2", = ["docs"] }
and call one of the generators (using the Config struct from the quick-start example):
// A `.env.example` style file.
println!;
// A Markdown table.
println!;
env_example::<Config>() produces:
# Config configuration
# The HTTP configuration.
# The port to listen on. (default: 8080)
# HTTP__PORT=8080
# The host to listen on. (example: localhost)
HTTP__HOST=
# The greeting to use. (default: Hello)
# GREETING=Hello
# The name of the user. (default: World, example: John)
# NAME=World
Optional fields and fields with defaults are emitted as commented-out assignments. Required fields remain active.
markdown_table::<Config>() produces:
| Variable | Description | Default | Example |
|---|---|---|---|
HTTP__PORT |
The port to listen on. | 8080 |
|
HTTP__HOST |
The host to listen on. | localhost |
|
GREETING |
The greeting to use. | Hello |
|
NAME |
The name of the user. | World |
John |
Default and example annotations are rendered only for scalar fields.
Vec fields appear as a single variable line without them.
HashMap fields use <key> to show where the map key belongs;
maps of structs also include the value struct's nested fields.
Errors
from_env returns Result<T, ConfroidError>.
Every error names both the environment variable and the dotted field path:
// A required variable is not set.
EnvVarNotFound
// A variable is set but its value cannot be parsed.
EnvVarInvalid
// A vector has a gap in its indices.
VecIndexGap
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.