Configuration¶
RTB's configuration system is built on figment
with one strong opinion: configuration is typed. The caller
defines a serde::Deserialize struct, the framework populates it by
layering sources, and access is via struct field, not string key.
Layers, in precedence order¶
From lowest to highest priority — later layers override matching keys from earlier ones:
- Embedded defaults — a YAML string baked into the binary with
include_str!and added via.embedded_default(yaml). - User files — paths on disk added via
.user_file(path). Missing files are silently ignored (contribute no keys); present but malformed YAML is an error. Directory paths — where a regular file was expected — returnConfigError::Io. - Env vars — added via
.env_prefixed(prefix). Underscores nest:MYTOOL_HTTP_PORT=80populateshttp.porton the config struct, with theMYTOOL_prefix stripped.
Building a Config<C>¶
```rust,ignore use rtb_config::Config; use serde::Deserialize;
[derive(Default, Deserialize)]¶
struct MyConfig { host: String, port: u16, http: HttpSection, }
[derive(Default, Deserialize)]¶
struct HttpSection { max_body_bytes: u64, }
let cfg: Config
cfg.get() returns Arc<MyConfig> — a snapshot of the current
value. Clone the Arc, keep it across awaits, pass it to tasks; it
costs a refcount bump.
Atomic reload¶
cfg.reload() re-reads every source and swaps the stored value via
arc_swap::ArcSwap. Callers that held an Arc<MyConfig> snapshot
keep their old view until they ask for a new one — no tearing.
Hot reload on file change¶
Automatic reload has shipped. It is opt-in behind rtb-config's
hot-reload Cargo feature, and it is deliberately opt-in rather than
default: a watcher costs a background thread and OS handles, which is
the wrong trade for a short-lived CLI invocation and the right one for a
long-running service.
Config::watch_files() starts a debounced watcher over every path
registered with user_file, calls reload() when one changes, and wakes
every Config::subscribe() receiver on a successful reload. It returns a
WatchHandle; dropping the handle stops the watcher, so keep it alive
for as long as you want reloading.
Two things to know before relying on it. The debounce window is 250 ms,
chosen so that editors which save by rename-and-replace produce one
reload rather than several. And calling watch_files() with no
user_file paths registered is an error — ConfigError::Watch("no user
files registered") — not a silently idle watcher.
Why no get_string("foo.bar")?¶
Go Tool Base ships a Viper-backed Containable interface with
GetString("foo.bar") accessors. RTB deliberately does not. Rust
gives us compile-time checking for free:
cfg.get().http.portfails at compile time ifportisn't au16.- A renamed field surfaces as a build error in every call site, not
a runtime
Noneor a panic. - Refactors are safe because
cargo checkcatches every referent.
String-keyed access is strictly worse in Rust than a struct-field chain, so we don't provide it.
Generic parameter default¶
Config<C = ()> — when a downstream crate (notably rtb-app's
App) holds an Arc<Config> without a type parameter, C defaults
to (). Tool authors that need typed config use Config<MyConfig>
explicitly.
When the framework's App eventually becomes App<C> (post-0.1),
the ergonomics of the generic will carry through — App<MyConfig>
holds Arc<Config<MyConfig>>.
What can go wrong, and what you get back¶
ConfigError is #[non_exhaustive], so match with a fallback arm. The
variants you will actually meet:
ConfigError::Parse(String)— figment rejected the merged sources. Missing required field, type mismatch, malformed YAML. The string message names the offending field or file.ConfigError::Io { path, source }— theuser_file(path)existed but wasn't a regular file, a directory for example. A merely missing file is not an error.ConfigError::Watch(String)— the file watcher could not start: no user-file paths registered, an OS handle limit, or a failure from thenotifybackend. Only constructable under thehot-reloadfeature, but the variant is always present so yourmatchdoes not need cfg-gating.ConfigError::Write(String)— serialisation or filesystem failure writing the merged value back to disk. Same cfg story, under themutablefeature.
All derive miette::Diagnostic under the rtb::config::* namespace, so
they render with a code and a help line rather than as a bare string.
Related¶
- App context — how
Arc<Config>threads through the framework. - Error diagnostics — where
ConfigErroris rendered. - rtb-config — the module's own docs and full API.