use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::PathBuf;
#[derive(serde::Deserialize)]
struct CatalogueDoc {
categories: Vec<String>,
rules: Vec<RawRule>,
}
#[derive(serde::Deserialize)]
struct RawRule {
rule_id: String,
category: String,
severity: String,
description: String,
authority: String,
authority_ref: String,
explanation: String,
inspects: Vec<String>,
auto_fixable: bool,
#[serde(default)]
confidence: Option<String>,
#[serde(default)]
guide_section: Option<String>,
#[serde(default)]
guide_url: Option<String>,
}
#[derive(serde::Deserialize)]
struct LatexSpecsDoc {
macros: BTreeMap<String, String>,
verbatim_macros: Vec<String>,
environments: BTreeMap<String, String>,
verbatim_environments: Vec<String>,
specials: Vec<String>,
}
fn find_repo_root(start: &std::path::Path) -> PathBuf {
let marker = "specs/003-jss-rule-catalogue/catalogue.yaml";
let mut dir = start;
for _ in 0..8 {
if dir.join(marker).is_file() {
return dir.to_path_buf();
}
match dir.parent() {
Some(parent) => dir = parent,
None => break,
}
}
panic!("could not find {marker} above {}", start.display());
}
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let repo_root = find_repo_root(&manifest_dir);
let catalogue_path = repo_root.join("specs/003-jss-rule-catalogue/catalogue.yaml");
let terms_path = repo_root.join("specs/003-jss-rule-catalogue/terms.json");
println!("cargo:rerun-if-changed={}", catalogue_path.display());
println!("cargo:rerun-if-changed={}", terms_path.display());
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
fs::copy(&terms_path, out_dir.join("terms.json"))
.unwrap_or_else(|e| panic!("failed to copy {}: {e}", terms_path.display()));
let yaml_src = fs::read_to_string(&catalogue_path)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", catalogue_path.display()));
let doc: CatalogueDoc = serde_yaml::from_str(&yaml_src)
.unwrap_or_else(|e| panic!("failed to parse {}: {e}", catalogue_path.display()));
let categories = doc.categories;
let mut by_id: BTreeMap<String, RawRule> = BTreeMap::new();
for rule in doc.rules {
by_id.insert(rule.rule_id.clone(), rule);
}
let mut out = String::new();
out.push_str(
"// AUTO-GENERATED by build.rs from specs/003-jss-rule-catalogue/catalogue.yaml.\n",
);
out.push_str("// Do not edit by hand.\n\n");
out.push_str("pub static RULES: &[RuleMeta] = &[\n");
for (rule_id, rule) in &by_id {
out.push_str(" RuleMeta {\n");
out.push_str(&format!(" rule_id: {:?},\n", rule_id));
out.push_str(&format!(" category: {:?},\n", rule.category));
out.push_str(&format!(
" severity: {},\n",
severity_variant(&rule.severity)
));
out.push_str(&format!(
" message_template: {:?},\n",
rule.description
));
out.push_str(&format!(" authority: {:?},\n", rule.authority));
out.push_str(&format!(
" authority_ref: {:?},\n",
rule.authority_ref
));
out.push_str(&format!(" explanation: {:?},\n", rule.explanation));
out.push_str(&format!(
" inspects: &[{}],\n",
rule.inspects
.iter()
.map(|s| format!("{:?}", s))
.collect::<Vec<_>>()
.join(", ")
));
out.push_str(&format!(" auto_fixable: {},\n", rule.auto_fixable));
out.push_str(&format!(
" confidence: {:?},\n",
rule.confidence.as_deref().unwrap_or("high")
));
out.push_str(&format!(
" guide_section: {:?},\n",
rule.guide_section.as_deref().unwrap_or("")
));
out.push_str(&format!(
" guide_url: {},\n",
match &rule.guide_url {
Some(url) => format!("Some({:?})", url),
None => "None".to_string(),
}
));
out.push_str(" },\n");
}
out.push_str("];\n\n");
out.push_str("// Rollout order, from catalogue.yaml's top-level `categories` field.\n");
out.push_str("pub static CATEGORIES: &[&str] = &[\n");
for cat in &categories {
out.push_str(&format!(" {cat:?},\n"));
}
out.push_str("];\n");
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
fs::write(out_dir.join("catalogue_data.rs"), out).expect("write catalogue_data.rs");
let specs_path = repo_root.join("specs/003-jss-rule-catalogue/latex-macro-specs.json");
println!("cargo:rerun-if-changed={}", specs_path.display());
let specs_src = fs::read_to_string(&specs_path)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", specs_path.display()));
let specs: LatexSpecsDoc = serde_json::from_str(&specs_src)
.unwrap_or_else(|e| panic!("failed to parse {}: {e}", specs_path.display()));
let mut specs_out = String::new();
specs_out.push_str(
"// AUTO-GENERATED by build.rs from specs/003-jss-rule-catalogue/latex-macro-specs.json.\n",
);
specs_out.push_str("// Do not edit by hand.\n\n");
specs_out.push_str("pub static MACROS: &[(&str, &str)] = &[\n");
for (name, argspec) in &specs.macros {
specs_out.push_str(&format!(" ({name:?}, {argspec:?}),\n"));
}
specs_out.push_str("];\n\n");
specs_out.push_str("pub static VERBATIM_MACROS: &[&str] = &[\n");
for name in &specs.verbatim_macros {
specs_out.push_str(&format!(" {name:?},\n"));
}
specs_out.push_str("];\n\n");
specs_out.push_str("pub static ENVIRONMENTS: &[(&str, &str)] = &[\n");
for (name, argspec) in &specs.environments {
specs_out.push_str(&format!(" ({name:?}, {argspec:?}),\n"));
}
specs_out.push_str("];\n\n");
specs_out.push_str("pub static VERBATIM_ENVIRONMENTS: &[&str] = &[\n");
for name in &specs.verbatim_environments {
specs_out.push_str(&format!(" {name:?},\n"));
}
specs_out.push_str("];\n\n");
specs_out
.push_str("// Longest-first, so literal-prefix matching prefers \"---\" over \"--\".\n");
specs_out.push_str("pub static SPECIALS: &[&str] = &[\n");
for name in &specs.specials {
specs_out.push_str(&format!(" {name:?},\n"));
}
specs_out.push_str("];\n");
fs::write(out_dir.join("latex_specs_data.rs"), specs_out).expect("write latex_specs_data.rs");
}
fn severity_variant(s: &str) -> &'static str {
match s {
"error" => "Severity::Error",
"warning" => "Severity::Warning",
"info" => "Severity::Info",
other => panic!("unknown severity {other:?} in catalogue.yaml"),
}
}