Skip to main content

dynamic_config/builder/
mod.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; the [`Builder`]
4//! owns the "where" — chosen at runtime, not compile time — and funnels
5//! into the same [`LoadSpec`] everything else reads, so the two surfaces
6//! cannot drift apart on semantics.
7//!
8//! ```no_run
9//! # #[cfg(feature = "json")] {
10//! use dynamic_config::Builder;
11//! use serde::Deserialize;
12//!
13//! #[derive(Debug, Deserialize)]
14//! struct Db { host: String }
15//!
16//! let db: Db = Builder::new("db")
17//!     .file("config.json")
18//!     .env("APP_")
19//!     .load()
20//!     .expect("the sources read cleanly");
21//! # }
22//! ```
23//!
24//! On a `#[dynamic_config]` type, the generated `builder()` goes further:
25//! its `init()` installs the result as the type's snapshot, so runtime-
26//! chosen sources feed the same `current()` everything already reads.
27//!
28//! One concern per file: this module holds the struct, the fluent surface
29//! and the one `with_spec` funnel; [`lifecycle`] loads, installs and
30//! recovers; [`diagnostics`] answers questions without installing;
31//! [`watching`] starts the file watcher; [`configured`] is the slot that
32//! remembers a builder at `init` so the type can answer later.
33
34mod configured;
35mod diagnostics;
36mod lifecycle;
37#[cfg(feature = "watch")]
38mod watching;
39
40pub use configured::Configured;
41
42use std::marker::PhantomData;
43use std::path::Path;
44
45use serde::de::DeserializeOwned;
46
47use crate::cache::CacheMode;
48use crate::error::Error;
49use crate::source::{Format, LoadSpec, Source};
50
51/// An application-level validation hook: deserialized, not yet installed.
52///
53/// A closure rather than a bare `fn`, because a validator that needs
54/// *context* — a policy object, a schema, a foreign runtime's validator —
55/// cannot be written as a function pointer, and that is the shape a
56/// language binding needs. The `Arc` is what keeps `Builder` cloneable;
57/// a plain `fn` still coerces, so every existing call site is unchanged.
58type Validator<T> = std::sync::Arc<dyn Fn(&T) -> Result<(), Error> + Send + Sync>;
59
60/// Where a load's outcome goes — the value on success, the news on failure.
61///
62/// Two known shapes rather than an `Arc<dyn Fn>`: the generated `builder()`
63/// points at a `static` cell through plain `fn`s — no allocation — while a
64/// [`Dynamic`](crate::Dynamic) instance owns its cell and shares it here.
65///
66/// The failure half is here rather than only in the caller because a failed
67/// reload is a fact about the *cell*: `status()` answers "how many have
68/// failed since one worked", and only the cell outlives the attempt. A
69/// generated type reaches its static cell through a `fn` for the same
70/// reason the install does.
71pub(crate) enum Installer<T> {
72    /// The generated path: `fn`s that reach the type's static cell.
73    Static {
74        /// Stores into the type's cell, stating why, and hands back what it
75        /// stored — so `init_and_current` returns the snapshot *this* call
76        /// installed rather than whatever a later reload made current.
77        install: fn(T, crate::ReloadReason) -> std::sync::Arc<T>,
78        /// Records a reload that installed nothing.
79        record_failure: fn(&Error),
80        /// Records the fingerprint of the tree the install came from.
81        record_fingerprint: fn(String),
82    },
83    /// The instance path: this builder installs into a shared cell.
84    Cell(std::sync::Arc<crate::cell::ConfigCell<T>>),
85}
86
87impl<T> Installer<T> {
88    pub(super) fn install(&self, value: T, reason: crate::ReloadReason) -> std::sync::Arc<T> {
89        match self {
90            Self::Static { install, .. } => install(value, reason),
91            Self::Cell(cell) => cell.store_with(value, reason),
92        }
93    }
94
95    pub(super) fn record_failure(&self, error: &Error) {
96        match self {
97            Self::Static { record_failure, .. } => record_failure(error),
98            Self::Cell(cell) => cell.record_failure(error),
99        }
100    }
101
102    pub(super) fn record_fingerprint(&self, fingerprint: String) {
103        match self {
104            Self::Static {
105                record_fingerprint, ..
106            } => record_fingerprint(fingerprint),
107            Self::Cell(cell) => cell.set_fingerprint(fingerprint),
108        }
109    }
110}
111
112impl<T> Clone for Installer<T> {
113    fn clone(&self) -> Self {
114        match self {
115            Self::Static {
116                install,
117                record_failure,
118                record_fingerprint,
119            } => Self::Static {
120                install: *install,
121                record_failure: *record_failure,
122                record_fingerprint: *record_fingerprint,
123            },
124            Self::Cell(cell) => Self::Cell(std::sync::Arc::clone(cell)),
125        }
126    }
127}
128
129/// Runtime-chosen sources for one configuration section.
130///
131/// Methods take and return `self`, are infallible, and defer every check to
132/// [`load`](Self::load) — a missing file or an unsupported extension is a
133/// load-time answer, same as everywhere else in this crate.
134///
135/// What the builder configures in this stage is the source side: files,
136/// the environment layer, `.env` files, profiles. The runtime layers
137/// (`set_default`, `set_override`) and remote stores stay on the generated
138/// type, whose statics they live in.
139pub struct Builder<T> {
140    key: String,
141    files: Vec<(String, bool)>,
142    env: Option<String>,
143    nest: Option<String>,
144    allow_empty_env: bool,
145    strict_env: bool,
146    whole_document: bool,
147    engine: Option<&'static dyn crate::engine::Engine>,
148    reader: Option<&'static dyn crate::reader::Reader>,
149    env_files: Vec<String>,
150    secrets_dir: Option<String>,
151    allow_external_symlinks: bool,
152    profile_env: Option<String>,
153    search: Option<(String, Vec<String>)>,
154    cache: Option<(String, CacheMode)>,
155    /// `Some` routes the cache through this encryptor: written encrypted,
156    /// recovered through the installed [`Decryptor`](crate::Decryptor).
157    #[cfg(feature = "decrypt")]
158    cache_encryptor: Option<std::sync::Arc<dyn crate::Encryptor>>,
159    /// `Some` even when empty: knowing there are *no* secret fields is
160    /// knowledge, and only the generated `builder()` has it.
161    secrets: Option<Vec<String>>,
162    validate: Option<Validator<T>>,
163    fields: &'static [&'static str],
164    install: Option<Installer<T>>,
165    /// Remembers this builder as the type's configuration on a successful
166    /// `init`, so `source_of`, `check`, `prepare` and friends can answer
167    /// later without being handed the builder again.
168    register: Option<fn(&Self)>,
169    defaults: Option<&'static crate::Layer>,
170    overrides: Option<&'static crate::Layer>,
171    flags: Option<&'static crate::Layer>,
172    bindings: Option<&'static crate::EnvBindings>,
173    aliases: Option<&'static crate::Aliases>,
174    remote: Option<&'static crate::Remote>,
175    _marker: PhantomData<fn() -> T>,
176}
177
178impl<T> Clone for Builder<T> {
179    fn clone(&self) -> Self {
180        Self {
181            key: self.key.clone(),
182            files: self.files.clone(),
183            env: self.env.clone(),
184            nest: self.nest.clone(),
185            allow_empty_env: self.allow_empty_env,
186            strict_env: self.strict_env,
187            whole_document: self.whole_document,
188            engine: self.engine,
189            reader: self.reader,
190            env_files: self.env_files.clone(),
191            secrets_dir: self.secrets_dir.clone(),
192            allow_external_symlinks: self.allow_external_symlinks,
193            profile_env: self.profile_env.clone(),
194            search: self.search.clone(),
195            cache: self.cache.clone(),
196            #[cfg(feature = "decrypt")]
197            cache_encryptor: self.cache_encryptor.clone(),
198            secrets: self.secrets.clone(),
199            validate: self.validate.clone(),
200            fields: self.fields,
201            install: self.install.clone(),
202            register: self.register,
203            defaults: self.defaults,
204            overrides: self.overrides,
205            flags: self.flags,
206            bindings: self.bindings,
207            aliases: self.aliases,
208            remote: self.remote,
209            _marker: PhantomData,
210        }
211    }
212}
213
214impl<T: DeserializeOwned> Builder<T> {
215    /// A builder for the section `key`, tied to no config type's storage.
216    ///
217    /// [`load`](Self::load) works; [`init`](Self::init) needs somewhere to
218    /// install and is how the generated `builder()` differs from this.
219    #[must_use]
220    pub fn new(key: impl Into<String>) -> Self {
221        Self {
222            key: key.into(),
223            files: Vec::new(),
224            env: None,
225            nest: None,
226            allow_empty_env: false,
227            strict_env: false,
228            whole_document: false,
229            engine: None,
230            reader: None,
231            env_files: Vec::new(),
232            secrets_dir: None,
233            allow_external_symlinks: false,
234            profile_env: None,
235            search: None,
236            cache: None,
237            #[cfg(feature = "decrypt")]
238            cache_encryptor: None,
239            secrets: None,
240            validate: None,
241            fields: &[],
242            install: None,
243            register: None,
244            defaults: None,
245            overrides: None,
246            flags: None,
247            bindings: None,
248            aliases: None,
249            remote: None,
250            _marker: PhantomData,
251        }
252    }
253
254    /// The generated `builder()`: everything installs into the type's cell,
255    /// and every reload that installs nothing is recorded there too.
256    #[doc(hidden)]
257    #[must_use]
258    pub fn with_installer(
259        mut self,
260        install: fn(T, crate::ReloadReason) -> std::sync::Arc<T>,
261        record_failure: fn(&Error),
262        record_fingerprint: fn(String),
263    ) -> Self {
264        self.install = Some(Installer::Static {
265            install,
266            record_failure,
267            record_fingerprint,
268        });
269        self
270    }
271
272    /// The instance path: this builder installs into `cell`. What
273    /// [`Dynamic::new`](crate::Dynamic::new) wires; not public API.
274    ///
275    /// The registration callback is severed along with the installer: a
276    /// generated builder's `register` points at the *type's* `Configured`
277    /// slot, and an instance-owned builder landing there would cross-wire
278    /// the type surface — `Config::reload()` installing into the
279    /// `Dynamic`'s cell while `Config::current()` reads a static nothing
280    /// writes.
281    pub(crate) fn with_cell(mut self, cell: std::sync::Arc<crate::cell::ConfigCell<T>>) -> Self {
282        self.install = Some(Installer::Cell(cell));
283        self.register = None;
284        self
285    }
286
287    /// The generated `builder()`: the type's `#[config(secret)]` fields, by
288    /// their serde names — what a redacted cache needs to know.
289    #[doc(hidden)]
290    #[must_use]
291    pub fn with_secrets(mut self, secrets: &[&str]) -> Self {
292        self.secrets = Some(secrets.iter().map(|name| (*name).to_owned()).collect());
293        self
294    }
295
296    /// Which paths hold secrets, stated by hand.
297    ///
298    /// `#[config(secret)]` is a *declaration*, and a configuration with no
299    /// struct has nowhere to make one — so a schemaless configuration
300    /// (`Builder::values`, or any bare [`Builder::new`]) starts with no
301    /// secret list at all, and every surface that redacts one has nothing
302    /// to redact. This is that list, supplied at the only place that knows
303    /// it. It buys exactly what the attribute buys:
304    ///
305    /// - [`explain`](Self::explain) returns `***` for a path that is, sits
306    ///   under, or contains one of these — the same three-way rule
307    ///   `#[config(secret)]` gets;
308    /// - [`CacheMode::Redacted`](crate::CacheMode::Redacted) and
309    ///   [`Fingerprint`](crate::CacheMode::Fingerprint) become usable —
310    ///   without a list they are **refused** at `init` rather than quietly
311    ///   writing a cache with the secrets in it.
312    ///
313    /// Paths are dotted and relative to the section, as in
314    /// `"credentials.password"`. Naming a table redacts everything below it.
315    ///
316    /// What it cannot buy is a redacting `Debug`: there is no type here to
317    /// generate one for. [`Value`](crate::Value)'s own `Debug` prints shape
318    /// and keys and never values, which is why that gap is a non-event.
319    ///
320    /// ```no_run
321    /// # #[cfg(feature = "json")] {
322    /// use dynamic_config::{Builder, CacheMode};
323    ///
324    /// let builder = Builder::values("db")
325    ///     .file("config.json")
326    ///     .secrets(&["password"])
327    ///     .cache("last-known-good.json", CacheMode::Redacted);
328    /// # let _ = builder;
329    /// # }
330    /// ```
331    #[must_use]
332    pub fn secrets(self, secrets: &[&str]) -> Self {
333        self.with_secrets(secrets)
334    }
335
336    /// The generated `builder()`: the type's runtime layers and remote
337    /// storage, which live in its statics.
338    #[doc(hidden)]
339    #[must_use]
340    #[allow(clippy::too_many_arguments)]
341    pub fn with_type_statics(
342        mut self,
343        defaults: &'static crate::Layer,
344        overrides: &'static crate::Layer,
345        flags: &'static crate::Layer,
346        bindings: &'static crate::EnvBindings,
347        aliases: &'static crate::Aliases,
348        remote: &'static crate::Remote,
349        register: fn(&Self),
350    ) -> Self {
351        self.defaults = Some(defaults);
352        self.overrides = Some(overrides);
353        self.flags = Some(flags);
354        self.bindings = Some(bindings);
355        self.aliases = Some(aliases);
356        self.remote = Some(remote);
357        self.register = Some(register);
358        self
359    }
360
361    /// The section key this builder reads.
362    #[must_use]
363    pub fn key(&self) -> &str {
364        &self.key
365    }
366
367    /// Application-level validation, run after deserializing and before
368    /// anything installs — on `init`, on every watch reload, and on a
369    /// recovery from the cache. The reload path keeps the previous snapshot
370    /// when this refuses, exactly like a parse failure.
371    #[must_use]
372    pub fn validate(
373        mut self,
374        check: impl Fn(&T) -> Result<(), Error> + Send + Sync + 'static,
375    ) -> Self {
376        self.validate = Some(std::sync::Arc::new(check));
377        self
378    }
379
380    /// Adds a configuration file. Merged in call order; later files win.
381    ///
382    /// The format comes from the extension at load time. A missing file is
383    /// skipped, which is what makes an optional `secrets.json` work.
384    #[must_use]
385    pub fn file(mut self, path: impl Into<String>) -> Self {
386        self.files.push((path.into(), false));
387        self
388    }
389
390    /// Adds an encrypted configuration file — `secrets.json.age`.
391    ///
392    /// The format comes from the extension *under* the suffix; the document
393    /// decrypts through the installed [`Decryptor`](crate::Decryptor).
394    #[cfg(feature = "decrypt")]
395    #[cfg_attr(docsrs, doc(cfg(feature = "decrypt")))]
396    #[must_use]
397    pub fn encrypted_file(mut self, path: impl Into<String>) -> Self {
398        self.files.push((path.into(), true));
399        self
400    }
401
402    /// The environment layer: `prefix` plus the key, as in `env = "APP_"`.
403    #[must_use]
404    pub fn env(mut self, prefix: impl Into<String>) -> Self {
405        self.env = Some(prefix.into());
406        self
407    }
408
409    /// The nesting separator inside variable names; `"__"` unless said.
410    #[must_use]
411    pub fn nest(mut self, separator: impl Into<String>) -> Self {
412        self.nest = Some(separator.into());
413        self
414    }
415
416    /// Treats `FOO=` as set-to-empty rather than unset.
417    #[must_use]
418    pub fn allow_empty_env(mut self) -> Self {
419        self.allow_empty_env = true;
420        self
421    }
422
423    /// Refuses ambiguous environment spellings; see
424    /// [`LoadSpec::with_strict_env`].
425    #[must_use]
426    pub fn strict_env(mut self) -> Self {
427        self.strict_env = true;
428        self
429    }
430
431    /// Folds this load's layers with `engine` rather than the installed one.
432    ///
433    /// Every engine that ships implements the same merge rule, so this says
434    /// whose code does the folding and not what the configuration means —
435    /// [`engine`](crate::engine) has the list and the two places they cannot
436    /// agree.
437    ///
438    /// ```no_run
439    /// # use serde::Deserialize;
440    /// # #[derive(Deserialize)] struct Db { host: String }
441    /// # #[cfg(feature = "figment")]
442    /// # fn example() -> Result<(), dynamic_config::Error> {
443    /// let config: Db = dynamic_config::Builder::new("db")
444    ///     .file("config.toml")
445    ///     .engine(dynamic_config::engine::figment())
446    ///     .load()?;
447    /// # Ok(())
448    /// # }
449    /// ```
450    #[must_use]
451    pub fn engine(mut self, engine: &'static dyn crate::engine::Engine) -> Self {
452        self.engine = Some(engine);
453        self
454    }
455
456    /// Parses this load's documents with `reader`.
457    ///
458    /// Every reader is a different *dialect* where the formats overlap, so
459    /// this is a choice with consequences — [`reader`](crate::reader) has
460    /// the table and the divergences.
461    #[must_use]
462    pub fn reader(mut self, reader: &'static dyn crate::reader::Reader) -> Self {
463        self.reader = Some(reader);
464        self
465    }
466
467    /// Reads each document as this section's values, with no section header.
468    ///
469    /// The default is one file, several sections — every top-level key names
470    /// one, and this builder's key says which is yours. That is what lets a
471    /// `config.toml` carry `[db]` and `[server]` for two configuration types
472    /// that know nothing about each other.
473    ///
474    /// Say this when the document is *only* this configuration:
475    ///
476    /// ```json
477    /// { "host": "0.0.0.0", "port": 8000 }
478    /// ```
479    ///
480    /// ```no_run
481    /// # #[cfg(feature = "json")] {
482    /// use dynamic_config::Builder;
483    /// # use serde::Deserialize;
484    /// # #[derive(Deserialize)]
485    /// # struct Server { host: String, port: u16 }
486    /// let server: Server = Builder::new("server")
487    ///     .whole_document()
488    ///     .file("server.json")
489    ///     .env("APP_")
490    ///     .load()
491    ///     .expect("the sources read cleanly");
492    /// # }
493    /// ```
494    ///
495    /// The key keeps every other job it has: `APP_SERVER_PORT` still reaches
496    /// `port`, the cache entry and the diagnostics are still named after it,
497    /// and `""` is allowed for a configuration with nothing to call itself —
498    /// the environment layer is then just the prefix, `APP_PORT`.
499    ///
500    /// Everything else is unchanged: profile variants
501    /// (`server.production.json`), defaults, flags, overrides, aliases, the
502    /// secrets directory and a remote store's document all behave exactly as
503    /// they do for a sectioned load. It applies to **every** document this
504    /// builder reads, because sources that disagreed about their own shape
505    /// would be a configuration nobody could reason about.
506    #[must_use]
507    pub fn whole_document(mut self) -> Self {
508        self.whole_document = true;
509        self
510    }
511
512    /// A `.env` file read as the environment layer, below the real thing.
513    #[must_use]
514    pub fn env_file(mut self, path: impl Into<String>) -> Self {
515        self.env_files.push(path.into());
516        self
517    }
518
519    /// A directory of single-value files: one file per key, the filename is
520    /// the key, the contents are the value.
521    ///
522    /// What Docker's `/run/secrets` and a Kubernetes secret volume look like.
523    /// Nesting is spelled in the filename with the same separator
524    /// [`nest`](Self::nest) sets, so `db__password` is `db.password`; one
525    /// trailing newline is removed, because every tool that writes a secret
526    /// writes one. The layer sits above the files and below `.env` and the
527    /// environment — a mounted secret is a deployment fact, and a variable
528    /// exported for this run is a more specific one.
529    ///
530    /// A directory that is not there is skipped, exactly like a missing
531    /// file; one that cannot be read is a load-time error naming it.
532    #[must_use]
533    pub fn secrets_dir(mut self, path: impl Into<String>) -> Self {
534        self.secrets_dir = Some(path.into());
535        self
536    }
537
538    /// Lets a symlink in the secrets directory resolve outside it.
539    ///
540    /// Off by default since 0.7.1: an escaping link is refused with an
541    /// error naming the entry, because a directory of mounted credentials
542    /// that silently reads an arbitrary path through a planted link is a
543    /// vulnerability, not a layout. Kubernetes' own `..data` indirection
544    /// stays inside the mount and keeps working untouched. Turn this on
545    /// only for a deliberate cross-mount arrangement — and say why in a
546    /// comment, because the next reader will ask.
547    #[must_use]
548    pub fn allow_external_symlinks(mut self, allow: bool) -> Self {
549        self.allow_external_symlinks = allow;
550        self
551    }
552
553    /// The environment variable naming the active profile.
554    #[must_use]
555    pub fn profile_env(mut self, variable: impl Into<String>) -> Self {
556        self.profile_env = Some(variable.into());
557        self
558    }
559
560    /// Discovery: look for `{name}.{ext}` in each of `paths`, below any
561    /// explicitly listed files — the same rule as the attribute's
562    /// `name` + `paths`.
563    #[must_use]
564    pub fn discover(
565        mut self,
566        name: impl Into<String>,
567        paths: impl IntoIterator<Item = impl Into<String>>,
568    ) -> Self {
569        self.search = Some((name.into(), paths.into_iter().map(Into::into).collect()));
570        self
571    }
572
573    /// A last-known-good cache: written after every clean [`init`](Self::init)
574    /// or watch reload, recovered from when the sources will not load.
575    ///
576    /// [`CacheMode::Redacted`] and [`CacheMode::Fingerprint`] need to know
577    /// which fields are secret, which only the generated `builder()` on a
578    /// `#[dynamic_config]` type carries — on a bare [`Builder::new`], those
579    /// modes are refused at `init` rather than silently caching everything.
580    #[must_use]
581    pub fn cache(mut self, path: impl Into<String>, mode: CacheMode) -> Self {
582        self.cache = Some((path.into(), mode));
583        // Last writer wins outright: a plaintext cache asked for after an
584        // encrypted one must not keep the encryptor and silently write a
585        // full encrypted document where redaction was requested.
586        #[cfg(feature = "decrypt")]
587        {
588            self.cache_encryptor = None;
589        }
590        self
591    }
592
593    /// A last-known-good cache, encrypted at rest.
594    ///
595    /// The fourth answer to the cache trade-off, and the one that collapses
596    /// it: full fidelity — recovery needs nothing from the live environment
597    /// — with nothing readable on disk. Written through `encryptor` after
598    /// every clean [`init`](Self::init) or watch reload; recovered through
599    /// the installed [`Decryptor`](crate::Decryptor), the same door
600    /// [`encrypted_file`](Self::encrypted_file) reads through, so one
601    /// `set_decryptor` covers both. The path carries the format under the
602    /// encryption suffix — `last.json.age` — exactly like an encrypted
603    /// source file.
604    ///
605    /// The recipient question that kept this out of the attribute era has
606    /// the builder's answer: the recipients live in the `encryptor` the
607    /// caller constructs, at the call site that owns them.
608    #[cfg(feature = "decrypt")]
609    #[cfg_attr(docsrs, doc(cfg(feature = "decrypt")))]
610    #[must_use]
611    pub fn cache_encrypted(
612        mut self,
613        path: impl Into<String>,
614        encryptor: impl crate::Encryptor + 'static,
615    ) -> Self {
616        self.cache = Some((path.into(), CacheMode::Full));
617        self.cache_encryptor = Some(std::sync::Arc::new(encryptor));
618        self
619    }
620
621    /// The generated `builder()`: the struct's field names, for unknown-key
622    /// detection in [`check`](Self::check).
623    #[doc(hidden)]
624    #[must_use]
625    pub fn with_fields(mut self, fields: &'static [&'static str]) -> Self {
626        self.fields = fields;
627        self
628    }
629
630    /// Runs `operation` with the [`LoadSpec`] this builder describes.
631    ///
632    /// The one funnel: everything the builder does goes through the same
633    /// spec the attribute generates, so the two surfaces cannot diverge.
634    fn with_spec<R>(
635        &self,
636        operation: impl FnOnce(&LoadSpec<'_>) -> Result<R, Error>,
637    ) -> Result<R, Error> {
638        let sources = self
639            .files
640            .iter()
641            .map(|(file, encrypted)| {
642                Format::from_path(Path::new(file)).map(|format| {
643                    if *encrypted {
644                        Source::encrypted(file, format)
645                    } else {
646                        Source::file(file, format)
647                    }
648                })
649            })
650            .collect::<Result<Vec<_>, _>>()?;
651        let env_files: Vec<&str> = self.env_files.iter().map(String::as_str).collect();
652
653        let mut spec = LoadSpec::new(&self.key, &sources)
654            .with_empty_env(self.allow_empty_env)
655            .with_strict_env(self.strict_env)
656            .with_whole_document(self.whole_document)
657            .with_env_files(&env_files);
658
659        if let Some(engine) = self.engine {
660            spec = spec.with_engine(engine);
661        }
662        if let Some(reader) = self.reader {
663            spec = spec.with_reader(reader);
664        }
665        if let Some(prefix) = &self.env {
666            spec = spec.with_env(prefix);
667        }
668        if let Some(separator) = &self.nest {
669            spec = spec.with_nest(separator);
670        }
671        if let Some(variable) = &self.profile_env {
672            spec = spec.with_profile_env(variable);
673        }
674        if let Some(directory) = &self.secrets_dir {
675            spec = spec.with_secrets_dir(directory);
676            spec = spec.with_allow_external_symlinks(self.allow_external_symlinks);
677        }
678
679        let search_paths: Vec<&str>;
680        if let Some((name, paths)) = &self.search {
681            search_paths = paths.iter().map(String::as_str).collect();
682            spec = spec.with_search(name, &search_paths);
683        }
684
685        if let Some(layer) = self.defaults {
686            spec = spec.with_defaults(layer);
687        }
688        if let Some(layer) = self.overrides {
689            spec = spec.with_overrides(layer);
690        }
691        if let Some(layer) = self.flags {
692            spec = spec.with_flags(layer);
693        }
694        if let Some(bindings) = self.bindings {
695            spec = spec.with_env_bindings(bindings);
696        }
697        if let Some(aliases) = self.aliases {
698            spec = spec.with_aliases(aliases);
699        }
700        if let Some(remote) = self.remote {
701            spec = spec.with_remote(remote);
702        }
703
704        operation(&spec)
705    }
706}
707
708impl Builder<crate::Value> {
709    /// A configuration with no struct: the resolved section as data.
710    ///
711    /// Sugar for `Builder::<Value>::new(key)`, and the entry point that
712    /// makes the schemaless shape findable — a plugin host, a feature-flag
713    /// table, a tool inspecting somebody else's configuration. Every source,
714    /// layer and diagnostic on this builder behaves exactly as it does for a
715    /// struct, because nothing in the engine ever needed one; what changes is
716    /// the reading, which is by path.
717    ///
718    /// ```
719    /// # #[cfg(feature = "json")] {
720    /// use dynamic_config::{Builder, Dynamic};
721    ///
722    /// # std::fs::create_dir_all("target/doctest").unwrap();
723    /// # std::fs::write("target/doctest/schemaless.json",
724    /// #     r#"{"db": {"host": "localhost", "pool": {"max_size": 32}}}"#).unwrap();
725    /// let config = Dynamic::new(
726    ///     Builder::values("db").file("target/doctest/schemaless.json"),
727    /// );
728    /// let values = config.init_and_current()?;
729    ///
730    /// // One atomic load above; a walk of the tree here. No struct, and no
731    /// // deserialize per read.
732    /// assert_eq!(values.get("host").and_then(|v| v.as_str()), Some("localhost"));
733    /// assert_eq!(values.get("pool.max_size").and_then(|v| v.as_i64()), Some(32));
734    /// # }
735    /// # Ok::<(), dynamic_config::Error>(())
736    /// ```
737    ///
738    /// # What it does not get
739    ///
740    /// A struct is a *declaration*, and four things follow from it that
741    /// nothing can reconstruct without one:
742    ///
743    /// | | With a struct | Here |
744    /// |---|---|---|
745    /// | Types | checked at the load | checked at each read |
746    /// | Unknown keys | [`check`](Self::check) names them | reported as **not checked** |
747    /// | Secrets | `#[config(secret)]` | [`secrets`](Self::secrets), by hand |
748    /// | Missing required values | the load fails | absent is `None` |
749    ///
750    /// Everything else — layering, profiles, discovery, `.env`,
751    /// `secrets_dir`, watching, the last-known-good cache, reload hooks,
752    /// `source_of` and `explain` — is unchanged. The exception is not
753    /// about schemas: remote stores and the runtime layers live in a
754    /// `#[dynamic_config]` type's statics, so no builder made with
755    /// [`new`](Self::new) or `values` reaches them, whatever `T` is.
756    #[must_use]
757    pub fn values(key: impl Into<String>) -> Self {
758        Self::new(key)
759    }
760}
761
762impl<T> std::fmt::Debug for Builder<T> {
763    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
764        f.debug_struct("Builder")
765            .field("key", &self.key)
766            .field("files", &self.files)
767            .field("env", &self.env)
768            .field("env_files", &self.env_files)
769            .field("strict_env", &self.strict_env)
770            .field("whole_document", &self.whole_document)
771            .field("installs", &self.install.is_some())
772            .finish_non_exhaustive()
773    }
774}