verit-cli 0.1.0

The `verit` command-line tool for Exavian Veritate: dump, id, verify, gen, build.
//! `verit` — the Exavian Veritate command-line tool.
//!
//! Operates on Veritate message files (the `.bin` an encoder produces). For the
//! schema-aware commands the message must carry its schema inline
//! (`SchemaMode::Inline`), so the bytes are self-describing.
//!
//! ```text
//! verit dump   <msg>   render the message as JSON, using only its bytes
//! verit id     <msg>   print the 128-bit schema id from the envelope
//! verit verify <msg>   amplification-bounded full walk; report ok / the error
//! verit gen    <msg>   emit a typed Rust reader/writer for the message's schema
//! ```

use std::process::ExitCode;

use verit::{codegen, dump_json, Budget, Message, Resolver};

const USAGE: &str = "\
verit — Exavian Veritate CLI

USAGE:
    verit <COMMAND> <FILE>

COMMANDS:
    dump    <msg>   Render the message as JSON from its bytes alone (needs inline schema).
    id      <msg>   Print the 128-bit schema id carried in the envelope.
    verify  <msg>   Walk the whole message under a traversal budget; report ok or the error.
    gen     <msg>   Emit a typed Rust reader/writer for the message's (inline) schema.
    build   <schema.verit>   Compile a .verit IDL file: print its schema id, or with
                             --lang rust|python|ts|go|cpp emit typed bindings to stdout.

Every schema-aware command (dump/id/verify/gen) needs a message encoded with the
inline schema. `build` operates on a .verit schema file instead. Bindings emitted
for any language embed the same canonical schema + 128-bit id, so their wire bytes
are byte-identical across all five.
";

fn main() -> ExitCode {
    let args: Vec<String> = std::env::args().skip(1).collect();
    match run(&args) {
        Ok(out) => {
            print!("{out}");
            ExitCode::SUCCESS
        }
        Err(e) => {
            eprintln!("verit: {e}");
            ExitCode::FAILURE
        }
    }
}

fn run(args: &[String]) -> Result<String, String> {
    let cmd = args.first().map(String::as_str).unwrap_or("");
    if matches!(cmd, "" | "-h" | "--help" | "help") {
        return Ok(USAGE.to_string());
    }
    if matches!(cmd, "-V" | "--version" | "version") {
        return Ok(format!("verit {}\n", verit::VERSION));
    }
    if cmd == "build" {
        return build_cmd(&args[1..]);
    }
    let file = args
        .get(1)
        .ok_or_else(|| format!("`{cmd}` needs a message file\n\n{USAGE}"))?;
    let bytes = std::fs::read(file).map_err(|e| format!("reading {file}: {e}"))?;

    match cmd {
        "dump" => {
            let json = dump_json(&bytes).map_err(|e| e.to_string())?;
            Ok(format!("{json}\n"))
        }
        "id" => {
            let msg = Message::parse(&bytes).map_err(|e| e.to_string())?;
            Ok(format!("{:032x}\n", msg.schema_id()))
        }
        "verify" => {
            let msg = Message::parse(&bytes).map_err(|e| e.to_string())?;
            let schema = msg
                .writer_schema()
                .map_err(|e| e.to_string())?
                .ok_or("message has no inline schema to verify against")?;
            let resolver = Resolver::identity(&schema).map_err(|e| e.to_string())?;
            let budget = Budget::new(msg.suggested_budget());
            msg.verify(&resolver, &budget).map_err(|e| e.to_string())?;
            Ok(format!(
                "ok: {} bytes, traversal within budget ({} of {} bytes left)\n",
                bytes.len(),
                budget.remaining(),
                msg.suggested_budget()
            ))
        }
        "gen" => {
            let msg = Message::parse(&bytes).map_err(|e| e.to_string())?;
            let schema = msg
                .writer_schema()
                .map_err(|e| e.to_string())?
                .ok_or("message has no inline schema to generate from")?;
            codegen::generate_rust(&schema).map_err(|e| e.to_string())
        }
        other => Err(format!("unknown command `{other}`\n\n{USAGE}")),
    }
}

/// `verit build <schema.verit> [--lang rust|python|ts|go|cpp]` — compile a
/// `.verit` IDL file. With no `--lang`, prints the schema id and a one-line
/// summary; with a language, emits typed bindings to stdout.
fn build_cmd(rest: &[String]) -> Result<String, String> {
    let mut path: Option<&str> = None;
    let mut lang: Option<String> = None;
    let mut it = rest.iter();
    while let Some(a) = it.next() {
        match a.as_str() {
            "--lang" => {
                lang = Some(
                    it.next()
                        .ok_or("--lang needs a value (rust|python)")?
                        .clone(),
                );
            }
            s if s.starts_with("--lang=") => lang = Some(s["--lang=".len()..].to_string()),
            s if s.starts_with('-') => return Err(format!("unknown flag `{s}`\n\n{USAGE}")),
            s if path.is_none() => path = Some(s),
            _ => return Err("build takes exactly one .verit file".into()),
        }
    }
    let path = path.ok_or_else(|| format!("build needs a .verit file\n\n{USAGE}"))?;
    let src = std::fs::read_to_string(path).map_err(|e| format!("reading {path}: {e}"))?;
    let schema = verit::idl::parse(&src).map_err(|e| e.to_string())?;

    match lang.as_deref() {
        None => Ok(format!(
            "schema id: {:032x}\nroot:      {}\ntypes:     {}\n",
            schema.id(),
            schema.type_name(schema.root_index()),
            schema.type_count()
        )),
        Some("rust") => verit::codegen::generate_rust(&schema).map_err(|e| e.to_string()),
        Some("python") => verit::codegen::generate_python(&schema).map_err(|e| e.to_string()),
        Some("ts") | Some("typescript") => {
            verit::codegen::generate_ts(&schema).map_err(|e| e.to_string())
        }
        Some("go") => verit::codegen::generate_go(&schema).map_err(|e| e.to_string()),
        Some("cpp") | Some("c++") => {
            verit::codegen::generate_cpp(&schema).map_err(|e| e.to_string())
        }
        Some(other) => Err(format!(
            "unknown --lang `{other}` (expected rust|python|ts|go|cpp)"
        )),
    }
}