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 (auto-reload on file change) is scheduled for v0.2; v0.1
is explicit. Tools that want file-watching today wire notify +
cfg.reload() themselves.
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>>.
Error shape¶
Layered sources produce one of two error variants:
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 (e.g. a directory). Regular missing files are not an error.
Both derive miette::Diagnostic under the rtb::config::*
namespace.
Related¶
- App context — how
Arc<Config>threads through the framework. - Error diagnostics — where
ConfigErroris rendered. docs/development/specs/2026-04-22-rtb-config-v0.1.md— the authoritative contract.