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