use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use clap::Subcommand;
use spec_spine_core::{Versioning, interface_verify, read_document};
use spec_spine_types::{Error, InterfaceReport, Outcome, SectionOutcome};
use crate::load_repo_config;
#[derive(Subcommand)]
pub enum InterfaceAction {
Verify {
#[arg(long = "export", value_name = "CORPUS=DIR")]
exports: Vec<String>,
#[arg(long, value_name = "ID")]
spec: Option<String>,
#[arg(long)]
json: bool,
},
}
pub fn run(repo: &Path, action: &InterfaceAction) -> Result<u8, Error> {
match action {
InterfaceAction::Verify {
exports,
spec,
json,
} => verify(repo, exports, spec.as_deref(), *json),
}
}
fn parse_exports(args: &[String]) -> Result<BTreeMap<String, PathBuf>, Error> {
let mut out = BTreeMap::new();
for arg in args {
let Some((name, dir)) = arg.split_once('=') else {
return Err(Error::Parse(format!(
"--export '{arg}' has no '=': the form is <corpus>=<dir>"
)));
};
if name.is_empty() {
return Err(Error::Parse(format!(
"--export '{arg}' names no corpus: the form is <corpus>=<dir>"
)));
}
if dir.is_empty() {
return Err(Error::Parse(format!(
"--export '{arg}' names no directory: the form is <corpus>=<dir>"
)));
}
if out.insert(name.to_string(), PathBuf::from(dir)).is_some() {
return Err(Error::Parse(format!(
"--export names corpus '{name}' twice: one directory per corpus"
)));
}
}
Ok(out)
}
fn verify(repo: &Path, exports: &[String], spec: Option<&str>, json: bool) -> Result<u8, Error> {
let dirs = parse_exports(exports)?;
let cfg = load_repo_config(repo)?;
let report = interface_verify(&cfg, repo, &dirs, spec)?;
if json {
out!("{}", read_document(&report, Versioning::Stamp)?);
} else {
print_text(&report);
}
Ok(if held(&report) { 0 } else { 1 })
}
fn held(report: &InterfaceReport) -> bool {
report
.references
.iter()
.all(|r| matches!(r.outcome, Outcome::Current | Outcome::SectionsCurrent))
}
fn label(o: Outcome) -> &'static str {
match o {
Outcome::Current => "current",
Outcome::SectionsCurrent => "sections-current",
Outcome::Stale => "stale",
Outcome::Missing => "missing",
Outcome::Unverified => "unverified",
}
}
fn print_text(report: &InterfaceReport) {
for r in &report.references {
if r.outcome == Outcome::Current {
continue;
}
outln!(
"{} {} -> {}:{}",
label(r.outcome),
r.declared_by,
r.corpus,
r.spec
);
match r.outcome {
Outcome::Unverified => {
outln!(" no --export was supplied for corpus '{}'", r.corpus)
}
Outcome::Missing => outln!(" the export for '{}' has no spec '{}'", r.corpus, r.spec),
_ => {
outln!(" pinned {}", r.digest);
if let Some(o) = &r.observed_digest {
outln!(" observed {o}");
}
}
}
for s in &r.sections {
match s.outcome {
SectionOutcome::Current => outln!(" section {}: current", s.anchor),
SectionOutcome::Stale => outln!(
" section {}: stale, pinned {} observed {}",
s.anchor,
s.digest,
s.observed_digest.as_deref().unwrap_or("-")
),
SectionOutcome::Missing => {
outln!(" section {}: missing from the cited spec", s.anchor)
}
}
}
}
let s = &report.summary;
outln!(
"references: {} (current {}, sections-current {}, stale {}, missing {}, unverified {})",
report.references.len(),
s.current,
s.sections_current,
s.stale,
s.missing,
s.unverified
);
}