use std::fs;
use std::io::{self, Write};
use std::process::ExitCode;
use index_compat_lab::{
ingest_summary, lint_pack_json, merge_pack_overrides, scaffold_pack_json, synthesize_quality,
synthesize_rules,
};
fn main() -> ExitCode {
match run() {
Ok(output) => {
let _ = writeln!(io::stdout(), "{output}");
ExitCode::SUCCESS
}
Err(error) => {
let _ = writeln!(io::stderr(), "{error}");
ExitCode::from(1)
}
}
}
fn run() -> Result<String, String> {
let mut args = std::env::args().skip(1);
let Some(command) = args.next() else {
return Err(help());
};
match command.as_str() {
"ingest" => run_ingest(args),
"scaffold" => run_scaffold(args),
"synthesize" => run_synthesize(args),
"lint" => run_lint(args),
"merge-overrides" => run_merge_overrides(args),
"--help" | "-h" => Err(help()),
other => Err(format!(
"unsupported index-compat-lab command: {other}\n\n{}",
help()
)),
}
}
fn run_ingest(mut args: impl Iterator<Item = String>) -> Result<String, String> {
let mut top100_path: Option<String> = None;
let mut forum_path: Option<String> = None;
let mut captures = Vec::new();
while let Some(argument) = args.next() {
match argument.as_str() {
"--top100" => {
let Some(path) = args.next() else {
return Err(help());
};
top100_path = Some(path);
}
"--forum" => {
let Some(path) = args.next() else {
return Err(help());
};
forum_path = Some(path);
}
"--capture" => {
let Some(path) = args.next() else {
return Err(help());
};
captures.push(path);
}
other => return Err(format!("unsupported ingest option: {other}")),
}
}
let top100_path = top100_path.ok_or_else(help)?;
let forum_path = forum_path.ok_or_else(help)?;
let top100 = fs::read_to_string(&top100_path)
.map_err(|error| format!("failed to read {top100_path}: {error}"))?;
let forum = fs::read_to_string(&forum_path)
.map_err(|error| format!("failed to read {forum_path}: {error}"))?;
let capture_bodies = captures
.iter()
.map(|path| {
fs::read_to_string(path).map_err(|error| format!("failed to read {path}: {error}"))
})
.collect::<Result<Vec<_>, _>>()?;
let summary = ingest_summary(&top100, &forum, &capture_bodies)?;
let mut output = vec![
"index-compat-lab-ingest-v1".to_owned(),
format!("top100_matrix: {top100_path}"),
format!("forum_matrix: {forum_path}"),
format!("rows_total: {}", summary.rows.len()),
format!("captures_total: {}", summary.captures_total),
"family_columns: family\trows".to_owned(),
];
for (family, rows) in &summary.family_counts {
output.push(format!("{family}\t{rows}"));
}
output.push("result: pass".to_owned());
Ok(output.join("\n"))
}
fn run_scaffold(mut args: impl Iterator<Item = String>) -> Result<String, String> {
let mut top100_path: Option<String> = None;
let mut forum_path: Option<String> = None;
let mut family: Option<String> = None;
while let Some(argument) = args.next() {
match argument.as_str() {
"--top100" => {
let Some(path) = args.next() else {
return Err(help());
};
top100_path = Some(path);
}
"--forum" => {
let Some(path) = args.next() else {
return Err(help());
};
forum_path = Some(path);
}
"--family" => {
let Some(value) = args.next() else {
return Err(help());
};
family = Some(value);
}
other => return Err(format!("unsupported scaffold option: {other}")),
}
}
let top100_path = top100_path.ok_or_else(help)?;
let forum_path = forum_path.ok_or_else(help)?;
let family = family.ok_or_else(help)?;
let top100 = fs::read_to_string(&top100_path)
.map_err(|error| format!("failed to read {top100_path}: {error}"))?;
let forum = fs::read_to_string(&forum_path)
.map_err(|error| format!("failed to read {forum_path}: {error}"))?;
let summary = ingest_summary(&top100, &forum, &[])?;
scaffold_pack_json(&summary.rows, &family)
}
fn run_synthesize(mut args: impl Iterator<Item = String>) -> Result<String, String> {
let mut top100_path: Option<String> = None;
let mut forum_path: Option<String> = None;
let mut family: Option<String> = None;
while let Some(argument) = args.next() {
match argument.as_str() {
"--top100" => {
let Some(path) = args.next() else {
return Err(help());
};
top100_path = Some(path);
}
"--forum" => {
let Some(path) = args.next() else {
return Err(help());
};
forum_path = Some(path);
}
"--family" => {
let Some(value) = args.next() else {
return Err(help());
};
family = Some(value);
}
other => return Err(format!("unsupported synthesize option: {other}")),
}
}
let top100_path = top100_path.ok_or_else(help)?;
let forum_path = forum_path.ok_or_else(help)?;
let family = family.ok_or_else(help)?;
let top100 = fs::read_to_string(&top100_path)
.map_err(|error| format!("failed to read {top100_path}: {error}"))?;
let forum = fs::read_to_string(&forum_path)
.map_err(|error| format!("failed to read {forum_path}: {error}"))?;
let summary = ingest_summary(&top100, &forum, &[])?;
let rules = synthesize_rules(&summary.rows, &family);
let quality = synthesize_quality(&summary.rows, &family, &rules);
let mut output = vec![
"index-compat-lab-synthesize-v1".to_owned(),
format!("family: {family}"),
format!("rules: {}", rules.len()),
format!(
"quality: score={:.4} covered={}/{}",
quality.score, quality.covered_rows, quality.eligible_rows
),
"columns: host\tpath_prefix".to_owned(),
];
for rule in &rules {
output.push(format!("{}\t{}", rule.host, rule.path_prefix));
}
output.push("quality_reasons:".to_owned());
for reason in quality.reasons {
output.push(format!("- {reason}"));
}
output.push("result: pass".to_owned());
Ok(output.join("\n"))
}
fn run_lint(mut args: impl Iterator<Item = String>) -> Result<String, String> {
let Some(path) = args.next() else {
return Err(help());
};
if args.next().is_some() {
return Err("too many arguments".to_owned());
}
let input =
fs::read_to_string(&path).map_err(|error| format!("failed to read {path}: {error}"))?;
let report = lint_pack_json(&input)?;
let passed = report.passed();
let mut output = vec![
"index-compat-lab-lint-v1".to_owned(),
format!("pack: {path}"),
];
if report.errors.is_empty() {
output.push("errors: none".to_owned());
} else {
output.push("errors:".to_owned());
for error in report.errors {
output.push(format!("- {error}"));
}
}
if report.warnings.is_empty() {
output.push("warnings: none".to_owned());
} else {
output.push("warnings:".to_owned());
for warning in &report.warnings {
output.push(format!("- {warning}"));
}
}
output.push(format!("result: {}", if passed { "pass" } else { "fail" }));
Ok(output.join("\n"))
}
fn run_merge_overrides(mut args: impl Iterator<Item = String>) -> Result<String, String> {
let Some(generated_path) = args.next() else {
return Err(help());
};
let Some(overrides_path) = args.next() else {
return Err(help());
};
if args.next().is_some() {
return Err("too many arguments".to_owned());
}
let generated = fs::read_to_string(&generated_path)
.map_err(|error| format!("failed to read {generated_path}: {error}"))?;
let overrides = fs::read_to_string(&overrides_path)
.map_err(|error| format!("failed to read {overrides_path}: {error}"))?;
merge_pack_overrides(&generated, &overrides)
}
fn help() -> String {
"index-compat-lab\n\nUsage:\n index-compat-lab ingest --top100 <matrix.tsv> --forum <matrix.tsv> [--capture <artifact> ...]\n index-compat-lab scaffold --top100 <matrix.tsv> --forum <matrix.tsv> --family <family>\n index-compat-lab synthesize --top100 <matrix.tsv> --forum <matrix.tsv> --family <family>\n index-compat-lab lint <pack.json>\n index-compat-lab merge-overrides <generated-pack.json> <override-pack.json>".to_owned()
}