verit 0.2.0

Exavian Veritate — zero-copy, self-describing, schema-evolvable binary serialization, safe on untrusted bytes, no unsafe, byte-identical across independent implementations.
Documentation
//! Cross-language interop harness (Rust side). Mirrored by `py/interop.py`.
//!
//!   cargo run --example interop -- emit   <dir>   # write fixtures + manifest
//!   cargo run --example interop -- verify <dir>   # check foreign fixtures
//!
//! A fixture is one INLINE-mode message per case (`<name>.bin`) plus a
//! `manifest.tsv` of `name <TAB> hex-schema-id <TAB> dump_json`. `verify`
//! reads fixtures produced by the *other* implementation and, for each:
//!
//!   1. parses and checks the envelope schema id matches the manifest,
//!   2. re-derives the schema from the inline bytes and checks its hash,
//!   3. **transcodes** — decodes to a value, re-encodes, asserts byte-identical
//!      output (proves both encoders agree bit-for-bit), and
//!   4. checks `dump_json` matches the manifest.
//!
//! Passing in both directions (Rust emits → Python verifies, and vice versa)
//! is the proof that the architecture docs is unambiguous.

use std::fs;
use std::path::Path;

use verit::{
    dump_json, encode, Dt, Message, Ref, Resolver, Schema, SchemaBuilder, SchemaMode, StructReader,
    Type, Value,
};

fn corpus() -> Vec<(&'static str, Schema, Value)> {
    let mut cases = Vec::new();

    // --- scalars: every width, signed, string, bytes (sparse struct) ---
    let scalars = SchemaBuilder::new()
        .add_struct(
            "Rec",
            vec![
                (1, "flag", Dt::Bool),
                (2, "n", Dt::U32),
                (3, "big", Dt::U64),
                (4, "sign", Dt::I32),
                (5, "ratio", Dt::F64),
                (6, "name", Dt::Str),
                (7, "blob", Dt::Bytes),
            ],
        )
        .build("Rec")
        .unwrap();
    cases.push((
        "scalars",
        scalars,
        Value::Struct(vec![
            (1, Value::Bool(true)),
            (2, Value::U32(42)),
            (3, Value::U64(0xFFFF_FFFF_0000)),
            (4, Value::I32(-12345)),
            (5, Value::F64(2.5)),
            (6, Value::str("veritate")),
            (7, Value::Bytes(vec![0xDE, 0xAD])),
        ]),
    ));

    // --- nested: enum + dense nested struct + list<struct> + list<str> ---
    let nested = SchemaBuilder::new()
        .add_enum("Level", vec![(0, "Debug"), (1, "Info"), (2, "Error")])
        .add_dense_struct("Point", vec![(1, "x", Dt::F64), (2, "y", Dt::F64)])
        .add_struct(
            "Doc",
            vec![
                (1, "title", Dt::Str),
                (2, "level", Dt::named("Level")),
                (3, "origin", Dt::named("Point")),
                (4, "path", Dt::list(Dt::named("Point"))),
                (5, "tags", Dt::list(Dt::Str)),
            ],
        )
        .build("Doc")
        .unwrap();
    cases.push((
        "nested",
        nested,
        Value::Struct(vec![
            (1, Value::str("doc")),
            (2, Value::Enum(2)),
            (
                3,
                Value::Struct(vec![(1, Value::F64(1.5)), (2, Value::F64(-2.5))]),
            ),
            (
                4,
                Value::List(vec![
                    Value::Struct(vec![(1, Value::F64(0.0)), (2, Value::F64(0.0))]),
                    Value::Struct(vec![(1, Value::F64(1.0)), (2, Value::F64(1.0))]),
                ]),
            ),
            (5, Value::List(vec![Value::str("a"), Value::str("bb")])),
        ]),
    ));

    // --- packed: wide record, partially populated (popcount path) ---
    let packed = SchemaBuilder::new()
        .add_packed_struct(
            "Wide",
            vec![
                (1, "a", Dt::U8),
                (2, "b", Dt::U64),
                (3, "c", Dt::Str),
                (4, "d", Dt::U32),
                (5, "e", Dt::Bool),
                (6, "f", Dt::F64),
            ],
        )
        .build("Wide")
        .unwrap();
    cases.push((
        "packed",
        packed,
        Value::Struct(vec![
            (2, Value::U64(9)),
            (3, Value::str("packed")),
            (6, Value::F64(3.25)),
        ]),
    ));

    // --- nested lists + list of strings ---
    let nestedlists = SchemaBuilder::new()
        .add_struct(
            "M",
            vec![
                (1, "grid", Dt::list(Dt::list(Dt::U16))),
                (2, "words", Dt::list(Dt::Str)),
            ],
        )
        .build("M")
        .unwrap();
    cases.push((
        "nestedlists",
        nestedlists,
        Value::Struct(vec![
            (
                1,
                Value::List(vec![
                    Value::List(vec![Value::U16(1), Value::U16(2), Value::U16(3)]),
                    Value::List(vec![]),
                    Value::List(vec![Value::U16(9)]),
                ]),
            ),
            (
                2,
                Value::List(vec![Value::str("x"), Value::str("yy"), Value::str("zzz")]),
            ),
        ]),
    ));

    // --- maps: int-keyed and string-keyed maps (canonical key ordering) ---
    let maps = SchemaBuilder::new()
        .add_struct(
            "Maps",
            vec![
                (1, "labels", Dt::map(Dt::U32, Dt::Str)),
                (2, "ratios", Dt::map(Dt::Str, Dt::F64)),
            ],
        )
        .build("Maps")
        .unwrap();
    cases.push((
        "maps",
        maps,
        Value::Struct(vec![
            (
                1,
                Value::Map(vec![
                    (Value::U32(10), Value::str("ten")),
                    (Value::U32(2), Value::str("two")),
                    (Value::U32(7), Value::str("seven")),
                ]),
            ),
            (
                2,
                Value::Map(vec![
                    (Value::str("half"), Value::F64(0.5)),
                    (Value::str("quarter"), Value::F64(0.25)),
                ]),
            ),
        ]),
    ));

    // --- unions: scalar-variant and struct-variant tagged sums ---
    let unions = SchemaBuilder::new()
        .add_dense_struct("Point", vec![(1, "x", Dt::F64), (2, "y", Dt::F64)])
        .add_struct(
            "Variants",
            vec![
                (1, "a", Dt::union(vec![Dt::U32, Dt::Str])),
                (2, "b", Dt::union(vec![Dt::named("Point"), Dt::Bool])),
            ],
        )
        .build("Variants")
        .unwrap();
    cases.push((
        "unions",
        unions,
        Value::Struct(vec![
            (1, Value::Union(1, Box::new(Value::str("hi")))),
            (
                2,
                Value::Union(
                    0,
                    Box::new(Value::Struct(vec![
                        (1, Value::F64(1.5)),
                        (2, Value::F64(-2.5)),
                    ])),
                ),
            ),
        ]),
    ));

    // --- defaults: an absent field with a custom default (dump fills it in) ---
    let defaults = SchemaBuilder::new()
        .add_struct("Cfg", vec![(1, "retries", Dt::U32), (2, "name", Dt::Str)])
        .set_default("Cfg", 1, Value::U32(3))
        .build("Cfg")
        .unwrap();
    // `retries` (id 1) is absent on the wire; the reader default surfaces it.
    cases.push((
        "defaults",
        defaults,
        Value::Struct(vec![(2, Value::str("svc"))]),
    ));

    // --- empty: all fields absent ---
    let empty = SchemaBuilder::new()
        .add_struct("E", vec![(1, "a", Dt::U32), (2, "b", Dt::Str)])
        .build("E")
        .unwrap();
    cases.push(("empty", empty, Value::Struct(vec![])));

    cases
}

// Decode a message's root back into a Value tree (id-ordered), for transcoding.
fn materialize_struct(r: &StructReader) -> Value {
    let mut fields = Vec::new();
    for f in &r.struct_def().fields {
        if let Some(v) = r.get(f.id).unwrap() {
            fields.push((f.id, materialize(&f.ty, &v)));
        }
    }
    Value::Struct(fields)
}

fn materialize(ty: &Type, v: &Ref) -> Value {
    match v {
        Ref::Bool(x) => Value::Bool(*x),
        Ref::U8(x) => Value::U8(*x),
        Ref::U16(x) => Value::U16(*x),
        Ref::U32(x) => Value::U32(*x),
        Ref::U64(x) => Value::U64(*x),
        Ref::I8(x) => Value::I8(*x),
        Ref::I16(x) => Value::I16(*x),
        Ref::I32(x) => Value::I32(*x),
        Ref::I64(x) => Value::I64(*x),
        Ref::F32(x) => Value::F32(*x),
        Ref::F64(x) => Value::F64(*x),
        Ref::Str(s) => Value::Str(s.to_string()),
        Ref::Bytes(b) => Value::Bytes(b.to_vec()),
        Ref::Enum(e) => Value::Enum(*e),
        Ref::Struct(s) => materialize_struct(s),
        Ref::List(l) => {
            let elem = match ty {
                Type::List(e) => e.as_ref(),
                _ => panic!("list value with non-list type"),
            };
            let mut items = Vec::new();
            for i in 0..l.len() {
                items.push(materialize(elem, &l.get(i).unwrap()));
            }
            Value::List(items)
        }
        Ref::Map(m) => {
            let (kty, vty) = match ty {
                Type::Map(k, v) => (k.as_ref(), v.as_ref()),
                _ => panic!("map value with non-map type"),
            };
            let mut entries = Vec::new();
            for i in 0..m.len() {
                let (k, v) = m.get(i).unwrap();
                entries.push((materialize(kty, &k), materialize(vty, &v)));
            }
            Value::Map(entries)
        }
        Ref::Union(u) => {
            let variants = match ty {
                Type::Union(vs) => vs,
                _ => panic!("union value with non-union type"),
            };
            let tag = u.tag();
            let inner = materialize(&variants[tag as usize], &u.value().unwrap());
            Value::Union(tag, Box::new(inner))
        }
    }
}

fn emit(dir: &str) {
    let dir = Path::new(dir);
    fs::create_dir_all(dir).unwrap();
    let mut manifest = String::new();
    for (name, schema, value) in corpus() {
        let bytes = encode(&schema, &value, SchemaMode::Inline).unwrap();
        fs::write(dir.join(format!("{name}.bin")), &bytes).unwrap();
        let dump = dump_json(&bytes).unwrap();
        manifest.push_str(&format!("{name}\t{:032x}\t{dump}\n", schema.id()));
    }
    fs::write(dir.join("manifest.tsv"), manifest).unwrap();
    println!(
        "rust: emitted {} cases to {}",
        corpus().len(),
        dir.display()
    );
}

fn verify(dir: &str) -> bool {
    let dir = Path::new(dir);
    let manifest = fs::read_to_string(dir.join("manifest.tsv")).unwrap();
    let mut pass = 0;
    let mut fail = 0;
    for line in manifest.lines() {
        let mut it = line.splitn(3, '\t');
        let name = it.next().unwrap();
        let id_hex = it.next().unwrap();
        let want_dump = it.next().unwrap();
        let bytes = fs::read(dir.join(format!("{name}.bin"))).unwrap();

        let mut errs: Vec<String> = Vec::new();
        let expected_id = u128::from_str_radix(id_hex, 16).unwrap();
        match Message::parse(&bytes) {
            Ok(msg) => {
                if msg.schema_id() != expected_id {
                    errs.push("envelope schema id != manifest".into());
                }
                match msg.writer_schema() {
                    Ok(Some(schema)) => {
                        if schema.id() != expected_id {
                            errs.push("re-derived schema id != manifest".into());
                        }
                        let resolver = Resolver::identity(&schema).unwrap();
                        let root = msg.root(&resolver).unwrap();
                        let value = materialize_struct(&root);
                        let reenc = encode(&schema, &value, SchemaMode::Inline).unwrap();
                        if reenc != bytes {
                            errs.push(format!(
                                "transcode not byte-identical ({} vs {} bytes)",
                                reenc.len(),
                                bytes.len()
                            ));
                        }
                        match dump_json(&bytes) {
                            Ok(d) if d == want_dump => {}
                            Ok(d) => errs.push(format!("dump mismatch: {d} != {want_dump}")),
                            Err(e) => errs.push(format!("dump error: {e}")),
                        }
                    }
                    Ok(None) => errs.push("no inline schema".into()),
                    Err(e) => errs.push(format!("schema decode error: {e}")),
                }
            }
            Err(e) => errs.push(format!("parse error: {e}")),
        }

        if errs.is_empty() {
            pass += 1;
            println!("rust verify: {name:<12} PASS");
        } else {
            fail += 1;
            println!("rust verify: {name:<12} FAIL — {}", errs.join("; "));
        }
    }
    println!("rust verify: {pass} passed, {fail} failed");
    fail == 0
}

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let usage = || {
        eprintln!("usage: interop <emit|verify> <dir>");
        std::process::exit(2);
    };
    if args.len() != 3 {
        usage();
    }
    match args[1].as_str() {
        "emit" => emit(&args[2]),
        "verify" => {
            if !verify(&args[2]) {
                std::process::exit(1);
            }
        }
        _ => usage(),
    }
}