Skip to main content

dynamic_config/
builder.rs

1//! Configuring a load at runtime — the builder half of the attribute split.
2//!
3//! The attribute declares that a type *is* a configuration; its arguments
4//! also say where the configuration comes from. This module is the first
5//! stage of separating those: a [`Builder`] owns the "where" — chosen at
6//! runtime, not compile time — and funnels into the same [`LoadSpec`] the
7//! attribute builds, so the two cannot drift apart on semantics.
8//!
9//! ```no_run
10//! # #[cfg(feature = "json")] {
11//! use dynamic_config::Builder;
12//! use serde::Deserialize;
13//!
14//! #[derive(Debug, Deserialize)]
15//! struct Db { host: String }
16//!
17//! let db: Db = Builder::new("db")
18//!     .file("config.json")
19//!     .env("APP_")
20//!     .load()
21//!     .expect("the sources read cleanly");
22//! # }
23//! ```
24//!
25//! On a `#[dynamic_config]` type, the generated `builder()` goes further:
26//! its `init()` installs the result as the type's snapshot, so runtime-
27//! chosen sources feed the same `current()` everything already reads.
28
29use std::marker::PhantomData;
30use std::path::Path;
31
32use serde::de::DeserializeOwned;
33
34use crate::cache::{CacheMode, Recovery};
35
36/// An application-level validation hook: deserialized, not yet installed.
37type Validator<T> = fn(&T) -> Result<(), Error>;
38use crate::error::{Error, ErrorKind};
39use crate::source::{Format, LoadSpec, Source};
40
41/// Runtime-chosen sources for one configuration section.
42///
43/// Methods take and return `self`, are infallible, and defer every check to
44/// [`load`](Self::load) — a missing file or an unsupported extension is a
45/// load-time answer, same as everywhere else in this crate.
46///
47/// What the builder configures in this stage is the source side: files,
48/// the environment layer, `.env` files, profiles. The runtime layers
49/// (`set_default`, `set_override`) and remote stores stay on the generated
50/// type, whose statics they live in.
51pub struct Builder<T> {
52    key: String,
53    files: Vec<(String, bool)>,
54    env: Option<String>,
55    nest: Option<String>,
56    allow_empty_env: bool,
57    strict_env: bool,
58    env_files: Vec<String>,
59    profile_env: Option<String>,
60    search: Option<(String, Vec<String>)>,
61    cache: Option<(String, CacheMode)>,
62    /// `Some` even when empty: knowing there are *no* secret fields is
63    /// knowledge, and only the generated `builder()` has it.
64    secrets: Option<Vec<String>>,
65    validate: Option<Validator<T>>,
66    fields: &'static [&'static str],
67    install: Option<fn(T)>,
68    /// Remembers this builder as the type's configuration on a successful
69    /// `init`, so `source_of`, `check`, `prepare` and friends can answer
70    /// later without being handed the builder again.
71    register: Option<fn(&Self)>,
72    defaults: Option<&'static crate::Layer>,
73    overrides: Option<&'static crate::Layer>,
74    flags: Option<&'static crate::Layer>,
75    bindings: Option<&'static crate::EnvBindings>,
76    aliases: Option<&'static crate::Aliases>,
77    remote: Option<&'static crate::Remote>,
78    _marker: PhantomData<fn() -> T>,
79}
80
81impl<T> Clone for Builder<T> {
82    fn clone(&self) -> Self {
83        Self {
84            key: self.key.clone(),
85            files: self.files.clone(),
86            env: self.env.clone(),
87            nest: self.nest.clone(),
88            allow_empty_env: self.allow_empty_env,
89            strict_env: self.strict_env,
90            env_files: self.env_files.clone(),
91            profile_env: self.profile_env.clone(),
92            search: self.search.clone(),
93            cache: self.cache.clone(),
94            secrets: self.secrets.clone(),
95            validate: self.validate,
96            fields: self.fields,
97            install: self.install,
98            register: self.register,
99            defaults: self.defaults,
100            overrides: self.overrides,
101            flags: self.flags,
102            bindings: self.bindings,
103            aliases: self.aliases,
104            remote: self.remote,
105            _marker: PhantomData,
106        }
107    }
108}
109
110impl<T: DeserializeOwned> Builder<T> {
111    /// A builder for the section `key`, tied to no config type's storage.
112    ///
113    /// [`load`](Self::load) works; [`init`](Self::init) needs somewhere to
114    /// install and is how the generated `builder()` differs from this.
115    #[must_use]
116    pub fn new(key: impl Into<String>) -> Self {
117        Self {
118            key: key.into(),
119            files: Vec::new(),
120            env: None,
121            nest: None,
122            allow_empty_env: false,
123            strict_env: false,
124            env_files: Vec::new(),
125            profile_env: None,
126            search: None,
127            cache: None,
128            secrets: None,
129            validate: None,
130            fields: &[],
131            install: None,
132            register: None,
133            defaults: None,
134            overrides: None,
135            flags: None,
136            bindings: None,
137            aliases: None,
138            remote: None,
139            _marker: PhantomData,
140        }
141    }
142
143    /// The generated `builder()`: everything installs into the type's cell.
144    #[doc(hidden)]
145    #[must_use]
146    pub fn with_installer(mut self, install: fn(T)) -> Self {
147        self.install = Some(install);
148        self
149    }
150
151    /// The generated `builder()`: the type's `#[config(secret)]` fields, by
152    /// their serde names — what a redacted cache needs to know.
153    #[doc(hidden)]
154    #[must_use]
155    pub fn with_secrets(mut self, secrets: &[&str]) -> Self {
156        self.secrets = Some(secrets.iter().map(|name| (*name).to_owned()).collect());
157        self
158    }
159
160    /// The generated `builder()`: the type's runtime layers and remote
161    /// storage, which live in its statics.
162    #[doc(hidden)]
163    #[must_use]
164    #[allow(clippy::too_many_arguments)]
165    pub fn with_type_statics(
166        mut self,
167        defaults: &'static crate::Layer,
168        overrides: &'static crate::Layer,
169        flags: &'static crate::Layer,
170        bindings: &'static crate::EnvBindings,
171        aliases: &'static crate::Aliases,
172        remote: &'static crate::Remote,
173        register: fn(&Self),
174    ) -> Self {
175        self.defaults = Some(defaults);
176        self.overrides = Some(overrides);
177        self.flags = Some(flags);
178        self.bindings = Some(bindings);
179        self.aliases = Some(aliases);
180        self.remote = Some(remote);
181        self.register = Some(register);
182        self
183    }
184
185    /// Application-level validation, run after deserializing and before
186    /// anything installs — on `init`, on every watch reload, and on a
187    /// recovery from the cache. The reload path keeps the previous snapshot
188    /// when this refuses, exactly like a parse failure.
189    #[must_use]
190    pub fn validate(mut self, check: Validator<T>) -> Self {
191        self.validate = Some(check);
192        self
193    }
194
195    /// Adds a configuration file. Merged in call order; later files win.
196    ///
197    /// The format comes from the extension at load time. A missing file is
198    /// skipped, which is what makes an optional `secrets.json` work.
199    #[must_use]
200    pub fn file(mut self, path: impl Into<String>) -> Self {
201        self.files.push((path.into(), false));
202        self
203    }
204
205    /// Adds an encrypted configuration file — `secrets.json.age`.
206    ///
207    /// The format comes from the extension *under* the suffix; the document
208    /// decrypts through the installed [`Decryptor`](crate::Decryptor).
209    #[cfg(feature = "decrypt")]
210    #[cfg_attr(docsrs, doc(cfg(feature = "decrypt")))]
211    #[must_use]
212    pub fn encrypted_file(mut self, path: impl Into<String>) -> Self {
213        self.files.push((path.into(), true));
214        self
215    }
216
217    /// The environment layer: `prefix` plus the key, as in `env = "APP_"`.
218    #[must_use]
219    pub fn env(mut self, prefix: impl Into<String>) -> Self {
220        self.env = Some(prefix.into());
221        self
222    }
223
224    /// The nesting separator inside variable names; `"__"` unless said.
225    #[must_use]
226    pub fn nest(mut self, separator: impl Into<String>) -> Self {
227        self.nest = Some(separator.into());
228        self
229    }
230
231    /// Treats `FOO=` as set-to-empty rather than unset.
232    #[must_use]
233    pub fn allow_empty_env(mut self) -> Self {
234        self.allow_empty_env = true;
235        self
236    }
237
238    /// Refuses ambiguous environment spellings; see
239    /// [`LoadSpec::with_strict_env`].
240    #[must_use]
241    pub fn strict_env(mut self) -> Self {
242        self.strict_env = true;
243        self
244    }
245
246    /// A `.env` file read as the environment layer, below the real thing.
247    #[must_use]
248    pub fn env_file(mut self, path: impl Into<String>) -> Self {
249        self.env_files.push(path.into());
250        self
251    }
252
253    /// The environment variable naming the active profile.
254    #[must_use]
255    pub fn profile_env(mut self, variable: impl Into<String>) -> Self {
256        self.profile_env = Some(variable.into());
257        self
258    }
259
260    /// Discovery: look for `{name}.{ext}` in each of `paths`, below any
261    /// explicitly listed files — the same rule as the attribute's
262    /// `name` + `paths`.
263    #[must_use]
264    pub fn discover(
265        mut self,
266        name: impl Into<String>,
267        paths: impl IntoIterator<Item = impl Into<String>>,
268    ) -> Self {
269        self.search = Some((name.into(), paths.into_iter().map(Into::into).collect()));
270        self
271    }
272
273    /// A last-known-good cache: written after every clean [`init`](Self::init)
274    /// or watch reload, recovered from when the sources will not load.
275    ///
276    /// [`CacheMode::Redacted`] and [`CacheMode::Fingerprint`] need to know
277    /// which fields are secret, which only the generated `builder()` on a
278    /// `#[dynamic_config]` type carries — on a bare [`Builder::new`], those
279    /// modes are refused at `init` rather than silently caching everything.
280    #[must_use]
281    pub fn cache(mut self, path: impl Into<String>, mode: CacheMode) -> Self {
282        self.cache = Some((path.into(), mode));
283        self
284    }
285
286    /// Reads the sources and deserializes, installing nothing.
287    ///
288    /// # Errors
289    ///
290    /// The same failures as any load: a file that will not parse, a missing
291    /// required value, an unsupported extension.
292    pub fn load(&self) -> Result<T, Error> {
293        let value: T = self.with_spec(crate::loader::load)?;
294
295        if let Some(check) = self.validate {
296            check(&value)?;
297        }
298
299        Ok(value)
300    }
301
302    /// Loads and installs as the type's snapshot.
303    ///
304    /// # Errors
305    ///
306    /// Whatever [`load`](Self::load) reports — and, on a builder made with
307    /// [`Builder::new`] rather than a generated `builder()`, the fact that
308    /// there is no storage to install into.
309    pub fn init(&self) -> Result<(), Error> {
310        // Before the installer check: "your cache cannot redact" is the more
311        // specific mistake, and the more dangerous one to leave unexplained.
312        self.check_cache_mode()?;
313
314        let Some(install) = self.install else {
315            return Err(Error::new(
316                ErrorKind::Backend,
317                "this builder is tied to no config type, so there is nowhere \
318                 to install; use `load()` here, or start from the generated \
319                 `builder()` on a `#[dynamic_config]` type",
320            ));
321        };
322
323        let outcome = match self.load() {
324            Ok(value) => {
325                install(value);
326                self.write_cache();
327
328                Ok(())
329            }
330            Err(failure) => {
331                let recovered = self.recover(failure)?;
332
333                if let Some(check) = self.validate {
334                    check(&recovered)?;
335                }
336
337                install(recovered);
338                crate::log::warning!(
339                    "{}: started from the last known good configuration",
340                    self.key
341                );
342
343                Ok(())
344            }
345        };
346
347        if outcome.is_ok() {
348            if let Some(register) = self.register {
349                register(self);
350            }
351        }
352
353        outcome
354    }
355
356    /// The first half of a grouped reload: load and validate now, install
357    /// later — what [`ReloadGroup`](crate::ReloadGroup) drives.
358    ///
359    /// # Errors
360    ///
361    /// The same failures as [`load`](Self::load); a builder with no
362    /// installer has nothing to commit into.
363    pub fn prepare(&self) -> Result<crate::group::Commit, Error>
364    where
365        T: Send + 'static,
366    {
367        let Some(install) = self.install else {
368            return Err(Error::new(
369                ErrorKind::Backend,
370                "this builder is tied to no config type, so a prepared \
371                 commit would have nowhere to install",
372            ));
373        };
374
375        let value = self.load()?;
376
377        Ok(Box::new(move || install(value)))
378    }
379
380    /// Refuses a redaction-dependent cache mode on a builder that cannot
381    /// know which fields are secret.
382    fn check_cache_mode(&self) -> Result<(), Error> {
383        if let Some((_, mode)) = &self.cache {
384            if !matches!(mode, CacheMode::Full) && self.secrets.is_none() {
385                return Err(Error::new(
386                    ErrorKind::Backend,
387                    "a redacted or fingerprint cache needs to know which \
388                     fields are secret, and only the generated `builder()` on \
389                     a `#[dynamic_config]` type knows; use that, or \
390                     `CacheMode::Full`, spelled out",
391                ));
392            }
393        }
394
395        Ok(())
396    }
397
398    /// Best-effort, exactly like the attribute's cache: a cache that cannot
399    /// be written is a worse tomorrow, not a broken today.
400    fn write_cache(&self) {
401        let Some((path, mode)) = &self.cache else {
402            return;
403        };
404
405        // The same refusal `init` makes, as a structural belt: every path
406        // that writes must hold it, not just the one that happens to run
407        // the check first — a file *marked* redacted with nothing redacted
408        // would be the quiet worst case.
409        if !matches!(mode, CacheMode::Full) && self.secrets.is_none() {
410            crate::log::warning!(
411                "{}: not writing the cache at {path}: a redaction-dependent \
412                 mode needs the generated builder's secret knowledge",
413                self.key
414            );
415
416            return;
417        }
418
419        let secrets = self.secrets.clone().unwrap_or_default();
420        let secret_refs: Vec<&str> = secrets.iter().map(String::as_str).collect();
421
422        let written = self.with_spec(|spec| {
423            let snapshot = crate::loader::snapshot(spec)?;
424
425            crate::cache::write(&snapshot, Path::new(path), *mode, &secret_refs)
426        });
427
428        if let Err(error) = written {
429            crate::log::warning!("could not write the configuration cache to {path}: {error}");
430        }
431    }
432
433    /// The last known good configuration, when the sources will not load —
434    /// or the original failure back, when there is nothing to recover from.
435    fn recover(&self, failure: Error) -> Result<T, Error> {
436        let Some((path, mode)) = &self.cache else {
437            return Err(failure);
438        };
439
440        // The configured mode decides, not the file on disk: a value-bearing
441        // cache left behind by an earlier deployment must not resurrect a
442        // configuration the operator deliberately switched away from.
443        // Fingerprint promises to diagnose and still fail.
444        let may_recover = mode.recovers();
445
446        // What the sources resolve to *now*, if they resolve at all — the
447        // drift report needs it, and a parse failure means there is nothing
448        // to compare.
449        let current = self.with_spec(crate::loader::snapshot).ok();
450
451        match crate::cache::read(Path::new(path), current.as_ref()) {
452            // Through the loader, not a bare extract: the environment and
453            // `.env` files layer over the cache exactly as they would over
454            // the files, which is what lets a redacted cache work — the
455            // values it dropped come back from wherever they were live.
456            Ok(Recovery::Usable(snapshot)) if may_recover => self
457                .with_spec(|spec| crate::loader::recover::<T>(spec, &snapshot))
458                .map(|(value, _snapshot)| value),
459            Ok(Recovery::Usable(_)) => {
460                crate::log::warning!(
461                    "{}: the cache at {path} holds values, but this builder \
462                     is configured `Fingerprint`, which diagnoses and never \
463                     recovers; refusing to start from it",
464                    self.key
465                );
466
467                Err(failure)
468            }
469            // A fingerprint cannot rebuild a configuration, but it can still
470            // say what moved since the last good state — the diagnosis that
471            // makes the failure actionable at three in the morning.
472            Ok(Recovery::Drift(moved)) => {
473                crate::log::warning!(
474                    "{}: cannot start: {failure}. Since the last good configuration: {}",
475                    self.key,
476                    match moved {
477                        Some(paths) if paths.is_empty() => "nothing detectably moved".to_owned(),
478                        Some(paths) => paths.join(", "),
479                        None => "could not compare — the sources do not resolve".to_owned(),
480                    }
481                );
482
483                Err(failure)
484            }
485            // A cache that will not read cures nothing: the original failure
486            // is the honest answer (the cache's own trouble is logged by
487            // `read` before this returns).
488            Ok(Recovery::Absent) | Err(_) => Err(failure),
489        }
490    }
491
492    /// Runs `operation` with the [`LoadSpec`] this builder describes.
493    ///
494    /// The one funnel: everything the builder does goes through the same
495    /// spec the attribute generates, so the two surfaces cannot diverge.
496    fn with_spec<R>(
497        &self,
498        operation: impl FnOnce(&LoadSpec<'_>) -> Result<R, Error>,
499    ) -> Result<R, Error> {
500        let sources = self
501            .files
502            .iter()
503            .map(|(file, encrypted)| {
504                Format::from_path(Path::new(file)).map(|format| {
505                    if *encrypted {
506                        Source::encrypted(file, format)
507                    } else {
508                        Source::file(file, format)
509                    }
510                })
511            })
512            .collect::<Result<Vec<_>, _>>()?;
513        let env_files: Vec<&str> = self.env_files.iter().map(String::as_str).collect();
514
515        let mut spec = LoadSpec::new(&self.key, &sources)
516            .with_empty_env(self.allow_empty_env)
517            .with_strict_env(self.strict_env)
518            .with_env_files(&env_files);
519
520        if let Some(prefix) = &self.env {
521            spec = spec.with_env(prefix);
522        }
523        if let Some(separator) = &self.nest {
524            spec = spec.with_nest(separator);
525        }
526        if let Some(variable) = &self.profile_env {
527            spec = spec.with_profile_env(variable);
528        }
529
530        let search_paths: Vec<&str>;
531        if let Some((name, paths)) = &self.search {
532            search_paths = paths.iter().map(String::as_str).collect();
533            spec = spec.with_search(name, &search_paths);
534        }
535
536        if let Some(layer) = self.defaults {
537            spec = spec.with_defaults(layer);
538        }
539        if let Some(layer) = self.overrides {
540            spec = spec.with_overrides(layer);
541        }
542        if let Some(layer) = self.flags {
543            spec = spec.with_flags(layer);
544        }
545        if let Some(bindings) = self.bindings {
546            spec = spec.with_env_bindings(bindings);
547        }
548        if let Some(aliases) = self.aliases {
549            spec = spec.with_aliases(aliases);
550        }
551        if let Some(remote) = self.remote {
552            spec = spec.with_remote(remote);
553        }
554
555        operation(&spec)
556    }
557
558    /// Explains `path` against this builder's sources; see [`crate::explain`].
559    ///
560    /// A builder that knows which fields are secret — every generated
561    /// `builder()` does — hands back a path under one of them already
562    /// redacted, the same as the type-level `explain`. A bare
563    /// [`Builder::new`] knows no secrets and redacts nothing; pass the
564    /// result through [`Explanation::redacted`](crate::Explanation::redacted)
565    /// for a path you know to be sensitive.
566    ///
567    /// # Errors
568    ///
569    /// The same failures as [`load`](Self::load).
570    pub fn explain(&self, path: &str) -> Result<crate::Explanation, Error> {
571        let explanation = self.with_spec(|spec| crate::explain::explain(spec, path))?;
572
573        // The same head-of-path check as the generated method: secrets are
574        // field names, and every path under one is the secret's.
575        if let Some(secrets) = &self.secrets {
576            let head = path.split('.').next().unwrap_or(path);
577
578            if secrets.iter().any(|secret| secret == head) {
579                return Ok(explanation.redacted());
580            }
581        }
582
583        Ok(explanation)
584    }
585
586    /// The generated `builder()`: the struct's field names, for unknown-key
587    /// detection in [`check`](Self::check).
588    #[doc(hidden)]
589    #[must_use]
590    pub fn with_fields(mut self, fields: &'static [&'static str]) -> Self {
591        self.fields = fields;
592        self
593    }
594
595    /// Where the value at `path` would come from, if anything supplies it.
596    ///
597    /// # Errors
598    ///
599    /// The same failures as [`load`](Self::load).
600    pub fn source_of(&self, path: &str) -> Result<Option<crate::Origin>, Error> {
601        self.with_spec(|spec| crate::loader::source_of(spec, path))
602    }
603
604    /// Whether anything supplies `path`.
605    ///
606    /// # Errors
607    ///
608    /// The same failures as [`load`](Self::load).
609    pub fn is_set(&self, path: &str) -> Result<bool, Error> {
610        self.with_spec(|spec| crate::loader::is_set(spec, path))
611    }
612
613    /// Resolves the section without deserializing it.
614    ///
615    /// # Errors
616    ///
617    /// The same failures as [`load`](Self::load).
618    pub fn snapshot(&self) -> Result<crate::Snapshot, Error> {
619        self.with_spec(crate::loader::snapshot)
620    }
621
622    /// What this configuration resolves to, and whether it would load —
623    /// see [`check`](crate::check). Unknown-key detection uses the field
624    /// names only the generated `builder()` carries; a bare builder reports
625    /// none.
626    ///
627    /// # Errors
628    ///
629    /// Only if the sources cannot be read at all.
630    pub fn check(&self) -> Result<crate::Report, Error> {
631        self.with_spec(|spec| crate::check::<T>(spec, self.fields))
632    }
633
634    /// One reload: load, validate, install, rewrite the cache.
635    ///
636    /// What a watch iteration and `apply_remote` both do. A failure
637    /// installs nothing — the previous snapshot keeps serving.
638    ///
639    /// # Errors
640    ///
641    /// The same failures as [`load`](Self::load); a builder with no
642    /// installer has nothing to reload into.
643    pub fn reload(&self) -> Result<(), Error> {
644        let Some(install) = self.install else {
645            return Err(Error::new(
646                ErrorKind::Backend,
647                "this builder is tied to no config type, so a reload would \
648                 have nowhere to install",
649            ));
650        };
651
652        install(self.load()?);
653        self.write_cache();
654
655        Ok(())
656    }
657}
658
659/// The builder a type was configured with, remembered at `init`.
660///
661/// Generated code keeps one per type — a `static` or a registry slot, the
662/// same split as every other slot — so `source_of`, `check`, `prepare` and
663/// the remote reload can answer for "the configuration this process runs
664/// on" without being handed the builder again.
665#[doc(hidden)]
666pub struct Configured<T> {
667    builder: std::sync::Mutex<Option<Builder<T>>>,
668}
669
670// Manual, not derived: a derive would demand `T: Default`, and the generic
671// path reaches this through `Registry::entry`'s `V: Default` bound — a
672// config type should not have to be `Default` to be configurable.
673impl<T> Default for Configured<T> {
674    fn default() -> Self {
675        Self::new()
676    }
677}
678
679impl<T> Configured<T> {
680    /// An empty slot.
681    #[must_use]
682    pub const fn new() -> Self {
683        Self {
684            builder: std::sync::Mutex::new(None),
685        }
686    }
687
688    /// Remembers `builder` as the type's configuration.
689    pub fn set(&self, builder: Builder<T>) {
690        *self
691            .builder
692            .lock()
693            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(builder);
694    }
695
696    /// The remembered configuration, or an error that says how to get one.
697    ///
698    /// # Errors
699    ///
700    /// When nothing was configured yet.
701    pub fn get(&self, name: &str) -> Result<Builder<T>, Error> {
702        self.builder
703            .lock()
704            .unwrap_or_else(std::sync::PoisonError::into_inner)
705            .clone()
706            .ok_or_else(|| {
707                Error::new(
708                    ErrorKind::Backend,
709                    format!(
710                        "`{name}` has not been configured yet; build and \
711                         install one first: `{name}::builder(\"..\")\
712                         .file(..).init()?`"
713                    ),
714                )
715            })
716    }
717}
718
719#[cfg(feature = "watch")]
720#[cfg_attr(docsrs, doc(cfg(feature = "watch")))]
721impl<T: DeserializeOwned + Send + Sync + 'static> Builder<T> {
722    /// Reloads on file changes until the returned handle is dropped.
723    ///
724    /// The same watcher as the attribute's `watch` — same debounce, same
725    /// registry: a type is watched once, whichever surface starts it, so a
726    /// builder watch while `start_watch()` runs (or the reverse) is
727    /// `AlreadyExists`. Each reload loads through this builder and installs
728    /// into the type's snapshot, firing `on_reload` hooks and waking
729    /// `changes()` exactly as any other install does; a configured
730    /// [`cache`](Self::cache) is rewritten after each clean reload.
731    ///
732    /// # Errors
733    ///
734    /// As the generated `start_watch()`: no watchable directory, a backend
735    /// that cannot start, or the type already being watched — plus a builder
736    /// with no installer, which has nothing to reload *into*.
737    pub fn watch(
738        &self,
739        debounce: core::time::Duration,
740    ) -> std::io::Result<crate::watch::WatchHandle> {
741        self.watch_with(debounce, crate::watch::WatchMode::Native)
742    }
743
744    /// [`watch`](Self::watch) with the detection strategy chosen explicitly
745    /// — polling is what network and overlay filesystems need.
746    ///
747    /// # Errors
748    ///
749    /// As [`watch`](Self::watch).
750    pub fn watch_with(
751        &self,
752        debounce: core::time::Duration,
753        mode: crate::watch::WatchMode,
754    ) -> std::io::Result<crate::watch::WatchHandle> {
755        let Some(install) = self.install else {
756            return Err(std::io::Error::new(
757                std::io::ErrorKind::InvalidInput,
758                "this builder is tied to no config type, so a reload would \
759                 have nowhere to install; start from the generated \
760                 `builder()` on a `#[dynamic_config]` type",
761            ));
762        };
763
764        let watched = self
765            .with_spec(|spec| Ok(crate::watch::Watched::from_spec(spec)))
766            .map_err(|error| {
767                std::io::Error::new(std::io::ErrorKind::InvalidInput, error.to_string())
768            })?;
769
770        let reloader = self.clone();
771        let name = watch_name::<T>(&self.key);
772
773        let handle = crate::watch::spawn_with(
774            std::any::TypeId::of::<T>(),
775            name,
776            watched,
777            debounce,
778            mode,
779            move || {
780                // `load` already validates, so a refused configuration keeps
781                // the previous snapshot exactly like a parse failure.
782                let value = reloader.load()?;
783                install(value);
784                reloader.write_cache();
785
786                Ok(None)
787            },
788        )?;
789
790        if let Some(register) = self.register {
791            register(self);
792        }
793
794        Ok(handle)
795    }
796}
797
798/// The registry wants a `&'static str`; leaking one per `watch()` call
799/// would grow with every stop/start cycle, so the leak is memoized: one
800/// name per type, ever.
801#[cfg(feature = "watch")]
802fn watch_name<T: 'static>(key: &str) -> &'static str {
803    use std::collections::HashMap;
804    use std::sync::{Mutex, OnceLock};
805
806    static NAMES: OnceLock<Mutex<HashMap<std::any::TypeId, &'static str>>> = OnceLock::new();
807
808    let mut names = NAMES
809        .get_or_init(|| Mutex::new(HashMap::new()))
810        .lock()
811        .unwrap_or_else(std::sync::PoisonError::into_inner);
812
813    names
814        .entry(std::any::TypeId::of::<T>())
815        .or_insert_with(|| Box::leak(format!("builder:{key}").into_boxed_str()))
816}
817
818#[cfg(feature = "schema")]
819#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
820impl<T: DeserializeOwned> Builder<T> {
821    /// A JSON Schema for the *file* this section lives in.
822    ///
823    /// The struct's schema wrapped under this builder's key, with
824    /// `#[config(secret)]` fields carrying `writeOnly` — which the generated
825    /// `builder()` knows and a bare one does not. Combine several with
826    /// [`schema::merge`](crate::schema::merge) when more than one config
827    /// type shares a file.
828    #[must_use]
829    pub fn schema(&self) -> serde_json::Value
830    where
831        T: schemars::JsonSchema,
832    {
833        let secrets = self.secrets.clone().unwrap_or_default();
834        let secret_refs: Vec<&str> = secrets.iter().map(String::as_str).collect();
835
836        crate::schema::section(&self.key, schemars::schema_for!(T).into(), &secret_refs)
837    }
838}
839
840#[cfg(feature = "async")]
841#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
842impl<T: DeserializeOwned + Send + 'static> Builder<T> {
843    /// [`load`](Self::load), off the async executor.
844    ///
845    /// # Errors
846    ///
847    /// The same failures as [`load`](Self::load).
848    pub async fn load_async(&self) -> Result<T, Error> {
849        let this = self.clone();
850
851        crate::asynchronous::off_thread(move || this.load()).await
852    }
853
854    /// [`init`](Self::init), off the async executor.
855    ///
856    /// # Errors
857    ///
858    /// The same failures as [`init`](Self::init).
859    pub async fn init_async(&self) -> Result<(), Error> {
860        let this = self.clone();
861
862        crate::asynchronous::off_thread(move || this.init()).await
863    }
864}
865
866impl<T> std::fmt::Debug for Builder<T> {
867    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
868        f.debug_struct("Builder")
869            .field("key", &self.key)
870            .field("files", &self.files)
871            .field("env", &self.env)
872            .field("env_files", &self.env_files)
873            .field("strict_env", &self.strict_env)
874            .field("installs", &self.install.is_some())
875            .finish_non_exhaustive()
876    }
877}