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://ctolon.github.io/dynamic-config/).
43//!
44//! # What the attribute generates
45//!
46//! The attribute declares; the builder configures. What gets generated is
47//! the type-bound surface:
48//!
49//! | Method | Description |
50//! |---|---|
51//! | `builder(key) -> Builder<Self>` | Where everything starts: state the sources, `init()`. |
52//! | `current() -> Arc<Self>` | The current snapshot. Panics before an install. |
53//! | `try_current() -> Option<Arc<Self>>` | The current snapshot, or `None`. |
54//! | `replace(Self)` | Atomically swap in a new snapshot. |
55//! | `on_reload(f)` | Run a callback on every later reload, for the life of the process. |
56//! | `on_reload_scoped(f) -> HookGuard` | The same, until the guard is dropped. |
57//! | `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://ctolon.github.io/dynamic-config/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;
321
322pub use aliases::Aliases;
323pub use bindings::EnvBindings;
324pub use builder::Builder;
325#[doc(hidden)]
326pub use builder::Configured;
327pub use cache::{CacheMode, Recovery};
328pub use cell::{ConfigCell, HookGuard, SnapshotMeta};
329pub use check::{check, Report, Resolved, UnknownKey};
330#[cfg(feature = "decrypt")]
331#[cfg_attr(docsrs, doc(cfg(feature = "decrypt")))]
332pub use decrypt::{has_decryptor, set_decryptor, Decryptor, Encryptor};
333pub use discovery::Search;
334pub use dynamic::Dynamic;
335pub use error::{Error, ErrorKind, Origin};
336pub use explain::{Contribution, Explanation};
337pub use group::{Commit, ReloadGroup, Reloadable};
338pub use layer::Layer;
339pub use registry::Registry;
340pub use reload::{ConfigStatus, FailureStatus, ReloadEvent, ReloadReason};
341#[cfg(feature = "async")]
342#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
343pub use remote::AsyncRemoteSource;
344pub use remote::{Fetched, Remote, RemoteSink, RemoteSource, RemoteStatus, RemoteWatch, Watching};
345pub use snapshot::{changed_paths, Change, ChangeKind, Snapshot};
346pub use source::{Format, LoadSpec, Source, DEFAULT_NEST};
347pub use units::{bytes, duration};
348pub use value::Value;
349
350/// This crate's version, for anything that embeds it and has to say so.
351///
352/// A language binding versions on its own schedule — its users read a
353/// different changelog — so "which engine is inside this wheel" stops
354/// being answerable from the outside. This answers it.
355pub const VERSION: &str = env!("CARGO_PKG_VERSION");
356
357/// Whether `path` touches a secret, given the secret list a type or a
358/// binding declared.
359///
360/// Three ways it can, and all three have to redact or the diagnostic
361/// leaks: the path *is* a secret, the path is *under* one (every path
362/// below a secret field is the secret's), or the path is an *ancestor* of
363/// one — asking to explain `credentials` must not render the password
364/// nested inside it. Secrets are named by a plain field for a
365/// `#[config(secret)]` field, and by a dotted path when they live inside
366/// a nested structure, which is what a language binding derives from a
367/// nested model.
368#[doc(hidden)]
369#[must_use]
370pub fn touches_secret(path: &str, secrets: &[impl AsRef<str>]) -> bool {
371    secrets.iter().any(|secret| {
372        let secret = secret.as_ref();
373
374        secret == path
375            || path
376                .strip_prefix(secret)
377                .is_some_and(|rest| rest.starts_with('.'))
378            || secret
379                .strip_prefix(path)
380                .is_some_and(|rest| rest.starts_with('.'))
381    })
382}
383#[cfg(feature = "decrypt")]
384#[cfg_attr(docsrs, doc(cfg(feature = "decrypt")))]
385pub use write::save_encrypted;
386pub use write::{save, save_new};
387
388/// Turns a struct into a hot-reloadable configuration snapshot.
389///
390/// See the [crate documentation](crate) for the full guide.
391///
392/// The attribute takes **no arguments**: it declares that the type *is* a
393/// configuration, and generates its storage and surface. Where the
394/// configuration comes from is stated on the [`Builder`] the generated
395/// `builder(key)` returns — see the front page for the shape, and [the
396/// book's
397/// reference](https://ctolon.github.io/dynamic-config/attribute-reference.html)
398/// for every method. An argument between the parentheses is a compile
399/// error whose message maps each old argument to its builder method.
400///
401/// One field attribute: `#[config(secret)]` generates a `Debug` that prints
402/// `***` for the marked fields, forbids `#[derive(Debug)]` alongside it,
403/// keeps the field out of the redacted cache, and marks it `writeOnly` in
404/// the schema.
405///
406/// # Requirements
407///
408/// The annotated struct must implement `serde::Deserialize` and be
409/// `Send + Sync + 'static`. Type and const parameters are supported — those go
410/// through a `TypeId` registry rather than a `static`, at a measured cost of
411/// roughly 10 ns per read. A **lifetime** parameter is rejected at compile
412/// time: the snapshot outlives every borrow that could name one.
413pub use dynamic_config_macros::dynamic_config;
414
415use serde::de::DeserializeOwned;
416
417/// Reads and deserializes a configuration section.
418///
419/// This is what the generated `load()` calls. Missing files are skipped;
420/// everything else — a parse failure, a missing required field, a value that
421/// cannot become the requested type — is an [`Error`] naming the key path and
422/// the source it came from.
423///
424/// # Errors
425///
426/// See [`ErrorKind`] for the categories.
427///
428/// # Example
429///
430/// ```
431/// # #[cfg(feature = "json")] {
432/// use dynamic_config::{load, Format, LoadSpec, Source};
433/// use serde::Deserialize;
434///
435/// #[derive(Deserialize)]
436/// struct Server { port: u16 }
437///
438/// let sources = [Source::inline(r#"{"server": {"port": 8080}}"#, Format::Json)];
439/// let server: Server = load(&LoadSpec::new("server", &sources).with_env("APP_"))
440///     .expect("the inline document is well formed");
441///
442/// assert_eq!(server.port, 8080);
443/// # }
444/// ```
445pub fn load<T: DeserializeOwned>(spec: &LoadSpec<'_>) -> Result<T, Error> {
446    loader::load(spec)
447}
448
449/// Resolves the section without deserializing it.
450///
451/// Two snapshots can be compared with [`Snapshot::diff`], which is how a reload
452/// reports *which* keys changed rather than only that something did.
453///
454/// # Errors
455///
456/// If a source cannot be read or parsed — the same failures as [`load`].
457pub fn snapshot(spec: &LoadSpec<'_>) -> Result<Snapshot, Error> {
458    loader::snapshot(spec)
459}
460
461/// Where the value at `path` would come from, if anything supplies it.
462///
463/// This is the answer to the question every configuration bug starts with:
464/// *which layer set this?* It re-reads the sources, so it reports what the
465/// **next** load would see rather than what the current snapshot holds.
466///
467/// `path` is dotted and relative to the section, as in `"pool.max_size"`.
468///
469/// # Errors
470///
471/// If a source cannot be read or parsed — the same failures as [`load`].
472///
473/// # Example
474///
475/// ```
476/// # #[cfg(feature = "json")] {
477/// use dynamic_config::{source_of, Format, LoadSpec, Origin, Source};
478///
479/// let sources = [Source::inline(r#"{"db": {"host": "localhost"}}"#, Format::Json)];
480/// let spec = LoadSpec::new("db", &sources);
481///
482/// assert_eq!(source_of(&spec, "host").unwrap(), Some(Origin::Inline));
483/// assert_eq!(source_of(&spec, "port").unwrap(), None);
484/// # }
485/// ```
486pub fn source_of(spec: &LoadSpec<'_>, path: &str) -> Result<Option<Origin>, Error> {
487    loader::source_of(spec, path)
488}
489
490/// Explains `path`: every configured layer's answer, not just the winner's.
491///
492/// The rendered [`Explanation`] **contains values** — that is its point; you
493/// asked. It is the one diagnostic in this crate that does, so treat its
494/// output accordingly. A path the caller knows to be sensitive goes through
495/// [`Explanation::redacted`]; the generated `explain()` does that for
496/// `#[config(secret)]` fields automatically.
497///
498/// # Errors
499///
500/// Whatever reading the sources reports — the same failures a load would hit.
501///
502/// # Example
503///
504/// ```
505/// # #[cfg(feature = "json")] {
506/// use dynamic_config::{explain, Format, LoadSpec, Source};
507///
508/// let sources = [Source::inline(r#"{"db": {"port": 5432}}"#, Format::Json)];
509/// let explanation = explain(&LoadSpec::new("db", &sources), "port")
510///     .expect("the inline document is well formed");
511///
512/// assert_eq!(explanation.winner().unwrap().layer, "file");
513/// println!("{explanation}");
514/// # }
515/// ```
516pub fn explain(spec: &LoadSpec<'_>, path: &str) -> Result<Explanation, Error> {
517    explain::explain(spec, path)
518}
519
520/// Whether anything supplies `path`.
521///
522/// Distinguishes "absent" from "present but falsy", which
523/// `#[serde(default)]` cannot.
524///
525/// # Errors
526///
527/// If a source cannot be read or parsed — the same failures as [`load`].
528pub fn is_set(spec: &LoadSpec<'_>, path: &str) -> Result<bool, Error> {
529    loader::is_set(spec, path)
530}
531
532/// [`load`], moved off the async executor.
533///
534/// Reading configuration touches the filesystem, which would block the worker
535/// it runs on. Where the work actually goes depends on what is available:
536/// tokio's blocking pool with the `tokio` feature, an executor installed by
537/// [`set_blocking_executor`], or a freshly spawned thread. A configuration load
538/// happens at startup and on reload, so a thread per call is a real answer
539/// rather than a placeholder.
540///
541/// `LoadSpec<'static>` is taken by value because the work outlives the call;
542/// the spec the macro emits satisfies that for free.
543///
544/// # Errors
545///
546/// Same as [`load`], plus an [`ErrorKind::Backend`] error if the work never
547/// produced a result — a panic inside it, or a runtime shutting down.
548#[cfg(feature = "async")]
549#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
550pub async fn load_async<T>(spec: LoadSpec<'static>) -> Result<T, Error>
551where
552    T: DeserializeOwned + Send + 'static,
553{
554    off_thread(move || load(&spec)).await
555}
556
557/// Runs blocking configuration work without blocking the caller's executor.
558///
559/// See [`load_async`] for where the work goes.
560///
561/// # Errors
562///
563/// Whatever `work` returns, plus [`ErrorKind::Backend`] if it never produced a
564/// result.
565#[cfg(feature = "async")]
566#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
567pub async fn off_thread<T, F>(work: F) -> Result<T, Error>
568where
569    F: FnOnce() -> Result<T, Error> + Send + 'static,
570    T: Send + 'static,
571{
572    asynchronous::off_thread(work).await
573}
574
575// ---------------------------------------------------------------------------
576// Support items used by the generated code. The redirect *macros* — the
577// feature-gated `__*!` wall — live in `redirects`; the functions stay here
578// because they are reached by path, and a path names the module it lives in.
579// ---------------------------------------------------------------------------
580/// Not public API. The parsers a fuzz target needs and a caller does not.
581///
582/// Three of this crate's parsing surfaces are private because nothing outside
583/// it has any business calling them — and `fuzz/fuzz_targets/` is outside it.
584/// The choice is between publishing them (a stability promise nobody wants for
585/// a `.env` line splitter) and leaving the surfaces whose failure mode is a
586/// crash at startup or a path traversal covered only by proptest. This is the
587/// third way: reachable by path, absent from the book and from rustdoc, and
588/// carrying no more of a compatibility promise than [`__private`] does.
589///
590/// The public parse seam needs nothing here — [`Value::parse`] is fuzzable as
591/// it stands.
592#[doc(hidden)]
593pub mod __fuzz {
594    /// `.env` text into `KEY` → `value`, or the one-based line that stopped it.
595    ///
596    /// Reads no file: the fuzzer supplies the bytes a file would have held.
597    #[cfg(feature = "dotenv")]
598    pub fn dotenv_entries(text: &str) -> Result<std::collections::BTreeMap<String, String>, usize> {
599        crate::dotenv::parse(text)
600    }
601
602    /// A top-level key as the profile the loader files that section under.
603    ///
604    /// The 0.4 bug was here: an unprefixed mapping handed figment's reserved
605    /// `global` and `default` profiles to any document with an innocently named
606    /// table.
607    #[must_use]
608    pub fn section_profile(key: &str) -> String {
609        crate::loader::section_profile(key)
610    }
611
612    /// Whether a profile can only ever name a sibling of the file it applies to.
613    ///
614    /// The guard; [`profile_variant`] is what it guards. A profile arrives from
615    /// an environment variable and is interpolated into a file name, so the
616    /// property worth fuzzing is the pair: anything this accepts must leave
617    /// [`profile_variant`] naming a path in the same directory it started in.
618    #[must_use]
619    pub fn profile_is_safe(profile: &str) -> bool {
620        crate::loader::sections::profile_is_safe(profile)
621    }
622
623    /// `config.toml` + `production` → `config.production.toml`.
624    ///
625    /// Strings rather than paths on both sides, so a target can hand over
626    /// generated bytes without building an `OsString` first. `None` where the
627    /// name has no extension to put the profile under.
628    #[must_use]
629    pub fn profile_variant(path: &str, profile: &str) -> Option<String> {
630        crate::loader::sections::profile_variant(std::path::Path::new(path), profile)
631            .map(|variant| variant.display().to_string())
632    }
633}
634
635/// Not public API. Lets the generated code name `serde` without the caller
636/// having to depend on it under that exact name.
637#[doc(hidden)]
638pub mod __private {
639    #[cfg(feature = "clap")]
640    pub use clap;
641    #[cfg(feature = "schema")]
642    pub use schemars;
643    pub use serde;
644    #[cfg(feature = "schema")]
645    pub use serde_json;
646}
647
648/// Not public API.
649///
650/// A reload a remote watch caused. Worded to name the trigger, because a
651/// program watching both files and a store wants its log to say which one
652/// moved.
653#[doc(hidden)]
654pub fn __log_remote_reload(name: &str, summary: Option<&str>) {
655    match summary {
656        Some(summary) => crate::log::info!("{name}: reloaded from the remote store, {summary}"),
657        None => crate::log::info!("{name}: reloaded from the remote store"),
658    }
659}
660
661/// Not public API.
662///
663/// A document the store pushed that this program cannot use. Logged as well as
664/// returned: the loop that called this has nobody to hand an error to either,
665/// and a store quietly serving a configuration nothing accepts is worth a line.
666#[doc(hidden)]
667pub fn __log_remote_failure(name: &str, error: &Error) {
668    crate::log::warning!(
669        "{name}: the remote store's document did not apply, keeping the previous \
670         snapshot: {error}"
671    );
672}