provcfg/lib.rs
1//! A Rust config loader that tracks where each value came from.
2//!
3//! `provcfg` layers configuration sources (compiled-in defaults, files,
4//! environment variables, CLI flags) into one struct, and keeps the
5//! *provenance* of every leaf field: which source set the active value, and
6//! which earlier sources it overrode.
7//!
8//! Derive [`Configurable`] on a plain config struct. [`Config::build`] returns
9//! a companion `*Prov` struct whose every leaf is a [`ValueHistory`]: the value
10//! paired with the [`Source`] it came from.
11//!
12//! ```
13//! use provcfg::{Category, Config, Configurable};
14//!
15//! #[derive(Configurable, serde::Deserialize, Clone, Default)]
16//! struct Settings {
17//! host: String,
18//! port: u16,
19//! }
20//!
21//! # unsafe { std::env::set_var("APP_HOST", "db.internal"); }
22//! // The environment has `APP_HOST=db.internal` set, but no `APP_PORT`.
23//! let settings = Config::new()
24//! .add_env("APP")
25//! .build::<SettingsProv>()
26//! .unwrap();
27//!
28//! // `host` was set by the environment source.
29//! assert_eq!(settings.host.value(), "db.internal");
30//! assert_eq!(settings.host.source().category(), Category::Env);
31//!
32//! // `port` was set by nobody, so it falls back to the compiled-in default.
33//! assert_eq!(settings.port.value(), &0);
34//! assert_eq!(settings.port.source().category(), Category::Default);
35//! ```
36//!
37//! # When this crate is useful
38//!
39//! Use `provcfg` when you need to know which source set each value: rendering a
40//! settings page that labels fields by origin, or debugging a value's history
41//! across layered sources. For plain merged-config loading,
42//! [`config`](https://crates.io/crates/config) is the standard choice and less
43//! ceremony.
44
45use std::collections::HashMap;
46use std::sync::{Arc, OnceLock};
47
48pub mod sources;
49
50/// Re-export so generated code can reference `provcfg::erased_serde::*`
51/// without users needing a direct dependency.
52pub use erased_serde;
53/// Re-export of the `ClapArgs` derive from the optional `provcfg-clap`
54/// integration. Enabled via the `clap-derive` feature.
55#[cfg(feature = "clap-derive")]
56pub use provcfg_clap::ClapArgs;
57/// Derive macro that turns a plain config struct into a provenance-tracking one.
58///
59/// Deriving `Configurable` on a struct `Foo` generates:
60///
61/// - **`FooProv`**: `Foo`'s shape with every leaf wrapped in a [`ValueHistory`].
62/// This is what [`Config::build`] returns.
63/// - **`FooPartial`**: every leaf wrapped in `Option`, used while merging the
64/// contribution of each source.
65/// - **`From<&FooProv> for Foo`**: collapses a `*Prov` back to the plain struct,
66/// dropping the per-leaf history and keeping each leaf's active value.
67///
68/// The struct must also derive [`Clone`], [`Default`] and [`serde::Deserialize`].
69///
70/// # Field attributes
71///
72/// - `#[configurable(nested)]`: recurse into a sub-struct that also derives
73/// `Configurable`, tracking provenance per leaf instead of per sub-struct.
74/// - `#[configurable(secret)]`: flag a leaf as sensitive; see
75/// [`ValueHistory::is_secret`].
76/// - `#[configurable(rename = "NAME")]`: change the key looked up in sources.
77/// The name is preserved verbatim for file formats (TOML/JSON keys match
78/// exactly) and a lowercase alias is emitted so env vars still match (env
79/// var segments are always lowercased internally).
80/// - `#[configurable(env_list)]`: let a `Vec<String>` leaf accept a
81/// comma-separated string (see [`deserialize_env_list`]).
82/// - `#[configurable(skip)]`: keep the field on the struct but exclude it from
83/// the config machinery entirely.
84///
85/// ```
86/// use provcfg::{Config, Configurable};
87///
88/// #[derive(Configurable, serde::Deserialize, Clone, Default)]
89/// struct Database {
90/// host: String,
91/// #[configurable(secret)]
92/// password: String,
93/// }
94///
95/// // With no sources every leaf falls back to its `Default`.
96/// let db = Config::new().build::<DatabaseProv>().unwrap();
97///
98/// assert_eq!(db.host.value(), "");
99/// assert!(db.password.is_secret());
100///
101/// // The `*Prov` can collapse back to the plain struct.
102/// let plain: Database = (&db).into();
103/// assert_eq!(plain.host, "");
104/// ```
105pub use provcfg_macros::Configurable;
106
107/// Shared, thread-safe handle to a [`Source`], stored throughout the
108/// value-history graph.
109pub type SourceArc = Arc<dyn Source + Send + Sync>;
110
111/// Implemented by every generated `*Prov` struct.
112///
113/// You normally interact with it through [`Config::build`] and the inspection
114/// helpers [`sources_map`](Provenance::sources_map) and
115/// [`walk_leaves`](Provenance::walk_leaves). `#[derive(Configurable)]` writes
116/// the implementation, so you rarely implement or call the trait directly.
117pub trait Provenance: Sized {
118 /// The generated `*Partial` companion: every leaf wrapped in `Option`.
119 type Partial: serde::de::DeserializeOwned;
120
121 /// Build the all-`Some` partial that seeds the implicit defaults layer.
122 ///
123 /// Generated by the derive macro; not called directly.
124 #[doc(hidden)]
125 fn defaults_partial() -> Self::Partial;
126
127 /// Merge one partial per source (plus the internal defaults layer) into a
128 /// fully-populated `*Prov` value.
129 ///
130 /// `partials[i] == None` means source `i` did not contribute to this
131 /// subtree. Generated by the derive macro; not called directly.
132 #[doc(hidden)]
133 fn merge(sources: &[SourceArc], partials: Vec<Option<Self::Partial>>) -> Self;
134
135 /// Recursively visit every leaf, inserting `(dotted_path, Category)` into
136 /// `out`. [`sources_map`](Self::sources_map) is the convenient wrapper.
137 ///
138 /// Generated by the derive macro; call [`sources_map`](Self::sources_map)
139 /// instead.
140 #[doc(hidden)]
141 fn collect_sources(&self, prefix: &str, out: &mut HashMap<String, Category>);
142
143 /// Recursively visit every leaf with its dotted path, current value, the
144 /// [`Category`] of the source that set it, and whether the leaf is marked
145 /// secret (via `#[configurable(secret)]`). Consumers that render values
146 /// should redact when `is_secret` is `true`.
147 ///
148 /// The value is type-erased so it can be re-serialized through any serde
149 /// format. Nested fields recurse with an extended `prefix`.
150 ///
151 /// ```
152 /// use provcfg::{Category, Config, Configurable, Provenance};
153 /// use provcfg::erased_serde::Serialize;
154 ///
155 /// # #[derive(Configurable, serde::Deserialize, Clone, Default)]
156 /// # struct Settings { host: String, port: u16 }
157 /// # unsafe { std::env::set_var("WALK_HOST", "h"); }
158 /// let settings = Config::new().add_env("WALK").build::<SettingsProv>().unwrap();
159 ///
160 /// let mut leaves: Vec<(String, Category)> = Vec::new();
161 /// settings.walk_leaves("", &mut |path: &str, _value: &dyn Serialize, category: Category, _secret: bool| {
162 /// leaves.push((path.to_string(), category));
163 /// });
164 /// leaves.sort_by(|a, b| a.0.cmp(&b.0));
165 ///
166 /// assert_eq!(leaves, [
167 /// ("host".to_string(), Category::Env),
168 /// ("port".to_string(), Category::Default),
169 /// ]);
170 /// ```
171 fn walk_leaves(
172 &self,
173 prefix: &str,
174 visitor: &mut dyn FnMut(&str, &dyn erased_serde::Serialize, Category, bool),
175 );
176
177 /// Flat `dotted.path -> Category` map of every leaf's active source.
178 ///
179 /// ```
180 /// # use provcfg::{Category, Config, Configurable, Provenance};
181 /// # #[derive(Configurable, serde::Deserialize, Clone, Default)]
182 /// # struct Settings { host: String, port: u16 }
183 /// # unsafe { std::env::set_var("SMAP_HOST", "h"); }
184 /// let settings = Config::new().add_env("SMAP").build::<SettingsProv>().unwrap();
185 ///
186 /// let map = settings.sources_map();
187 /// assert_eq!(map.get("host"), Some(&Category::Env));
188 /// assert_eq!(map.get("port"), Some(&Category::Default));
189 /// ```
190 fn sources_map(&self) -> HashMap<String, Category> {
191 let mut map = HashMap::new();
192 self.collect_sources("", &mut map);
193 map
194 }
195}
196
197/// The synthetic [`Source`] for the compiled-in defaults layer.
198///
199/// Every [`Config::build`] starts from this layer, so any leaf no other source
200/// sets reports [`Category::Default`]. Exposed mainly for generated code; use
201/// [`defaults_source`] for the shared handle.
202pub struct DefaultsSource;
203
204impl Source for DefaultsSource {
205 fn name(&self) -> &str {
206 "default"
207 }
208
209 fn category(&self) -> Category {
210 Category::Default
211 }
212
213 fn deserialize(
214 &self,
215 _seed: &mut dyn for<'de> FnMut(
216 &mut dyn erased_serde::Deserializer<'de>,
217 ) -> Result<(), erased_serde::Error>,
218 ) -> Result<(), erased_serde::Error> {
219 Ok(())
220 }
221}
222
223static DEFAULTS_SOURCE: OnceLock<SourceArc> = OnceLock::new();
224
225/// Shared [`SourceArc`] handle for the [`DefaultsSource`] layer.
226///
227/// ```
228/// use provcfg::{Category, Source};
229///
230/// assert_eq!(provcfg::defaults_source().category(), Category::Default);
231/// ```
232#[must_use]
233pub fn defaults_source() -> SourceArc {
234 DEFAULTS_SOURCE
235 .get_or_init(|| Arc::new(DefaultsSource))
236 .clone()
237}
238
239/// `serde` `deserialize_with` helper for `Vec<String>` leaves that accepts
240/// either a real array (TOML/JSON arrays) or a comma-separated string (the form
241/// env vars and bare CLI flags use).
242///
243/// Wired automatically by `#[configurable(env_list)]`, so you rarely name it
244/// directly. Empty strings become an empty `Vec`; whitespace around each
245/// element is trimmed.
246pub fn deserialize_env_list<'de, D>(d: D) -> Result<Option<Vec<String>>, D::Error>
247where
248 D: serde::Deserializer<'de>,
249{
250 use serde::Deserialize as _;
251
252 #[derive(serde::Deserialize)]
253 #[serde(untagged)]
254 enum StringOrVec {
255 Vec(Vec<String>),
256 Csv(String),
257 }
258
259 let opt = Option::<StringOrVec>::deserialize(d)?;
260 Ok(opt.map(|x| match x {
261 StringOrVec::Vec(v) => v,
262 StringOrVec::Csv(s) if s.is_empty() => Vec::new(),
263 StringOrVec::Csv(s) => s.split(',').map(|t| t.trim().to_string()).collect(),
264 }))
265}
266
267/// A named origin of raw configuration data.
268///
269/// The [`sources`] module provides implementations for the common cases:
270/// `JsonStr`, `TomlStr`, `EnvSource` and `CliSource` (each behind a feature).
271/// Implement this trait yourself to support a format the built-ins miss
272/// (XML, a database, a secret store, etc.).
273///
274/// # Contract
275///
276/// - [`deserialize`](Self::deserialize) is called inline from
277/// [`Config::build`] and is **synchronous**. If your source needs I/O or
278/// async work, do it on construction (the way `sources::TomlStr` reads a
279/// file in the file-loading helper) and store the materialized data on the
280/// struct.
281/// - To surface a parse/load error, convert it through
282/// `<erased_serde::Error as serde::de::Error>::custom(my_error)`.
283/// - [`category`](Self::category) defaults to `Category::Custom("user")`.
284/// Override it to a built-in bucket or your own `Custom("...")` label so
285/// provenance maps render the source the way you want.
286///
287/// # Example: a custom in-memory source
288///
289/// The shape any source ends up using: build a [`serde_json::Value`] (or any
290/// type that implements [`serde::Deserializer`]) and erase it through
291/// `erased_serde`. The built-in env source uses exactly this pattern.
292///
293/// ```
294/// use provcfg::erased_serde;
295/// use provcfg::{Category, Config, Configurable, Source};
296///
297/// #[derive(Configurable, serde::Deserialize, Clone, Default)]
298/// struct Settings {
299/// host: String,
300/// }
301///
302/// struct MapSource {
303/// name: String,
304/// data: serde_json::Value,
305/// }
306///
307/// impl Source for MapSource {
308/// fn name(&self) -> &str {
309/// &self.name
310/// }
311///
312/// fn category(&self) -> Category {
313/// Category::Custom("map")
314/// }
315///
316/// fn deserialize(
317/// &self,
318/// seed: &mut dyn for<'de> FnMut(
319/// &mut dyn erased_serde::Deserializer<'de>,
320/// ) -> Result<(), erased_serde::Error>,
321/// ) -> Result<(), erased_serde::Error> {
322/// let mut erased = <dyn erased_serde::Deserializer>::erase(&self.data);
323/// seed(&mut erased)
324/// }
325/// }
326///
327/// let settings = Config::new()
328/// .add_source(MapSource {
329/// name: "demo".into(),
330/// data: serde_json::json!({ "host": "from-map" }),
331/// })
332/// .build::<SettingsProv>()
333/// .unwrap();
334///
335/// assert_eq!(settings.host.value(), "from-map");
336/// assert_eq!(settings.host.source().category(), Category::Custom("map"));
337/// ```
338pub trait Source {
339 /// Human-readable label for this source, surfaced in [`Error`] messages.
340 fn name(&self) -> &str;
341
342 /// Hand this source's raw data to `seed` as a type-erased deserializer.
343 /// Called once per [`Config::build`].
344 fn deserialize(
345 &self,
346 seed: &mut dyn for<'de> FnMut(
347 &mut dyn erased_serde::Deserializer<'de>,
348 ) -> Result<(), erased_serde::Error>,
349 ) -> Result<(), erased_serde::Error>;
350
351 /// The [`Category`] bucket this source belongs to. Defaults to
352 /// `Category::Custom("user")`.
353 fn category(&self) -> Category {
354 Category::Custom("user")
355 }
356}
357
358/// Which kind of source contributed a value.
359///
360/// Surfaced separately from a source's human-readable name so a UI can render
361/// one visual per bucket. Serializes as a lowercase string (`"default"`,
362/// `"file"`, `"env"`, `"cli"`, or the `Custom` label).
363///
364/// ```
365/// use provcfg::Category;
366///
367/// assert_eq!(Category::Env.as_str(), "env");
368/// assert_eq!(Category::Custom("vault").as_str(), "vault");
369/// ```
370#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
371#[non_exhaustive]
372pub enum Category {
373 /// Built-in defaults layer.
374 Default,
375 /// A configuration file (JSON, TOML, ...).
376 File,
377 /// Environment variables.
378 Env,
379 /// Command-line arguments.
380 Cli,
381 /// A caller-defined source; the string is a free-form label.
382 Custom(&'static str),
383}
384
385impl Category {
386 /// The stable lowercase label, as used by the [`serde::Serialize`] impl.
387 #[must_use]
388 pub fn as_str(&self) -> &'static str {
389 match self {
390 Category::Default => "default",
391 Category::File => "file",
392 Category::Env => "env",
393 Category::Cli => "cli",
394 Category::Custom(s) => s,
395 }
396 }
397}
398
399impl serde::Serialize for Category {
400 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
401 serializer.serialize_str(self.as_str())
402 }
403}
404
405/// A builder that collects [`Source`]s in priority order, then merges them.
406///
407/// Sources are applied in the order they are added; a later source overrides an
408/// earlier one for any leaf it sets. Unset leaves keep the earlier value, and a
409/// leaf no source sets falls back to the compiled-in defaults layer.
410///
411/// ```
412/// # use provcfg::{Config, Configurable};
413/// # #[derive(Configurable, serde::Deserialize, Clone, Default)]
414/// # struct Settings { host: String }
415/// # unsafe {
416/// # std::env::set_var("BASE_HOST", "from-base");
417/// # std::env::set_var("EXTRA_HOST", "from-extra");
418/// # }
419/// // `EXTRA` is added after `BASE`, so it wins for `host`.
420/// let settings = Config::new()
421/// .add_env("BASE")
422/// .add_env("EXTRA")
423/// .build::<SettingsProv>()
424/// .unwrap();
425///
426/// assert_eq!(settings.host.value(), "from-extra");
427/// ```
428pub struct Config {
429 sources: Vec<SourceArc>,
430}
431
432impl Config {
433 /// Create a `Config` with no sources yet.
434 #[must_use]
435 pub fn new() -> Self {
436 Self {
437 sources: Vec::new(),
438 }
439 }
440
441 /// Append any custom [`Source`]. The typed `add_*` helpers wrap the
442 /// built-in sources; reach for this to plug in your own.
443 #[must_use]
444 pub fn add_source<S: Source + Send + Sync + 'static>(mut self, source: S) -> Self {
445 self.sources.push(Arc::new(source));
446 self
447 }
448
449 /// Add a TOML file source. The file is read immediately; a read failure is
450 /// surfaced as [`Error::Io`].
451 ///
452 /// # Errors
453 ///
454 /// Returns [`Error::Io`] if `path` cannot be read.
455 #[cfg(feature = "toml")]
456 pub fn add_toml_file(self, path: impl AsRef<std::path::Path>) -> Result<Self, Error> {
457 let path = path.as_ref();
458 let content = std::fs::read_to_string(path).map_err(|error| Error::Io {
459 path: path.display().to_string(),
460 error,
461 })?;
462 Ok(self.add_toml_str(path.display().to_string(), content))
463 }
464
465 /// Add an in-memory TOML source. `name` is a human-readable label used in
466 /// error messages.
467 ///
468 /// ```
469 /// # use provcfg::{Category, Config, Configurable};
470 /// # #[derive(Configurable, serde::Deserialize, Clone, Default)]
471 /// # struct Settings { host: String, port: u16 }
472 /// let settings = Config::new()
473 /// .add_toml_str("app.toml", "host = \"localhost\"\nport = 8080")
474 /// .build::<SettingsProv>()
475 /// .unwrap();
476 ///
477 /// assert_eq!(settings.host.value(), "localhost");
478 /// assert_eq!(settings.host.source().category(), Category::File);
479 /// ```
480 #[cfg(feature = "toml")]
481 #[must_use]
482 pub fn add_toml_str(self, name: impl Into<String>, content: impl Into<String>) -> Self {
483 self.add_source(sources::TomlStr::new(name, content))
484 }
485
486 /// Add a JSON file source. The file is read immediately; a read failure is
487 /// surfaced as [`Error::Io`].
488 ///
489 /// # Errors
490 ///
491 /// Returns [`Error::Io`] if `path` cannot be read.
492 #[cfg(feature = "json")]
493 pub fn add_json_file(self, path: impl AsRef<std::path::Path>) -> Result<Self, Error> {
494 let path = path.as_ref();
495 let content = std::fs::read_to_string(path).map_err(|error| Error::Io {
496 path: path.display().to_string(),
497 error,
498 })?;
499 Ok(self.add_json_str(path.display().to_string(), content))
500 }
501
502 /// Add an in-memory JSON source. `name` is a human-readable label used in
503 /// error messages.
504 ///
505 /// ```
506 /// # use provcfg::{Category, Config, Configurable};
507 /// # #[derive(Configurable, serde::Deserialize, Clone, Default)]
508 /// # struct Settings { host: String, port: u16 }
509 /// let settings = Config::new()
510 /// .add_json_str("app.json", r#"{ "host": "localhost", "port": 8080 }"#)
511 /// .build::<SettingsProv>()
512 /// .unwrap();
513 ///
514 /// assert_eq!(settings.port.value(), &8080);
515 /// assert_eq!(settings.port.source().category(), Category::File);
516 /// ```
517 #[cfg(feature = "json")]
518 #[must_use]
519 pub fn add_json_str(self, name: impl Into<String>, content: impl Into<String>) -> Self {
520 self.add_source(sources::JsonStr::new(name, content))
521 }
522
523 /// Add an environment-variable source reading `<PREFIX>_*` variables.
524 /// Nested fields use `__` as the separator.
525 ///
526 /// ```
527 /// # use provcfg::{Category, Config, Configurable};
528 /// # #[derive(Configurable, serde::Deserialize, Clone, Default)]
529 /// # struct Settings { host: String }
530 /// # unsafe { std::env::set_var("MYAPP_HOST", "db.internal"); }
531 /// let settings = Config::new()
532 /// .add_env("MYAPP")
533 /// .build::<SettingsProv>()
534 /// .unwrap();
535 ///
536 /// assert_eq!(settings.host.value(), "db.internal");
537 /// assert_eq!(settings.host.source().category(), Category::Env);
538 /// ```
539 #[cfg(feature = "env")]
540 #[must_use]
541 pub fn add_env(self, prefix: impl AsRef<str>) -> Self {
542 self.add_source(sources::EnvSource::new("env", prefix))
543 }
544
545 /// Add an environment-variable source where the given dotted paths are
546 /// treated as comma-separated lists. See [`sources::EnvSource::with_list_keys`].
547 ///
548 /// ```
549 /// # use provcfg::{Config, Configurable};
550 /// # #[derive(Configurable, serde::Deserialize, Clone, Default)]
551 /// # struct Settings { hosts: Vec<String> }
552 /// # unsafe { std::env::set_var("SVC_HOSTS", "a.example,b.example"); }
553 /// // `SVC_HOSTS` is a comma-separated list; name it so the source splits it.
554 /// let settings = Config::new()
555 /// .add_env_with_list_keys("SVC", ["hosts"])
556 /// .build::<SettingsProv>()
557 /// .unwrap();
558 ///
559 /// assert_eq!(settings.hosts.value(), &["a.example", "b.example"]);
560 /// ```
561 #[cfg(feature = "env")]
562 #[must_use]
563 pub fn add_env_with_list_keys<I, S>(self, prefix: impl AsRef<str>, list_keys: I) -> Self
564 where
565 I: IntoIterator<Item = S>,
566 S: Into<String>,
567 {
568 self.add_source(sources::EnvSource::new("env", prefix).with_list_keys(list_keys))
569 }
570
571 /// Add a CLI source from a pre-built partial (every field an `Option`).
572 /// Typically produced from clap via the `provcfg-clap` `ClapArgs` derive.
573 ///
574 /// ```
575 /// # use provcfg::{Category, Config, Configurable};
576 /// # #[derive(Configurable, serde::Deserialize, Clone, Default)]
577 /// # struct Settings { host: String, port: u16 }
578 /// let from_cli = SettingsPartial { host: Some("cli.example".to_string()), port: None };
579 ///
580 /// let settings = Config::new()
581 /// .add_cli(from_cli)
582 /// .build::<SettingsProv>()
583 /// .unwrap();
584 ///
585 /// assert_eq!(settings.host.value(), "cli.example");
586 /// assert_eq!(settings.host.source().category(), Category::Cli);
587 /// // `port` was `None` on the partial, so it falls back to the default.
588 /// assert_eq!(settings.port.source().category(), Category::Default);
589 /// ```
590 #[cfg(feature = "cli")]
591 #[must_use]
592 pub fn add_cli<P>(self, partial: P) -> Self
593 where
594 P: serde::Serialize + Send + Sync + 'static,
595 {
596 self.add_source(sources::CliSource::new("cli", partial))
597 }
598
599 /// Merge every added source plus the implicit defaults layer into `T`, a
600 /// generated `*Prov` struct.
601 ///
602 /// # Errors
603 ///
604 /// Returns [`Error::Deserialize`] if a source's data does not fit `T`.
605 pub fn build<T: Provenance>(&self) -> Result<T, Error> {
606 let mut partials = Vec::with_capacity(self.sources.len());
607 for s in &self.sources {
608 let mut slot: Option<T::Partial> = None;
609 s.deserialize(&mut |de| {
610 slot = Some(erased_serde::deserialize::<T::Partial>(de)?);
611 Ok(())
612 })
613 .map_err(|error| Error::Deserialize {
614 source: s.name().to_string(),
615 error,
616 })?;
617 partials.push(Some(
618 slot.expect("Source::deserialize did not invoke its callback"),
619 ));
620 }
621 Ok(T::merge(&self.sources, partials))
622 }
623}
624
625impl Default for Config {
626 fn default() -> Self {
627 Self::new()
628 }
629}
630
631/// A typed value together with the [`Source`] that supplied it. One entry of a
632/// [`ValueHistory`].
633pub struct Value<T> {
634 /// The value contributed by `source`.
635 pub value: T,
636 /// The source this value came from.
637 pub source: SourceArc,
638}
639
640/// The ordered history of values a single config leaf received.
641///
642/// The last entry is the *active* value; earlier entries are values that were
643/// overridden, kept so callers can audit each source's contribution. Every leaf
644/// of a generated `*Prov` struct is a `ValueHistory`.
645///
646/// ```
647/// # use provcfg::{Category, Config, Configurable};
648/// # #[derive(Configurable, serde::Deserialize, Clone, Default)]
649/// # struct Settings {
650/// # #[configurable(secret)]
651/// # token: String,
652/// # }
653/// # unsafe { std::env::set_var("VH_TOKEN", "s3cret"); }
654/// let settings = Config::new().add_env("VH").build::<SettingsProv>().unwrap();
655///
656/// assert_eq!(settings.token.value(), "s3cret");
657/// assert_eq!(settings.token.source().category(), Category::Env);
658/// assert!(settings.token.is_secret());
659/// ```
660pub struct ValueHistory<T> {
661 inner: Vec<Value<T>>,
662 secret: bool,
663}
664
665impl<T> ValueHistory<T> {
666 /// Create an empty history. The merge step fills it; you rarely call this.
667 #[must_use]
668 pub fn new() -> Self {
669 Self {
670 inner: Vec::new(),
671 secret: false,
672 }
673 }
674
675 /// Builder: marks this history as carrying sensitive data. Used by the
676 /// derive macro for fields annotated `#[configurable(secret)]`.
677 #[must_use]
678 pub fn mark_secret(mut self) -> Self {
679 self.secret = true;
680 self
681 }
682
683 /// Whether this history holds sensitive data. Consumers (UIs, serializers)
684 /// should redact the value when this returns `true`.
685 #[must_use]
686 pub fn is_secret(&self) -> bool {
687 self.secret
688 }
689
690 /// Append a value, making it the new active value.
691 pub fn push(&mut self, value: Value<T>) {
692 self.inner.push(value);
693 }
694
695 /// Active value: the last entry in the history.
696 ///
697 /// # Panics
698 /// Panics if the history is empty. By design every `ValueHistory` built
699 /// via `Config::build` is populated by the defaults layer, so this should
700 /// not happen for properly-built configs.
701 pub fn value(&self) -> &T {
702 &self.inner.last().expect("ValueHistory is empty").value
703 }
704
705 /// Source of the active value.
706 ///
707 /// # Panics
708 /// Panics if the history is empty (see [`Self::value`]).
709 pub fn source(&self) -> &dyn Source {
710 &*self.inner.last().expect("ValueHistory is empty").source
711 }
712
713 /// The full history, oldest first. The last entry is the active value;
714 /// earlier entries are values that were overridden. The first entry is
715 /// always the compiled-in defaults layer.
716 ///
717 /// ```
718 /// # use provcfg::{Category, Config, Configurable};
719 /// # #[derive(Configurable, serde::Deserialize, Clone, Default)]
720 /// # struct Settings { host: String }
721 /// # unsafe {
722 /// # std::env::set_var("HIST_BASE_HOST", "from-base");
723 /// # std::env::set_var("HIST_OVERRIDE_HOST", "from-override");
724 /// # }
725 /// let settings = Config::new()
726 /// .add_env("HIST_BASE")
727 /// .add_env("HIST_OVERRIDE")
728 /// .build::<SettingsProv>()
729 /// .unwrap();
730 ///
731 /// // defaults layer + two env sources, oldest first.
732 /// let chain = settings.host.history();
733 /// assert_eq!(chain.len(), 3);
734 /// assert_eq!(chain[0].value, "");
735 /// assert_eq!(chain[0].source.category(), Category::Default);
736 /// assert_eq!(chain[1].value, "from-base");
737 /// assert_eq!(chain[2].value, "from-override"); // active
738 /// ```
739 #[must_use]
740 pub fn history(&self) -> &[Value<T>] {
741 &self.inner
742 }
743}
744
745impl<T> Default for ValueHistory<T> {
746 fn default() -> Self {
747 Self::new()
748 }
749}
750
751/// The error type returned by [`Config::build`] and the `add_*_file` helpers.
752#[derive(Debug, thiserror::Error)]
753pub enum Error {
754 /// A source's raw data could not be deserialized into the target struct.
755 #[error("failed to deserialize source {source:?}: {error}")]
756 Deserialize {
757 source: String,
758 #[source]
759 error: erased_serde::Error,
760 },
761 /// A config file could not be read from disk.
762 #[error("failed to read config file {path:?}: {error}")]
763 Io {
764 path: String,
765 #[source]
766 error: std::io::Error,
767 },
768}
769
770#[cfg(test)]
771mod tests;