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