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 error;
234mod explain;
235mod group;
236mod layer;
237mod loader;
238mod log;
239mod redirects;
240mod registry;
241mod remote;
242#[cfg(feature = "schema")]
243#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
244pub mod schema;
245mod snapshot;
246mod source;
247mod units;
248mod write;
249
250#[cfg(feature = "watch")]
251#[cfg_attr(docsrs, doc(cfg(feature = "watch")))]
252pub mod watch;
253
254#[cfg(feature = "async")]
255#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
256pub use asynchronous::{set_blocking_executor, BlockingExecutor, Changes};
257/// figment itself, re-exported.
258///
259/// So that writing a [`Source::provider`] needs no direct dependency, and no
260/// second version of figment in the graph. This is the one place figment
261/// appears in this crate's API, which is why it is behind a feature.
262#[cfg(feature = "figment")]
263#[cfg_attr(docsrs, doc(cfg(feature = "figment")))]
264pub use figment;
265
266pub use aliases::Aliases;
267pub use bindings::EnvBindings;
268pub use builder::Builder;
269#[doc(hidden)]
270pub use builder::Configured;
271pub use cache::{CacheMode, Recovery};
272pub use cell::{ConfigCell, HookGuard};
273pub use check::{check, Report, Resolved, UnknownKey};
274#[cfg(feature = "decrypt")]
275#[cfg_attr(docsrs, doc(cfg(feature = "decrypt")))]
276pub use decrypt::{has_decryptor, set_decryptor, Decryptor, Encryptor};
277pub use discovery::Search;
278pub use error::{Error, ErrorKind, Origin};
279pub use explain::{Contribution, Explanation};
280pub use group::{Commit, ReloadGroup, Reloadable};
281pub use layer::Layer;
282pub use registry::Registry;
283#[cfg(feature = "async")]
284#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
285pub use remote::AsyncRemoteSource;
286pub use remote::{Fetched, Remote, RemoteSource, RemoteWatch, Watching};
287pub use snapshot::{changed_paths, Change, ChangeKind, Snapshot};
288pub use source::{Format, LoadSpec, Source, DEFAULT_NEST};
289pub use units::{bytes, duration};
290#[cfg(feature = "decrypt")]
291#[cfg_attr(docsrs, doc(cfg(feature = "decrypt")))]
292pub use write::save_encrypted;
293pub use write::{save, save_new};
294
295/// Turns a struct into a hot-reloadable configuration snapshot.
296///
297/// See the [crate documentation](crate) for the full guide.
298///
299/// The attribute takes **no arguments**: it declares that the type *is* a
300/// configuration, and generates its storage and surface. Where the
301/// configuration comes from is stated on the [`Builder`] the generated
302/// `builder(key)` returns — see the front page for the shape, and [the
303/// book's
304/// reference](https://ctolon.github.io/dynamic-config/attribute-reference.html)
305/// for every method. An argument between the parentheses is a compile
306/// error whose message maps each old argument to its builder method.
307///
308/// One field attribute: `#[config(secret)]` generates a `Debug` that prints
309/// `***` for the marked fields, forbids `#[derive(Debug)]` alongside it,
310/// keeps the field out of the redacted cache, and marks it `writeOnly` in
311/// the schema.
312///
313/// # Requirements
314///
315/// The annotated struct must implement `serde::Deserialize` and be
316/// `Send + Sync + 'static`. Type and const parameters are supported — those go
317/// through a `TypeId` registry rather than a `static`, at a measured cost of
318/// roughly 10 ns per read. A **lifetime** parameter is rejected at compile
319/// time: the snapshot outlives every borrow that could name one.
320pub use dynamic_config_macros::dynamic_config;
321
322use serde::de::DeserializeOwned;
323
324/// Reads and deserializes a configuration section.
325///
326/// This is what the generated `load()` calls. Missing files are skipped;
327/// everything else — a parse failure, a missing required field, a value that
328/// cannot become the requested type — is an [`Error`] naming the key path and
329/// the source it came from.
330///
331/// # Errors
332///
333/// See [`ErrorKind`] for the categories.
334///
335/// # Example
336///
337/// ```
338/// # #[cfg(feature = "json")] {
339/// use dynamic_config::{load, Format, LoadSpec, Source};
340/// use serde::Deserialize;
341///
342/// #[derive(Deserialize)]
343/// struct Server { port: u16 }
344///
345/// let sources = [Source::inline(r#"{"server": {"port": 8080}}"#, Format::Json)];
346/// let server: Server = load(&LoadSpec::new("server", &sources).with_env("APP_"))
347/// .expect("the inline document is well formed");
348///
349/// assert_eq!(server.port, 8080);
350/// # }
351/// ```
352pub fn load<T: DeserializeOwned>(spec: &LoadSpec<'_>) -> Result<T, Error> {
353 loader::load(spec)
354}
355
356/// Resolves the section without deserializing it.
357///
358/// Two snapshots can be compared with [`Snapshot::diff`], which is how a reload
359/// reports *which* keys changed rather than only that something did.
360///
361/// # Errors
362///
363/// If a source cannot be read or parsed — the same failures as [`load`].
364pub fn snapshot(spec: &LoadSpec<'_>) -> Result<Snapshot, Error> {
365 loader::snapshot(spec)
366}
367
368/// Where the value at `path` would come from, if anything supplies it.
369///
370/// This is the answer to the question every configuration bug starts with:
371/// *which layer set this?* It re-reads the sources, so it reports what the
372/// **next** load would see rather than what the current snapshot holds.
373///
374/// `path` is dotted and relative to the section, as in `"pool.max_size"`.
375///
376/// # Errors
377///
378/// If a source cannot be read or parsed — the same failures as [`load`].
379///
380/// # Example
381///
382/// ```
383/// # #[cfg(feature = "json")] {
384/// use dynamic_config::{source_of, Format, LoadSpec, Origin, Source};
385///
386/// let sources = [Source::inline(r#"{"db": {"host": "localhost"}}"#, Format::Json)];
387/// let spec = LoadSpec::new("db", &sources);
388///
389/// assert_eq!(source_of(&spec, "host").unwrap(), Some(Origin::Inline));
390/// assert_eq!(source_of(&spec, "port").unwrap(), None);
391/// # }
392/// ```
393pub fn source_of(spec: &LoadSpec<'_>, path: &str) -> Result<Option<Origin>, Error> {
394 loader::source_of(spec, path)
395}
396
397/// Explains `path`: every configured layer's answer, not just the winner's.
398///
399/// The rendered [`Explanation`] **contains values** — that is its point; you
400/// asked. It is the one diagnostic in this crate that does, so treat its
401/// output accordingly. A path the caller knows to be sensitive goes through
402/// [`Explanation::redacted`]; the generated `explain()` does that for
403/// `#[config(secret)]` fields automatically.
404///
405/// # Errors
406///
407/// Whatever reading the sources reports — the same failures a load would hit.
408///
409/// # Example
410///
411/// ```
412/// # #[cfg(feature = "json")] {
413/// use dynamic_config::{explain, Format, LoadSpec, Source};
414///
415/// let sources = [Source::inline(r#"{"db": {"port": 5432}}"#, Format::Json)];
416/// let explanation = explain(&LoadSpec::new("db", &sources), "port")
417/// .expect("the inline document is well formed");
418///
419/// assert_eq!(explanation.winner().unwrap().layer, "file");
420/// println!("{explanation}");
421/// # }
422/// ```
423pub fn explain(spec: &LoadSpec<'_>, path: &str) -> Result<Explanation, Error> {
424 explain::explain(spec, path)
425}
426
427/// Whether anything supplies `path`.
428///
429/// Distinguishes "absent" from "present but falsy", which
430/// `#[serde(default)]` cannot.
431///
432/// # Errors
433///
434/// If a source cannot be read or parsed — the same failures as [`load`].
435pub fn is_set(spec: &LoadSpec<'_>, path: &str) -> Result<bool, Error> {
436 loader::is_set(spec, path)
437}
438
439/// [`load`], moved off the async executor.
440///
441/// Reading configuration touches the filesystem, which would block the worker
442/// it runs on. Where the work actually goes depends on what is available:
443/// tokio's blocking pool with the `tokio` feature, an executor installed by
444/// [`set_blocking_executor`], or a freshly spawned thread. A configuration load
445/// happens at startup and on reload, so a thread per call is a real answer
446/// rather than a placeholder.
447///
448/// `LoadSpec<'static>` is taken by value because the work outlives the call;
449/// the spec the macro emits satisfies that for free.
450///
451/// # Errors
452///
453/// Same as [`load`], plus an [`ErrorKind::Backend`] error if the work never
454/// produced a result — a panic inside it, or a runtime shutting down.
455#[cfg(feature = "async")]
456#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
457pub async fn load_async<T>(spec: LoadSpec<'static>) -> Result<T, Error>
458where
459 T: DeserializeOwned + Send + 'static,
460{
461 off_thread(move || load(&spec)).await
462}
463
464/// Runs blocking configuration work without blocking the caller's executor.
465///
466/// See [`load_async`] for where the work goes.
467///
468/// # Errors
469///
470/// Whatever `work` returns, plus [`ErrorKind::Backend`] if it never produced a
471/// result.
472#[cfg(feature = "async")]
473#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
474pub async fn off_thread<T, F>(work: F) -> Result<T, Error>
475where
476 F: FnOnce() -> Result<T, Error> + Send + 'static,
477 T: Send + 'static,
478{
479 asynchronous::off_thread(work).await
480}
481
482// ---------------------------------------------------------------------------
483// Support items used by the generated code. The redirect *macros* — the
484// feature-gated `__*!` wall — live in `redirects`; the functions stay here
485// because they are reached by path, and a path names the module it lives in.
486// ---------------------------------------------------------------------------
487/// Not public API. Lets the generated code name `serde` without the caller
488/// having to depend on it under that exact name.
489#[doc(hidden)]
490pub mod __private {
491 #[cfg(feature = "clap")]
492 pub use clap;
493 #[cfg(feature = "schema")]
494 pub use schemars;
495 pub use serde;
496 #[cfg(feature = "schema")]
497 pub use serde_json;
498}
499
500/// Not public API.
501///
502/// A reload a remote watch caused. Worded to name the trigger, because a
503/// program watching both files and a store wants its log to say which one
504/// moved.
505#[doc(hidden)]
506pub fn __log_remote_reload(name: &str, summary: Option<&str>) {
507 match summary {
508 Some(summary) => crate::log::info!("{name}: reloaded from the remote store, {summary}"),
509 None => crate::log::info!("{name}: reloaded from the remote store"),
510 }
511}
512
513/// Not public API.
514///
515/// A document the store pushed that this program cannot use. Logged as well as
516/// returned: the loop that called this has nobody to hand an error to either,
517/// and a store quietly serving a configuration nothing accepts is worth a line.
518#[doc(hidden)]
519pub fn __log_remote_failure(name: &str, error: &Error) {
520 crate::log::warning!(
521 "{name}: the remote store's document did not apply, keeping the previous \
522 snapshot: {error}"
523 );
524}