//! 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:?}"); }