Skip to main content

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;