Graven

Validation

Graven is a language-neutral binary format, and its validation model is designed so every language agrees on the same rules without changing the wire format:

1. Built-in rules (portable across languages)

Declare built-in rules directly on table fields (structs are frozen and cannot carry rules):

namespace shop;

enum Status : u8 { Active = 0, Draft = 1 }

table Product {
  #[len(min = 1, max = 64)]
  name @0: string;

  #[range(min = 1, max = 10_000_000)]
  price @1: u32;

  #[finite, range(min = 0.0, max = 100.0)]
  discount @2: f32?;

  #[known]
  status @3: Status;
}
Rule Applies to Meaning
range(min = a, max = b) integers (u8..u128, i8..i128) and floats (f32, f64) a ≤ value ≤ b (inclusive; NaN fails). Either bound may be omitted.
len(min = a, max = b) string, bytes, [T] Byte length (string/bytes) or element count ([T]). Either bound may be omitted.
finite f32, f64 Rejects NaN and infinities.
known enum and [Enum] Rejects unknown enum variants (Unknown(_)).

2. Custom checks (check(name))

Graven intentionally has no expression language (like CEL) and no regexes in schemas: regex dialects and evaluators differ across languages. Instead, #[check(name)] declares a named hook in the schema, and each language implements it with its own standard libraries:

namespace shop;

#[check(valid_period)]
table Promo {
  #[len(min = 1, max = 64), check(slug)]
  name @0: string;

  #[check(email)]
  contact @1: string;

  starts @2: u64;
  ends   @3: u64;
}

In Rust (reference implementation)

From Promo, gravenc generates a Checks trait in mod shop:

// Generated in `mod shop` (do not write this yourself):
pub trait Checks {
    fn email(&self, v: &str) -> bool;
    fn slug(&self, v: &str) -> bool;
    fn valid_period(&self, v: &PromoVerified<'_>) -> bool;
}

Implement Checks in your application (delegating to crates like regex or validator):

use shop::{Checks, Promo, PromoVerified, PromoW};

struct AppChecks;

impl Checks for AppChecks {
    fn email(&self, v: &str) -> bool {
        v.contains('@') // or delegate to `validator::ValidateEmail`
    }

    fn slug(&self, v: &str) -> bool {
        !v.is_empty() && v.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-')
    }

    fn valid_period(&self, p: &PromoVerified<'_>) -> bool {
        p.starts() < p.ends()
    }
}

(If a schema has only built-in rules, Checks is implemented for () so you can pass &().)

In other languages

The schema stays identical. Any language generator maps #[check(name)] to its own interface or callback object — for example, in TypeScript (once a TypeScript generator exists), delegating to Zod:

const checks: shop.Checks = {
  email: (v) => z.string().email().safeParse(v).success,
  slug: (v) => /^[a-z0-9-]+$/.test(v),
  valid_period: (p) => p.starts() < p.ends(),
};

3. Running validation

Validation runs only when asked:

// On write: builds, verifies structure, and runs `AppChecks` + built-in rules.
let bytes = PromoW::to_bytes_validated(&mut builder, &AppChecks, |p| {
    p.name("summer-sale").contact("hi@example.com").starts(10).ends(20);
})?;

// On read: `from_bytes` checks structure only; call `validate` when needed.
let view = Promo::from_bytes(&bytes)?;
view.validate(&AppChecks)?;                  // stops at first failure (no allocation)
let all = view.validate_all(&AppChecks)?;    // collects every failure

Every failure reports the table, optional field, optional list index (for [Enum] or [Table]), and the rule that failed (Range, Len, Finite, Known, or Check("slug")).

Schemas without any rules generate the exact same code as before.

4. Evolution (graven.lock)

graven.lock records every field's rules and each table's checks:

5. Dynamic validation & gravenc inspect