Skip to main content

dynamic_config/
lib.rs

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