Skip to content

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.

[dependencies]
rtb-config = "0.6"

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

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.

Join the Discord