Layer configuration sources¶
Register the sources your tool should read, in one builder chain, and let the merge produce a single typed value.
Register defaults, a file and an environment prefix¶
use rtb_config::Config;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct MyConfig {
host: String,
port: u16,
}
let cfg = Config::<MyConfig>::builder()
.embedded_default(include_str!("../defaults.yaml"))
.user_file("/etc/mytool/config.yaml")
.env_prefixed("MYTOOL_")
.build()?;
let current = cfg.get();
println!("{}:{}", current.host, current.port);
Nothing is read until build(). The builder methods only record what to read.
The order of the calls does not set precedence — embedded defaults are always overridden by files, and files by the environment, whichever order you registered them in. How layering and precedence work covers why.
Ship the defaults as a file in the repository¶
embedded_default takes a &'static str, so the practical way to supply real
defaults is include_str!:
Keeping them in defaults.yaml rather than a string literal means they are
syntax-highlighted, reviewable, and can be shipped as an example config for users to
copy. They are compiled in, so the file does not have to exist at runtime.
Support a system file and a per-user override¶
Register both. Later registrations of the same kind win, and a file that does not exist contributes nothing and is not an error — so you can register both unconditionally:
let mut builder = Config::<MyConfig>::builder()
.embedded_default(include_str!("../defaults.yaml"))
.user_file("/etc/mytool/config.yaml");
// wherever your tool decides the per-user path lives
if let Some(path) = user_config_path() {
builder = builder.user_file(path);
}
let cfg = builder.env_prefixed("MYTOOL_").build()?;
The per-user file overrides the system file key by key: setting only port in the
user file leaves host as the system file left it.
Override one section without repeating the rest¶
Nested maps merge, so a file only needs the keys it changes:
The result has host: localhost, port: 9090, timeout: 30.
Lists are the exception — a list in a later layer replaces the whole list rather than adding to it. There is no way to append one element to a default list.
Check which value actually won¶
There is no provenance API, so the way to check a merge is to print the merged
result. Derive Debug on your config struct and print the snapshot:
Printing the Config itself instead shows only the sources — file paths, environment
prefixes and the number of embedded layers — never the values:
That is useful for "is it even reading the file I think it is", and it is deliberate
that the values are absent: a Config in a debug log cannot leak a token held in a
config field. Your own struct's Debug has no such protection, so do not print the
snapshot if it holds secrets.
Handle a missing file, a bad file and a bad value¶
| Situation | What happens |
|---|---|
| A registered file does not exist | Skipped silently, no error |
| A registered path is a directory | ConfigError::Io, carrying the path |
| A file exists but cannot be read | ConfigError::Parse, wrapping the OS error |
| A file is not valid YAML | ConfigError::Parse |
| A value does not fit the field's type | ConfigError::Parse, naming the key and layer |
| A key your struct does not have | Ignored, silently |
The last row is the one that costs people time. A misspelled key is not an error; it
simply has no effect. If your tool owns its config file, add
#[serde(deny_unknown_fields)] to your struct — with the
caveat about environment prefixes.
Next¶
- Override settings with environment variables
- API reference — every builder method in detail