1use std::fmt;
14
15use figment::value::Value;
16
17use crate::error::{Error, Origin};
18use crate::source::LoadSpec;
19
20#[derive(Clone)]
22#[non_exhaustive]
23pub struct Contribution {
24 pub layer: &'static str,
26 pub origin: Option<Origin>,
30 pub value: Option<String>,
34}
35
36#[derive(Clone)]
42pub struct Explanation {
43 path: String,
44 rows: Vec<Contribution>,
45}
46
47impl Explanation {
48 pub(crate) fn new(path: String, rows: Vec<Contribution>) -> Self {
49 Self { path, rows }
50 }
51
52 #[must_use]
54 pub fn path(&self) -> &str {
55 &self.path
56 }
57
58 #[must_use]
60 pub fn rows(&self) -> &[Contribution] {
61 &self.rows
62 }
63
64 #[must_use]
67 pub fn winner(&self) -> Option<&Contribution> {
68 self.rows.iter().rev().find(|row| row.value.is_some())
69 }
70
71 #[must_use]
76 pub fn redacted(mut self) -> Self {
77 for row in &mut self.rows {
78 if row.value.is_some() {
79 row.value = Some("***".to_owned());
80 }
81 }
82
83 self
84 }
85}
86
87impl fmt::Debug for Contribution {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 f.debug_struct("Contribution")
94 .field("layer", &self.layer)
95 .field("origin", &self.origin)
96 .field("value", &self.value.as_ref().map(|_| "..."))
97 .finish()
98 }
99}
100
101impl fmt::Debug for Explanation {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 f.debug_struct("Explanation")
104 .field("path", &self.path)
105 .field("rows", &self.rows)
106 .finish()
107 }
108}
109
110impl fmt::Display for Explanation {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 match self.winner() {
113 Some(winner) => writeln!(
114 f,
115 "{} = {}",
116 self.path,
117 winner.value.as_deref().unwrap_or("***")
118 )?,
119 None => writeln!(f, "{}: nothing supplies it", self.path)?,
120 }
121 writeln!(f)?;
122
123 let sources: Vec<String> = self
124 .rows
125 .iter()
126 .map(|row| {
127 row.origin
128 .as_ref()
129 .map_or_else(|| "—".to_owned(), Origin::to_string)
130 })
131 .collect();
132
133 let layer_width = self
134 .rows
135 .iter()
136 .map(|row| row.layer.len())
137 .chain(["layer".len()])
138 .max()
139 .unwrap_or(0);
140 let source_width = sources
141 .iter()
142 .map(String::len)
143 .chain(["source".len()])
144 .max()
145 .unwrap_or(0);
146
147 writeln!(
148 f,
149 "{:layer_width$} {:source_width$} value",
150 "layer", "source"
151 )?;
152
153 let winner_at = self.rows.iter().rposition(|row| row.value.is_some());
154
155 for (index, (row, source)) in self.rows.iter().zip(&sources).enumerate() {
156 let value = row.value.as_deref().unwrap_or("absent");
157 let marker = if Some(index) == winner_at {
158 " ← winner"
159 } else {
160 ""
161 };
162
163 writeln!(
164 f,
165 "{:layer_width$} {source:source_width$} {value}{marker}",
166 row.layer
167 )?;
168 }
169
170 Ok(())
171 }
172}
173
174pub(crate) fn explain(spec: &LoadSpec<'_>, path: &str) -> Result<Explanation, Error> {
180 let mut rows = Vec::new();
181
182 for (name, figment) in crate::loader::layer_figments(spec)? {
183 let value = figment.find_value(path).ok();
184 let origin = value
185 .is_some()
186 .then(|| crate::loader::origin_in(&figment, path));
187
188 rows.push(Contribution {
189 layer: name,
190 origin,
191 value: value.map(|value| render(&value)),
192 });
193 }
194
195 let merged = crate::loader::merged(spec)?;
203
204 if let Ok(value) = merged.find_value(path) {
205 let origin = crate::loader::origin_in(&merged, path);
206 let walk_winner = rows.iter().rev().find(|row| row.value.is_some());
207 let disagrees = match walk_winner {
208 None => true,
209 Some(winner) => {
210 !matches!(origin, Origin::Unknown) && winner.origin.as_ref() != Some(&origin)
211 }
212 };
213
214 if disagrees {
215 rows.push(Contribution {
216 layer: "alias",
217 origin: Some(origin),
218 value: Some(render(&value)),
219 });
220 }
221 }
222
223 Ok(Explanation::new(path.to_owned(), rows))
224}
225
226fn render(value: &Value) -> String {
229 match value {
230 Value::String(_, string) => string.clone(),
231 Value::Char(_, character) => character.to_string(),
232 Value::Bool(_, boolean) => boolean.to_string(),
233 Value::Num(_, number) => number
234 .to_i128()
235 .map(|whole| whole.to_string())
236 .or_else(|| number.to_u128().map(|whole| whole.to_string()))
237 .or_else(|| number.to_f64().map(|float| float.to_string()))
238 .unwrap_or_else(|| "a number".to_owned()),
239 Value::Empty(..) => "null".to_owned(),
240 Value::Dict(_, table) => format!("a table ({} keys)", table.len()),
241 Value::Array(_, items) => format!("a list ({} items)", items.len()),
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 fn row(layer: &'static str, value: Option<&str>) -> Contribution {
250 Contribution {
251 layer,
252 origin: value.map(|_| Origin::Runtime("default")),
253 value: value.map(str::to_owned),
254 }
255 }
256
257 #[test]
258 fn the_winner_is_the_highest_layer_that_supplies_anything() {
259 let explanation = Explanation::new(
260 "port".to_owned(),
261 vec![
262 row("default", Some("3000")),
263 row("file", Some("8080")),
264 row("environment", None),
265 ],
266 );
267
268 assert_eq!(explanation.winner().unwrap().layer, "file");
269 }
270
271 #[test]
272 fn redaction_blanks_values_and_keeps_origins() {
273 let explanation = Explanation::new(
274 "token".to_owned(),
275 vec![row("file", Some("hunter2")), row("environment", None)],
276 )
277 .redacted();
278
279 let rendered = explanation.to_string();
280
281 assert!(!rendered.contains("hunter2"), "{rendered}");
282 assert!(rendered.contains("***"), "{rendered}");
283 assert!(explanation.rows()[0].origin.is_some());
284 assert_eq!(explanation.rows()[1].value, None, "absent stays absent");
285 }
286
287 #[test]
288 fn the_table_marks_the_winner() {
289 let explanation = Explanation::new(
290 "port".to_owned(),
291 vec![row("default", Some("3000")), row("file", Some("8080"))],
292 );
293
294 let rendered = explanation.to_string();
295 let winner_line = rendered
296 .lines()
297 .find(|line| line.contains("← winner"))
298 .expect("one row is marked");
299
300 assert!(winner_line.starts_with("file"), "{rendered}");
301 assert!(rendered.starts_with("port = 8080"), "{rendered}");
302 }
303}