Skip to main content

dynamic_config/
lib.rs

1//! Hot-reloadable, lock-free application configuration, built on
2//! [figment](https://docs.rs/figment).
3//!
4//! Declare a struct, configure it with the builder, and read it from
5//! anywhere:
6//!
7//! ```
8//! # #[cfg(feature = "json")] {
9//! use dynamic_config::dynamic_config;
10//! use serde::Deserialize;
11//!
12//! #[dynamic_config]
13//! #[derive(Debug, Deserialize)]
14//! struct ServerConfig {
15//!     #[serde(default = "default_host")]
16//!     host: String,
17//!     #[serde(default = "default_port")]
18//!     port: u16,
19//! }
20//!
21//! # fn default_host() -> String { "0.0.0.0".into() }
22//! # fn default_port() -> u16 { 8080 }
23//! // `config.json` does not exist here, so every field falls back to its
24//! // default — a missing file is skipped, not an error.
25//! ServerConfig::builder("server")
26//!     .file("config.json")
27//!     .env("APP_")
28//!     .init()
29//!     .expect("defaults cover every field");
30//!
31//! let config = ServerConfig::current();
32//! println!("{}:{}", config.host, config.port);
33//! # }
34//! ```
35//!
36//! The attribute declares — *this type is a configuration* — and generates
37//! its storage and accessors. The [`Builder`] configures: where the
38//! sources are is runtime data, and it lives in runtime code.
39//!
40//! This page is the API reference. The guide — profiles, discovery, hot
41//! reload, remote stores, encryption, testing — is
42//! [**the book**](https://ctolon.github.io/dynamic-config/).
43//!
44//! # What the attribute generates
45//!
46//! The attribute declares; the builder configures. What gets generated is
47//! the type-bound surface:
48//!
49//! | Method | Description |
50//! |---|---|
51//! | `builder(key) -> Builder<Self>` | Where everything starts: state the sources, `init()`. |
52//! | `current() -> Arc<Self>` | The current snapshot. Panics before an install. |
53//! | `try_current() -> Option<Arc<Self>>` | The current snapshot, or `None`. |
54//! | `replace(Self)` | Atomically swap in a new snapshot. |
55//! | `on_reload(f)` | Run a callback on every later reload, for the life of the process. |
56//! | `on_reload_scoped(f) -> HookGuard` | The same, until the guard is dropped. |
57//! | `set_default(path, value)` | A fallback used only when nothing else supplies the key. |
58//! | `set_override(path, value)` | A value that wins over every file and variable. |
59//! | `clear_defaults()` / `clear_overrides()` | Drop them again. |
60//! | `changes()` | With `async`: a handle woken by every later reload. |
61//!
62//! Everything about *sources* lives on the [`Builder`] the generated
63//! `builder(key)` returns: `.file(..)`, `.discover(name, paths)`,
64//! `.env(prefix)`, `.strict_env()`, `.env_file(..)`, `.profile_env(..)`,
65//! `.cache(path, mode)`, `.validate(f)` — then `.load()`, `.init()`,
66//! `.watch(debounce)`, `.explain(path)`, `.check()`, and with `async`,
67//! `.load_async()` / `.init_async()`. A successful `init` also *remembers*
68//! the builder, so `source_of`, `is_set`, `snapshot`, `check`, `explain`,
69//! `prepare` and the remote reload on the type answer for the running
70//! configuration. The rest — remote stores, aliases, bindings, flags,
71//! `bind_clap` — is in [the book's
72//! reference](https://ctolon.github.io/dynamic-config/attribute-reference.html).
73//!
74//! # Precedence
75//!
76//! ```text
77//! set_default < discovered < config.toml < secrets.json < remote < APP_DB_* < bind_env < set_flag < set_override
78//!  (runtime)   (search path)   (first)      (last file)   (etcd…) (environment) (by name)  (CLI)     (runtime)
79//! ```
80//!
81//! Files merge left to right and tables merge key by key, so a small
82//! `secrets.json` can override two fields of a large `config.toml` without
83//! restating it.
84//!
85//! The two runtime layers bracket the rest. Defaults cover a fallback the
86//! program can compute but a file need not state; overrides are what make a
87//! test or a `--set key=value` flag authoritative without touching disk. Both
88//! take effect on the next `load()`.
89//!
90//! # Reading configuration is lock-free
91//!
92//! `current()` hands out an `Arc` cloned from an `ArcSwap`, so a reload never
93//! blocks a request handler. A reader that already holds an `Arc` keeps its own
94//! generation — call `current()` once per request and reuse it, or a reload
95//! landing mid-request will show you two different configurations.
96//!
97//! # Reloading cannot take the process down
98//!
99//! A reload re-runs `load()`. If the new configuration is invalid, or a file is
100//! caught half-written, the error is reported and the previous snapshot stays
101//! in place. A bad edit degrades to "no change".
102//!
103//! # Environment variables
104//!
105//! `env = "APP_"` with `key = "db"` reads `APP_DB_*`. A single underscore is
106//! part of a field name; a doubled one introduces nesting:
107//!
108//! | Variable | Sets |
109//! |---|---|
110//! | `APP_DB_HOST` | `host` |
111//! | `APP_DB_MAX_SIZE` | `max_size` |
112//! | `APP_DB_POOL__MAX_SIZE` | `pool.max_size` |
113//!
114//! Values are interpreted by figment, which reads them loosely: `8080` reaches
115//! a `u16`, `true` reaches a `bool`, and `[a, b, c]` reaches a `Vec<String>`.
116//! A value that cannot become the field's type is an error naming the field.
117//!
118//! # Units
119//!
120//! `timeout = 30` is ambiguous and `max_body = 67108864` is unreadable, so both
121//! are usually written with a unit — which no stock `Deserialize` accepts:
122//!
123//! ```
124//! use std::time::Duration;
125//! use serde::Deserialize;
126//!
127//! #[derive(Deserialize)]
128//! struct Limits {
129//!     #[serde(with = "dynamic_config::duration")]
130//!     timeout: Duration,          // "30s", "1h30m", "500ms", or a number of seconds
131//!     #[serde(with = "dynamic_config::bytes")]
132//!     max_body: u64,              // "64MiB", "1GB", or a number of bytes
133//! }
134//! ```
135//!
136//! # Async
137//!
138//! With the `async` feature, configuration loads without blocking the
139//! executor, and tasks can await reloads instead of polling. No runtime is
140//! named anywhere: `changes()` is a `Future`, so any executor drives it.
141//!
142//! ```ignore
143//! #[dynamic_config]
144//! #[derive(Debug, Deserialize)]
145//! struct DbConfig { pool_size: u32 }
146//!
147//! let builder = DbConfig::builder("db").file("config.json");
148//! builder.init_async().await?;
149//! // Keep the handle: dropping it stops the watch.
150//! let _watch = builder.watch(Duration::from_millis(250))?;
151//!
152//! let mut reloads = DbConfig::changes();
153//!
154//! spawn(async move {
155//!     loop {
156//!         let config = reloads.changed().await;
157//!         pool.resize(config.pool_size);
158//!     }
159//! });
160//! ```
161//!
162//! The watcher itself stays on a plain thread. `notify`'s channel is
163//! synchronous, and keeping it off the runtime means file watching works
164//! whether or not a runtime is running.
165//!
166//! # Features
167//!
168//! | Feature | Default | Effect |
169//! |---|---|---|
170//! | `json` | yes | `.json` sources |
171//! | `toml` | no | `.toml` sources |
172//! | `yaml` | no | `.yaml` / `.yml` sources |
173//! | `watch` | no | `start_watch()` and the file watcher |
174//! | `async` | no | `load_async`, `init_async`, `changes` — no runtime dependency |
175//! | `tokio` | no | `async`, plus tokio's blocking pool instead of a thread per load |
176//! | `clap` | no | `bind_clap`: named `clap` arguments as the flags layer |
177//! | `schema` | no | `schema()`: a JSON Schema for the resolved configuration |
178//! | `decrypt` | no | the [`Decryptor`]/[`Encryptor`] traits and `.age`-suffix handling |
179//! | `age` | no | `decrypt`, plus the `age` module's implementation of it |
180//! | `figment` | no | foreign figment providers as sources, via `Source::provider` |
181//! | `dotenv` | no | `env_files = [".env"]`: `.env` files as the environment layer |
182//! | `tracing` | no | Watcher diagnostics via `tracing` instead of stderr |
183//! | `full` | no | all of the above |
184//!
185//! Using a format, `watch` or `async` whose feature is disabled is a compile
186//! error naming the feature to add.
187//!
188//! # Without the macro
189//!
190//! [`load`], [`ConfigCell`] and [`LoadSpec`] are the whole engine and are
191//! usable on their own:
192//!
193//! ```
194//! # #[cfg(feature = "json")] {
195//! use dynamic_config::{load, Format, LoadSpec, Source};
196//! use serde::Deserialize;
197//!
198//! #[derive(Deserialize)]
199//! struct Db { host: String }
200//!
201//! let sources = [Source::inline(r#"{"db": {"host": "localhost"}}"#, Format::Json)];
202//! let db: Db = load(&LoadSpec::new("db", &sources))
203//!     .expect("the inline document is well formed");
204//!
205//! assert_eq!(db.host, "localhost");
206//! # }
207//! ```
208
209#![forbid(unsafe_code)]
210#![deny(missing_docs)]
211// A getter whose result is discarded is a mistake in a library like this one —
212// `is_set`, `contains`, `document`, `describe` all answer a question and change
213// nothing. Warned about rather than left to review, and CI denies warnings.
214#![warn(clippy::must_use_candidate)]
215#![cfg_attr(docsrs, feature(doc_cfg))]
216
217#[cfg(feature = "age")]
218#[cfg_attr(docsrs, doc(cfg(feature = "age")))]
219pub mod age;
220mod aliases;
221#[cfg(feature = "async")]
222mod asynchronous;
223mod bindings;
224mod builder;
225mod cache;
226mod cell;
227mod check;
228#[cfg(feature = "decrypt")]
229mod decrypt;
230mod discovery;
231#[cfg(feature = "dotenv")]
232mod dotenv;
233mod dynamic;
234mod error;
235mod explain;
236mod group;
237mod layer;
238mod loader;
239mod log;
240mod redirects;
241mod registry;
242mod remote;
243#[cfg(feature = "schema")]
244#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
245pub mod schema;
246mod snapshot;
247mod source;
248pub(crate) mod sync;
249mod units;
250mod value;
251mod write;
252
253#[cfg(feature = "watch")]
254#[cfg_attr(docsrs, doc(cfg(feature = "watch")))]
255pub mod watch;
256
257/// Not public API: the loom suite drives the wake protocol directly.
258#[cfg(all(feature = "async", loom))]
259#[doc(hidden)]
260pub use asynchronous::Notify as LoomNotify;
261#[cfg(feature = "async")]
262#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
263pub use asynchronous::{set_blocking_executor, BlockingExecutor, Changes};
264/// figment itself, re-exported.
265///
266/// So that writing a [`Source::provider`] needs no direct dependency, and no
267/// second version of figment in the graph. This is the one place figment
268/// appears in this crate's API, which is why it is behind a feature.
269#[cfg(feature = "figment")]
270#[cfg_attr(docsrs, doc(cfg(feature = "figment")))]
271pub use figment;
272
273pub use aliases::Aliases;
274pub use bindings::EnvBindings;
275pub use builder::Builder;
276#[doc(hidden)]
277pub use builder::Configured;
278pub use cache::{CacheMode, Recovery};
279pub use cell::{ConfigCell, HookGuard};
280pub use check::{check, Report, Resolved, UnknownKey};
281#[cfg(feature = "decrypt")]
282#[cfg_attr(docsrs, doc(cfg(feature = "decrypt")))]
283pub use decrypt::{has_decryptor, set_decryptor, Decryptor, Encryptor};
284pub use discovery::Search;
285pub use dynamic::Dynamic;
286pub use error::{Error, ErrorKind, Origin};
287pub use explain::{Contribution, Explanation};
288pub use group::{Commit, ReloadGroup, Reloadable};
289pub use layer::Layer;
290pub use registry::Registry;
291#[cfg(feature = "async")]
292#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
293pub use remote::AsyncRemoteSource;
294pub use remote::{Fetched, Remote, RemoteSink, RemoteSource, RemoteWatch, Watching};
295pub use snapshot::{changed_paths, Change, ChangeKind, Snapshot};
296pub use source::{Format, LoadSpec, Source, DEFAULT_NEST};
297pub use units::{bytes, duration};
298pub use value::Value;
299#[cfg(feature = "decrypt")]
300#[cfg_attr(docsrs, doc(cfg(feature = "decrypt")))]
301pub use write::save_encrypted;
302pub use write::{save, save_new};
303
304/// Turns a struct into a hot-reloadable configuration snapshot.
305///
306/// See the [crate documentation](crate) for the full guide.
307///
308/// The attribute takes **no arguments**: it declares that the type *is* a
309/// configuration, and generates its storage and surface. Where the
310/// configuration comes from is stated on the [`Builder`] the generated
311/// `builder(key)` returns — see the front page for the shape, and [the
312/// book's
313/// reference](https://ctolon.github.io/dynamic-config/attribute-reference.html)
314/// for every method. An argument between the parentheses is a compile
315/// error whose message maps each old argument to its builder method.
316///
317/// One field attribute: `#[config(secret)]` generates a `Debug` that prints
318/// `***` for the marked fields, forbids `#[derive(Debug)]` alongside it,
319/// keeps the field out of the redacted cache, and marks it `writeOnly` in
320/// the schema.
321///
322/// # Requirements
323///
324/// The annotated struct must implement `serde::Deserialize` and be
325/// `Send + Sync + 'static`. Type and const parameters are supported — those go
326/// through a `TypeId` registry rather than a `static`, at a measured cost of
327/// roughly 10 ns per read. A **lifetime** parameter is rejected at compile
328/// time: the snapshot outlives every borrow that could name one.
329pub use dynamic_config_macros::dynamic_config;
330
331use serde::de::DeserializeOwned;
332
333/// Reads and deserializes a configuration section.
334///
335/// This is what the generated `load()` calls. Missing files are skipped;
336/// everything else — a parse failure, a missing required field, a value that
337/// cannot become the requested type — is an [`Error`] naming the key path and
338/// the source it came from.
339///
340/// # Errors
341///
342/// See [`ErrorKind`] for the categories.
343///
344/// # Example
345///
346/// ```
347/// # #[cfg(feature = "json")] {
348/// use dynamic_config::{load, Format, LoadSpec, Source};
349/// use serde::Deserialize;
350///
351/// #[derive(Deserialize)]
352/// struct Server { port: u16 }
353///
354/// let sources = [Source::inline(r#"{"server": {"port": 8080}}"#, Format::Json)];
355/// let server: Server = load(&LoadSpec::new("server", &sources).with_env("APP_"))
356///     .expect("the inline document is well formed");
357///
358/// assert_eq!(server.port, 8080);
359/// # }
360/// ```
361pub fn load<T: DeserializeOwned>(spec: &LoadSpec<'_>) -> Result<T, Error> {
362    loader::load(spec)
363}
364
365/// Resolves the section without deserializing it.
366///
367/// Two snapshots can be compared with [`Snapshot::diff`], which is how a reload
368/// reports *which* keys changed rather than only that something did.
369///
370/// # Errors
371///
372/// If a source cannot be read or parsed — the same failures as [`load`].
373pub fn snapshot(spec: &LoadSpec<'_>) -> Result<Snapshot, Error> {
374    loader::snapshot(spec)
375}
376
377/// Where the value at `path` would come from, if anything supplies it.
378///
379/// This is the answer to the question every configuration bug starts with:
380/// *which layer set this?* It re-reads the sources, so it reports what the
381/// **next** load would see rather than what the current snapshot holds.
382///
383/// `path` is dotted and relative to the section, as in `"pool.max_size"`.
384///
385/// # Errors
386///
387/// If a source cannot be read or parsed — the same failures as [`load`].
388///
389/// # Example
390///
391/// ```
392/// # #[cfg(feature = "json")] {
393/// use dynamic_config::{source_of, Format, LoadSpec, Origin, Source};
394///
395/// let sources = [Source::inline(r#"{"db": {"host": "localhost"}}"#, Format::Json)];
396/// let spec = LoadSpec::new("db", &sources);
397///
398/// assert_eq!(source_of(&spec, "host").unwrap(), Some(Origin::Inline));
399/// assert_eq!(source_of(&spec, "port").unwrap(), None);
400/// # }
401/// ```
402pub fn source_of(spec: &LoadSpec<'_>, path: &str) -> Result<Option<Origin>, Error> {
403    loader::source_of(spec, path)
404}
405
406/// Explains `path`: every configured layer's answer, not just the winner's.
407///
408/// The rendered [`Explanation`] **contains values** — that is its point; you
409/// asked. It is the one diagnostic in this crate that does, so treat its
410/// output accordingly. A path the caller knows to be sensitive goes through
411/// [`Explanation::redacted`]; the generated `explain()` does that for
412/// `#[config(secret)]` fields automatically.
413///
414/// # Errors
415///
416/// Whatever reading the sources reports — the same failures a load would hit.
417///
418/// # Example
419///
420/// ```
421/// # #[cfg(feature = "json")] {
422/// use dynamic_config::{explain, Format, LoadSpec, Source};
423///
424/// let sources = [Source::inline(r#"{"db": {"port": 5432}}"#, Format::Json)];
425/// let explanation = explain(&LoadSpec::new("db", &sources), "port")
426///     .expect("the inline document is well formed");
427///
428/// assert_eq!(explanation.winner().unwrap().layer, "file");
429/// println!("{explanation}");
430/// # }
431/// ```
432pub fn explain(spec: &LoadSpec<'_>, path: &str) -> Result<Explanation, Error> {
433    explain::explain(spec, path)
434}
435
436/// Whether anything supplies `path`.
437///
438/// Distinguishes "absent" from "present but falsy", which
439/// `#[serde(default)]` cannot.
440///
441/// # Errors
442///
443/// If a source cannot be read or parsed — the same failures as [`load`].
444pub fn is_set(spec: &LoadSpec<'_>, path: &str) -> Result<bool, Error> {
445    loader::is_set(spec, path)
446}
447
448/// [`load`], moved off the async executor.
449///
450/// Reading configuration touches the filesystem, which would block the worker
451/// it runs on. Where the work actually goes depends on what is available:
452/// tokio's blocking pool with the `tokio` feature, an executor installed by
453/// [`set_blocking_executor`], or a freshly spawned thread. A configuration load
454/// happens at startup and on reload, so a thread per call is a real answer
455/// rather than a placeholder.
456///
457/// `LoadSpec<'static>` is taken by value because the work outlives the call;
458/// the spec the macro emits satisfies that for free.
459///
460/// # Errors
461///
462/// Same as [`load`], plus an [`ErrorKind::Backend`] error if the work never
463/// produced a result — a panic inside it, or a runtime shutting down.
464#[cfg(feature = "async")]
465#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
466pub async fn load_async<T>(spec: LoadSpec<'static>) -> Result<T, Error>
467where
468    T: DeserializeOwned + Send + 'static,
469{
470    off_thread(move || load(&spec)).await
471}
472
473/// Runs blocking configuration work without blocking the caller's executor.
474///
475/// See [`load_async`] for where the work goes.
476///
477/// # Errors
478///
479/// Whatever `work` returns, plus [`ErrorKind::Backend`] if it never produced a
480/// result.
481#[cfg(feature = "async")]
482#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
483pub async fn off_thread<T, F>(work: F) -> Result<T, Error>
484where
485    F: FnOnce() -> Result<T, Error> + Send + 'static,
486    T: Send + 'static,
487{
488    asynchronous::off_thread(work).await
489}
490
491// ---------------------------------------------------------------------------
492// Support items used by the generated code. The redirect *macros* — the
493// feature-gated `__*!` wall — live in `redirects`; the functions stay here
494// because they are reached by path, and a path names the module it lives in.
495// ---------------------------------------------------------------------------
496/// Not public API. Lets the generated code name `serde` without the caller
497/// having to depend on it under that exact name.
498#[doc(hidden)]
499pub mod __private {
500    #[cfg(feature = "clap")]
501    pub use clap;
502    #[cfg(feature = "schema")]
503    pub use schemars;
504    pub use serde;
505    #[cfg(feature = "schema")]
506    pub use serde_json;
507}
508
509/// Not public API.
510///
511/// A reload a remote watch caused. Worded to name the trigger, because a
512/// program watching both files and a store wants its log to say which one
513/// moved.
514#[doc(hidden)]
515pub fn __log_remote_reload(name: &str, summary: Option<&str>) {
516    match summary {
517        Some(summary) => crate::log::info!("{name}: reloaded from the remote store, {summary}"),
518        None => crate::log::info!("{name}: reloaded from the remote store"),
519    }
520}
521
522/// Not public API.
523///
524/// A document the store pushed that this program cannot use. Logged as well as
525/// returned: the loop that called this has nobody to hand an error to either,
526/// and a store quietly serving a configuration nothing accepts is worth a line.
527#[doc(hidden)]
528pub fn __log_remote_failure(name: &str, error: &Error) {
529    crate::log::warning!(
530        "{name}: the remote store's document did not apply, keeping the previous \
531         snapshot: {error}"
532    );
533}