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 group;
281mod layer;
282mod loader;
283mod log;
284pub mod reader;
285mod redirects;
286mod registry;
287mod reload;
288mod remote;
289// The composition the loader moves onto next: the layers already build this
290// crate's trees, and what remains is handing them here in precedence order
291// instead of to the backend. Exercised meanwhile by its own differential
292// test, which composes the same layers both ways and compares the tree and
293// the winner of every leaf.
294#[allow(dead_code)]
295mod resolve;
296#[cfg(feature = "schema")]
297#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
298pub mod schema;
299mod ser;
300mod snapshot;
301mod source;
302pub(crate) mod sync;
303#[cfg(feature = "telemetry")]
304#[cfg_attr(docsrs, doc(cfg(feature = "telemetry")))]
305pub mod telemetry;
306// The same module with only its `tracing` half compiled, and private: the
307// reload events are emitted from inside this crate, so they need no public
308// surface to reach.
309#[cfg(all(feature = "tracing", not(feature = "telemetry")))]
310mod telemetry;
311mod text_value;
312mod units;
313mod value;
314mod write;
315
316#[cfg(feature = "watch")]
317#[cfg_attr(docsrs, doc(cfg(feature = "watch")))]
318pub mod watch;
319
320/// Not public API: the loom suite drives the wake protocol directly.
321#[cfg(all(feature = "async", loom))]
322#[doc(hidden)]
323pub use asynchronous::Notify as LoomNotify;
324#[cfg(feature = "async")]
325#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
326pub use asynchronous::{set_blocking_executor, BlockingExecutor, Changes, Event, Events};
327/// figment itself, re-exported.
328///
329/// So that writing a [`Source::provider`] needs no direct dependency, and no
330/// second version of figment in the graph. This is the one place figment
331/// appears in this crate's API, which is why it is behind a feature.
332#[cfg(feature = "figment")]
333#[cfg_attr(docsrs, doc(cfg(feature = "figment")))]
334pub use figment;
335pub use log::{clear_log_sink, set_log_level, set_log_sink, LogLevel, LogSink};
336
337pub use aliases::Aliases;
338pub use bindings::EnvBindings;
339pub use builder::Builder;
340#[doc(hidden)]
341pub use builder::Configured;
342pub use cache::{CacheMode, Recovery};
343pub use cell::{ConfigCell, HookGuard, SnapshotMeta};
344pub use check::{check, Report, Resolved, UnknownKey};
345#[cfg(feature = "decrypt")]
346#[cfg_attr(docsrs, doc(cfg(feature = "decrypt")))]
347pub use decrypt::{has_decryptor, set_decryptor, Decryptor, Encryptor};
348pub use discovery::Search;
349pub use dynamic::Dynamic;
350pub use error::{Error, ErrorKind, Origin};
351pub use explain::{Contribution, Explanation};
352pub use group::{Commit, ReloadGroup, Reloadable};
353pub use layer::Layer;
354pub use registry::Registry;
355pub use reload::{ConfigStatus, FailureStatus, ReloadEvent, ReloadReason};
356#[cfg(feature = "async")]
357#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
358pub use remote::AsyncRemoteSource;
359pub use remote::{
360    Fetched, Pace, Remote, RemoteSink, RemoteSource, RemoteStatus, RemoteWatch, WatchCapability,
361    Watching,
362};
363pub use snapshot::{changed_paths, Change, ChangeKind, Snapshot};
364pub use source::{Format, LoadSpec, Source, DEFAULT_NEST};
365pub use units::{bytes, duration};
366pub use value::Value;
367
368/// This crate's version, for anything that embeds it and has to say so.
369///
370/// A language binding versions on its own schedule — its users read a
371/// different changelog — so "which engine is inside this wheel" stops
372/// being answerable from the outside. This answers it.
373pub const VERSION: &str = env!("CARGO_PKG_VERSION");
374
375/// Whether `path` touches a secret, given the secret list a type or a
376/// binding declared.
377///
378/// Three ways it can, and all three have to redact or the diagnostic
379/// leaks: the path *is* a secret, the path is *under* one (every path
380/// below a secret field is the secret's), or the path is an *ancestor* of
381/// one — asking to explain `credentials` must not render the password
382/// nested inside it. Secrets are named by a plain field for a
383/// `#[config(secret)]` field, and by a dotted path when they live inside
384/// a nested structure, which is what a language binding derives from a
385/// nested model.
386#[doc(hidden)]
387#[must_use]
388pub fn touches_secret(path: &str, secrets: &[impl AsRef<str>]) -> bool {
389    secrets.iter().any(|secret| {
390        let secret = secret.as_ref();
391
392        secret == path
393            || path
394                .strip_prefix(secret)
395                .is_some_and(|rest| rest.starts_with('.'))
396            || secret
397                .strip_prefix(path)
398                .is_some_and(|rest| rest.starts_with('.'))
399    })
400}
401#[cfg(feature = "decrypt")]
402#[cfg_attr(docsrs, doc(cfg(feature = "decrypt")))]
403pub use write::save_encrypted;
404pub use write::{save, save_new};
405
406/// Turns a struct into a hot-reloadable configuration snapshot.
407///
408/// See the [crate documentation](crate) for the full guide.
409///
410/// The attribute takes **no arguments**: it declares that the type *is* a
411/// configuration, and generates its storage and surface. Where the
412/// configuration comes from is stated on the [`Builder`] the generated
413/// `builder(key)` returns — see the front page for the shape, and [the
414/// book's
415/// reference](https://dynamic-config-rs.github.io/attribute-reference.html)
416/// for every method. An argument between the parentheses is a compile
417/// error whose message maps each old argument to its builder method.
418///
419/// One field attribute: `#[config(secret)]` generates a `Debug` that prints
420/// `***` for the marked fields, forbids `#[derive(Debug)]` alongside it,
421/// keeps the field out of the redacted cache, and marks it `writeOnly` in
422/// the schema.
423///
424/// # Requirements
425///
426/// The annotated struct must implement `serde::Deserialize` and be
427/// `Send + Sync + 'static`. Type and const parameters are supported — those go
428/// through a `TypeId` registry rather than a `static`, at a measured cost of
429/// roughly 10 ns per read. A **lifetime** parameter is rejected at compile
430/// time: the snapshot outlives every borrow that could name one.
431pub use dynamic_config_macros::dynamic_config;
432
433use serde::de::DeserializeOwned;
434
435/// Reads and deserializes a configuration section.
436///
437/// This is what the generated `load()` calls. Missing files are skipped;
438/// everything else — a parse failure, a missing required field, a value that
439/// cannot become the requested type — is an [`Error`] naming the key path and
440/// the source it came from.
441///
442/// # Errors
443///
444/// See [`ErrorKind`] for the categories.
445///
446/// # Example
447///
448/// ```
449/// # #[cfg(feature = "json")] {
450/// use dynamic_config::{load, Format, LoadSpec, Source};
451/// use serde::Deserialize;
452///
453/// #[derive(Deserialize)]
454/// struct Server { port: u16 }
455///
456/// let sources = [Source::inline(r#"{"server": {"port": 8080}}"#, Format::Json)];
457/// let server: Server = load(&LoadSpec::new("server", &sources).with_env("APP_"))
458///     .expect("the inline document is well formed");
459///
460/// assert_eq!(server.port, 8080);
461/// # }
462/// ```
463pub fn load<T: DeserializeOwned>(spec: &LoadSpec<'_>) -> Result<T, Error> {
464    loader::load(spec)
465}
466
467/// Resolves the section without deserializing it.
468///
469/// Two snapshots can be compared with [`Snapshot::diff`], which is how a reload
470/// reports *which* keys changed rather than only that something did.
471///
472/// # Errors
473///
474/// If a source cannot be read or parsed — the same failures as [`load`].
475pub fn snapshot(spec: &LoadSpec<'_>) -> Result<Snapshot, Error> {
476    loader::snapshot(spec)
477}
478
479/// Where the value at `path` would come from, if anything supplies it.
480///
481/// This is the answer to the question every configuration bug starts with:
482/// *which layer set this?* It re-reads the sources, so it reports what the
483/// **next** load would see rather than what the current snapshot holds.
484///
485/// `path` is dotted and relative to the section, as in `"pool.max_size"`.
486///
487/// # Errors
488///
489/// If a source cannot be read or parsed — the same failures as [`load`].
490///
491/// # Example
492///
493/// ```
494/// # #[cfg(feature = "json")] {
495/// use dynamic_config::{source_of, Format, LoadSpec, Origin, Source};
496///
497/// let sources = [Source::inline(r#"{"db": {"host": "localhost"}}"#, Format::Json)];
498/// let spec = LoadSpec::new("db", &sources);
499///
500/// assert_eq!(source_of(&spec, "host").unwrap(), Some(Origin::Inline));
501/// assert_eq!(source_of(&spec, "port").unwrap(), None);
502/// # }
503/// ```
504pub fn source_of(spec: &LoadSpec<'_>, path: &str) -> Result<Option<Origin>, Error> {
505    loader::source_of(spec, path)
506}
507
508/// Explains `path`: every configured layer's answer, not just the winner's.
509///
510/// The rendered [`Explanation`] **contains values** — that is its point; you
511/// asked. It is the one diagnostic in this crate that does, so treat its
512/// output accordingly. A path the caller knows to be sensitive goes through
513/// [`Explanation::redacted`]; the generated `explain()` does that for
514/// `#[config(secret)]` fields automatically.
515///
516/// # Errors
517///
518/// Whatever reading the sources reports — the same failures a load would hit.
519///
520/// # Example
521///
522/// ```
523/// # #[cfg(feature = "json")] {
524/// use dynamic_config::{explain, Format, LoadSpec, Source};
525///
526/// let sources = [Source::inline(r#"{"db": {"port": 5432}}"#, Format::Json)];
527/// let explanation = explain(&LoadSpec::new("db", &sources), "port")
528///     .expect("the inline document is well formed");
529///
530/// assert_eq!(explanation.winner().unwrap().layer, "file");
531/// println!("{explanation}");
532/// # }
533/// ```
534pub fn explain(spec: &LoadSpec<'_>, path: &str) -> Result<Explanation, Error> {
535    explain::explain(spec, path)
536}
537
538/// Whether anything supplies `path`.
539///
540/// Distinguishes "absent" from "present but falsy", which
541/// `#[serde(default)]` cannot.
542///
543/// # Errors
544///
545/// If a source cannot be read or parsed — the same failures as [`load`].
546pub fn is_set(spec: &LoadSpec<'_>, path: &str) -> Result<bool, Error> {
547    loader::is_set(spec, path)
548}
549
550/// [`load`], moved off the async executor.
551///
552/// Reading configuration touches the filesystem, which would block the worker
553/// it runs on. Where the work actually goes depends on what is available:
554/// tokio's blocking pool with the `tokio` feature, an executor installed by
555/// [`set_blocking_executor`], or a freshly spawned thread. A configuration load
556/// happens at startup and on reload, so a thread per call is a real answer
557/// rather than a placeholder.
558///
559/// `LoadSpec<'static>` is taken by value because the work outlives the call;
560/// the spec the macro emits satisfies that for free.
561///
562/// # Errors
563///
564/// Same as [`load`], plus an [`ErrorKind::Backend`] error if the work never
565/// produced a result — a panic inside it, or a runtime shutting down.
566#[cfg(feature = "async")]
567#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
568pub async fn load_async<T>(spec: LoadSpec<'static>) -> Result<T, Error>
569where
570    T: DeserializeOwned + Send + 'static,
571{
572    off_thread(move || load(&spec)).await
573}
574
575/// Runs blocking configuration work without blocking the caller's executor.
576///
577/// See [`load_async`] for where the work goes.
578///
579/// # Errors
580///
581/// Whatever `work` returns, plus [`ErrorKind::Backend`] if it never produced a
582/// result.
583#[cfg(feature = "async")]
584#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
585pub async fn off_thread<T, F>(work: F) -> Result<T, Error>
586where
587    F: FnOnce() -> Result<T, Error> + Send + 'static,
588    T: Send + 'static,
589{
590    asynchronous::off_thread(work).await
591}
592
593// ---------------------------------------------------------------------------
594// Support items used by the generated code. The redirect *macros* — the
595// feature-gated `__*!` wall — live in `redirects`; the functions stay here
596// because they are reached by path, and a path names the module it lives in.
597// ---------------------------------------------------------------------------
598/// Not public API. The parsers a fuzz target needs and a caller does not.
599///
600/// Three of this crate's parsing surfaces are private because nothing outside
601/// it has any business calling them — and `fuzz/fuzz_targets/` is outside it.
602/// The choice is between publishing them (a stability promise nobody wants for
603/// a `.env` line splitter) and leaving the surfaces whose failure mode is a
604/// crash at startup or a path traversal covered only by proptest. This is the
605/// third way: reachable by path, absent from the book and from rustdoc, and
606/// carrying no more of a compatibility promise than [`__private`] does.
607///
608/// The public parse seam needs nothing here — [`Value::parse`] is fuzzable as
609/// it stands.
610#[doc(hidden)]
611pub mod __fuzz {
612    /// One load, composed by every engine this build has.
613    ///
614    /// `(engine name, rendered tree)` per engine, in a fixed order — so a
615    /// test can assert that they all say the same thing, and name the one
616    /// that drifted when they do not. Rendered rather than returned as a
617    /// tree, so the harness needs no value type and a divergence shows up
618    /// as a changed string.
619    ///
620    /// This is the whole-stack differential: the engines fold the *same*
621    /// collected layers, which is the only way the comparison is about the
622    /// fold rather than about the walk in front of it.
623    #[must_use]
624    pub fn compositions(spec: &crate::LoadSpec<'_>) -> Vec<(String, String)> {
625        crate::engine::all()
626            .into_iter()
627            .map(|engine| {
628                let rendered = crate::loader::contributions(spec)
629                    .and_then(|mut collected| {
630                        crate::resolve::compose(engine, collected.take_layers())
631                    })
632                    .map_or_else(
633                        |error| format!("error: {error}"),
634                        |(tree, _)| format!("{:?}", crate::Value::Table(tree)),
635                    );
636
637                (engine.name().to_owned(), rendered)
638            })
639            .collect()
640    }
641
642    /// The same load, composed by every engine, with the winner of every
643    /// leaf as well as the value.
644    ///
645    /// The trees agreeing is half the contract; §4 is the other half, and a
646    /// provenance drift is invisible in a rendered tree.
647    #[must_use]
648    pub fn provenances(spec: &crate::LoadSpec<'_>) -> Vec<(String, String)> {
649        crate::engine::all()
650            .into_iter()
651            .map(|engine| {
652                let rendered = crate::loader::contributions(spec)
653                    .and_then(|mut collected| {
654                        crate::resolve::compose(engine, collected.take_layers())
655                    })
656                    .map_or_else(
657                        |error| format!("error: {error}"),
658                        |(_, provenance)| {
659                            provenance
660                                .iter()
661                                .map(|(path, origin)| format!("{path} = {origin}"))
662                                .collect::<Vec<_>>()
663                                .join("\n")
664                        },
665                    );
666
667                (engine.name().to_owned(), rendered)
668            })
669            .collect()
670    }
671
672    /// A stack of layer trees, through one engine and the crate's own
673    /// provenance repair — the whole seam, without a filesystem.
674    ///
675    /// `(tree, leaf path → the index of the layer that supplied it)`. What
676    /// the engine-agreement tests drive: a fold reached this way is the fold
677    /// a load gets, repair included.
678    ///
679    /// # Errors
680    ///
681    /// If the engine refuses the layers.
682    pub fn fold_through(
683        engine: &'static dyn crate::engine::Engine,
684        layers: &[crate::Value],
685    ) -> Result<(crate::Value, std::collections::BTreeMap<String, String>), crate::Error> {
686        let contributions = layers
687            .iter()
688            .enumerate()
689            .map(|(index, values)| {
690                let table = match values {
691                    crate::Value::Table(table) => table.clone(),
692                    _ => std::collections::BTreeMap::new(),
693                };
694
695                // The index as the origin, so a disagreement about *which*
696                // layer won reads as a number rather than as a file name
697                // nobody chose.
698                crate::resolve::Contribution::new(
699                    "test",
700                    crate::Origin::Env(index.to_string()),
701                    table,
702                )
703            })
704            .collect();
705
706        // `Always`: this door exists to drive the *engine*, and `compose`
707        // answers a single layer itself. A corpus case with one layer would
708        // otherwise compare two answers no engine produced.
709        let (tree, provenance) =
710            crate::resolve::compose_with(engine, contributions, crate::resolve::Fold::Always)?;
711
712        Ok((
713            crate::Value::Table(tree),
714            provenance
715                .into_iter()
716                .map(|(path, origin)| (path, origin.to_string()))
717                .collect(),
718        ))
719    }
720
721    /// Configuration text read the way an environment variable is read.
722    ///
723    /// Total by contract: every input is a value, none is an error and none
724    /// is a panic. The rendered form rather than the tree, so the target
725    /// needs no value type — and so a reading that changes shows up as a
726    /// changed string rather than as nothing at all.
727    #[must_use]
728    pub fn text_value(text: &str) -> String {
729        format!("{:?}", crate::text_value::from_text(text))
730    }
731
732    /// INI text through the real provider: the parsed table, or the error.
733    ///
734    /// Reads no file — the fuzzer supplies the bytes. What must hold: no
735    /// panic, and an `Err` never echoes document content (the redaction
736    /// harness asserts that separately).
737    #[cfg(feature = "ini")]
738    pub fn ini_document(text: &str) -> Result<usize, String> {
739        crate::loader::__fuzz_ini(text)
740            .map(|document| match document {
741                crate::Value::Table(table) => table.len(),
742                _ => 0,
743            })
744            .map_err(|error| error.to_string())
745    }
746
747    /// Properties text through the real provider, same contract.
748    #[cfg(feature = "properties")]
749    pub fn properties_document(text: &str) -> Result<usize, String> {
750        crate::loader::__fuzz_properties(text)
751            .map(|document| match document {
752                crate::Value::Table(table) => table.len(),
753                _ => 0,
754            })
755            .map_err(|error| error.to_string())
756    }
757
758    /// `.env` text into `KEY` → `value`, or the one-based line that stopped it.
759    ///
760    /// Reads no file: the fuzzer supplies the bytes a file would have held.
761    #[cfg(feature = "dotenv")]
762    pub fn dotenv_entries(text: &str) -> Result<std::collections::BTreeMap<String, String>, usize> {
763        crate::dotenv::parse(text)
764    }
765
766    /// Whether a profile can only ever name a sibling of the file it applies to.
767    ///
768    /// The guard; [`profile_variant`] is what it guards. A profile arrives from
769    /// an environment variable and is interpolated into a file name, so the
770    /// property worth fuzzing is the pair: anything this accepts must leave
771    /// [`profile_variant`] naming a path in the same directory it started in.
772    #[must_use]
773    pub fn profile_is_safe(profile: &str) -> bool {
774        crate::loader::sections::profile_is_safe(profile)
775    }
776
777    /// `config.toml` + `production` → `config.production.toml`.
778    ///
779    /// Strings rather than paths on both sides, so a target can hand over
780    /// generated bytes without building an `OsString` first. `None` where the
781    /// name has no extension to put the profile under.
782    #[must_use]
783    pub fn profile_variant(path: &str, profile: &str) -> Option<String> {
784        crate::loader::sections::profile_variant(std::path::Path::new(path), profile)
785            .map(|variant| variant.display().to_string())
786    }
787}
788
789/// Not public API. Lets the generated code name `serde` without the caller
790/// having to depend on it under that exact name.
791#[doc(hidden)]
792pub mod __private {
793    #[cfg(feature = "clap")]
794    pub use clap;
795    #[cfg(feature = "schema")]
796    pub use schemars;
797    pub use serde;
798    #[cfg(feature = "schema")]
799    pub use serde_json;
800}
801
802/// Not public API.
803///
804/// A reload a remote watch caused. Worded to name the trigger, because a
805/// program watching both files and a store wants its log to say which one
806/// moved.
807#[doc(hidden)]
808pub fn __log_remote_reload(name: &str, summary: Option<&str>) {
809    match summary {
810        Some(summary) => crate::log::info!("{name}: reloaded from the remote store, {summary}"),
811        None => crate::log::info!("{name}: reloaded from the remote store"),
812    }
813}
814
815/// Not public API.
816///
817/// A document the store pushed that this program cannot use. Logged as well as
818/// returned: the loop that called this has nobody to hand an error to either,
819/// and a store quietly serving a configuration nothing accepts is worth a line.
820#[doc(hidden)]
821pub fn __log_remote_failure(name: &str, error: &Error) {
822    crate::log::warning!(
823        "{name}: the remote store's document did not apply, keeping the previous \
824         snapshot: {error}"
825    );
826}