Skip to main content

dynamic_config/
lib.rs

1//! Hot-reloadable, lock-free application configuration.
2//!
3//! Declare a struct, configure it with the builder, and read it from
4//! anywhere:
5//!
6//! ```
7//! # #[cfg(feature = "json")] {
8//! use dynamic_config::dynamic_config;
9//! use serde::Deserialize;
10//!
11//! #[dynamic_config]
12//! #[derive(Debug, Deserialize)]
13//! struct ServerConfig {
14//!     #[serde(default = "default_host")]
15//!     host: String,
16//!     #[serde(default = "default_port")]
17//!     port: u16,
18//! }
19//!
20//! # fn default_host() -> String { "0.0.0.0".into() }
21//! # fn default_port() -> u16 { 8080 }
22//! // `config.json` does not exist here, so every field falls back to its
23//! // default — a missing file is skipped, not an error.
24//! ServerConfig::builder("server")
25//!     .file("config.json")
26//!     .env("APP_")
27//!     .init()
28//!     .expect("defaults cover every field");
29//!
30//! let config = ServerConfig::current();
31//! println!("{}:{}", config.host, config.port);
32//! # }
33//! ```
34//!
35//! The attribute declares — *this type is a configuration* — and generates
36//! its storage and accessors. The [`Builder`] configures: where the
37//! sources are is runtime data, and it lives in runtime code.
38//!
39//! This page is the API reference. The guide — profiles, discovery, hot
40//! reload, remote stores, encryption, testing — is
41//! [**the book**](https://dynamic-config-rs.github.io/).
42//!
43//! # What the attribute generates
44//!
45//! The attribute declares; the builder configures. What gets generated is
46//! the type-bound surface:
47//!
48//! | Method | Description |
49//! |---|---|
50//! | `builder(key) -> Builder<Self>` | Where everything starts: state the sources, `init()`. |
51//! | `current() -> Arc<Self>` | The current snapshot. Panics before an install. |
52//! | `try_current() -> Option<Arc<Self>>` | The current snapshot, or `None`. |
53//! | `replace(Self)` | Atomically swap in a new snapshot. |
54//! | `on_reload(f)` | Run a callback on every later reload, for the life of the process. |
55//! | `on_reload_scoped(f) -> HookGuard` | The same, until the guard is dropped. |
56//! | `on_reload_with(f)` | The same, told *why*: a [`ReloadEvent`] carrying the [`ReloadReason`], the metadata, and `previous: None` on the first install. |
57//! | `status() -> ConfigStatus` | Which generation is live, when it landed, why, and how many reloads have failed since one worked. No I/O. |
58//! | `set_default(path, value)` | A fallback used only when nothing else supplies the key. |
59//! | `set_override(path, value)` | A value that wins over every file and variable. |
60//! | `clear_defaults()` / `clear_overrides()` | Drop them again. |
61//! | `changes()` | With `async`: a handle woken by every later reload. |
62//!
63//! Everything about *sources* lives on the [`Builder`] the generated
64//! `builder(key)` returns: `.file(..)`, `.discover(name, paths)`,
65//! `.env(prefix)`, `.strict_env()`, `.env_file(..)`, `.profile_env(..)`,
66//! `.cache(path, mode)`, `.validate(f)` — then `.load()`, `.init()`,
67//! `.init_and_current()` (the pair, in one call), `.watch(debounce)`,
68//! `.explain(path)`, `.check()`, and with `async`, `.load_async()` /
69//! `.init_async()` / `.init_and_current_async()`. A successful `init` also *remembers*
70//! the builder, so `source_of`, `is_set`, `snapshot`, `check`, `explain`,
71//! `prepare` and the remote reload on the type answer for the running
72//! configuration. The rest — remote stores, aliases, bindings, flags,
73//! `bind_clap` — is in [the book's
74//! reference](https://dynamic-config-rs.github.io/attribute-reference.html).
75//!
76//! # Precedence
77//!
78//! ```text
79//! set_default < discovered < config.toml < secrets.json < remote < secrets_dir < APP_DB_* < bind_env < set_flag < set_override
80//!  (runtime)   (search path)   (first)      (last file)   (etcd…)   (a mount)   (environment) (by name)  (CLI)     (runtime)
81//! ```
82//!
83//! Files merge left to right and tables merge key by key, so a small
84//! `secrets.json` can override two fields of a large `config.toml` without
85//! restating it.
86//!
87//! The two runtime layers bracket the rest. Defaults cover a fallback the
88//! program can compute but a file need not state; overrides are what make a
89//! test or a `--set key=value` flag authoritative without touching disk. Both
90//! take effect on the next `load()`.
91//!
92//! # Reading configuration is lock-free
93//!
94//! `current()` hands out an `Arc` cloned from an `ArcSwap`, so a reload never
95//! blocks a request handler. A reader that already holds an `Arc` keeps its own
96//! generation — call `current()` once per request and reuse it, or a reload
97//! landing mid-request will show you two different configurations.
98//!
99//! # Reloading cannot take the process down
100//!
101//! A reload re-runs `load()`. If the new configuration is invalid, or a file is
102//! caught half-written, the error is reported and the previous snapshot stays
103//! in place. A bad edit degrades to "no change".
104//!
105//! # Environment variables
106//!
107//! `env = "APP_"` with `key = "db"` reads `APP_DB_*`. A single underscore is
108//! part of a field name; a doubled one introduces nesting:
109//!
110//! | Variable | Sets |
111//! |---|---|
112//! | `APP_DB_HOST` | `host` |
113//! | `APP_DB_MAX_SIZE` | `max_size` |
114//! | `APP_DB_POOL__MAX_SIZE` | `pool.max_size` |
115//!
116//! Values are read loosely, because a variable is always text: `8080` reaches
117//! a `u16`, `true` reaches a `bool`, and `[a, b, c]` reaches a `Vec<String>`.
118//! A value that cannot become the field's type is an error naming the field.
119//!
120//! # Units
121//!
122//! `timeout = 30` is ambiguous and `max_body = 67108864` is unreadable, so both
123//! are usually written with a unit — which no stock `Deserialize` accepts:
124//!
125//! ```
126//! use std::time::Duration;
127//! use serde::Deserialize;
128//!
129//! #[derive(Deserialize)]
130//! struct Limits {
131//!     #[serde(with = "dynamic_config::duration")]
132//!     timeout: Duration,          // "30s", "1h30m", "500ms", or a number of seconds
133//!     #[serde(with = "dynamic_config::bytes")]
134//!     max_body: u64,              // "64MiB", "1GB", or a number of bytes
135//! }
136//! ```
137//!
138//! # Async
139//!
140//! With the `async` feature, configuration loads without blocking the
141//! executor, and tasks can await reloads instead of polling. No runtime is
142//! named anywhere: `changes()` is a `Future`, so any executor drives it.
143//!
144//! ```ignore
145//! #[dynamic_config]
146//! #[derive(Debug, Deserialize)]
147//! struct DbConfig { pool_size: u32 }
148//!
149//! let builder = DbConfig::builder("db").file("config.json");
150//! builder.init_async().await?;
151//! // Keep the handle: dropping it stops the watch.
152//! let _watch = builder.watch(Duration::from_millis(250))?;
153//!
154//! let mut reloads = DbConfig::changes();
155//!
156//! spawn(async move {
157//!     loop {
158//!         let config = reloads.changed().await;
159//!         pool.resize(config.pool_size);
160//!     }
161//! });
162//! ```
163//!
164//! The watcher itself stays on a plain thread. `notify`'s channel is
165//! synchronous, and keeping it off the runtime means file watching works
166//! whether or not a runtime is running.
167//!
168//! # Features
169//!
170//! | Feature | Default | Effect |
171//! |---|---|---|
172//! | `json` | yes | `.json` sources |
173//! | `toml` | no | `.toml` sources |
174//! | `yaml` | no | `.yaml` / `.yml` sources |
175//! | `watch` | no | `start_watch()` and the file watcher |
176//! | `async` | no | `load_async`, `init_async`, `changes` — no runtime dependency |
177//! | `tokio` | no | `async`, plus tokio's blocking pool instead of a thread per load |
178//! | `clap` | no | `bind_clap`: named `clap` arguments as the flags layer |
179//! | `schema` | no | `schema()`: a JSON Schema for the resolved configuration |
180//! | `decrypt` | no | the [`Decryptor`]/[`Encryptor`] traits and `.age`-suffix handling |
181//! | `age` | no | `decrypt`, plus the `age` module's implementation of it |
182//! | `figment` | no | foreign figment providers as sources, via `Source::provider` |
183//! | `dotenv` | no | `env_files = [".env"]`: `.env` files as the environment layer |
184//! | `tracing` | no | Watcher diagnostics, and a record per reload and per remote fetch, via `tracing` instead of stderr |
185//! | `telemetry` | no | [`telemetry::Exposition`]: a `ConfigStatus` and a [`RemoteStatus`] as Prometheus text, with no dependency |
186//! | `full` | no | all of the above |
187//!
188//! Using a format, `watch` or `async` whose feature is disabled is a compile
189//! error naming the feature to add.
190//!
191//! # Without the macro
192//!
193//! [`load`], [`ConfigCell`] and [`LoadSpec`] are the whole engine and are
194//! usable on their own:
195//!
196//! ```
197//! # #[cfg(feature = "json")] {
198//! use dynamic_config::{load, Format, LoadSpec, Source};
199//! use serde::Deserialize;
200//!
201//! #[derive(Deserialize)]
202//! struct Db { host: String }
203//!
204//! let sources = [Source::inline(r#"{"db": {"host": "localhost"}}"#, Format::Json)];
205//! let db: Db = load(&LoadSpec::new("db", &sources))
206//!     .expect("the inline document is well formed");
207//!
208//! assert_eq!(db.host, "localhost");
209//! # }
210//! ```
211//!
212//! # Without a struct
213//!
214//! A plugin host, a feature-flag table or a tool inspecting somebody else's
215//! configuration cannot write the struct, so [`Value`] is one:
216//! [`Builder::values`] loads a section as data, and `Dynamic<Value>` installs
217//! it like any other configuration — same layers, same watcher, same
218//! last-known-good cache, same reload hooks.
219//!
220//! ```no_run
221//! # #[cfg(feature = "json")] {
222//! use dynamic_config::{Builder, Dynamic, Value};
223//!
224//! let config = Dynamic::new(Builder::values("db").file("config.json"));
225//! let values = config.init_and_current()?;
226//!
227//! assert_eq!(values.get("pool.max_size").and_then(Value::as_i64), Some(32));
228//! # }
229//! # Ok::<(), dynamic_config::Error>(())
230//! ```
231//!
232//! It costs what it looks like it costs: `current()` is still one atomic
233//! load, and the path is then a walk of the tree — a couple of times a field
234//! access, rather than the same thing. No feature flag, no extra dependency,
235//! and no schema either: types are checked at the read instead of at the
236//! load, `check()` reports that unknown keys were **not** checked, and
237//! secrets have to be named with [`Builder::secrets`] because there is no
238//! `#[config(secret)]` to derive them from. The book's *Schemaless
239//! Configuration* chapter has the measured numbers and the whole list.
240//!
241//! ```text
242//! current().port, on a struct   20 ns   0 allocations
243//! values.get("port")            27 ns   0 allocations
244//! values.get("pool.max_size")   32 ns   0 allocations
245//! values.get_as::<u16>(path)    37 ns   0 allocations
246//! ```
247
248#![forbid(unsafe_code)]
249#![deny(missing_docs)]
250// A getter whose result is discarded is a mistake in a library like this one —
251// `is_set`, `contains`, `document`, `describe` all answer a question and change
252// nothing. Warned about rather than left to review, and CI denies warnings.
253#![warn(clippy::must_use_candidate)]
254#![cfg_attr(docsrs, feature(doc_cfg))]
255
256#[cfg(feature = "age")]
257#[cfg_attr(docsrs, doc(cfg(feature = "age")))]
258pub mod age;
259mod aliases;
260#[cfg(feature = "async")]
261mod asynchronous;
262mod backend;
263mod bindings;
264mod builder;
265mod cache;
266mod cell;
267mod check;
268mod de;
269#[cfg(feature = "decrypt")]
270mod decrypt;
271mod discovery;
272mod document;
273#[cfg(feature = "dotenv")]
274mod dotenv;
275mod dynamic;
276pub mod engine;
277mod env_layer;
278mod error;
279mod explain;
280mod fingerprint;
281mod group;
282mod layer;
283mod loader;
284mod log;
285#[cfg(feature = "otel")]
286#[cfg_attr(docsrs, doc(cfg(feature = "otel")))]
287pub mod otel;
288pub mod reader;
289mod redirects;
290mod registry;
291mod reload;
292mod remote;
293// The composition the loader moves onto next: the layers already build this
294// crate's trees, and what remains is handing them here in precedence order
295// instead of to the backend. Exercised meanwhile by its own differential
296// test, which composes the same layers both ways and compares the tree and
297// the winner of every leaf.
298#[allow(dead_code)]
299mod resolve;
300#[cfg(feature = "schema")]
301#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
302pub mod schema;
303mod ser;
304mod snapshot;
305mod source;
306pub(crate) mod sync;
307#[cfg(feature = "telemetry")]
308#[cfg_attr(docsrs, doc(cfg(feature = "telemetry")))]
309pub mod telemetry;
310// The same module with only its `tracing` half compiled, and private: the
311// reload events are emitted from inside this crate, so they need no public
312// surface to reach.
313#[cfg(all(feature = "tracing", not(feature = "telemetry")))]
314mod telemetry;
315mod text_value;
316mod units;
317mod value;
318mod write;
319
320#[cfg(feature = "watch")]
321#[cfg_attr(docsrs, doc(cfg(feature = "watch")))]
322pub mod watch;
323#[cfg(feature = "watch")]
324#[cfg_attr(docsrs, doc(cfg(feature = "watch")))]
325pub use watch::WatchOptions;
326
327/// Not public API: the loom suite drives the wake protocol directly.
328#[cfg(all(feature = "async", loom))]
329#[doc(hidden)]
330pub use asynchronous::Notify as LoomNotify;
331#[cfg(feature = "async")]
332#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
333pub use asynchronous::{set_blocking_executor, BlockingExecutor, Changes, Event, Events};
334/// figment itself, re-exported.
335///
336/// So that writing a [`Source::provider`] needs no direct dependency, and no
337/// second version of figment in the graph. This is the one place figment
338/// appears in this crate's API, which is why it is behind a feature.
339#[cfg(feature = "figment")]
340#[cfg_attr(docsrs, doc(cfg(feature = "figment")))]
341pub use figment;
342pub use log::{clear_log_sink, set_log_level, set_log_sink, LogLevel, LogSink};
343
344pub use aliases::Aliases;
345pub use bindings::EnvBindings;
346pub use builder::Builder;
347#[doc(hidden)]
348pub use builder::Configured;
349pub use cache::{CacheMode, Recovery};
350pub use cell::{ConfigCell, HookGuard, SnapshotMeta};
351pub use check::{check, Report, Resolved, UnknownKey};
352#[cfg(feature = "decrypt")]
353#[cfg_attr(docsrs, doc(cfg(feature = "decrypt")))]
354pub use decrypt::{has_decryptor, set_decryptor, Decryptor, Encryptor};
355pub use discovery::Search;
356pub use dynamic::Dynamic;
357pub use error::{Error, ErrorKind, Origin};
358pub use explain::{Contribution, Explanation};
359pub use group::{Commit, ReloadGroup, Reloadable};
360pub use layer::Layer;
361pub use registry::Registry;
362pub use reload::{ConfigStatus, FailureStatus, ReloadEvent, ReloadReason};
363#[cfg(feature = "async")]
364#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
365pub use remote::AsyncRemoteSource;
366pub use remote::{
367    Fetched, Lease, Pace, Remote, RemoteSink, RemoteSource, RemoteStatus, RemoteWatch,
368    RenewableSource, Revision, WatchCapability, Watching, MAX_DOCUMENT_BYTES,
369};
370pub use snapshot::{changed_paths, Change, ChangeKind, Snapshot};
371pub use source::{Format, LoadSpec, Source, DEFAULT_NEST};
372pub use units::{bytes, duration};
373pub use value::Value;
374
375/// This crate's version, for anything that embeds it and has to say so.
376///
377/// A language binding versions on its own schedule — its users read a
378/// different changelog — so "which engine is inside this wheel" stops
379/// being answerable from the outside. This answers it.
380pub const VERSION: &str = env!("CARGO_PKG_VERSION");
381
382/// Whether `path` touches a secret, given the secret list a type or a
383/// binding declared.
384///
385/// Three ways it can, and all three have to redact or the diagnostic
386/// leaks: the path *is* a secret, the path is *under* one (every path
387/// below a secret field is the secret's), or the path is an *ancestor* of
388/// one — asking to explain `credentials` must not render the password
389/// nested inside it. Secrets are named by a plain field for a
390/// `#[config(secret)]` field, and by a dotted path when they live inside
391/// a nested structure, which is what a language binding derives from a
392/// nested model.
393#[doc(hidden)]
394#[must_use]
395pub fn touches_secret(path: &str, secrets: &[impl AsRef<str>]) -> bool {
396    secrets.iter().any(|secret| {
397        let secret = secret.as_ref();
398
399        secret == path
400            || path
401                .strip_prefix(secret)
402                .is_some_and(|rest| rest.starts_with('.'))
403            || secret
404                .strip_prefix(path)
405                .is_some_and(|rest| rest.starts_with('.'))
406    })
407}
408#[cfg(feature = "decrypt")]
409#[cfg_attr(docsrs, doc(cfg(feature = "decrypt")))]
410pub use write::save_encrypted;
411pub use write::{save, save_new};
412
413/// Turns a struct into a hot-reloadable configuration snapshot.
414///
415/// See the [crate documentation](crate) for the full guide.
416///
417/// The attribute takes **no arguments**: it declares that the type *is* a
418/// configuration, and generates its storage and surface. Where the
419/// configuration comes from is stated on the [`Builder`] the generated
420/// `builder(key)` returns — see the front page for the shape, and [the
421/// book's
422/// reference](https://dynamic-config-rs.github.io/attribute-reference.html)
423/// for every method. An argument between the parentheses is a compile
424/// error whose message maps each old argument to its builder method.
425///
426/// One field attribute: `#[config(secret)]` generates a `Debug` that prints
427/// `***` for the marked fields, forbids `#[derive(Debug)]` alongside it,
428/// keeps the field out of the redacted cache, and marks it `writeOnly` in
429/// the schema.
430///
431/// # Requirements
432///
433/// The annotated struct must implement `serde::Deserialize` and be
434/// `Send + Sync + 'static`. Type and const parameters are supported — those go
435/// through a `TypeId` registry rather than a `static`, at a measured cost of
436/// roughly 10 ns per read. A **lifetime** parameter is rejected at compile
437/// time: the snapshot outlives every borrow that could name one.
438pub use dynamic_config_macros::dynamic_config;
439
440use serde::de::DeserializeOwned;
441
442/// Reads and deserializes a configuration section.
443///
444/// This is what the generated `load()` calls. Missing files are skipped;
445/// everything else — a parse failure, a missing required field, a value that
446/// cannot become the requested type — is an [`Error`] naming the key path and
447/// the source it came from.
448///
449/// # Errors
450///
451/// See [`ErrorKind`] for the categories.
452///
453/// # Example
454///
455/// ```
456/// # #[cfg(feature = "json")] {
457/// use dynamic_config::{load, Format, LoadSpec, Source};
458/// use serde::Deserialize;
459///
460/// #[derive(Deserialize)]
461/// struct Server { port: u16 }
462///
463/// let sources = [Source::inline(r#"{"server": {"port": 8080}}"#, Format::Json)];
464/// let server: Server = load(&LoadSpec::new("server", &sources).with_env("APP_"))
465///     .expect("the inline document is well formed");
466///
467/// assert_eq!(server.port, 8080);
468/// # }
469/// ```
470pub fn load<T: DeserializeOwned>(spec: &LoadSpec<'_>) -> Result<T, Error> {
471    loader::load(spec)
472}
473
474/// Resolves the section without deserializing it.
475///
476/// Two snapshots can be compared with [`Snapshot::diff`], which is how a reload
477/// reports *which* keys changed rather than only that something did.
478///
479/// # Errors
480///
481/// If a source cannot be read or parsed — the same failures as [`load`].
482pub fn snapshot(spec: &LoadSpec<'_>) -> Result<Snapshot, Error> {
483    loader::snapshot(spec)
484}
485
486/// Where the value at `path` would come from, if anything supplies it.
487///
488/// This is the answer to the question every configuration bug starts with:
489/// *which layer set this?* It re-reads the sources, so it reports what the
490/// **next** load would see rather than what the current snapshot holds.
491///
492/// `path` is dotted and relative to the section, as in `"pool.max_size"`.
493///
494/// # Errors
495///
496/// If a source cannot be read or parsed — the same failures as [`load`].
497///
498/// # Example
499///
500/// ```
501/// # #[cfg(feature = "json")] {
502/// use dynamic_config::{source_of, Format, LoadSpec, Origin, Source};
503///
504/// let sources = [Source::inline(r#"{"db": {"host": "localhost"}}"#, Format::Json)];
505/// let spec = LoadSpec::new("db", &sources);
506///
507/// assert_eq!(source_of(&spec, "host").unwrap(), Some(Origin::Inline));
508/// assert_eq!(source_of(&spec, "port").unwrap(), None);
509/// # }
510/// ```
511pub fn source_of(spec: &LoadSpec<'_>, path: &str) -> Result<Option<Origin>, Error> {
512    loader::source_of(spec, path)
513}
514
515/// Explains `path`: every configured layer's answer, not just the winner's.
516///
517/// The rendered [`Explanation`] **contains values** — that is its point; you
518/// asked. It is the one diagnostic in this crate that does, so treat its
519/// output accordingly. A path the caller knows to be sensitive goes through
520/// [`Explanation::redacted`]; the generated `explain()` does that for
521/// `#[config(secret)]` fields automatically.
522///
523/// # Errors
524///
525/// Whatever reading the sources reports — the same failures a load would hit.
526///
527/// # Example
528///
529/// ```
530/// # #[cfg(feature = "json")] {
531/// use dynamic_config::{explain, Format, LoadSpec, Source};
532///
533/// let sources = [Source::inline(r#"{"db": {"port": 5432}}"#, Format::Json)];
534/// let explanation = explain(&LoadSpec::new("db", &sources), "port")
535///     .expect("the inline document is well formed");
536///
537/// assert_eq!(explanation.winner().unwrap().layer, "file");
538/// println!("{explanation}");
539/// # }
540/// ```
541pub fn explain(spec: &LoadSpec<'_>, path: &str) -> Result<Explanation, Error> {
542    explain::explain(spec, path)
543}
544
545/// Whether anything supplies `path`.
546///
547/// Distinguishes "absent" from "present but falsy", which
548/// `#[serde(default)]` cannot.
549///
550/// # Errors
551///
552/// If a source cannot be read or parsed — the same failures as [`load`].
553pub fn is_set(spec: &LoadSpec<'_>, path: &str) -> Result<bool, Error> {
554    loader::is_set(spec, path)
555}
556
557/// [`load`], moved off the async executor.
558///
559/// Reading configuration touches the filesystem, which would block the worker
560/// it runs on. Where the work actually goes depends on what is available:
561/// tokio's blocking pool with the `tokio` feature, an executor installed by
562/// [`set_blocking_executor`], or a freshly spawned thread. A configuration load
563/// happens at startup and on reload, so a thread per call is a real answer
564/// rather than a placeholder.
565///
566/// `LoadSpec<'static>` is taken by value because the work outlives the call;
567/// the spec the macro emits satisfies that for free.
568///
569/// # Errors
570///
571/// Same as [`load`], plus an [`ErrorKind::Backend`] error if the work never
572/// produced a result — a panic inside it, or a runtime shutting down.
573#[cfg(feature = "async")]
574#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
575pub async fn load_async<T>(spec: LoadSpec<'static>) -> Result<T, Error>
576where
577    T: DeserializeOwned + Send + 'static,
578{
579    off_thread(move || load(&spec)).await
580}
581
582/// Runs blocking configuration work without blocking the caller's executor.
583///
584/// See [`load_async`] for where the work goes.
585///
586/// # Errors
587///
588/// Whatever `work` returns, plus [`ErrorKind::Backend`] if it never produced a
589/// result.
590#[cfg(feature = "async")]
591#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
592pub async fn off_thread<T, F>(work: F) -> Result<T, Error>
593where
594    F: FnOnce() -> Result<T, Error> + Send + 'static,
595    T: Send + 'static,
596{
597    asynchronous::off_thread(work).await
598}
599
600// ---------------------------------------------------------------------------
601// Support items used by the generated code. The redirect *macros* — the
602// feature-gated `__*!` wall — live in `redirects`; the functions stay here
603// because they are reached by path, and a path names the module it lives in.
604// ---------------------------------------------------------------------------
605/// Not public API. The parsers a fuzz target needs and a caller does not.
606///
607/// Three of this crate's parsing surfaces are private because nothing outside
608/// it has any business calling them — and `fuzz/fuzz_targets/` is outside it.
609/// The choice is between publishing them (a stability promise nobody wants for
610/// a `.env` line splitter) and leaving the surfaces whose failure mode is a
611/// crash at startup or a path traversal covered only by proptest. This is the
612/// third way: reachable by path, absent from the book and from rustdoc, and
613/// carrying no more of a compatibility promise than [`__private`] does.
614///
615/// The public parse seam needs nothing here — [`Value::parse`] is fuzzable as
616/// it stands.
617#[doc(hidden)]
618pub mod __fuzz {
619    /// One load, composed by every engine this build has.
620    ///
621    /// `(engine name, rendered tree)` per engine, in a fixed order — so a
622    /// test can assert that they all say the same thing, and name the one
623    /// that drifted when they do not. Rendered rather than returned as a
624    /// tree, so the harness needs no value type and a divergence shows up
625    /// as a changed string.
626    ///
627    /// This is the whole-stack differential: the engines fold the *same*
628    /// collected layers, which is the only way the comparison is about the
629    /// fold rather than about the walk in front of it.
630    #[must_use]
631    pub fn compositions(spec: &crate::LoadSpec<'_>) -> Vec<(String, String)> {
632        crate::engine::all()
633            .into_iter()
634            .map(|engine| {
635                let rendered = crate::loader::contributions(spec)
636                    .and_then(|mut collected| {
637                        crate::resolve::compose(engine, collected.take_layers())
638                    })
639                    .map_or_else(
640                        |error| format!("error: {error}"),
641                        |(tree, _)| format!("{:?}", crate::Value::Table(tree)),
642                    );
643
644                (engine.name().to_owned(), rendered)
645            })
646            .collect()
647    }
648
649    /// The same load, composed by every engine, with the winner of every
650    /// leaf as well as the value.
651    ///
652    /// The trees agreeing is half the contract; §4 is the other half, and a
653    /// provenance drift is invisible in a rendered tree.
654    #[must_use]
655    pub fn provenances(spec: &crate::LoadSpec<'_>) -> Vec<(String, String)> {
656        crate::engine::all()
657            .into_iter()
658            .map(|engine| {
659                let rendered = crate::loader::contributions(spec)
660                    .and_then(|mut collected| {
661                        crate::resolve::compose(engine, collected.take_layers())
662                    })
663                    .map_or_else(
664                        |error| format!("error: {error}"),
665                        |(_, provenance)| {
666                            provenance
667                                .iter()
668                                .map(|(path, origin)| format!("{path} = {origin}"))
669                                .collect::<Vec<_>>()
670                                .join("\n")
671                        },
672                    );
673
674                (engine.name().to_owned(), rendered)
675            })
676            .collect()
677    }
678
679    /// A stack of layer trees, through one engine and the crate's own
680    /// provenance repair — the whole seam, without a filesystem.
681    ///
682    /// `(tree, leaf path → the index of the layer that supplied it)`. What
683    /// the engine-agreement tests drive: a fold reached this way is the fold
684    /// a load gets, repair included.
685    ///
686    /// # Errors
687    ///
688    /// If the engine refuses the layers.
689    pub fn fold_through(
690        engine: &'static dyn crate::engine::Engine,
691        layers: &[crate::Value],
692    ) -> Result<(crate::Value, std::collections::BTreeMap<String, String>), crate::Error> {
693        let contributions = layers
694            .iter()
695            .enumerate()
696            .map(|(index, values)| {
697                let table = match values {
698                    crate::Value::Table(table) => table.clone(),
699                    _ => std::collections::BTreeMap::new(),
700                };
701
702                // The index as the origin, so a disagreement about *which*
703                // layer won reads as a number rather than as a file name
704                // nobody chose.
705                crate::resolve::Contribution::new(
706                    "test",
707                    crate::Origin::Env(index.to_string()),
708                    table,
709                )
710            })
711            .collect();
712
713        // `Always`: this door exists to drive the *engine*, and `compose`
714        // answers a single layer itself. A corpus case with one layer would
715        // otherwise compare two answers no engine produced.
716        let (tree, provenance) =
717            crate::resolve::compose_with(engine, contributions, crate::resolve::Fold::Always)?;
718
719        Ok((
720            crate::Value::Table(tree),
721            provenance
722                .into_iter()
723                .map(|(path, origin)| (path, origin.to_string()))
724                .collect(),
725        ))
726    }
727
728    /// Configuration text read the way an environment variable is read.
729    ///
730    /// Total by contract: every input is a value, none is an error and none
731    /// is a panic. The rendered form rather than the tree, so the target
732    /// needs no value type — and so a reading that changes shows up as a
733    /// changed string rather than as nothing at all.
734    #[must_use]
735    pub fn text_value(text: &str) -> String {
736        format!("{:?}", crate::text_value::from_text(text))
737    }
738
739    /// INI text through the real provider: the parsed table, or the error.
740    ///
741    /// Reads no file — the fuzzer supplies the bytes. What must hold: no
742    /// panic, and an `Err` never echoes document content (the redaction
743    /// harness asserts that separately).
744    #[cfg(feature = "ini")]
745    pub fn ini_document(text: &str) -> Result<usize, String> {
746        crate::loader::__fuzz_ini(text)
747            .map(|document| match document {
748                crate::Value::Table(table) => table.len(),
749                _ => 0,
750            })
751            .map_err(|error| error.to_string())
752    }
753
754    /// Properties text through the real provider, same contract.
755    #[cfg(feature = "properties")]
756    pub fn properties_document(text: &str) -> Result<usize, String> {
757        crate::loader::__fuzz_properties(text)
758            .map(|document| match document {
759                crate::Value::Table(table) => table.len(),
760                _ => 0,
761            })
762            .map_err(|error| error.to_string())
763    }
764
765    /// `.env` text into `KEY` → `value`, or the one-based line that stopped it.
766    ///
767    /// Reads no file: the fuzzer supplies the bytes a file would have held.
768    #[cfg(feature = "dotenv")]
769    pub fn dotenv_entries(text: &str) -> Result<std::collections::BTreeMap<String, String>, usize> {
770        crate::dotenv::parse(text)
771    }
772
773    /// Whether a profile can only ever name a sibling of the file it applies to.
774    ///
775    /// The guard; [`profile_variant`] is what it guards. A profile arrives from
776    /// an environment variable and is interpolated into a file name, so the
777    /// property worth fuzzing is the pair: anything this accepts must leave
778    /// [`profile_variant`] naming a path in the same directory it started in.
779    #[must_use]
780    pub fn profile_is_safe(profile: &str) -> bool {
781        crate::loader::sections::profile_is_safe(profile)
782    }
783
784    /// `config.toml` + `production` → `config.production.toml`.
785    ///
786    /// Strings rather than paths on both sides, so a target can hand over
787    /// generated bytes without building an `OsString` first. `None` where the
788    /// name has no extension to put the profile under.
789    #[must_use]
790    pub fn profile_variant(path: &str, profile: &str) -> Option<String> {
791        crate::loader::sections::profile_variant(std::path::Path::new(path), profile)
792            .map(|variant| variant.display().to_string())
793    }
794}
795
796/// Not public API. Lets the generated code name `serde` without the caller
797/// having to depend on it under that exact name.
798#[doc(hidden)]
799pub mod __private {
800    #[cfg(feature = "clap")]
801    pub use clap;
802    #[cfg(feature = "schema")]
803    pub use schemars;
804    pub use serde;
805    #[cfg(feature = "schema")]
806    pub use serde_json;
807}
808
809/// Not public API.
810///
811/// A reload a remote watch caused. Worded to name the trigger, because a
812/// program watching both files and a store wants its log to say which one
813/// moved.
814#[doc(hidden)]
815pub fn __log_remote_reload(name: &str, summary: Option<&str>) {
816    match summary {
817        Some(summary) => crate::log::info!("{name}: reloaded from the remote store, {summary}"),
818        None => crate::log::info!("{name}: reloaded from the remote store"),
819    }
820}
821
822/// Not public API.
823///
824/// A document the store pushed that this program cannot use. Logged as well as
825/// returned: the loop that called this has nobody to hand an error to either,
826/// and a store quietly serving a configuration nothing accepts is worth a line.
827#[doc(hidden)]
828pub fn __log_remote_failure(name: &str, error: &Error) {
829    crate::log::warning!(
830        "{name}: the remote store's document did not apply, keeping the previous \
831         snapshot: {error}"
832    );
833}