Step 4 of the BAST pivot: the layout engines, materializer, tunion
dispatch, and engine now consume the BAST typed tree (BastDoc/
BastStruct/BastField/BastType/...) instead of walking raw JSON with
get_alktype_kind*.
Breaking changes (per the pivot plan's semver contract):
- AlkTypeEngine::compile signature:
compile(schema: &mut Value, mode)
-> compile(bast_doc: &Value, root_name: &str, mode)
Drops &mut (BAST needs no in-place normalize_refs); adds required
root_name (D-BAST-001); input is a BAST document, not a custom-keyword
JSON Schema.
- OffsetMap::compute, LayoutBuilder::new, SequentialReader::new now take
a BAST document (&Value) + root_name (or &BastDoc) instead of a
v0.1.0 schema.
- tunion::read_byte_discriminator / read_field_discriminator /
resolve_variant / discriminator_size now take &BastUnion instead of
&Value.
- materialize::materialize_packed / materialize_aligned now take
&BastDoc instead of &Value.
Key design points:
- The engine stores a clone of the BAST Value + root_name so
sequential_reader() and read_field() can re-parse the typed tree on
demand without lifetime entanglement with the caller's Value.
- resolution is a single hash lookup via BastDoc::resolve_typeref;
no normalize_refs, no inline_union_variant_refs.
- bast.rs gains BastField::synthetic() (pub(crate)) for constructing
synthetic fields wrapping TypeRefs (array elements, record values,
union variants — these aren't fields and carry no field annotations).
- The v0.1.0 schema.rs helpers and validation.rs custom-keyword
validators remain defined (step 8 removes them). build_validator still
runs on the BAST doc — with no AlkType:* keywords present, the custom
factories don't trigger and jsonschema performs structural validation
only. The validate_bytes value-constraint enforcement (maxLength, enum
bounds) is step 5's concern (the BAST-native validator).
Tests:
- All engine, layout, materialize, tunion, and integration tests
converted to BAST format (kind/fields vocabulary, /
composition). Expected validation outcomes for the layout path are
identical; the maxLength/enum-bounds validate_bytes tests are step 5's
regression target.
- builder.rs::builder_chunk_header_compiles_in_packed_mode uses a
hand-written BAST doc (the builder still emits v0.1.0 format; step 7
converts it).
Verification:
- cargo test --release: 425 pass (348 lib + 77 integration)
- cargo clippy --all-targets -- -D warnings: clean
- cargo build --target wasm32-unknown-unknown --release: clean
- cargo doc --no-deps: clean
344 lines
12 KiB
Rust
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<'a>(root: &'a serde_json::Value, name: &'a str) -> BastUnion<'a> {
|
|
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:?}");
|
|
} |