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}