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.
52type Validator<T> = fn(&T) -> Result<(), Error>;
53
54/// Runtime-chosen sources for one configuration section.
55///
56/// Methods take and return `self`, are infallible, and defer every check to
57/// [`load`](Self::load) — a missing file or an unsupported extension is a
58/// load-time answer, same as everywhere else in this crate.
59///
60/// What the builder configures in this stage is the source side: files,
61/// the environment layer, `.env` files, profiles. The runtime layers
62/// (`set_default`, `set_override`) and remote stores stay on the generated
63/// type, whose statics they live in.
64pub struct Builder<T> {
65    key: String,
66    files: Vec<(String, bool)>,
67    env: Option<String>,
68    nest: Option<String>,
69    allow_empty_env: bool,
70    strict_env: bool,
71    env_files: Vec<String>,
72    profile_env: Option<String>,
73    search: Option<(String, Vec<String>)>,
74    cache: Option<(String, CacheMode)>,
75    /// `Some` even when empty: knowing there are *no* secret fields is
76    /// knowledge, and only the generated `builder()` has it.
77    secrets: Option<Vec<String>>,
78    validate: Option<Validator<T>>,
79    fields: &'static [&'static str],
80    install: Option<fn(T)>,
81    /// Remembers this builder as the type's configuration on a successful
82    /// `init`, so `source_of`, `check`, `prepare` and friends can answer
83    /// later without being handed the builder again.
84    register: Option<fn(&Self)>,
85    defaults: Option<&'static crate::Layer>,
86    overrides: Option<&'static crate::Layer>,
87    flags: Option<&'static crate::Layer>,
88    bindings: Option<&'static crate::EnvBindings>,
89    aliases: Option<&'static crate::Aliases>,
90    remote: Option<&'static crate::Remote>,
91    _marker: PhantomData<fn() -> T>,
92}
93
94impl<T> Clone for Builder<T> {
95    fn clone(&self) -> Self {
96        Self {
97            key: self.key.clone(),
98            files: self.files.clone(),
99            env: self.env.clone(),
100            nest: self.nest.clone(),
101            allow_empty_env: self.allow_empty_env,
102            strict_env: self.strict_env,
103            env_files: self.env_files.clone(),
104            profile_env: self.profile_env.clone(),
105            search: self.search.clone(),
106            cache: self.cache.clone(),
107            secrets: self.secrets.clone(),
108            validate: self.validate,
109            fields: self.fields,
110            install: self.install,
111            register: self.register,
112            defaults: self.defaults,
113            overrides: self.overrides,
114            flags: self.flags,
115            bindings: self.bindings,
116            aliases: self.aliases,
117            remote: self.remote,
118            _marker: PhantomData,
119        }
120    }
121}
122
123impl<T: DeserializeOwned> Builder<T> {
124    /// A builder for the section `key`, tied to no config type's storage.
125    ///
126    /// [`load`](Self::load) works; [`init`](Self::init) needs somewhere to
127    /// install and is how the generated `builder()` differs from this.
128    #[must_use]
129    pub fn new(key: impl Into<String>) -> Self {
130        Self {
131            key: key.into(),
132            files: Vec::new(),
133            env: None,
134            nest: None,
135            allow_empty_env: false,
136            strict_env: false,
137            env_files: Vec::new(),
138            profile_env: None,
139            search: None,
140            cache: None,
141            secrets: None,
142            validate: None,
143            fields: &[],
144            install: None,
145            register: None,
146            defaults: None,
147            overrides: None,
148            flags: None,
149            bindings: None,
150            aliases: None,
151            remote: None,
152            _marker: PhantomData,
153        }
154    }
155
156    /// The generated `builder()`: everything installs into the type's cell.
157    #[doc(hidden)]
158    #[must_use]
159    pub fn with_installer(mut self, install: fn(T)) -> Self {
160        self.install = Some(install);
161        self
162    }
163
164    /// The generated `builder()`: the type's `#[config(secret)]` fields, by
165    /// their serde names — what a redacted cache needs to know.
166    #[doc(hidden)]
167    #[must_use]
168    pub fn with_secrets(mut self, secrets: &[&str]) -> Self {
169        self.secrets = Some(secrets.iter().map(|name| (*name).to_owned()).collect());
170        self
171    }
172
173    /// The generated `builder()`: the type's runtime layers and remote
174    /// storage, which live in its statics.
175    #[doc(hidden)]
176    #[must_use]
177    #[allow(clippy::too_many_arguments)]
178    pub fn with_type_statics(
179        mut self,
180        defaults: &'static crate::Layer,
181        overrides: &'static crate::Layer,
182        flags: &'static crate::Layer,
183        bindings: &'static crate::EnvBindings,
184        aliases: &'static crate::Aliases,
185        remote: &'static crate::Remote,
186        register: fn(&Self),
187    ) -> Self {
188        self.defaults = Some(defaults);
189        self.overrides = Some(overrides);
190        self.flags = Some(flags);
191        self.bindings = Some(bindings);
192        self.aliases = Some(aliases);
193        self.remote = Some(remote);
194        self.register = Some(register);
195        self
196    }
197
198    /// Application-level validation, run after deserializing and before
199    /// anything installs — on `init`, on every watch reload, and on a
200    /// recovery from the cache. The reload path keeps the previous snapshot
201    /// when this refuses, exactly like a parse failure.
202    #[must_use]
203    pub fn validate(mut self, check: Validator<T>) -> Self {
204        self.validate = Some(check);
205        self
206    }
207
208    /// Adds a configuration file. Merged in call order; later files win.
209    ///
210    /// The format comes from the extension at load time. A missing file is
211    /// skipped, which is what makes an optional `secrets.json` work.
212    #[must_use]
213    pub fn file(mut self, path: impl Into<String>) -> Self {
214        self.files.push((path.into(), false));
215        self
216    }
217
218    /// Adds an encrypted configuration file — `secrets.json.age`.
219    ///
220    /// The format comes from the extension *under* the suffix; the document
221    /// decrypts through the installed [`Decryptor`](crate::Decryptor).
222    #[cfg(feature = "decrypt")]
223    #[cfg_attr(docsrs, doc(cfg(feature = "decrypt")))]
224    #[must_use]
225    pub fn encrypted_file(mut self, path: impl Into<String>) -> Self {
226        self.files.push((path.into(), true));
227        self
228    }
229
230    /// The environment layer: `prefix` plus the key, as in `env = "APP_"`.
231    #[must_use]
232    pub fn env(mut self, prefix: impl Into<String>) -> Self {
233        self.env = Some(prefix.into());
234        self
235    }
236
237    /// The nesting separator inside variable names; `"__"` unless said.
238    #[must_use]
239    pub fn nest(mut self, separator: impl Into<String>) -> Self {
240        self.nest = Some(separator.into());
241        self
242    }
243
244    /// Treats `FOO=` as set-to-empty rather than unset.
245    #[must_use]
246    pub fn allow_empty_env(mut self) -> Self {
247        self.allow_empty_env = true;
248        self
249    }
250
251    /// Refuses ambiguous environment spellings; see
252    /// [`LoadSpec::with_strict_env`].
253    #[must_use]
254    pub fn strict_env(mut self) -> Self {
255        self.strict_env = true;
256        self
257    }
258
259    /// A `.env` file read as the environment layer, below the real thing.
260    #[must_use]
261    pub fn env_file(mut self, path: impl Into<String>) -> Self {
262        self.env_files.push(path.into());
263        self
264    }
265
266    /// The environment variable naming the active profile.
267    #[must_use]
268    pub fn profile_env(mut self, variable: impl Into<String>) -> Self {
269        self.profile_env = Some(variable.into());
270        self
271    }
272
273    /// Discovery: look for `{name}.{ext}` in each of `paths`, below any
274    /// explicitly listed files — the same rule as the attribute's
275    /// `name` + `paths`.
276    #[must_use]
277    pub fn discover(
278        mut self,
279        name: impl Into<String>,
280        paths: impl IntoIterator<Item = impl Into<String>>,
281    ) -> Self {
282        self.search = Some((name.into(), paths.into_iter().map(Into::into).collect()));
283        self
284    }
285
286    /// A last-known-good cache: written after every clean [`init`](Self::init)
287    /// or watch reload, recovered from when the sources will not load.
288    ///
289    /// [`CacheMode::Redacted`] and [`CacheMode::Fingerprint`] need to know
290    /// which fields are secret, which only the generated `builder()` on a
291    /// `#[dynamic_config]` type carries — on a bare [`Builder::new`], those
292    /// modes are refused at `init` rather than silently caching everything.
293    #[must_use]
294    pub fn cache(mut self, path: impl Into<String>, mode: CacheMode) -> Self {
295        self.cache = Some((path.into(), mode));
296        self
297    }
298
299    /// The generated `builder()`: the struct's field names, for unknown-key
300    /// detection in [`check`](Self::check).
301    #[doc(hidden)]
302    #[must_use]
303    pub fn with_fields(mut self, fields: &'static [&'static str]) -> Self {
304        self.fields = fields;
305        self
306    }
307
308    /// Runs `operation` with the [`LoadSpec`] this builder describes.
309    ///
310    /// The one funnel: everything the builder does goes through the same
311    /// spec the attribute generates, so the two surfaces cannot diverge.
312    fn with_spec<R>(
313        &self,
314        operation: impl FnOnce(&LoadSpec<'_>) -> Result<R, Error>,
315    ) -> Result<R, Error> {
316        let sources = self
317            .files
318            .iter()
319            .map(|(file, encrypted)| {
320                Format::from_path(Path::new(file)).map(|format| {
321                    if *encrypted {
322                        Source::encrypted(file, format)
323                    } else {
324                        Source::file(file, format)
325                    }
326                })
327            })
328            .collect::<Result<Vec<_>, _>>()?;
329        let env_files: Vec<&str> = self.env_files.iter().map(String::as_str).collect();
330
331        let mut spec = LoadSpec::new(&self.key, &sources)
332            .with_empty_env(self.allow_empty_env)
333            .with_strict_env(self.strict_env)
334            .with_env_files(&env_files);
335
336        if let Some(prefix) = &self.env {
337            spec = spec.with_env(prefix);
338        }
339        if let Some(separator) = &self.nest {
340            spec = spec.with_nest(separator);
341        }
342        if let Some(variable) = &self.profile_env {
343            spec = spec.with_profile_env(variable);
344        }
345
346        let search_paths: Vec<&str>;
347        if let Some((name, paths)) = &self.search {
348            search_paths = paths.iter().map(String::as_str).collect();
349            spec = spec.with_search(name, &search_paths);
350        }
351
352        if let Some(layer) = self.defaults {
353            spec = spec.with_defaults(layer);
354        }
355        if let Some(layer) = self.overrides {
356            spec = spec.with_overrides(layer);
357        }
358        if let Some(layer) = self.flags {
359            spec = spec.with_flags(layer);
360        }
361        if let Some(bindings) = self.bindings {
362            spec = spec.with_env_bindings(bindings);
363        }
364        if let Some(aliases) = self.aliases {
365            spec = spec.with_aliases(aliases);
366        }
367        if let Some(remote) = self.remote {
368            spec = spec.with_remote(remote);
369        }
370
371        operation(&spec)
372    }
373}
374
375impl<T> std::fmt::Debug for Builder<T> {
376    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
377        f.debug_struct("Builder")
378            .field("key", &self.key)
379            .field("files", &self.files)
380            .field("env", &self.env)
381            .field("env_files", &self.env_files)
382            .field("strict_env", &self.strict_env)
383            .field("installs", &self.install.is_some())
384            .finish_non_exhaustive()
385    }
386}