Files
alktype/tests/tunion_dispatch.rs
glm-5.3-flash c583762352 Make BastDoc owned: drop Bast* lifetimes (ADR-012 §2a, plan phase 3)
Every Bast* type drops <'a>: &'a str -> String, &'a Value -> Value
(deferred decision 1: plain String/Value — the tree is built once;
Arc<str> name-sharing needs a bench justification that doesn't exist).
BastDoc::new(&Value, &str) still takes references in and clones into
owned storage; the doc gains Clone. resolve_ref/resolve_typeref/
resolve_typeref_as_def return owned types.

- Engine ownership flip: AlkTypeEngine holds the owned BastDoc
  (replacing bast_doc: Value + root_name: String; root_name()
  delegates to the doc), killing its three per-call BastDoc::new
  re-parses (aligned validate_bytes, read_field, write_field — the
  review #004 M1 pattern removed by construction; phase 5 retires the
  lookup_leaf_field walk itself). New public accessor root_name()
  (additive). Engine Send + Sync with the owned doc, asserted in the
  existing thread-share test.
- Bonus cleanup: materialize_typeref_packed's dead _field param
  dropped (phase 2 left it dangling). Under ownership, keeping it
  would force a deep Value clone per array element / record value /
  union variant via dummy_field_for. The param, dummy_field_for, and
  ty_source are gone; no behavior change (the arg was already
  ignored). BastField::synthetic keeps an owned-signature
  #[allow(dead_code)] definition (no remaining callers today).
- Consumers adapted: OffsetMap::compute(&BastDoc),
  materialize_aligned(&BastDoc, ...) (no lifetime), BuildCtx/
  ComputeCtx hold &'d BastDoc, tunion/discriminator name borrows,
  lib.rs module doc. LayoutBuilder's doc_value re-parse cache is
  unchanged pending phase 4.

Verification: 465 tests pass with zero test-logic changes (bast.rs
suites exercise every parser path through the public API); clippy
-D warnings clean; cargo doc zero warnings; wasm32 release build
green.
2026-09-02 08:31:58 +00:00

344 lines
12 KiB
Rust

//! Integration tests for TUnion discriminator dispatch.
//!
//! Exercises both discriminator kinds end-to-end: byte-offset (SFTP
//! pattern) and field-name (typedef.ts pattern). Verifies that
//! `read_byte_discriminator` / `read_field_discriminator` produce the
//! correct mapping key and variant offset, that `resolve_variant`
//! follows `$ref` pointers, and that `discriminator_size` reports the
//! right fixed sizes.
//!
//! All schemas are BAST documents.
use alktype::bast::{BastDefKind, BastDoc, BastType, BastUnion};
use alktype::data_access;
use alktype::tunion;
use alktype::{Endian, AlkTypeError};
use serde_json::json;
fn sftp_like_byte_union_doc() -> serde_json::Value {
json!({
"$defs": {
"Packet": {
"kind": "union",
"discriminator": {
"kind": "byte",
"offset": 0,
"type": "uint8"
},
"mapping": {
"5": { "$ref": "#/$defs/Read" },
"6": { "$ref": "#/$defs/Write" }
}
},
"Read": {
"kind": "struct",
"fields": [
{ "name": "handle", "kind": "uint32" },
{ "name": "length", "kind": "uint32" }
]
},
"Write": {
"kind": "struct",
"fields": [
{ "name": "handle", "kind": "uint32" },
{ "name": "length", "kind": "uint32" },
{ "name": "data", "kind": "uint32" }
]
}
}
})
}
fn union_of(root: &serde_json::Value, name: &str) -> BastUnion {
let doc = BastDoc::new(root, name).expect("bast doc");
match doc.root_def().kind() {
BastDefKind::Union(u) => u.clone(),
_ => panic!("root must be a union"),
}
}
#[test]
fn read_byte_discriminator_uint8_dispatches_to_read() -> Result<(), AlkTypeError> {
let root = sftp_like_byte_union_doc();
let union_def = union_of(&root, "Packet");
let mut buffer = vec![0u8; 16];
buffer[0] = 5;
data_access::write_u32(&mut buffer, 1, 0x01020304, "Read.handle", Endian::Big)?;
let dispatch = tunion::read_byte_discriminator(&buffer, &union_def, Endian::Big)?;
assert_eq!(dispatch.key, "5");
assert_eq!(dispatch.variant_offset, 1);
assert_eq!(dispatch.discriminator_size, 1);
let variant = tunion::resolve_variant(&union_def, &dispatch.key)?;
match variant {
BastType::Ref(r) => assert_eq!(r.name(), "Read"),
other => panic!("expected Ref to Read, got {other:?}"),
}
Ok(())
}
#[test]
fn read_byte_discriminator_uint8_dispatches_to_write() -> Result<(), AlkTypeError> {
let root = sftp_like_byte_union_doc();
let union_def = union_of(&root, "Packet");
let mut buffer = vec![0u8; 16];
buffer[0] = 6;
data_access::write_u32(&mut buffer, 1, 0xDEADBEEF, "Write.handle", Endian::Big)?;
let dispatch = tunion::read_byte_discriminator(&buffer, &union_def, Endian::Big)?;
assert_eq!(dispatch.key, "6");
assert_eq!(dispatch.variant_offset, 1);
assert_eq!(dispatch.discriminator_size, 1);
let variant = tunion::resolve_variant(&union_def, &dispatch.key)?;
match variant {
BastType::Ref(r) => assert_eq!(r.name(), "Write"),
other => panic!("expected Ref to Write, got {other:?}"),
}
Ok(())
}
#[test]
fn read_byte_discriminator_uint16_little_endian() -> Result<(), AlkTypeError> {
let root = json!({
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "byte", "offset": 2, "type": "uint16" },
"mapping": {
"5": { "kind": "struct", "fields": [ { "name": "id", "kind": "uint32" } ] }
}
}
}
});
let union_def = union_of(&root, "U");
let mut buffer = vec![0u8; 16];
buffer[2..4].copy_from_slice(&5u16.to_le_bytes());
let dispatch = tunion::read_byte_discriminator(&buffer, &union_def, Endian::Little)?;
assert_eq!(dispatch.key, "5");
assert_eq!(dispatch.variant_offset, 4);
assert_eq!(dispatch.discriminator_size, 2);
Ok(())
}
#[test]
fn read_byte_discriminator_uint32_big_endian() -> Result<(), AlkTypeError> {
let root = json!({
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "byte", "offset": 0, "type": "uint32" },
"mapping": {
"101": { "kind": "struct", "fields": [ { "name": "id", "kind": "uint32" } ] }
}
}
}
});
let union_def = union_of(&root, "U");
let mut buffer = vec![0u8; 16];
buffer[0..4].copy_from_slice(&101u32.to_be_bytes());
let dispatch = tunion::read_byte_discriminator(&buffer, &union_def, Endian::Big)?;
assert_eq!(dispatch.key, "101");
assert_eq!(dispatch.variant_offset, 4);
assert_eq!(dispatch.discriminator_size, 4);
Ok(())
}
#[test]
fn read_byte_discriminator_unknown_value_returns_access_error() -> Result<(), AlkTypeError> {
let root = sftp_like_byte_union_doc();
let union_def = union_of(&root, "Packet");
let buffer = [99u8, 0x00, 0x00, 0x00];
let err = tunion::read_byte_discriminator(&buffer, &union_def, Endian::Big).unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
Ok(())
}
#[test]
fn read_field_discriminator_string_dispatches_to_read() -> Result<(), AlkTypeError> {
let root = json!({
"$defs": {
"Event": {
"kind": "union",
"discriminator": { "kind": "field", "name": "type" },
"fields": [ { "name": "type", "kind": "string" } ],
"mapping": {
"read": { "$ref": "#/$defs/Read" },
"write": { "$ref": "#/$defs/Write" }
}
},
"Read": {
"kind": "struct",
"fields": [
{ "name": "handle", "kind": "uint32" },
{ "name": "length", "kind": "uint32" }
]
},
"Write": {
"kind": "struct",
"fields": [
{ "name": "handle", "kind": "uint32" },
{ "name": "data", "kind": "bytes" }
]
}
}
});
let union_def = union_of(&root, "Event");
let value = "read";
let mut buffer = vec![0u8; 32];
data_access::write_string(&mut buffer, 0, value, "type", Endian::Little)?;
let dispatch = tunion::read_field_discriminator(&buffer, &union_def, 0, Endian::Little)?;
assert_eq!(dispatch.key, "read");
assert_eq!(dispatch.variant_offset, 4 + value.len());
assert_eq!(dispatch.discriminator_size, 4 + value.len());
let variant = tunion::resolve_variant(&union_def, &dispatch.key)?;
match variant {
BastType::Ref(r) => assert_eq!(r.name(), "Read"),
other => panic!("expected Ref to Read, got {other:?}"),
}
Ok(())
}
#[test]
fn read_field_discriminator_string_dispatches_to_write() -> Result<(), AlkTypeError> {
let root = json!({
"$defs": {
"Event": {
"kind": "union",
"discriminator": { "kind": "field", "name": "type" },
"fields": [ { "name": "type", "kind": "string" } ],
"mapping": {
"read": { "$ref": "#/$defs/Read" },
"write": { "$ref": "#/$defs/Write" }
}
},
"Read": {
"kind": "struct",
"fields": [ { "name": "x", "kind": "uint8" } ]
},
"Write": {
"kind": "struct",
"fields": [ { "name": "y", "kind": "uint16" } ]
}
}
});
let union_def = union_of(&root, "Event");
let value = "write";
let mut buffer = vec![0u8; 32];
data_access::write_string(&mut buffer, 0, value, "type", Endian::Little)?;
let dispatch = tunion::read_field_discriminator(&buffer, &union_def, 0, Endian::Little)?;
assert_eq!(dispatch.key, "write");
assert_eq!(dispatch.variant_offset, 4 + value.len());
let variant = tunion::resolve_variant(&union_def, &dispatch.key)?;
match variant {
BastType::Ref(r) => assert_eq!(r.name(), "Write"),
other => panic!("expected Ref to Write, got {other:?}"),
}
Ok(())
}
#[test]
fn read_field_discriminator_uint8_field() -> Result<(), AlkTypeError> {
let root = json!({
"$defs": {
"Event": {
"kind": "union",
"discriminator": { "kind": "field", "name": "tag" },
"fields": [ { "name": "tag", "kind": "uint8" } ],
"mapping": {
"0": { "kind": "struct", "fields": [ { "name": "a", "kind": "uint32" } ] },
"1": { "kind": "struct", "fields": [ { "name": "b", "kind": "uint16" } ] }
}
}
}
});
let union_def = union_of(&root, "Event");
let mut buffer = vec![0u8; 8];
buffer[0] = 0;
let dispatch = tunion::read_field_discriminator(&buffer, &union_def, 0, Endian::Little)?;
assert_eq!(dispatch.key, "0");
assert_eq!(dispatch.variant_offset, 1);
assert_eq!(dispatch.discriminator_size, 1);
buffer[0] = 1;
let dispatch = tunion::read_field_discriminator(&buffer, &union_def, 0, Endian::Little)?;
assert_eq!(dispatch.key, "1");
Ok(())
}
#[test]
fn read_field_discriminator_unknown_value_returns_access_error() -> Result<(), AlkTypeError> {
let root = json!({
"$defs": {
"Event": {
"kind": "union",
"discriminator": { "kind": "field", "name": "tag" },
"fields": [ { "name": "tag", "kind": "uint8" } ],
"mapping": {
"0": { "kind": "struct", "fields": [ { "name": "a", "kind": "uint32" } ] }
}
}
}
});
let union_def = union_of(&root, "Event");
let mut buffer = vec![0u8; 8];
buffer[0] = 99;
let err =
tunion::read_field_discriminator(&buffer, &union_def, 0, Endian::Little).unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
Ok(())
}
#[test]
fn discriminator_size_returns_correct_values() -> Result<(), AlkTypeError> {
fn union_with(disc_type: &str) -> serde_json::Value {
json!({
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "byte", "type": disc_type },
"mapping": { "1": { "$ref": "#/$defs/A" } }
},
"A": { "kind": "struct", "fields": [] }
}
})
}
let u8_root = union_with("uint8");
let u16_root = union_with("uint16");
let u32_root = union_with("uint32");
let u8_union = union_of(&u8_root, "U");
let u16_union = union_of(&u16_root, "U");
let u32_union = union_of(&u32_root, "U");
assert_eq!(tunion::discriminator_size(&u8_union)?, 1);
assert_eq!(tunion::discriminator_size(&u16_union)?, 2);
assert_eq!(tunion::discriminator_size(&u32_union)?, 4);
Ok(())
}
#[test]
fn discriminator_size_field_kind_returns_schema_error() {
let root = json!({
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "field", "name": "type" },
"fields": [ { "name": "type", "kind": "uint8" } ],
"mapping": { "0": { "kind": "struct", "fields": [] } }
}
}
});
let union_def = union_of(&root, "U");
let err = tunion::discriminator_size(&union_def).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn resolve_variant_returns_schema_error_for_unknown_key() {
let root = sftp_like_byte_union_doc();
let union_def = union_of(&root, "Packet");
let err = tunion::resolve_variant(&union_def, "999").unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}