use verit::{
dump_json, encode, Dt, Error, Message, Ref, Resolver, Schema, SchemaBuilder, SchemaMode, Value,
};
fn wide_schema() -> Schema {
SchemaBuilder::new()
.add_packed_struct(
"Wide",
vec![
(1, "a", Dt::U8),
(2, "b", Dt::U64),
(3, "c", Dt::U16),
(4, "d", Dt::Str),
(5, "e", Dt::U32),
(6, "f", Dt::Bool),
(7, "g", Dt::F64),
(8, "h", Dt::U8),
],
)
.build("Wide")
.unwrap()
}
fn field_value(id: u16) -> Value {
match id {
1 => Value::U8(0xAB),
2 => Value::U64(0x1122_3344_5566_7788),
3 => Value::U16(0xBEEF),
4 => Value::str("popcount"),
5 => Value::U32(0xDEAD_BEEF),
6 => Value::Bool(true),
7 => Value::F64(std::f64::consts::PI),
8 => Value::U8(0xCD),
_ => unreachable!(),
}
}
fn check_field(root: &verit::StructReader, id: u16, present: bool) {
let got = root.get(id).unwrap();
if !present {
assert!(got.is_none(), "field {id} should be absent");
return;
}
match (id, got.unwrap()) {
(1, Ref::U8(v)) => assert_eq!(v, 0xAB),
(2, Ref::U64(v)) => assert_eq!(v, 0x1122_3344_5566_7788),
(3, Ref::U16(v)) => assert_eq!(v, 0xBEEF),
(4, Ref::Str(v)) => assert_eq!(v, "popcount"),
(5, Ref::U32(v)) => assert_eq!(v, 0xDEAD_BEEF),
(6, Ref::Bool(v)) => assert!(v),
(7, Ref::F64(v)) => assert_eq!(v, std::f64::consts::PI),
(8, Ref::U8(v)) => assert_eq!(v, 0xCD),
(id, other) => panic!("field {id} wrong type {}", other.kind()),
}
}
#[test]
fn popcount_offsets_correct_for_every_subset() {
let schema = wide_schema();
let resolver = Resolver::identity(&schema).unwrap();
for mask in 0u32..256 {
let mut fields = Vec::new();
for bit in 0..8u16 {
if mask & (1 << bit) != 0 {
let id = bit + 1;
fields.push((id, field_value(id)));
}
}
let bytes = encode(&schema, &Value::Struct(fields), SchemaMode::HashOnly).unwrap();
let msg = Message::parse(&bytes).unwrap();
let root = msg.root(&resolver).unwrap();
for bit in 0..8u16 {
check_field(&root, bit + 1, mask & (1 << bit) != 0);
}
}
}
#[test]
fn empty_packed_struct_is_tiny() {
let schema = wide_schema();
let bytes = encode(&schema, &Value::Struct(vec![]), SchemaMode::HashOnly).unwrap();
let msg = Message::parse(&bytes).unwrap();
let resolver = Resolver::identity(&schema).unwrap();
let root = msg.root(&resolver).unwrap();
for id in 1..=8u16 {
assert!(root.get(id).unwrap().is_none());
}
}
#[test]
fn packed_beats_sparse_on_sparse_data() {
let fields = || {
vec![
(1, "f1", Dt::U64),
(2, "f2", Dt::U64),
(3, "f3", Dt::U64),
(4, "f4", Dt::U64),
(5, "f5", Dt::U64),
(6, "f6", Dt::U64),
(7, "f7", Dt::U64),
(8, "f8", Dt::U64),
]
};
let packed = SchemaBuilder::new()
.add_packed_struct("R", fields())
.build("R")
.unwrap();
let sparse = SchemaBuilder::new()
.add_struct("R", fields())
.build("R")
.unwrap();
let value = Value::Struct(vec![(3, Value::U64(42))]);
let p = encode(&packed, &value, SchemaMode::HashOnly).unwrap();
let s = encode(&sparse, &value, SchemaMode::HashOnly).unwrap();
assert!(
s.len() >= p.len() + 40,
"packed {} should be far smaller than sparse {}",
p.len(),
s.len()
);
let resolver = Resolver::identity(&packed).unwrap();
let msg = Message::parse(&p).unwrap();
let root = msg.root(&resolver).unwrap();
assert_eq!(root.get_u64(3).unwrap(), Some(42));
assert_eq!(root.get_u64(1).unwrap(), None);
}
#[test]
fn packed_as_nested_field_and_list_element() {
let schema = SchemaBuilder::new()
.add_packed_struct(
"Attr",
vec![
(1, "key", Dt::Str),
(2, "int_val", Dt::I64),
(3, "flag", Dt::Bool),
],
)
.add_struct(
"Event",
vec![
(1, "primary", Dt::named("Attr")),
(2, "extras", Dt::list(Dt::named("Attr"))),
],
)
.build("Event")
.unwrap();
let bytes = encode(
&schema,
&Value::Struct(vec![
(
1,
Value::Struct(vec![(1, Value::str("root")), (3, Value::Bool(true))]),
),
(
2,
Value::List(vec![
Value::Struct(vec![(2, Value::I64(-7))]),
Value::Struct(vec![
(1, Value::str("k2")),
(2, Value::I64(99)),
(3, Value::Bool(false)),
]),
Value::Struct(vec![]), ]),
),
]),
SchemaMode::HashOnly,
)
.unwrap();
let resolver = Resolver::identity(&schema).unwrap();
let msg = Message::parse(&bytes).unwrap();
let root = msg.root(&resolver).unwrap();
let primary = root.get_struct(1).unwrap().unwrap();
assert_eq!(primary.get_str(1).unwrap(), Some("root"));
assert_eq!(primary.get_i64(2).unwrap(), None);
assert_eq!(primary.get_bool(3).unwrap(), Some(true));
let extras = root.get_list(2).unwrap().unwrap();
assert_eq!(extras.len(), 3);
let e0 = match extras.get(0).unwrap() {
Ref::Struct(s) => s,
o => panic!("{}", o.kind()),
};
assert_eq!(e0.get_i64(2).unwrap(), Some(-7));
assert_eq!(e0.get_str(1).unwrap(), None);
let e1 = match extras.get(1).unwrap() {
Ref::Struct(s) => s,
o => panic!("{}", o.kind()),
};
assert_eq!(e1.get_str(1).unwrap(), Some("k2"));
assert_eq!(e1.get_i64(2).unwrap(), Some(99));
assert_eq!(e1.get_bool(3).unwrap(), Some(false));
let e2 = match extras.get(2).unwrap() {
Ref::Struct(s) => s,
o => panic!("{}", o.kind()),
};
assert_eq!(e2.get_str(1).unwrap(), None);
assert_eq!(e2.get_i64(2).unwrap(), None);
}
#[test]
fn packed_missing_field_is_just_absent_not_an_error() {
let schema = wide_schema();
let bytes = encode(
&schema,
&Value::Struct(vec![(2, Value::U64(1))]),
SchemaMode::HashOnly,
)
.unwrap();
let resolver = Resolver::identity(&schema).unwrap();
let msg = Message::parse(&bytes).unwrap();
let root = msg.root(&resolver).unwrap();
assert_eq!(root.get_u64(2).unwrap(), Some(1));
assert_eq!(root.get_u8(1).unwrap(), None);
}
#[test]
fn packed_duplicate_and_unknown_fields_rejected() {
let schema = wide_schema();
assert!(matches!(
encode(
&schema,
&Value::Struct(vec![(2, Value::U64(1)), (2, Value::U64(2))]),
SchemaMode::HashOnly
),
Err(Error::DuplicateField(2))
));
assert!(matches!(
encode(
&schema,
&Value::Struct(vec![(99, Value::U64(1))]),
SchemaMode::HashOnly
),
Err(Error::UnknownFieldId(99))
));
}
#[test]
fn mode_is_invisible_to_evolution() {
let packed = SchemaBuilder::new()
.add_packed_struct("P", vec![(1, "x", Dt::U16), (2, "s", Dt::Str)])
.build("P")
.unwrap();
let sparse = SchemaBuilder::new()
.add_struct(
"P",
vec![(1, "x", Dt::U32), (2, "s", Dt::Str), (3, "extra", Dt::Bool)],
)
.build("P")
.unwrap();
let bytes = encode(
&packed,
&Value::Struct(vec![(1, Value::U16(1000)), (2, Value::str("hi"))]),
SchemaMode::HashOnly,
)
.unwrap();
let up = Resolver::new(&packed, &sparse).unwrap();
let msg = Message::parse(&bytes).unwrap();
let root = msg.root(&up).unwrap();
assert_eq!(root.get_u32(1).unwrap(), Some(1000));
assert_eq!(root.get_str(2).unwrap(), Some("hi"));
assert_eq!(root.get_bool(3).unwrap(), None);
let down = Resolver::new(&sparse, &packed);
assert!(
matches!(down, Err(Error::Incompatible(_))),
"u32->u16 must fail"
);
let sparse16 = SchemaBuilder::new()
.add_struct("P", vec![(1, "x", Dt::U16), (2, "s", Dt::Str)])
.build("P")
.unwrap();
let bytes = encode(
&sparse16,
&Value::Struct(vec![(1, Value::U16(7)), (2, Value::str("yo"))]),
SchemaMode::HashOnly,
)
.unwrap();
let r = Resolver::new(&sparse16, &packed).unwrap();
let msg = Message::parse(&bytes).unwrap();
let root = msg.root(&r).unwrap();
assert_eq!(root.get_u16(1).unwrap(), Some(7));
assert_eq!(root.get_str(2).unwrap(), Some("yo"));
}
#[test]
fn packed_is_self_describing() {
let schema = wide_schema();
let bytes = encode(
&schema,
&Value::Struct(vec![
(1, Value::U8(5)),
(4, Value::str("hi")),
(7, Value::F64(1.5)),
]),
SchemaMode::Inline,
)
.unwrap();
assert_eq!(dump_json(&bytes).unwrap(), r#"{"a":5,"d":"hi","g":1.5}"#);
}
#[test]
fn packed_survives_canonical_roundtrip_and_has_distinct_id() {
let packed = wide_schema();
let recovered = Schema::from_canonical(packed.canonical_bytes()).unwrap();
assert_eq!(recovered.id(), packed.id());
let sparse = SchemaBuilder::new()
.add_struct(
"Wide",
vec![
(1, "a", Dt::U8),
(2, "b", Dt::U64),
(3, "c", Dt::U16),
(4, "d", Dt::Str),
(5, "e", Dt::U32),
(6, "f", Dt::Bool),
(7, "g", Dt::F64),
(8, "h", Dt::U8),
],
)
.build("Wide")
.unwrap();
assert_ne!(packed.id(), sparse.id());
}
#[test]
fn packed_field_limit_enforced() {
let mut fields = Vec::new();
for i in 0..65u16 {
fields.push((
i + 1,
Box::leak(format!("f{i}").into_boxed_str()) as &str,
Dt::U8,
));
}
let result = SchemaBuilder::new()
.add_packed_struct("Big", fields)
.build("Big");
assert!(matches!(result, Err(Error::BadSchema(_))));
}
#[test]
fn codegen_rejects_packed() {
let schema = wide_schema();
assert!(verit::codegen::generate_rust(&schema).is_err());
}