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