use super::{join_lines, style};
use rich::table::Table;
use rich::{Console, ConsoleOptions, Renderable, Segment, Style, Text};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Layer {
pub name: String,
pub origin: Option<String>,
pub values: Vec<(String, String)>,
}
impl Layer {
pub fn new(name: impl Into<String>) -> Self {
Layer {
name: name.into(),
..Default::default()
}
}
pub fn origin(mut self, origin: impl Into<String>) -> Self {
self.origin = Some(origin.into());
self
}
pub fn value(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.values.push((key.into(), value.into()));
self
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Resolved {
pub key: String,
pub value: String,
pub winner: usize,
pub shadowed: Vec<(usize, String)>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Precedence {
pub layers: Vec<Layer>,
}
impl Precedence {
pub fn new() -> Self {
Precedence::default()
}
pub fn layer(mut self, layer: Layer) -> Self {
self.layers.push(layer);
self
}
pub fn resolve(&self) -> Vec<Resolved> {
let mut out: Vec<Resolved> = Vec::new();
for (index, layer) in self.layers.iter().enumerate() {
for (key, value) in &layer.values {
match out.iter_mut().find(|r| &r.key == key) {
Some(resolved) => {
let old = std::mem::replace(&mut resolved.value, value.clone());
resolved.shadowed.push((resolved.winner, old));
resolved.winner = index;
}
None => out.push(Resolved {
key: key.clone(),
value: value.clone(),
winner: index,
shadowed: Vec::new(),
}),
}
}
}
out
}
pub fn view(&self) -> PrecedenceView {
PrecedenceView {
precedence: self.clone(),
}
}
pub fn explain(&self, key: &str) -> Option<Explanation> {
let resolved = self.resolve().into_iter().find(|r| r.key == key)?;
Some(Explanation {
layers: self.layers.clone(),
resolved,
})
}
}
fn markers(c: &Console) -> (&'static str, &'static str) {
if c.ascii_only() {
("*", "x")
} else {
("✔", "✗")
}
}
#[derive(Clone, Debug)]
pub struct PrecedenceView {
precedence: Precedence,
}
impl Renderable for PrecedenceView {
fn rich_render(&self, c: &Console, o: &ConsoleOptions) -> Vec<Segment> {
let (win, lose) = markers(c);
let winner = style(c, "config.winner");
let shadowed = style(c, "config.shadowed");
let key_style = style(c, "config.key");
let mut table = Table::new();
table.add_column("Key");
for layer in &self.precedence.layers {
table.add_column_text(Text::new(layer.name.clone()), rich::Justify::Left);
}
table.add_column("Effective");
let origins: Vec<String> = self
.precedence
.layers
.iter()
.filter_map(|l| l.origin.as_ref().map(|o| format!("{}: {o}", l.name)))
.collect();
if !origins.is_empty() {
table = table.caption(origins.join(", "));
}
for resolved in self.precedence.resolve() {
let mut row = vec![Text::styled(resolved.key.clone(), key_style.clone())];
for index in 0..self.precedence.layers.len() {
let cell = if index == resolved.winner {
Text::styled(format!("{win} {}", resolved.value), winner.clone())
} else {
match resolved.shadowed.iter().rev().find(|(i, _)| *i == index) {
Some((_, value)) => {
Text::styled(format!("{lose} {value}"), shadowed.clone())
}
None => Text::new(""),
}
};
row.push(cell);
}
row.push(Text::new(resolved.value.clone()));
table.add_row_text(row);
}
join_lines(c.render_lines(&table, o, false))
}
}
#[derive(Clone, Debug)]
pub struct Explanation {
layers: Vec<Layer>,
resolved: Resolved,
}
impl Explanation {
pub fn resolved(&self) -> &Resolved {
&self.resolved
}
}
impl Renderable for Explanation {
fn rich_render(&self, c: &Console, o: &ConsoleOptions) -> Vec<Segment> {
let (win, lose) = markers(c);
let resolved = &self.resolved;
let mut chain: Vec<(usize, &str)> = vec![(resolved.winner, resolved.value.as_str())];
chain.extend(
resolved
.shadowed
.iter()
.rev()
.map(|(i, v)| (*i, v.as_str())),
);
let label = |index: usize| {
let layer = &self.layers[index];
match &layer.origin {
Some(origin) => (layer.name.clone(), format!(" ({origin})")),
None => (layer.name.clone(), String::new()),
}
};
let name_width = chain
.iter()
.map(|(i, _)| {
let (name, origin) = label(*i);
rich::cells::cell_len(&name) + rich::cells::cell_len(&origin)
})
.max()
.unwrap_or(0);
let value_width = chain
.iter()
.map(|(_, v)| rich::cells::cell_len(v))
.max()
.unwrap_or(0);
let key_style = style(c, "config.key");
let mut header = Text::new("");
header.append(&resolved.key, Some(key_style.into()));
header.append(" = ", None);
header.append(&resolved.value, Some(style(c, "config.winner").into()));
let plain = Style::new();
let mut rows = header.render_lines(c.theme(), &plain, Some(o.max_width.max(1)));
for (position, (index, value)) in chain.iter().enumerate() {
let (name, origin) = label(*index);
let pad = name_width - rich::cells::cell_len(&name) - rich::cells::cell_len(&origin);
let value_pad = value_width - rich::cells::cell_len(value);
let mut line = Text::new(" ");
let (marker, value_style, note) = if position == 0 {
(win, style(c, "config.winner"), "effective".to_string())
} else {
let above = &self.layers[chain[position - 1].0].name;
(
lose,
style(c, "config.shadowed"),
format!("overridden by {above}"),
)
};
line.append(marker, Some(value_style.clone().into()));
line.append(" ", None);
line.append(&name, None);
if !origin.is_empty() {
line.append(&origin, Some(style(c, "config.origin").into()));
}
line.append(&" ".repeat(pad + 2), None);
line.append(value, Some(value_style.into()));
line.append(&" ".repeat(value_pad + 2), None);
line.append(¬e, None);
rows.extend(line.render_lines(c.theme(), &plain, Some(o.max_width.max(1))));
}
join_lines(rows)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_repeated_key_within_a_layer_keeps_the_last_value() {
let precedence = Precedence::new().layer(Layer::new("a").value("k", "1").value("k", "2"));
let resolved = precedence.resolve();
assert_eq!(resolved[0].value, "2");
assert_eq!(resolved[0].shadowed, vec![(0, "1".to_string())]);
}
#[test]
fn explain_is_none_for_an_unknown_key() {
assert!(Precedence::new()
.layer(Layer::new("a"))
.explain("nope")
.is_none());
}
}