rtb-config¶
Typed, layered configuration backed by figment. Config<C> is a
generic container over your serde::Deserialize struct — populated by
layering sources through figment and snapshot-swapped atomically via
arc-swap.
Part of the phpboyscout Rust toolkit; extracted from — and battle-tested by — rust-tool-base. It carries no dependency back on the framework, so it works in any Rust project.
Where to start¶
| You want to | Go to |
|---|---|
| Learn the crate by building something | Add layered configuration to a Rust CLI |
| Do one specific task | How-to guides |
| Look up a method, an error or a feature | Reference |
| Understand why it works this way | Explanation |
| Know what it won't do | What rtb-config does not do |
Typed, not dynamic¶
Many config layers expose dynamic accessors — GetString("foo.bar")
style. rtb-config deliberately rejects that pattern. Rust gives us
compile-time checking for free: declare a struct, derive Deserialize,
and let cargo check catch every mistyped field across every call site.
Hierarchical access uses nested structs; there is no Sub() or
get_string() API.
use rtb_config::Config;
use serde::Deserialize;
#[derive(Deserialize, Default)]
struct MyConfig { host: String, port: u16 }
let cfg = Config::<MyConfig>::builder()
.embedded_default("host: localhost\nport: 8080\n")
.env_prefixed("MYTOOL_")
.build()?;
let current = cfg.get();
assert_eq!(current.host, "localhost");
The reasoning, and what the choice costs, is in
why there is no get_string("foo.bar").
Layered precedence¶
Sources merge last-wins:
embedded default → user file → env vars (<PREFIX>_*)
So MYTOOL_PORT=9999 beats port: 9090 in the user file, which beats
port: 8080 in the embedded default. That order is fixed — it does not depend on the
order the sources were registered in. Underscore is the key separator for environment
variables, so MYTOOL_HTTP_PORT=8080 populates a nested http.port field; for the
same reason a flat field named http_port cannot be set from the environment at
all.
Missing files are not an error — an absent file contributes no keys, while a path
that exists but is not a regular file surfaces as ConfigError::Io. Keys your struct
does not have are ignored silently.
More: how layering and precedence work and layer configuration sources.
Atomic reload and subscriptions¶
Config::reload() re-reads every source and atomically swaps the stored
value via arc_swap::ArcSwap. Readers holding a pre-reload Arc<C>
snapshot keep their consistent view until they ask for a new one —
snapshots never tear, and the read path takes no locks. A reload that fails to parse
leaves the stored value untouched and wakes nobody.
Config::subscribe() returns a tokio::sync::watch::Receiver that
wakes on every successful reload.
More: why configuration is a snapshot and reload configuration at runtime.
Features¶
| Feature | Adds | Cost |
|---|---|---|
hot-reload |
Config::watch_files() — a debounced background watcher (notify) that calls reload() on change and hands back a WatchHandle to stop it. |
notify, notify-debouncer-full |
mutable |
Config::schema() (JSON Schema for C via schemars) and Config::write() (write the current value to YAML / TOML / JSON, chosen by extension). |
schemars, serde_json, serde_yaml, toml |
Both are default-off so tools that don't need them don't pay the dependency weight. See Cargo features.
Before relying on the watcher, read which file changes it actually sees — a save that replaces the file rather than writing into it stops it.
Full API reference: reference/api.md, with generated rustdoc on docs.rs/rtb-config.
Roadmap¶
A v2 Store architecture is approved and will land in this repository. It adds the two things this crate cannot do today: value provenance — asking which layer supplied a given field — and structure-preserving writes, so editing a setting keeps the comments and ordering of the file a person wrote. Neither exists in any released version; the figment-based v1 documented here is the supported surface until they do.
Further reading¶
The blog carries a curated route through this subject: Rust, and what survived the port collects everything written about it, ordered so you can start at the beginning rather than newest-first.
Ask phpbotscout

He answers questions about the projects over on the Discord, citing the docs where they already cover it, and offering to raise an issue where they don't. Bring a bug, an idea, or a questionable engineering decision.