Skip to main content

siderite_config/
explain.rs

1//! Every resolved configuration value with the source that set it, from
2//! the same merged sources [`ConfigBuilder::build`](crate::ConfigBuilder)
3//! reads, so the explanation and the loaded settings cannot disagree.
4
5use crate::builder::ALIASES;
6use crate::error::is_secret_key;
7use crate::redact::{REDACTED, redact_text};
8use figment::Figment;
9use figment::value::{Dict, Value};
10use serde::Serialize;
11
12/// The kind of source that set a value.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
14#[serde(rename_all = "snake_case")]
15pub enum SourceKind {
16    /// A compiled default.
17    Default,
18    /// A TOML file.
19    File,
20    /// An environment variable.
21    Environment,
22    /// A programmatic override (`ConfigBuilder::set`).
23    Override,
24}
25
26/// Where a value came from.
27#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
28pub struct ConfigSource {
29    /// The kind of source.
30    pub kind: SourceKind,
31    /// The file path or environment variable name, when known.
32    pub detail: Option<String>,
33}
34
35/// One resolved value.
36#[derive(Debug, Clone, PartialEq, Serialize)]
37pub struct ConfigEntry {
38    /// Dotted key, `databases.default.url`.
39    pub key: String,
40    /// The value, redacted: secret keys show only a URL without its
41    /// credentials, or `***`; other text loses any credential it holds.
42    pub value: serde_json::Value,
43    /// The source with the highest precedence that set it.
44    pub source: ConfigSource,
45}
46
47type Entries = Vec<ConfigEntry>;
48
49/// The resolved entries of `merged`, sorted by key; `overrides` tells
50/// programmatic overrides from defaults.
51pub(crate) fn entries(merged: &Figment, overrides: &Figment) -> Entries {
52    let Ok(root) = merged.extract::<Dict>() else {
53        return Vec::new();
54    };
55    let mut out = Vec::new();
56    walk(&root, "", &mut |key, value| {
57        out.push(ConfigEntry {
58            key: key.to_owned(),
59            value: shown(key, value),
60            source: source(merged, overrides, key),
61        });
62    });
63    out.sort_by(|a, b| a.key.cmp(&b.key));
64    out
65}
66
67/// Call `leaf` for every non-dictionary value under `dict`.
68fn walk(dict: &Dict, prefix: &str, leaf: &mut dyn FnMut(&str, &Value)) {
69    for (name, value) in dict {
70        let key = if prefix.is_empty() {
71            name.clone()
72        } else {
73            format!("{prefix}.{name}")
74        };
75        match value {
76            Value::Dict(_, inner) => walk(inner, &key, leaf),
77            other => leaf(&key, other),
78        }
79    }
80}
81
82/// The value as it may be shown.
83fn shown(key: &str, value: &Value) -> serde_json::Value {
84    let secret = key.split('.').any(is_secret_key);
85    match value {
86        Value::String(_, text) if secret && text.contains("://") => {
87            serde_json::Value::String(redact_text(text))
88        }
89        _ if secret => serde_json::Value::String(REDACTED.to_owned()),
90        Value::String(_, text) => serde_json::Value::String(redact_text(text)),
91        other => {
92            let json = serde_json::to_value(other);
93            json.unwrap_or(serde_json::Value::Null)
94        }
95    }
96}
97
98/// The environment variable that set `key`: the provider's prefix (shown
99/// in backticks in its name) plus the key nested on `__`, or an alias.
100fn variable(provider: &str, key: &str) -> Option<String> {
101    let prefix = provider.split('`').nth(1).filter(|p| !p.is_empty());
102    match prefix {
103        Some(prefix) => {
104            let nested = key.split('.').collect::<Vec<_>>().join("__");
105            Some(format!("{prefix}{}", nested.to_ascii_uppercase()))
106        }
107        None => ALIASES
108            .iter()
109            .find(|(_, path)| *path == key)
110            .map(|(var, _)| (*var).to_owned()),
111    }
112}
113
114fn source(merged: &Figment, overrides: &Figment, key: &str) -> ConfigSource {
115    if overrides.find_value(key).is_ok() {
116        return ConfigSource {
117            kind: SourceKind::Override,
118            detail: None,
119        };
120    }
121    let Some(meta) = merged.find_metadata(key) else {
122        return ConfigSource {
123            kind: SourceKind::Default,
124            detail: None,
125        };
126    };
127    if let Some(figment::Source::File(path)) = &meta.source {
128        return ConfigSource {
129            kind: SourceKind::File,
130            detail: Some(path.display().to_string()),
131        };
132    }
133    if meta.name.contains("environment") {
134        return ConfigSource {
135            kind: SourceKind::Environment,
136            detail: variable(&meta.name, key),
137        };
138    }
139    ConfigSource {
140        kind: SourceKind::Default,
141        detail: None,
142    }
143}