Expand description
Hot-reloadable, lock-free application configuration, built on figment.
Annotate a struct, call init() once, and read it from anywhere:
use dynamic_config::dynamic_config;
use serde::Deserialize;
#[dynamic_config(files = ["config.json"], key = "server", env = "APP_")]
#[derive(Debug, Deserialize)]
struct ServerConfig {
#[serde(default = "default_host")]
host: String,
#[serde(default = "default_port")]
port: u16,
}
// `config.json` does not exist here, so every field falls back to its
// default — a missing file is skipped, not an error.
ServerConfig::init().expect("defaults cover every field");
let config = ServerConfig::current();
println!("{}:{}", config.host, config.port);This page is the API reference. The guide — profiles, discovery, hot reload, remote stores, encryption, testing — is the book.
§What the attribute generates
The everyday core:
| Method | Description |
|---|---|
load() -> Result<Self, Error> | Read the sources and deserialize. Does not touch the snapshot. |
init() -> Result<(), Error> | load() plus install as the initial snapshot. Call once at startup. |
replace(Self) | Atomically swap in a new snapshot. |
current() -> Arc<Self> | The current snapshot. Panics before init(). |
try_current() -> Option<Arc<Self>> | The current snapshot, or None before init(). |
start_watch() -> io::Result<WatchHandle> | With watch: reload on file changes until the handle is dropped. A second watch while one runs is AlreadyExists. |
on_reload(f) | Run a callback on every later reload, for the life of the process. |
on_reload_scoped(f) -> HookGuard | The same, until the guard is dropped. |
set_default(path, value) | A fallback used only when nothing else supplies the key. |
set_override(path, value) | A value that wins over every file and variable. |
clear_defaults() / clear_overrides() | Drop them again. |
load_async() / init_async() | With async: the same, off the async executor. |
changes() | With async: a handle woken by every later reload. |
The rest of the surface — introspection (snapshot, source_of, is_set,
check), persistence (save, save_new, save_encrypted), remote stores
(set_remote, refresh_remote, apply_remote), aliases, environment
bindings, flags, bind_clap, schema — is in the book’s attribute
reference.
§Precedence
set_default < discovered < config.toml < secrets.json < remote < APP_DB_* < bind_env < set_flag < set_override
(runtime) (search path) (first) (last file) (etcd…) (environment) (by name) (CLI) (runtime)Files merge left to right and tables merge key by key, so a small
secrets.json can override two fields of a large config.toml without
restating it.
The two runtime layers bracket the rest. Defaults cover a fallback the
program can compute but a file need not state; overrides are what make a
test or a --set key=value flag authoritative without touching disk. Both
take effect on the next load().
§Reading configuration is lock-free
current() hands out an Arc cloned from an ArcSwap, so a reload never
blocks a request handler. A reader that already holds an Arc keeps its own
generation — call current() once per request and reuse it, or a reload
landing mid-request will show you two different configurations.
§Reloading cannot take the process down
A reload re-runs load(). If the new configuration is invalid, or a file is
caught half-written, the error is reported and the previous snapshot stays
in place. A bad edit degrades to “no change”.
§Environment variables
env = "APP_" with key = "db" reads APP_DB_*. A single underscore is
part of a field name; a doubled one introduces nesting:
| Variable | Sets |
|---|---|
APP_DB_HOST | host |
APP_DB_MAX_SIZE | max_size |
APP_DB_POOL__MAX_SIZE | pool.max_size |
Values are interpreted by figment, which reads them loosely: 8080 reaches
a u16, true reaches a bool, and [a, b, c] reaches a Vec<String>.
A value that cannot become the field’s type is an error naming the field.
§Units
timeout = 30 is ambiguous and max_body = 67108864 is unreadable, so both
are usually written with a unit — which no stock Deserialize accepts:
use std::time::Duration;
use serde::Deserialize;
#[derive(Deserialize)]
struct Limits {
#[serde(with = "dynamic_config::duration")]
timeout: Duration, // "30s", "1h30m", "500ms", or a number of seconds
#[serde(with = "dynamic_config::bytes")]
max_body: u64, // "64MiB", "1GB", or a number of bytes
}§Async
With the async feature and the async argument, configuration loads
without blocking the executor, and tasks can await reloads instead of
polling. No runtime is named anywhere: changes() is a Future, so any
executor drives it.
#[dynamic_config(files = ["config.json"], key = "db", watch, async)]
#[derive(Debug, Deserialize)]
struct DbConfig { pool_size: u32 }
DbConfig::init_async().await?;
// Keep the handle: dropping it stops the watch.
let _watch = DbConfig::start_watch()?;
let mut reloads = DbConfig::changes();
spawn(async move {
loop {
let config = reloads.changed().await;
pool.resize(config.pool_size);
}
});The watcher itself stays on a plain thread. notify’s channel is
synchronous, and keeping it off the runtime means file watching works
whether or not a runtime is running.
§Features
| Feature | Default | Effect |
|---|---|---|
json | yes | .json sources |
toml | no | .toml sources |
yaml | no | .yaml / .yml sources |
watch | no | start_watch() and the file watcher |
async | no | load_async, init_async, changes — no runtime dependency |
tokio | no | async, plus tokio’s blocking pool instead of a thread per load |
clap | no | bind_clap: named clap arguments as the flags layer |
schema | no | schema(): a JSON Schema for the resolved configuration |
decrypt | no | the Decryptor/Encryptor traits and .age-suffix handling |
age | no | decrypt, plus the age module’s implementation of it |
figment | no | foreign figment providers as sources, via Source::provider |
dotenv | no | env_files = [".env"]: .env files as the environment layer |
tracing | no | Watcher diagnostics via tracing instead of stderr |
full | no | all of the above |
Using a format, watch or async whose feature is disabled is a compile
error naming the feature to add.
§Without the macro
load, ConfigCell and LoadSpec are the whole engine and are
usable on their own:
use dynamic_config::{load, Format, LoadSpec, Source};
use serde::Deserialize;
#[derive(Deserialize)]
struct Db { host: String }
let sources = [Source::inline(r#"{"db": {"host": "localhost"}}"#, Format::Json)];
let db: Db = load(&LoadSpec::new("db", &sources))
.expect("the inline document is well formed");
assert_eq!(db.host, "localhost");Re-exports§
pub use figment;figment
Modules§
- age
age age-encrypted config files.- bytes
- A byte count from
"64MiB","1GB", or a bare number. - duration
Durationfrom"30s","1h30m","500ms", or a bare number of seconds.- schema
schema - A JSON Schema for the files this program reads.
- watch
watch - The filesystem watcher behind hot reload.
Structs§
- Aliases
- The old paths that still resolve, for one configuration type.
- Change
- One difference between two snapshots.
- Changes
async - A handle that resolves each time the configuration is replaced.
- Config
Cell - Holds the current configuration snapshot for one type.
- EnvBindings
- The environment variables bound to fields of one configuration type.
- Error
- A configuration error.
- Fetched
- A document a remote store handed back.
- Hook
Guard - Unregisters its hook when dropped. From
on_reload_scoped. - Layer
- A set of values addressed by dotted path.
- Load
Spec - Everything the loader needs: which layers, which section, which env prefix.
- Registry
- One slot per type, allocated on first use and never freed.
- Reload
Group - Several configuration types that reload together or not at all.
- Remote
- The remote source for one configuration type, and its last document.
- Remote
Watch - A running blocking watch, from the caller’s side.
- Report
- What a configuration resolves to, and whether it would load.
- Resolved
- Where one key’s value comes from.
- Search
- Where to look for configuration, and under what name.
- Snapshot
- A resolved configuration section, before it becomes a struct.
- Source
- One layer of configuration.
- Unknown
Key - A key the configuration supplies that the struct does not name.
- Watching
- The loop’s half of a
RemoteWatch.
Enums§
- Cache
Mode - How much of a configuration to keep on disk.
- Change
Kind - What happened to one key between two snapshots.
- Error
Kind - Broad category of a configuration failure.
- Format
- A configuration file format.
- Origin
- Where a value came from.
- Recovery
- What a cache file turned out to hold.
Constants§
- DEFAULT_
NEST - Separator that introduces one level of nesting in a variable name.
Traits§
- Async
Remote Source async - A remote store that is read asynchronously.
- Blocking
Executor async - Somewhere to run blocking work from an async context.
- Decryptor
decrypt - Turns the bytes of an encrypted config file into configuration text.
- Encryptor
decrypt - Turns configuration text into the bytes of an encrypted file.
- Reloadable
- A configuration type that a
ReloadGroupcan drive. - Remote
Source - A remote store that can be read without an async runtime.
Functions§
- check
- Builds a report for
spec. - has_
decryptor decrypt - Whether a decryptor is installed.
- is_set
- Whether anything supplies
path. - load
- Reads and deserializes a configuration section.
- load_
async async load, moved off the async executor.- off_
thread async - Runs blocking configuration work without blocking the caller’s executor.
- save
- Writes
valuetopathas thekeysection of aformatdocument. - save_
encrypted decrypt - As
save, encrypting the document before it reaches the disk. - save_
new - As
save, but refuses ifpathalready exists. - set_
blocking_ executor async - Installs the blocking executor, once per process.
- set_
decryptor decrypt - Installs the decryptor, once per process.
- snapshot
- Resolves the section without deserializing it.
- source_
of - Where the value at
pathwould come from, if anything supplies it.
Type Aliases§
- Commit
- The second half of a reload: the part that cannot fail.
Attribute Macros§
- dynamic_
config - Turns a struct into a hot-reloadable configuration snapshot.