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 successful load goes.
61///
62/// Two known shapes rather than an `Arc<dyn Fn>`: the generated `builder()`
63/// points at a `static` cell through a plain `fn` — no allocation, and the
64/// generated code keeps compiling unchanged — while a
65/// [`Dynamic`](crate::Dynamic) instance owns its cell and shares it here.
66pub(crate) enum Installer<T> {
67    /// The generated path: a `fn` that stores into the type's static cell.
68    Static(fn(T)),
69    /// The instance path: this builder installs into a shared cell.
70    Cell(std::sync::Arc<crate::cell::ConfigCell<T>>),
71}
72
73impl<T> Installer<T> {
74    pub(super) fn install(&self, value: T) {
75        match self {
76            Self::Static(install) => install(value),
77            Self::Cell(cell) => cell.store(value),
78        }
79    }
80}
81
82impl<T> Clone for Installer<T> {
83    fn clone(&self) -> Self {
84        match self {
85            Self::Static(install) => Self::Static(*install),
86            Self::Cell(cell) => Self::Cell(std::sync::Arc::clone(cell)),
87        }
88    }
89}
90
91/// Runtime-chosen sources for one configuration section.
92///
93/// Methods take and return `self`, are infallible, and defer every check to
94/// [`load`](Self::load) — a missing file or an unsupported extension is a
95/// load-time answer, same as everywhere else in this crate.
96///
97/// What the builder configures in this stage is the source side: files,
98/// the environment layer, `.env` files, profiles. The runtime layers
99/// (`set_default`, `set_override`) and remote stores stay on the generated
100/// type, whose statics they live in.
101pub struct Builder<T> {
102    key: String,
103    files: Vec<(String, bool)>,
104    env: Option<String>,
105    nest: Option<String>,
106    allow_empty_env: bool,
107    strict_env: bool,
108    env_files: Vec<String>,
109    profile_env: Option<String>,
110    search: Option<(String, Vec<String>)>,
111    cache: Option<(String, CacheMode)>,
112    /// `Some` routes the cache through this encryptor: written encrypted,
113    /// recovered through the installed [`Decryptor`](crate::Decryptor).
114    #[cfg(feature = "decrypt")]
115    cache_encryptor: Option<std::sync::Arc<dyn crate::Encryptor>>,
116    /// `Some` even when empty: knowing there are *no* secret fields is
117    /// knowledge, and only the generated `builder()` has it.
118    secrets: Option<Vec<String>>,
119    validate: Option<Validator<T>>,
120    fields: &'static [&'static str],
121    install: Option<Installer<T>>,
122    /// Remembers this builder as the type's configuration on a successful
123    /// `init`, so `source_of`, `check`, `prepare` and friends can answer
124    /// later without being handed the builder again.
125    register: Option<fn(&Self)>,
126    defaults: Option<&'static crate::Layer>,
127    overrides: Option<&'static crate::Layer>,
128    flags: Option<&'static crate::Layer>,
129    bindings: Option<&'static crate::EnvBindings>,
130    aliases: Option<&'static crate::Aliases>,
131    remote: Option<&'static crate::Remote>,
132    _marker: PhantomData<fn() -> T>,
133}
134
135impl<T> Clone for Builder<T> {
136    fn clone(&self) -> Self {
137        Self {
138            key: self.key.clone(),
139            files: self.files.clone(),
140            env: self.env.clone(),
141            nest: self.nest.clone(),
142            allow_empty_env: self.allow_empty_env,
143            strict_env: self.strict_env,
144            env_files: self.env_files.clone(),
145            profile_env: self.profile_env.clone(),
146            search: self.search.clone(),
147            cache: self.cache.clone(),
148            #[cfg(feature = "decrypt")]
149            cache_encryptor: self.cache_encryptor.clone(),
150            secrets: self.secrets.clone(),
151            validate: self.validate.clone(),
152            fields: self.fields,
153            install: self.install.clone(),
154            register: self.register,
155            defaults: self.defaults,
156            overrides: self.overrides,
157            flags: self.flags,
158            bindings: self.bindings,
159            aliases: self.aliases,
160            remote: self.remote,
161            _marker: PhantomData,
162        }
163    }
164}
165
166impl<T: DeserializeOwned> Builder<T> {
167    /// A builder for the section `key`, tied to no config type's storage.
168    ///
169    /// [`load`](Self::load) works; [`init`](Self::init) needs somewhere to
170    /// install and is how the generated `builder()` differs from this.
171    #[must_use]
172    pub fn new(key: impl Into<String>) -> Self {
173        Self {
174            key: key.into(),
175            files: Vec::new(),
176            env: None,
177            nest: None,
178            allow_empty_env: false,
179            strict_env: false,
180            env_files: Vec::new(),
181            profile_env: None,
182            search: None,
183            cache: None,
184            #[cfg(feature = "decrypt")]
185            cache_encryptor: None,
186            secrets: None,
187            validate: None,
188            fields: &[],
189            install: None,
190            register: None,
191            defaults: None,
192            overrides: None,
193            flags: None,
194            bindings: None,
195            aliases: None,
196            remote: None,
197            _marker: PhantomData,
198        }
199    }
200
201    /// The generated `builder()`: everything installs into the type's cell.
202    #[doc(hidden)]
203    #[must_use]
204    pub fn with_installer(mut self, install: fn(T)) -> Self {
205        self.install = Some(Installer::Static(install));
206        self
207    }
208
209    /// The instance path: this builder installs into `cell`. What
210    /// [`Dynamic::new`](crate::Dynamic::new) wires; not public API.
211    ///
212    /// The registration callback is severed along with the installer: a
213    /// generated builder's `register` points at the *type's* `Configured`
214    /// slot, and an instance-owned builder landing there would cross-wire
215    /// the type surface — `Config::reload()` installing into the
216    /// `Dynamic`'s cell while `Config::current()` reads a static nothing
217    /// writes.
218    pub(crate) fn with_cell(mut self, cell: std::sync::Arc<crate::cell::ConfigCell<T>>) -> Self {
219        self.install = Some(Installer::Cell(cell));
220        self.register = None;
221        self
222    }
223
224    /// The generated `builder()`: the type's `#[config(secret)]` fields, by
225    /// their serde names — what a redacted cache needs to know.
226    #[doc(hidden)]
227    #[must_use]
228    pub fn with_secrets(mut self, secrets: &[&str]) -> Self {
229        self.secrets = Some(secrets.iter().map(|name| (*name).to_owned()).collect());
230        self
231    }
232
233    /// The generated `builder()`: the type's runtime layers and remote
234    /// storage, which live in its statics.
235    #[doc(hidden)]
236    #[must_use]
237    #[allow(clippy::too_many_arguments)]
238    pub fn with_type_statics(
239        mut self,
240        defaults: &'static crate::Layer,
241        overrides: &'static crate::Layer,
242        flags: &'static crate::Layer,
243        bindings: &'static crate::EnvBindings,
244        aliases: &'static crate::Aliases,
245        remote: &'static crate::Remote,
246        register: fn(&Self),
247    ) -> Self {
248        self.defaults = Some(defaults);
249        self.overrides = Some(overrides);
250        self.flags = Some(flags);
251        self.bindings = Some(bindings);
252        self.aliases = Some(aliases);
253        self.remote = Some(remote);
254        self.register = Some(register);
255        self
256    }
257
258    /// The section key this builder reads.
259    #[must_use]
260    pub fn key(&self) -> &str {
261        &self.key
262    }
263
264    /// Application-level validation, run after deserializing and before
265    /// anything installs — on `init`, on every watch reload, and on a
266    /// recovery from the cache. The reload path keeps the previous snapshot
267    /// when this refuses, exactly like a parse failure.
268    #[must_use]
269    pub fn validate(
270        mut self,
271        check: impl Fn(&T) -> Result<(), Error> + Send + Sync + 'static,
272    ) -> Self {
273        self.validate = Some(std::sync::Arc::new(check));
274        self
275    }
276
277    /// Adds a configuration file. Merged in call order; later files win.
278    ///
279    /// The format comes from the extension at load time. A missing file is
280    /// skipped, which is what makes an optional `secrets.json` work.
281    #[must_use]
282    pub fn file(mut self, path: impl Into<String>) -> Self {
283        self.files.push((path.into(), false));
284        self
285    }
286
287    /// Adds an encrypted configuration file — `secrets.json.age`.
288    ///
289    /// The format comes from the extension *under* the suffix; the document
290    /// decrypts through the installed [`Decryptor`](crate::Decryptor).
291    #[cfg(feature = "decrypt")]
292    #[cfg_attr(docsrs, doc(cfg(feature = "decrypt")))]
293    #[must_use]
294    pub fn encrypted_file(mut self, path: impl Into<String>) -> Self {
295        self.files.push((path.into(), true));
296        self
297    }
298
299    /// The environment layer: `prefix` plus the key, as in `env = "APP_"`.
300    #[must_use]
301    pub fn env(mut self, prefix: impl Into<String>) -> Self {
302        self.env = Some(prefix.into());
303        self
304    }
305
306    /// The nesting separator inside variable names; `"__"` unless said.
307    #[must_use]
308    pub fn nest(mut self, separator: impl Into<String>) -> Self {
309        self.nest = Some(separator.into());
310        self
311    }
312
313    /// Treats `FOO=` as set-to-empty rather than unset.
314    #[must_use]
315    pub fn allow_empty_env(mut self) -> Self {
316        self.allow_empty_env = true;
317        self
318    }
319
320    /// Refuses ambiguous environment spellings; see
321    /// [`LoadSpec::with_strict_env`].
322    #[must_use]
323    pub fn strict_env(mut self) -> Self {
324        self.strict_env = true;
325        self
326    }
327
328    /// A `.env` file read as the environment layer, below the real thing.
329    #[must_use]
330    pub fn env_file(mut self, path: impl Into<String>) -> Self {
331        self.env_files.push(path.into());
332        self
333    }
334
335    /// The environment variable naming the active profile.
336    #[must_use]
337    pub fn profile_env(mut self, variable: impl Into<String>) -> Self {
338        self.profile_env = Some(variable.into());
339        self
340    }
341
342    /// Discovery: look for `{name}.{ext}` in each of `paths`, below any
343    /// explicitly listed files — the same rule as the attribute's
344    /// `name` + `paths`.
345    #[must_use]
346    pub fn discover(
347        mut self,
348        name: impl Into<String>,
349        paths: impl IntoIterator<Item = impl Into<String>>,
350    ) -> Self {
351        self.search = Some((name.into(), paths.into_iter().map(Into::into).collect()));
352        self
353    }
354
355    /// A last-known-good cache: written after every clean [`init`](Self::init)
356    /// or watch reload, recovered from when the sources will not load.
357    ///
358    /// [`CacheMode::Redacted`] and [`CacheMode::Fingerprint`] need to know
359    /// which fields are secret, which only the generated `builder()` on a
360    /// `#[dynamic_config]` type carries — on a bare [`Builder::new`], those
361    /// modes are refused at `init` rather than silently caching everything.
362    #[must_use]
363    pub fn cache(mut self, path: impl Into<String>, mode: CacheMode) -> Self {
364        self.cache = Some((path.into(), mode));
365        // Last writer wins outright: a plaintext cache asked for after an
366        // encrypted one must not keep the encryptor and silently write a
367        // full encrypted document where redaction was requested.
368        #[cfg(feature = "decrypt")]
369        {
370            self.cache_encryptor = None;
371        }
372        self
373    }
374
375    /// A last-known-good cache, encrypted at rest.
376    ///
377    /// The fourth answer to the cache trade-off, and the one that collapses
378    /// it: full fidelity — recovery needs nothing from the live environment
379    /// — with nothing readable on disk. Written through `encryptor` after
380    /// every clean [`init`](Self::init) or watch reload; recovered through
381    /// the installed [`Decryptor`](crate::Decryptor), the same door
382    /// [`encrypted_file`](Self::encrypted_file) reads through, so one
383    /// `set_decryptor` covers both. The path carries the format under the
384    /// encryption suffix — `last.json.age` — exactly like an encrypted
385    /// source file.
386    ///
387    /// The recipient question that kept this out of the attribute era has
388    /// the builder's answer: the recipients live in the `encryptor` the
389    /// caller constructs, at the call site that owns them.
390    #[cfg(feature = "decrypt")]
391    #[cfg_attr(docsrs, doc(cfg(feature = "decrypt")))]
392    #[must_use]
393    pub fn cache_encrypted(
394        mut self,
395        path: impl Into<String>,
396        encryptor: impl crate::Encryptor + 'static,
397    ) -> Self {
398        self.cache = Some((path.into(), CacheMode::Full));
399        self.cache_encryptor = Some(std::sync::Arc::new(encryptor));
400        self
401    }
402
403    /// The generated `builder()`: the struct's field names, for unknown-key
404    /// detection in [`check`](Self::check).
405    #[doc(hidden)]
406    #[must_use]
407    pub fn with_fields(mut self, fields: &'static [&'static str]) -> Self {
408        self.fields = fields;
409        self
410    }
411
412    /// Runs `operation` with the [`LoadSpec`] this builder describes.
413    ///
414    /// The one funnel: everything the builder does goes through the same
415    /// spec the attribute generates, so the two surfaces cannot diverge.
416    fn with_spec<R>(
417        &self,
418        operation: impl FnOnce(&LoadSpec<'_>) -> Result<R, Error>,
419    ) -> Result<R, Error> {
420        let sources = self
421            .files
422            .iter()
423            .map(|(file, encrypted)| {
424                Format::from_path(Path::new(file)).map(|format| {
425                    if *encrypted {
426                        Source::encrypted(file, format)
427                    } else {
428                        Source::file(file, format)
429                    }
430                })
431            })
432            .collect::<Result<Vec<_>, _>>()?;
433        let env_files: Vec<&str> = self.env_files.iter().map(String::as_str).collect();
434
435        let mut spec = LoadSpec::new(&self.key, &sources)
436            .with_empty_env(self.allow_empty_env)
437            .with_strict_env(self.strict_env)
438            .with_env_files(&env_files);
439
440        if let Some(prefix) = &self.env {
441            spec = spec.with_env(prefix);
442        }
443        if let Some(separator) = &self.nest {
444            spec = spec.with_nest(separator);
445        }
446        if let Some(variable) = &self.profile_env {
447            spec = spec.with_profile_env(variable);
448        }
449
450        let search_paths: Vec<&str>;
451        if let Some((name, paths)) = &self.search {
452            search_paths = paths.iter().map(String::as_str).collect();
453            spec = spec.with_search(name, &search_paths);
454        }
455
456        if let Some(layer) = self.defaults {
457            spec = spec.with_defaults(layer);
458        }
459        if let Some(layer) = self.overrides {
460            spec = spec.with_overrides(layer);
461        }
462        if let Some(layer) = self.flags {
463            spec = spec.with_flags(layer);
464        }
465        if let Some(bindings) = self.bindings {
466            spec = spec.with_env_bindings(bindings);
467        }
468        if let Some(aliases) = self.aliases {
469            spec = spec.with_aliases(aliases);
470        }
471        if let Some(remote) = self.remote {
472            spec = spec.with_remote(remote);
473        }
474
475        operation(&spec)
476    }
477}
478
479impl<T> std::fmt::Debug for Builder<T> {
480    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
481        f.debug_struct("Builder")
482            .field("key", &self.key)
483            .field("files", &self.files)
484            .field("env", &self.env)
485            .field("env_files", &self.env_files)
486            .field("strict_env", &self.strict_env)
487            .field("installs", &self.install.is_some())
488            .finish_non_exhaustive()
489    }
490}