Skip to main content

Crate dynamic_config

Crate dynamic_config 

Source
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:

MethodDescription
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) -> HookGuardThe 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:

VariableSets
APP_DB_HOSThost
APP_DB_MAX_SIZEmax_size
APP_DB_POOL__MAX_SIZEpool.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

FeatureDefaultEffect
jsonyes.json sources
tomlno.toml sources
yamlno.yaml / .yml sources
watchnostart_watch() and the file watcher
asyncnoload_async, init_async, changes — no runtime dependency
tokionoasync, plus tokio’s blocking pool instead of a thread per load
clapnobind_clap: named clap arguments as the flags layer
schemanoschema(): a JSON Schema for the resolved configuration
decryptnothe Decryptor/Encryptor traits and .age-suffix handling
agenodecrypt, plus the age module’s implementation of it
figmentnoforeign figment providers as sources, via Source::provider
dotenvnoenv_files = [".env"]: .env files as the environment layer
tracingnoWatcher diagnostics via tracing instead of stderr
fullnoall 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§

ageage
age-encrypted config files.
bytes
A byte count from "64MiB", "1GB", or a bare number.
duration
Duration from "30s", "1h30m", "500ms", or a bare number of seconds.
schemaschema
A JSON Schema for the files this program reads.
watchwatch
The filesystem watcher behind hot reload.

Structs§

Aliases
The old paths that still resolve, for one configuration type.
Change
One difference between two snapshots.
Changesasync
A handle that resolves each time the configuration is replaced.
ConfigCell
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.
HookGuard
Unregisters its hook when dropped. From on_reload_scoped.
Layer
A set of values addressed by dotted path.
LoadSpec
Everything the loader needs: which layers, which section, which env prefix.
Registry
One slot per type, allocated on first use and never freed.
ReloadGroup
Several configuration types that reload together or not at all.
Remote
The remote source for one configuration type, and its last document.
RemoteWatch
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.
UnknownKey
A key the configuration supplies that the struct does not name.
Watching
The loop’s half of a RemoteWatch.

Enums§

CacheMode
How much of a configuration to keep on disk.
ChangeKind
What happened to one key between two snapshots.
ErrorKind
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§

AsyncRemoteSourceasync
A remote store that is read asynchronously.
BlockingExecutorasync
Somewhere to run blocking work from an async context.
Decryptordecrypt
Turns the bytes of an encrypted config file into configuration text.
Encryptordecrypt
Turns configuration text into the bytes of an encrypted file.
Reloadable
A configuration type that a ReloadGroup can drive.
RemoteSource
A remote store that can be read without an async runtime.

Functions§

check
Builds a report for spec.
has_decryptordecrypt
Whether a decryptor is installed.
is_set
Whether anything supplies path.
load
Reads and deserializes a configuration section.
load_asyncasync
load, moved off the async executor.
off_threadasync
Runs blocking configuration work without blocking the caller’s executor.
save
Writes value to path as the key section of a format document.
save_encrypteddecrypt
As save, encrypting the document before it reaches the disk.
save_new
As save, but refuses if path already exists.
set_blocking_executorasync
Installs the blocking executor, once per process.
set_decryptordecrypt
Installs the decryptor, once per process.
snapshot
Resolves the section without deserializing it.
source_of
Where the value at path would 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.