Skip to main content

Crate dynamic_config

Crate dynamic_config 

Source
Expand description

Hot-reloadable, lock-free application configuration, built on figment.

Declare a struct, configure it with the builder, and read it from anywhere:

use dynamic_config::dynamic_config;
use serde::Deserialize;

#[dynamic_config]
#[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::builder("server")
    .file("config.json")
    .env("APP_")
    .init()
    .expect("defaults cover every field");

let config = ServerConfig::current();
println!("{}:{}", config.host, config.port);

The attribute declares — this type is a configuration — and generates its storage and accessors. The Builder configures: where the sources are is runtime data, and it lives in runtime code.

This page is the API reference. The guide — profiles, discovery, hot reload, remote stores, encryption, testing — is the book.

§What the attribute generates

The attribute declares; the builder configures. What gets generated is the type-bound surface:

MethodDescription
builder(key) -> Builder<Self>Where everything starts: state the sources, init().
current() -> Arc<Self>The current snapshot. Panics before an install.
try_current() -> Option<Arc<Self>>The current snapshot, or None.
replace(Self)Atomically swap in a new snapshot.
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.
changes()With async: a handle woken by every later reload.

Everything about sources lives on the Builder the generated builder(key) returns: .file(..), .discover(name, paths), .env(prefix), .strict_env(), .env_file(..), .profile_env(..), .cache(path, mode), .validate(f) — then .load(), .init(), .watch(debounce), .explain(path), .check(), and with async, .load_async() / .init_async(). A successful init also remembers the builder, so source_of, is_set, snapshot, check, explain, prepare and the remote reload on the type answer for the running configuration. The rest — remote stores, aliases, bindings, flags, bind_clap — is in the book’s 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, 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]
#[derive(Debug, Deserialize)]
struct DbConfig { pool_size: u32 }

let builder = DbConfig::builder("db").file("config.json");
builder.init_async().await?;
// Keep the handle: dropping it stops the watch.
let _watch = builder.watch(Duration::from_millis(250))?;

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.
Builder
Runtime-chosen sources for one configuration section.
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.
Contribution
One layer’s answer for one path.
EnvBindings
The environment variables bound to fields of one configuration type.
Error
A configuration error.
Explanation
Every configured layer’s answer for one path, lowest precedence first.
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§

changed_paths
The dotted paths that differ between two configuration values.
check
Builds a report for spec.
explain
Explains path: every configured layer’s answer, not just the winner’s.
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.