use std::fmt;
use figment::value::Value;
use crate::error::{Error, Origin};
use crate::source::LoadSpec;
#[derive(Clone)]
#[non_exhaustive]
pub struct Contribution {
pub layer: &'static str,
pub origin: Option<Origin>,
pub value: Option<String>,
}
#[derive(Clone)]
pub struct Explanation {
path: String,
rows: Vec<Contribution>,
}
impl Explanation {
pub(crate) fn new(path: String, rows: Vec<Contribution>) -> Self {
Self { path, rows }
}
#[must_use]
pub fn path(&self) -> &str {
&self.path
}
#[must_use]
pub fn rows(&self) -> &[Contribution] {
&self.rows
}
#[must_use]
pub fn winner(&self) -> Option<&Contribution> {
self.rows.iter().rev().find(|row| row.value.is_some())
}
#[must_use]
pub fn redacted(mut self) -> Self {
for row in &mut self.rows {
if row.value.is_some() {
row.value = Some("***".to_owned());
}
}
self
}
}
impl fmt::Debug for Contribution {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Contribution")
.field("layer", &self.layer)
.field("origin", &self.origin)
.field("value", &self.value.as_ref().map(|_| "..."))
.finish()
}
}
impl fmt::Debug for Explanation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Explanation")
.field("path", &self.path)
.field("rows", &self.rows)
.finish()
}
}
impl fmt::Display for Explanation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.winner() {
Some(winner) => writeln!(
f,
"{} = {}",
self.path,
winner.value.as_deref().unwrap_or("***")
)?,
None => writeln!(f, "{}: nothing supplies it", self.path)?,
}
writeln!(f)?;
let sources: Vec<String> = self
.rows
.iter()
.map(|row| {
row.origin
.as_ref()
.map_or_else(|| "—".to_owned(), Origin::to_string)
})
.collect();
let layer_width = self
.rows
.iter()
.map(|row| row.layer.len())
.chain(["layer".len()])
.max()
.unwrap_or(0);
let source_width = sources
.iter()
.map(String::len)
.chain(["source".len()])
.max()
.unwrap_or(0);
writeln!(
f,
"{:layer_width$} {:source_width$} value",
"layer", "source"
)?;
let winner_at = self.rows.iter().rposition(|row| row.value.is_some());
for (index, (row, source)) in self.rows.iter().zip(&sources).enumerate() {
let value = row.value.as_deref().unwrap_or("absent");
let marker = if Some(index) == winner_at {
" ← winner"
} else {
""
};
writeln!(
f,
"{:layer_width$} {source:source_width$} {value}{marker}",
row.layer
)?;
}
Ok(())
}
}
pub(crate) fn explain(spec: &LoadSpec<'_>, path: &str) -> Result<Explanation, Error> {
let mut rows = Vec::new();
for (name, figment) in crate::loader::layer_figments(spec)? {
let value = figment.find_value(path).ok();
let origin = value
.is_some()
.then(|| crate::loader::origin_in(&figment, path, spec.nest));
rows.push(Contribution {
layer: name,
origin,
value: value.map(|value| render(&value)),
});
}
let merged = crate::loader::merged(spec)?;
if let Ok(value) = merged.find_value(path) {
let origin = crate::loader::origin_in(&merged, path, spec.nest);
let walk_winner = rows.iter().rev().find(|row| row.value.is_some());
let disagrees = match walk_winner {
None => true,
Some(winner) => {
!matches!(origin, Origin::Unknown) && winner.origin.as_ref() != Some(&origin)
}
};
if disagrees {
rows.push(Contribution {
layer: "alias",
origin: Some(origin),
value: Some(render(&value)),
});
}
}
Ok(Explanation::new(path.to_owned(), rows))
}
fn render(value: &Value) -> String {
match value {
Value::String(_, string) => string.clone(),
Value::Char(_, character) => character.to_string(),
Value::Bool(_, boolean) => boolean.to_string(),
Value::Num(_, number) => number
.to_i128()
.map(|whole| whole.to_string())
.or_else(|| number.to_u128().map(|whole| whole.to_string()))
.or_else(|| number.to_f64().map(|float| float.to_string()))
.unwrap_or_else(|| "a number".to_owned()),
Value::Empty(..) => "null".to_owned(),
Value::Dict(_, table) => format!("a table ({} keys)", table.len()),
Value::Array(_, items) => format!("a list ({} items)", items.len()),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn row(layer: &'static str, value: Option<&str>) -> Contribution {
Contribution {
layer,
origin: value.map(|_| Origin::Runtime("default")),
value: value.map(str::to_owned),
}
}
#[test]
fn the_winner_is_the_highest_layer_that_supplies_anything() {
let explanation = Explanation::new(
"port".to_owned(),
vec![
row("default", Some("3000")),
row("file", Some("8080")),
row("environment", None),
],
);
assert_eq!(explanation.winner().unwrap().layer, "file");
}
#[test]
fn redaction_blanks_values_and_keeps_origins() {
let explanation = Explanation::new(
"token".to_owned(),
vec![row("file", Some("hunter2")), row("environment", None)],
)
.redacted();
let rendered = explanation.to_string();
assert!(!rendered.contains("hunter2"), "{rendered}");
assert!(rendered.contains("***"), "{rendered}");
assert!(explanation.rows()[0].origin.is_some());
assert_eq!(explanation.rows()[1].value, None, "absent stays absent");
}
#[test]
fn the_table_marks_the_winner() {
let explanation = Explanation::new(
"port".to_owned(),
vec![row("default", Some("3000")), row("file", Some("8080"))],
);
let rendered = explanation.to_string();
let winner_line = rendered
.lines()
.find(|line| line.contains("← winner"))
.expect("one row is marked");
assert!(winner_line.starts_with("file"), "{rendered}");
assert!(rendered.starts_with("port = 8080"), "{rendered}");
}
}