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}