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 swapped atomically via arc-swap.
Part of the phpboyscout Rust toolkit, extracted from
rust-tool-base, whose rtb crate
still depends on it. 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¶
The Go toolkit's config module reads values through dynamic accessors
(GetString("foo.bar")). rtb-config deliberately does not. Declare a
struct, derive Deserialize, and cargo check catches every mistyped field
at 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, so
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-saphyr's serialize feature, toml |
Both are off by default, so a tool that needs neither does not pay for the extra dependencies. See Cargo features.
Which file changes the watcher sees covers in-place writes, rename-replace saves and Kubernetes-style symlink swaps, and says which platforms have been checked.
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.