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
//! Narrative demo of the three properties. Run with:
//!   cargo run --release --example demo

use std::time::Instant;

use verit::{dump_json, encode, Dt, Message, Resolver, SchemaBuilder, SchemaMode, Value};

fn main() {
    // ------------------------------------------------------------------
    // A writer somewhere defines a schema and encodes a message.
    // ------------------------------------------------------------------
    let schema_v1 = SchemaBuilder::new()
        .add_enum("Level", vec![(0, "Debug"), (1, "Info"), (2, "Error")])
        .add_struct(
            "LogEvent",
            vec![
                (1, "service", Dt::Str),
                (2, "level", Dt::named("Level")),
                (3, "message", Dt::Str),
                (4, "latency_us", Dt::U32),
            ],
        )
        .build("LogEvent")
        .unwrap();

    let event = Value::Struct(vec![
        (1, Value::str("checkout")),
        (2, Value::Enum(2)),
        (3, Value::str("payment gateway timed out")),
        (4, Value::U32(2_500_000)),
    ]);

    let inline = encode(&schema_v1, &event, SchemaMode::Inline).unwrap();
    let hash_only = encode(&schema_v1, &event, SchemaMode::HashOnly).unwrap();

    println!("=== Veritate: zero-copy + self-describing + schema-evolvable ===\n");
    println!(
        "message size: {} bytes self-contained, {} bytes with schema amortized \
         (schema id {:#034x} travels in every message)",
        inline.len(),
        hash_only.len(),
        schema_v1.id()
    );

    // ------------------------------------------------------------------
    // 1) SELF-DESCRIBING: decode with zero prior knowledge.
    // ------------------------------------------------------------------
    println!("\n[1] self-describing: dump_json(bytes) with no schema object in scope:");
    println!("    {}", dump_json(&inline).unwrap());

    // ------------------------------------------------------------------
    // 2) ZERO-COPY: field reads are indexed loads from the buffer.
    // ------------------------------------------------------------------
    let resolver = Resolver::identity(&schema_v1).unwrap();
    let msg = Message::parse(&hash_only).unwrap();
    let root = msg.root(&resolver).unwrap();
    let service = root.get_str(1).unwrap().unwrap();
    let buf_range = hash_only.as_ptr() as usize..hash_only.as_ptr() as usize + hash_only.len();
    println!(
        "\n[2] zero-copy: get_str returned &str at {:p},",
        service.as_ptr()
    );
    println!(
        "    which lies inside the message buffer {:p}..{:#x} — no copy, no allocation",
        hash_only.as_ptr(),
        buf_range.end
    );

    let iterations = 10_000_000u32;
    let start = Instant::now();
    let mut acc = 0u64;
    for _ in 0..iterations {
        acc = acc.wrapping_add(std::hint::black_box(
            root.get_u32(4).unwrap().unwrap() as u64
        ));
    }
    let elapsed = start.elapsed();
    println!(
        "    {} field reads in {:.1?} ({:.2} ns/read, acc {})",
        iterations,
        elapsed,
        elapsed.as_nanos() as f64 / iterations as f64,
        acc
    );

    // ------------------------------------------------------------------
    // 3) EVOLUTION: a reader on schema v2 reads v1 data and vice versa.
    // ------------------------------------------------------------------
    let schema_v2 = SchemaBuilder::new()
        .add_enum("Level", vec![(0, "Debug"), (1, "Info"), (2, "Error")])
        .add_struct(
            "LogEvent",
            vec![
                (1, "service_name", Dt::Str), // renamed: same id, new name
                (2, "level", Dt::named("Level")),
                (3, "message", Dt::Str),
                (4, "latency_us", Dt::U64), // widened: u32 -> u64
                (5, "trace_id", Dt::Str),   // added: fresh id
            ],
        )
        .build("LogEvent")
        .unwrap();

    println!("\n[3] evolution: v2 renames field 1, widens field 4 to u64, adds field 5");
    let up = Resolver::new(&schema_v1, &schema_v2).unwrap(); // built once per schema pair
    let root2 = msg.root(&up).unwrap();
    println!(
        "    v2 reader on v1 data: service_name={:?}, latency_us(u64)={:?}, trace_id={:?}",
        root2.get_str(1).unwrap(),
        root2.get_u64(4).unwrap(),
        root2.get_str(5).unwrap()
    );

    let event_v2 = Value::Struct(vec![
        (1, Value::str("checkout")),
        (4, Value::U64(1_200)),
        (5, Value::str("abc123")),
    ]);
    let bytes_v2 = encode(&schema_v2, &event_v2, SchemaMode::HashOnly).unwrap();
    let down = Resolver::new(&schema_v2, &schema_v1);
    match down {
        Err(e) => println!(
            "    v1 reader on v2 data: refused at plan time ({e}) — narrowing never \
             corrupts silently",
        ),
        Ok(r) => {
            let m2 = Message::parse(&bytes_v2).unwrap();
            let r2 = m2.root(&r).unwrap();
            println!(
                "    v1 reader on v2 data: service={:?}",
                r2.get_str(1).unwrap()
            );
        }
    }

    // A v2 variant that only renames/adds (no widening) reads fine backwards:
    let schema_v2_compat = SchemaBuilder::new()
        .add_enum("Level", vec![(0, "Debug"), (1, "Info"), (2, "Error")])
        .add_struct(
            "LogEvent",
            vec![
                (1, "service_name", Dt::Str),
                (2, "level", Dt::named("Level")),
                (3, "message", Dt::Str),
                (4, "latency_us", Dt::U32),
                (5, "trace_id", Dt::Str),
            ],
        )
        .build("LogEvent")
        .unwrap();
    let event_c = Value::Struct(vec![
        (1, Value::str("checkout")),
        (4, Value::U32(1_200)),
        (5, Value::str("abc123")),
    ]);
    let bytes_c = encode(&schema_v2_compat, &event_c, SchemaMode::HashOnly).unwrap();
    let back = Resolver::new(&schema_v2_compat, &schema_v1).unwrap();
    let mc = Message::parse(&bytes_c).unwrap();
    let rc = mc.root(&back).unwrap();
    println!(
        "    v1 reader on add-only v2 data: service={:?}, latency_us={:?} (unknown \
         field 5 costs nothing — its bytes are never visited)",
        rc.get_str(1).unwrap(),
        rc.get_u32(4).unwrap()
    );
}