use std::io::Read;
use std::path::Path;
use std::process::ExitCode;
use anyhow::{bail, Context, Result};
use scema_tools::conform::{conform, Finding, Level};
use scema_world::{Domain, EntityKind, WorldState, WORLD_SCHEMA};
fn read(locator: &str) -> Result<(String, String)> {
if locator == "-" || locator.eq_ignore_ascii_case("stdin") {
let mut text = String::new();
std::io::stdin().read_to_string(&mut text).context("reading a world from stdin")?;
if text.trim().is_empty() {
bail!(
"nothing arrived on stdin. A producer that printed its help text or failed \
silently looks exactly like this — check its exit code."
);
}
return Ok((text, "stdin".into()));
}
let p = Path::new(locator);
let text = std::fs::read_to_string(p)
.with_context(|| format!("reading {}", p.display()))?;
Ok((text, p.display().to_string()))
}
pub fn vocabulary() -> ExitCode {
println!("CONTRACT {WORLD_SCHEMA}\n");
println!(" Both vocabularies are open: a name that is not listed is legal and is carried");
println!(" through untouched. Prefer a listed name where one fits — this build cannot tell");
println!(" `k8s` from `kubernetes`, and nothing downstream can either.\n");
println!("DOMAINS");
for d in Domain::KNOWN {
let rev = Domain::parse(d).edit_reversibility();
println!(" {d:<16} undoing an edit here: {rev:?}");
}
println!("\n Anything else undoing an edit here: Unknown (an unmeasured term)");
println!("\nENTITY KINDS");
for k in EntityKind::KNOWN {
println!(" {k}");
}
ExitCode::SUCCESS
}
fn print_finding(f: &Finding) {
println!(" {:<5} {:<28} {}", f.level.as_str(), f.code, f.message);
if let Some(fix) = &f.fix {
println!(" {:<28} fix: {fix}", "");
}
}
pub fn run(locator: &str, json: bool) -> Result<ExitCode> {
let (text, source) = read(locator)?;
let world: WorldState = match serde_json::from_str(&text) {
Ok(w) => w,
Err(e) => {
if json {
println!(
"{}",
serde_json::json!({
"source": source,
"parsed": false,
"error": e.to_string(),
})
);
} else {
println!("{source}\n");
println!(" FAIL parse {e}");
println!(
" {:<28} fix: the document must be a scema-world WorldState; run \
`scema check --vocabulary`",
""
);
println!("\n Not a world. Nothing else could be checked.");
}
return Ok(ExitCode::FAILURE);
}
};
let findings = conform(&world);
let fails = findings.iter().filter(|f| f.level == Level::Fail).count();
let warns = findings.iter().filter(|f| f.level == Level::Warn).count();
if json {
let items: Vec<_> = findings
.iter()
.map(|f| {
serde_json::json!({
"level": f.level.as_str(),
"code": f.code,
"message": f.message,
"fix": f.fix,
})
})
.collect();
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"source": source,
"parsed": true,
"conforms": fails == 0,
"failures": fails,
"warnings": warns,
"findings": items,
}))?
);
return Ok(if fails == 0 { ExitCode::SUCCESS } else { ExitCode::FAILURE });
}
println!("{source}\n");
for level in [Level::Fail, Level::Warn, Level::Note] {
for f in findings.iter().filter(|f| f.level == level) {
print_finding(f);
}
}
println!();
match (fails, warns) {
(0, 0) => println!(" Conforms to {WORLD_SCHEMA}. This world would import."),
(0, w) => println!(
" Conforms to {WORLD_SCHEMA}, with {w} warning(s). This world would import."
),
(f, _) => println!(
" {f} failure(s). This world would be refused on import.\n\n \
Nothing here checks the *claims* — a producer reporting a stale reading as \
live,\n or counting something it did not count, passes every rule above. That \
is what\n the `imported:` prefix on the observer is for."
),
}
Ok(if fails == 0 { ExitCode::SUCCESS } else { ExitCode::FAILURE })
}