use std::collections::BTreeMap;
use std::sync::OnceLock;
use tabled::Table;
use tabled::builder::Builder as TableBuilder;
use tabled::settings::Style;
use tabled::settings::style::HorizontalLine;
#[derive(Debug)]
pub struct TableDef {
pub name: &'static str,
pub command: &'static str,
pub columns: &'static [&'static str],
pub verbose_only: &'static [&'static str],
pub dynamic: Option<&'static str>,
}
pub mod defs {
use super::TableDef;
const fn def(
name: &'static str,
command: &'static str,
columns: &'static [&'static str],
verbose_only: &'static [&'static str],
) -> TableDef {
TableDef {
name,
command,
columns,
verbose_only,
dynamic: None,
}
}
const fn dynamic(name: &'static str, command: &'static str, columns: &'static str) -> TableDef {
TableDef {
name,
command,
columns: &[],
verbose_only: &[],
dynamic: Some(columns),
}
}
pub const ANALYZER_LIST: TableDef = def(
"analyzer_list",
"analyzer list",
&["Name", "Native", "Description"],
&["Description"],
);
pub const ANALYZER_DEFCONFIG: TableDef = def(
"analyzer_defconfig",
"analyzer defconfig",
&["Field", "Type", "Default"],
&[],
);
pub const ANALYZER_STATS: TableDef = def(
"analyzer_stats",
"analyzer stats, analyzer build",
&["Analyzer", "Files", "Dependencies"],
&[],
);
pub const ANALYZER_USED: TableDef = def(
"analyzer_used",
"analyzer used",
&["Name", "Detected", "Description"],
&["Description"],
);
pub const BUILD_TIMINGS: TableDef = def(
"build_timings",
"build --timings",
&["Phase", "Duration"],
&[],
);
pub const ERROR_LIST: TableDef = def(
"error_list",
"error",
&["Code", "Name", "Description"],
&["Description"],
);
pub const HOOK_LIST: TableDef = def(
"hook_list",
"hook",
&["Hook", "Function", "Location", "Description"],
&["Function", "Location"],
);
pub const INFO_SOURCE: TableDef =
def("info_source", "info source", &["Extension", "Count"], &[]);
pub const PROCESSOR_CONFIG: TableDef = def(
"processor_config",
"processor config",
&["Field", "Value"],
&[],
);
pub const PROCESSOR_FIELDS: TableDef = def(
"processor_fields",
"processor defconfig, processor info",
&[
"Field",
"Type",
"Default",
"Required",
"Checksum",
"Description",
],
&["Description"],
);
pub const PROCESSOR_LIST: TableDef = def(
"processor_list",
"processor list",
&[
"Name",
"Type",
"Native",
"Rust",
"Batch",
"Cache",
"Rust alternative",
"Description",
],
&["Description"],
);
pub const PROCESSOR_RECOMMEND: TableDef = def(
"processor_recommend",
"processor recommend",
&["Extension / File", "Processor", "Also", "Reason"],
&[],
);
pub const PROCESSOR_SEARCH: TableDef = def(
"processor_search",
"processor search",
&["Name", "Type", "Description", "Keywords"],
&[],
);
pub const PROCESSOR_TYPES: TableDef = def(
"processor_types",
"processor types",
&["Type", "Batch", "Description", "Batching"],
&["Batching"],
);
pub const PROCESSOR_USED: TableDef = def(
"processor_used",
"processor used",
&["Name", "Type", "Detected", "Description"],
&["Description"],
);
pub const PRODUCT_STATUS: TableDef = dynamic(
"product_status",
"status, build --dry-run",
"Processor, one column per product state (up-to-date, restorable, stale, new for \
`status`; skip, restore, build, build-new for `build --dry-run`), native, rust",
);
pub const SLOC: TableDef = def(
"sloc",
"sloc",
&["Language", "Files", "Blank", "Comment", "Code"],
&[],
);
pub const STATUS_BREAKDOWN: TableDef = def(
"status_breakdown",
"status --breakdown",
&["Processor", "Files", "Breakdown"],
&[],
);
pub const TAG_COUNT: TableDef = def("tag_count", "tag count", &["Count", "Tag"], &[]);
pub const TAG_COVERAGE: TableDef = def(
"tag_coverage",
"tag coverage",
&["Category", "Files", "Coverage"],
&[],
);
pub const TAG_MATRIX: TableDef = dynamic(
"tag_matrix",
"tag matrix",
"File, then one column per tag category",
);
pub const TAG_SUGGEST: TableDef = def("tag_suggest", "tag suggest", &["Tag", "Score"], &[]);
pub const TOOL_LIST: TableDef = def(
"tool_list",
"tool list",
&["Tool", "Status", "Runtime", "Install"],
&["Install"],
);
pub const TOOL_RUNTIMES: TableDef = def(
"tool_runtimes",
"tool stats",
&["Runtime", "Installed"],
&[],
);
pub const TOOL_STATS: TableDef = def(
"tool_stats",
"tool stats",
&["Tool", "Status", "Processors", "Install"],
&[],
);
pub const WEBCACHE_LIST: TableDef = def(
"webcache_list",
"webcache list",
&["URL", "Size", "Age", "State"],
&[],
);
pub const ALL: &[&TableDef] = &[
&ANALYZER_DEFCONFIG,
&ANALYZER_LIST,
&ANALYZER_STATS,
&ANALYZER_USED,
&BUILD_TIMINGS,
&ERROR_LIST,
&HOOK_LIST,
&INFO_SOURCE,
&PROCESSOR_CONFIG,
&PROCESSOR_FIELDS,
&PROCESSOR_LIST,
&PROCESSOR_RECOMMEND,
&PROCESSOR_SEARCH,
&PROCESSOR_TYPES,
&PROCESSOR_USED,
&PRODUCT_STATUS,
&SLOC,
&STATUS_BREAKDOWN,
&TAG_COUNT,
&TAG_COVERAGE,
&TAG_MATRIX,
&TAG_SUGGEST,
&TOOL_LIST,
&TOOL_RUNTIMES,
&TOOL_STATS,
&WEBCACHE_LIST,
];
pub fn find(name: &str) -> Option<&'static TableDef> {
ALL.iter().copied().find(|d| d.name == name)
}
}
static CONFIG: OnceLock<BTreeMap<String, Vec<String>>> = OnceLock::new();
pub fn init(config: BTreeMap<String, Vec<String>>) {
let _ = CONFIG.set(config);
}
fn configured(name: &str) -> Option<&'static [String]> {
CONFIG.get()?.get(name).map(Vec::as_slice)
}
#[must_use]
pub fn validate_config(config: &BTreeMap<String, Vec<String>>) -> Vec<String> {
let mut errors = Vec::new();
for (name, fields) in config {
let Some(def) = defs::find(name) else {
errors.push(format!(
"[tables] {name}: no table has this name; the tables are {}",
defs::ALL
.iter()
.map(|d| d.name)
.collect::<Vec<_>>()
.join(", ")
));
continue;
};
if fields.is_empty() {
errors.push(format!(
"[tables] {name}: the field list is empty; name at least one field, or remove the entry to show the default columns"
));
continue;
}
if def.dynamic.is_some() {
continue;
}
for field in fields {
if !def.columns.contains(&field.as_str()) {
errors.push(format!(
"[tables] {name}: no field named \"{field}\"; the fields are \"{}\"",
def.columns.join("\", \"")
));
}
}
}
errors
}
fn build_table(headers: &[&str], rows: &[Vec<String>]) -> Table {
let mut builder = TableBuilder::new();
builder.push_record(headers.iter().copied());
for row in rows {
builder.push_record(row.iter().map(std::string::String::as_str));
}
builder.build()
}
fn visible_columns(def: &TableDef, headers: &[&str], verbose: bool) -> Vec<usize> {
if crate::runtime_flags::all_fields() {
return (0..headers.len()).collect();
}
if let Some(fields) = configured(def.name) {
let mut indices = Vec::new();
for field in fields {
match headers.iter().position(|h| h == field) {
Some(i) => indices.push(i),
None => crate::output::warn(&format!(
"[tables] {}: no field named \"{field}\" in this table; the fields are \"{}\"",
def.name,
headers.join("\", \"")
)),
}
}
if !indices.is_empty() {
return indices;
}
}
(0..headers.len())
.filter(|&i| verbose || !def.verbose_only.contains(&headers[i]))
.collect()
}
fn project(row: &[String], indices: &[usize]) -> Vec<String> {
indices.iter().map(|&i| row[i].clone()).collect()
}
fn print_fields_only(def: &TableDef, headers: &[&str]) {
let mut table = build_table(headers, &[]);
crate::output::info(&format!(
"{} (rsconstruct {})\n{}",
crate::color::bold(def.name),
def.command,
table.with(Style::rounded())
));
}
fn render(
def: &TableDef,
headers: &[&str],
verbose: bool,
rows: &[Vec<String>],
total: Option<&[String]>,
) {
if crate::runtime_flags::fields_only() {
print_fields_only(def, headers);
return;
}
let indices = visible_columns(def, headers, verbose);
let headers: Vec<&str> = indices.iter().map(|&i| headers[i]).collect();
let mut all_rows: Vec<Vec<String>> = rows.iter().map(|r| project(r, &indices)).collect();
let Some(total) = total else {
let mut table = build_table(&headers, &all_rows);
crate::output::info(&format!("{}", table.with(Style::rounded())));
return;
};
all_rows.push(project(total, &indices));
let mut table = build_table(&headers, &all_rows);
let n_rows = table.count_rows();
let header_line = HorizontalLine::inherit(Style::modern());
let total_line = HorizontalLine::inherit(Style::modern());
let style = Style::rounded().horizontals([(1, header_line), (n_rows - 1, total_line)]);
crate::output::info(&format!("{}", table.with(style)));
}
pub fn print(def: &TableDef, verbose: bool, rows: &[Vec<String>]) {
debug_assert!(def.dynamic.is_none(), "{} has run-time columns", def.name);
render(def, def.columns, verbose, rows, None);
}
pub fn print_with_total(def: &TableDef, verbose: bool, rows: &[Vec<String>], total: &[String]) {
debug_assert!(def.dynamic.is_none(), "{} has run-time columns", def.name);
render(def, def.columns, verbose, rows, Some(total));
}
pub fn print_dynamic(def: &TableDef, headers: &[&str], rows: &[Vec<String>]) {
debug_assert!(def.dynamic.is_some(), "{} has fixed columns", def.name);
render(def, headers, true, rows, None);
}
pub fn print_dynamic_with_total(
def: &TableDef,
headers: &[&str],
rows: &[Vec<String>],
total: &[String],
) {
debug_assert!(def.dynamic.is_some(), "{} has fixed columns", def.name);
render(def, headers, true, rows, Some(total));
}
pub fn short_processor_name(name: &str) -> &str {
name.strip_prefix("processor.").unwrap_or(name)
}
pub const fn yes_no(b: bool) -> &'static str {
if b { "Yes" } else { "No" }
}
pub const NONE_LABEL: &str = "None";
pub fn opt_json(value: Option<&serde_json::Value>) -> String {
match value {
Some(v) => serde_json::to_string(v).unwrap_or_default(),
None => NONE_LABEL.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn definitions_are_consistent() {
let names: Vec<&str> = defs::ALL.iter().map(|d| d.name).collect();
let mut sorted = names.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(
names, sorted,
"defs::ALL must be sorted by name, without duplicates"
);
for def in defs::ALL {
assert!(
def.name.chars().all(|c| c.is_ascii_lowercase() || c == '_'),
"{}: names are lower-case words joined by _",
def.name
);
assert!(
def.columns.is_empty() == def.dynamic.is_some(),
"{}: fixed columns or a dynamic description, not both",
def.name
);
for v in def.verbose_only {
assert!(
def.columns.contains(v),
"{}: verbose_only column {v} is not a column",
def.name
);
}
}
}
#[test]
fn validate_config_names_the_problem() {
let mut cfg = BTreeMap::new();
cfg.insert("nope".to_string(), vec!["Name".to_string()]);
cfg.insert(
"processor_list".to_string(),
vec!["Name".to_string(), "Colour".to_string()],
);
cfg.insert("tool_list".to_string(), Vec::new());
cfg.insert("tag_matrix".to_string(), vec!["anything".to_string()]);
let errors = validate_config(&cfg);
assert_eq!(errors.len(), 3, "{errors:?}");
assert!(errors[0].contains("no table has this name") && errors[0].contains("tool_list"));
assert!(errors[1].contains("\"Colour\"") && errors[1].contains("\"Rust alternative\""));
assert!(errors[2].contains("empty"));
}
#[test]
fn default_hides_verbose_only_columns_and_verbose_shows_them() {
let def = &defs::PROCESSOR_LIST;
let quiet = visible_columns(def, def.columns, false);
assert_eq!(quiet.len(), def.columns.len() - 1);
assert!(!quiet.contains(&(def.columns.len() - 1)));
let verbose = visible_columns(def, def.columns, true);
assert_eq!(verbose.len(), def.columns.len());
}
#[test]
fn docs_list_every_table() {
let path =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("docs/src/configuration.md");
let text = std::fs::read_to_string(&path).unwrap();
let start = text
.find("### `[tables]`")
.expect("configuration.md has a [tables] section");
let section = &text[start..];
let end = section[4..].find("\n### ").map_or(section.len(), |i| i + 4);
let section = §ion[..end];
let mut missing = Vec::new();
for def in defs::ALL {
let row = section
.lines()
.find(|l| l.starts_with(&format!("| `{}` |", def.name)));
let Some(row) = row else {
missing.push(format!("{}: no row", def.name));
continue;
};
if !row.contains(def.command) {
missing.push(format!("{}: row does not name `{}`", def.name, def.command));
}
for col in def.columns {
if !row.contains(&format!("`{col}`")) {
missing.push(format!("{}: row does not list column `{col}`", def.name));
}
}
}
assert!(
missing.is_empty(),
"docs/src/configuration.md [tables] section is out of date:\n{}",
missing.join("\n")
);
}
}