API reference¶
Everything rtb-config exposes, with the behaviour each item actually has. Verified
against the crate source at v0.6.3.
Does rtb-config have its own configuration keys?¶
No. rtb-config has no keys, no flags and no environment variables of its own. The
keys are the field names of your serde::Deserialize struct, and the environment
variables are whatever prefix you register with
ConfigBuilder::env_prefixed.
The knobs the crate itself owns are its two Cargo features — see Cargo features.
What the crate exports¶
| Item | Kind | Feature | Purpose |
|---|---|---|---|
Config<C = ()> |
struct | — | Holds the parsed value and the sources it came from |
ConfigBuilder<C> |
struct | — | Registers sources, then parses them |
ConfigError |
enum | — | Every failure the crate can return |
WatchHandle |
struct | hot-reload |
Owns a running file watcher; drop it to stop |
The modules rtb_config::config, rtb_config::error and (under hot-reload)
rtb_config::watch are public, but everything worth naming is re-exported at the
crate root:
use rtb_config::{Config, ConfigBuilder, ConfigError};
#[cfg(feature = "hot-reload")]
use rtb_config::WatchHandle;
Which trait bounds does my config struct need?¶
Config<C> requires C: DeserializeOwned + Send + Sync + 'static everywhere. Two
methods ask for more, and they only exist when the bound is met:
| Method | Extra bounds | Feature |
|---|---|---|
Config::write |
serde::Serialize, schemars::JsonSchema |
mutable |
Config::schema |
serde::Serialize, schemars::JsonSchema |
mutable |
Config::default |
Default |
— |
If Config::<MyConfig>::write does not exist for your type, the missing piece is
almost always #[derive(Serialize, JsonSchema)] rather than the feature flag.
C defaults to (), so Config with no angle brackets means Config<()>. That
exists so downstream code holding an Arc<Config> does not have to carry a type
parameter it never uses.
Config::builder — start registering sources¶
Equivalent to ConfigBuilder::<C>::new(). Nothing is read from disk or the
environment until build
is called.
Config::with_value — wrap a value with no sources behind it¶
Stores value directly. The resulting Config has an empty source list, which
has one consequence worth knowing before you use it in a test:
calling reload() on it re-parses nothing, so the stored value is replaced by
whatever an empty source set deserialises into. For a struct whose fields all carry
#[serde(default)] that is C::default() — the value you passed in is gone. For a
struct with a required field it is ConfigError::Parse.
Use builder().embedded_default(...) instead when a test needs a value that
survives a reload.
Config::get — take a snapshot of the current value¶
Returns a snapshot. Cheap: an atomic load and an Arc clone, no parsing, no locks.
A snapshot is immutable and complete. Hold one across a reload() and you keep
seeing the pre-reload value for as long as you hold it; the next get() returns the
post-reload value. There is no state in which half the fields are old and half are
new.
Config::reload — re-read every source¶
Re-reads every registered source — embedded strings, files, and environment
variables — merges them again, and swaps the result in atomically. Environment
variables are read at reload time, not cached from build(), so a variable set
after startup takes effect on the next reload.
On success, every subscribe
receiver is woken.
On failure the stored value is left untouched and no subscriber is woken. A config file that has been saved mid-edit and does not parse cannot take a running process down to defaults.
Config::subscribe — be woken when the value changes¶
The receiver holds the current value immediately — rx.borrow() is valid before any
reload has happened — and rx.changed().await resolves after each successful
reload.
Subscribing late is safe. A receiver created after several reloads sees the newest value, not the value the process started with, and dropping every receiver does not break subsequent reloads.
Config::watch_files — reload automatically when a file changes¶
Starts one background thread (named rtb-config-watcher) watching every path
registered with user_file, non-recursively. File-system events are debounced for
250 ms — a fixed constant, not configurable — and each batch calls reload().
A failing reload is swallowed so a bad save does not kill the watcher.
Returns ConfigError::Watch when:
- no
user_filepath was registered, or - a registered path does not exist yet — the underlying watcher refuses to watch a path that is not there, so create the file before calling this, or
- the OS refuses another watch (handle limits, unsupported filesystem).
The handle is #[must_use]: dropping it immediately stops the watcher. Keep it alive
for as long as you want reloads.
Read the limitations before relying on this in production — a save that replaces the file rather than writing into it stops the watcher permanently.
Config::schema — emit a JSON Schema for the config struct¶
An associated function — there is no &self, so it can be called before any config
is loaded. Generates the schema from C's JsonSchema derive at call time; there
is no caching, and it is cheap enough for CLI-startup use.
The returned object carries the usual top-level keys — $schema, title, type
and properties. If serialisation somehow fails, the function returns
serde_json::Value::Null rather than panicking.
Config::write — write the current value to a file¶
Serialises the merged, current value — the same thing get() returns — and
writes it to path. Missing parent directories are created.
Format is chosen by extension:
| Extension | Format written |
|---|---|
.yml, .yaml |
YAML |
.toml |
TOML |
.json |
JSON |
| anything else, or none | YAML |
Two things to know before you wire this to a config set command:
- What is written is the merged result, not just the file layer. Any environment override active at the time is baked into the file as a literal value, and so are the embedded defaults. Comments and key ordering in an existing file are not preserved — the file is replaced.
- Only the YAML output can be read back by
ConfigBuilder::user_file, which parses every file as YAML. JSON survives the round trip because YAML is a superset of it; TOML does not. See limitations.
The write is not atomic. If a torn file would matter, write to a temporary path and rename it yourself.
Errors are ConfigError::Write for both serialisation and filesystem failures,
including failing to create the parent directory.
Config trait implementations¶
| Impl | Behaviour |
|---|---|
Clone |
Cheap. Clones share the same stored value, sources and subscriber channel — a reload through one handle is visible through all of them. |
Default |
Requires C: Default. Stores C::default() with no sources, so it behaves like with_value for reload purposes. |
Debug |
Prints the source inventory only — file paths, env prefixes and the number of embedded layers. The stored value is deliberately never rendered, so a Debug log of a Config cannot leak a secret held in a config field. |
Example of the Debug output:
ConfigBuilder::embedded_default — bake defaults into the binary¶
Takes a &'static str of YAML, which in practice means a literal or an
include_str!. Call it more than once and the layers merge in call order, the later
call winning on any key both define.
ConfigBuilder::user_file — read a YAML file from disk¶
Registers a file path. Paths are read in registration order and later files win on shared keys.
| Situation | Result |
|---|---|
| File does not exist | Not an error — contributes no keys |
| File exists and parses | Merged over the layers before it |
| File exists, is not valid YAML | ConfigError::Parse |
| Path exists but is a directory | ConfigError::Io, carrying the path |
| File exists but is unreadable (permissions) | ConfigError::Parse — not Io |
The file is always parsed as YAML regardless of its extension.
ConfigBuilder::env_prefixed — read prefixed environment variables¶
Registers an environment-variable layer. Matching is case-insensitive on the variable name, the prefix is stripped, the remainder is lower-cased, and each remaining underscore becomes a level of nesting:
| Variable | Prefix | Populates |
|---|---|---|
MYTOOL_PORT=8080 |
MYTOOL_ |
port |
MYTOOL_HTTP_PORT=8080 |
MYTOOL_ |
http.port |
mytool_port=8080 |
MYTOOL_ |
port |
Values are parsed, not taken as raw strings: true becomes a boolean, 8080 a
number, and [alpha,beta] a two-element list. A value that cannot be parsed into the
field's type fails with ConfigError::Parse, naming both the key and the prefix.
Two things bite here, both silent:
- Include the trailing underscore in the prefix.
env_prefixed("MYTOOL")withMYTOOL_PORTset leaves a stray leading underscore on the key and the variable is ignored — no error, no warning. - A field name containing an underscore cannot be set from the environment at all, because that underscore is read as nesting. See limitations.
Register several prefixes if you want to accept more than one; later registrations win.
ConfigBuilder::build — parse every source and construct the Config¶
Reads and merges every registered source, deserialises the result into C, and
returns the Config. This is the point at which files are opened and environment
variables are read — the builder methods only record intent.
A builder with no sources at all is valid. It deserialises an empty document, which
succeeds for a struct whose fields all have defaults and fails with
ConfigError::Parse for one that has a required field.
In what order are sources merged?¶
Embedded defaults, then files, then environment variables — always, regardless
of the order you called the builder methods in. Registering .env_prefixed() before
.embedded_default() does not make the embedded layer win; the environment still
overrides it.
Within a single kind, registration order applies and the last one wins.
Merging is per key and goes into nested maps: a file that sets only http.port
leaves http.host from the layer beneath it intact. Sequences are not merged —
a list in a later layer replaces the whole list, it does not append to it.
What happens to a key my struct does not have?¶
It is ignored, silently and successfully. port: 8080 next to a misspelled
tiemout: 30 deserialises fine and the typo does nothing. rtb-config has no strict
mode of its own that turns unknown keys into errors.
#[serde(deny_unknown_fields)] on your struct does turn them into a
ConfigError::Parse, and it works through every layer — but it is blunter than it
looks once an environment layer is registered. Every variable sharing the prefix is
a candidate key, so an unrelated MYTOOL_SOMETHING_ELSE in the environment fails the
build with unknown field: found \something``. Reserve the prefix for this tool
before turning strict mode on.
Next¶
- Error reference — every variant, its diagnostic code, and what causes it
- Cargo features — what each feature adds and costs
- Limitations — what the crate deliberately does not do