use std::process::ExitCode;
use sitevet::{Severity, Stage};
use crate::args::{ListFormat, RulesArgs};
struct Row {
id: &'static str,
category: &'static str,
title: &'static str,
severity: &'static str,
browser: bool,
}
fn rows(category: Option<&str>) -> Vec<Row> {
sitevet::all_rules()
.iter()
.map(|r| r.meta())
.filter(|m| category.is_none_or(|c| m.category == c))
.map(|m| Row {
id: m.id,
category: m.category,
title: m.title,
severity: match m.severity {
Severity::Error => "error",
Severity::Warning => "warning",
_ => "info",
},
browser: m.stage == Stage::Rendered,
})
.collect()
}
pub fn run(args: &RulesArgs) -> anyhow::Result<ExitCode> {
let rows = rows(args.category.as_deref());
if rows.is_empty() {
anyhow::bail!("no rules in category `{}`", args.category.as_deref().unwrap_or(""));
}
print!("{}", render(&rows, args.format));
Ok(ExitCode::SUCCESS)
}
fn render(rows: &[Row], format: ListFormat) -> String {
match format {
ListFormat::Json => {
let items: Vec<serde_json::Value> = rows
.iter()
.map(|r| {
serde_json::json!({"id": r.id, "category": r.category, "title": r.title, "severity": r.severity, "browser": r.browser})
})
.collect();
serde_json::to_string_pretty(&items).unwrap_or_default() + "\n"
}
ListFormat::Md => {
let mut out = format!(
"# Rules\n\nsitevet runs {} rules. Generated by `sitevet rules --format md`. Rules marked \"browser\" need `--browser` or a PageSpeed Insights key.\n\n| ID | Category | Title | Severity | Needs |\n| --- | --- | --- | --- | --- |\n",
rows.len()
);
for r in rows {
let needs = if r.browser { "browser" } else { "" };
out.push_str(&format!(
"| `{}` | {} | {} | {} | {needs} |\n",
r.id,
r.category,
r.title.replace('|', "\\|"),
r.severity
));
}
out
}
ListFormat::Table => {
let w = |f: fn(&Row) -> usize| rows.iter().map(f).max().unwrap_or(0);
let (wi, wc, ws) = (w(|r| r.id.len()), w(|r| r.category.len()), w(|r| r.severity.len()));
let mut out = format!("{:<wi$} {:<wc$} {:<ws$} BROWSER\n", "ID", "CATEGORY", "SEVERITY");
for r in rows {
out.push_str(&format!(
"{:<wi$} {:<wc$} {:<ws$} {}\n",
r.id,
r.category,
r.severity,
if r.browser { "yes" } else { "no" }
));
}
out.push_str(&format!("{} rules\n", rows.len()));
out
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn list_matches_registry_and_docs() {
assert_eq!(rows(None).len(), sitevet::all_rules().len());
let md = render(&rows(None), ListFormat::Md);
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/rules.md");
if std::env::var_os("UPDATE_DOCS").is_some() {
std::fs::write(&path, &md).unwrap();
}
let committed = std::fs::read_to_string(path).unwrap().replace("\r\n", "\n");
assert_eq!(committed, md, "run UPDATE_DOCS=1 cargo test -p sitevet-cli");
assert_eq!(rows(Some("security")).len(), 12);
}
}