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) and 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, bearing in mind the
caveat about environment prefixes.
Next¶
- Override settings with environment variables
- API reference, every builder method in detail