settings_loader/
provenance.rs

1//! Source provenance tracking for configuration settings.
2//!
3//! This module provides the structures and logic to track where each configuration
4//! value originated (e.g., specific file, environment variable, default value).
5//! This is essential for:
6//! - Multi-scope configuration (knowing if a value is UserGlobal or ProjectLocal)
7//! - Layer-aware editing (knowing which file to update)
8//! - UI visualization (showing the user the source of a setting)
9
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::fmt;
13use std::path::PathBuf;
14
15use crate::scope::ConfigScope;
16pub use crate::scope::ConfigScope as SourceScope;
17
18/// Identifies the specific source that provided a configuration value.
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(tag = "type", rename_all = "snake_case")]
21pub enum SettingSource {
22    /// Default values provided by the application code or metadata
23    Default,
24    /// Configuration loaded from a file
25    File {
26        /// Standardized absolute path to the file
27        path: PathBuf,
28        /// The configuration scope (e.g., UserGlobal, ProjectLocal)
29        #[serde(skip_serializing_if = "Option::is_none")]
30        scope: Option<ConfigScope>,
31    },
32    /// Configuration loaded from a specific environment variable
33    EnvVar {
34        /// Name of the environment variable
35        name: String,
36    },
37    /// Configuration loaded from a set of environment variables (e.g., APP_*)
38    EnvVars {
39        /// Prefix used for the search
40        prefix: String,
41    },
42    /// Configuration loaded from a secrets file
43    Secrets {
44        /// Path to the secrets file
45        path: PathBuf,
46    },
47    /// Configuration provided by CLI arguments or runtime overrides
48    Override {
49        /// Name/identifier of the override
50        name: String,
51    },
52}
53
54impl fmt::Display for SettingSource {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        match self {
57            Self::Default => write!(f, "default"),
58            Self::File { path, scope } => {
59                let scope_str = scope.map(|s| format!(" ({:?})", s)).unwrap_or_default();
60                write!(f, "file:{}{}", path.display(), scope_str)
61            },
62            Self::EnvVar { name } => write!(f, "env:{}", name),
63            Self::EnvVars { prefix } => write!(f, "env_vars:{}*", prefix),
64            Self::Secrets { path } => write!(f, "secrets:{}", path.display()),
65            Self::Override { name } => write!(f, "override:{}", name),
66        }
67    }
68}
69
70/// Metadata describing the origin and precedence of a configuration value.
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72pub struct SourceMetadata {
73    /// The specific source origin
74    pub source: SettingSource,
75    /// Precedence layer index (higher values mean higher precedence)
76    pub layer_index: usize,
77}
78
79impl SourceMetadata {
80    /// Create metadata for a file source
81    pub fn file(path: PathBuf, scope: Option<ConfigScope>, layer_index: usize) -> Self {
82        Self {
83            source: SettingSource::File { path, scope },
84            layer_index,
85        }
86    }
87
88    /// Create metadata for an environment variable source
89    pub fn env(name: String, layer_index: usize) -> Self {
90        Self {
91            source: SettingSource::EnvVar { name },
92            layer_index,
93        }
94    }
95
96    /// Create metadata for a default source
97    pub fn default(layer_index: usize) -> Self {
98        Self { source: SettingSource::Default, layer_index }
99    }
100}
101
102/// A map tracking the source of every configuration key.
103///
104/// Keys are dotted paths (e.g., "database.host").
105/// Values are the metadata of the source that provided the *winning* value for that key.
106#[derive(Debug, Clone, Default, Serialize, Deserialize)]
107pub struct SourceMap {
108    /// Map of setting key -> SourceMetadata
109    entries: HashMap<String, SourceMetadata>,
110}
111
112impl SourceMap {
113    /// Create a new empty source map
114    pub fn new() -> Self {
115        Self { entries: HashMap::new() }
116    }
117
118    /// Record a source for a given key.
119    ///
120    /// If a key already exists, it is only updated if the new metadata has
121    /// a higher or equal layer index (higher precedence).
122    pub fn insert(&mut self, key: String, metadata: SourceMetadata) {
123        if let Some(existing) = self.entries.get(&key) {
124            if metadata.layer_index >= existing.layer_index {
125                self.entries.insert(key, metadata);
126            }
127        } else {
128            self.entries.insert(key, metadata);
129        }
130    }
131
132    /// Get the source metadata for a specific key
133    pub fn source_of(&self, key: &str) -> Option<&SourceMetadata> {
134        self.entries.get(key)
135    }
136
137    /// Get all entries in the source map
138    pub fn entries(&self) -> &HashMap<String, SourceMetadata> {
139        &self.entries
140    }
141
142    /// Generate a structured audit report of all configuration sources.
143    /// Inserts all keys from a source into the map, only if they have higher precedence (higher layer_index).
144    pub fn insert_layer(&mut self, metadata: SourceMetadata, props: HashMap<String, config::Value>) {
145        for key in props.keys() {
146            let should_insert = match self.entries.get(key) {
147                Some(existing) => metadata.layer_index >= existing.layer_index,
148                None => true,
149            };
150
151            if should_insert {
152                self.entries.insert(key.clone(), metadata.clone());
153            }
154        }
155    }
156
157    pub fn audit_report(&self) -> String {
158        let mut report = String::from("Configuration Audit Report\n");
159        report.push_str("==========================\n\n");
160
161        let mut sorted_keys: Vec<_> = self.entries.keys().collect();
162        sorted_keys.sort();
163
164        for key in sorted_keys {
165            let meta = &self.entries[key];
166            report.push_str(&format!("{:<30} -> Layer {}: {}\n", key, meta.layer_index, meta.source));
167        }
168
169        report
170    }
171}