//! `AlkTypeEngine` — the compiled form of a BAST document. //! //! Combines the layout engine (both packed and aligned modes) and the //! two validators into a single struct. Built once at schema load time //! via [`AlkTypeEngine::compile`]. Used for repeated read/write/validate //! operations at access time. //! //! Two validators, two inputs (D-BAST-006, D-BAST-007): //! - `validate_bytes` — the BAST-native validator walks the BAST type //! tree over a materialized `Value` (no `jsonschema` involvement). //! - `validate_json` — a standard `jsonschema::Validator` compiled from //! a consumer-provided JSON Schema (no custom keywords, no BAST //! involvement). //! //! See [validation.md](../../docs/architecture/validation.md) //! §"The AlkTypeEngine struct" and //! [overview.md](../../docs/architecture/overview.md). use crate::bast::{BastDefKind, BastDoc, BastStruct, BastType}; use crate::bast_validation; use crate::data_access; use crate::error::AlkTypeError; use crate::layout_builder::LayoutBuilder; use crate::materialize; use crate::offset_map::OffsetMap; use crate::schema::{AlkTypeKind, Endian, VariableEncoding}; use crate::sequential_reader::{FieldValue, SequentialReader}; use crate::validation; use serde_json::Value; use std::fmt; /// The layout mode selected at engine construction time. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LayoutMode { /// Packed sequential — for protocol wire formats (SFTP, channels, TTY). Packed, /// Aligned static — for mmap-friendly formats (metatensor, safetensors). Aligned, } /// The layout strategy — packed sequential or aligned static. /// /// Carries the layout-specific handles needed for read/write access in /// the selected mode. The consumer chooses the mode at construction time /// via [`AlkTypeEngine::compile`]; the engine then exposes only the /// APIs that make sense for that mode. #[derive(Debug)] enum Layout { /// Packed sequential layout. The write-side is [`LayoutBuilder`]; the /// read-side is a fresh [`SequentialReader`] constructed on demand /// (ADR-101 — the reader has mutable cursor state that the consumer /// owns, so the engine is a factory, not a holder). Packed { builder: LayoutBuilder, }, /// Aligned static layout. Field offsets are precomputed in an /// [`OffsetMap`] for random access. Aligned { offset_map: OffsetMap }, } /// The compiled form of a BAST document. Combines the layout engine /// (both packed and aligned modes) and the two validators. /// /// Built once at schema load time via [`AlkTypeEngine::compile`]. /// Used for repeated read/write/validate operations at access time. /// /// The consumer selects the layout mode at construction time. The engine /// then exposes mode-appropriate accessors: [`AlkTypeEngine::offset_map`] /// for aligned mode, [`AlkTypeEngine::layout_builder`] and /// [`AlkTypeEngine::sequential_reader`] for packed mode. /// /// Validation is split (D-BAST-006, D-BAST-007): /// - [`AlkTypeEngine::validate_bytes`] uses the BAST-native validator /// ([`bast_validation`]) — no `jsonschema` involvement, no external /// JSON Schema required. /// - [`AlkTypeEngine::validate_json`] / [`AlkTypeEngine::is_valid_json`] /// use a standard `jsonschema::Validator` compiled at construction /// time from a consumer-provided JSON Schema. If no JSON Schema was /// supplied to [`AlkTypeEngine::compile`], the JSON-validation methods /// return [`AlkTypeError::Schema`]. pub struct AlkTypeEngine { layout: Layout, json_validator: Option, endian: Endian, bast_doc: Value, root_name: String, } impl AlkTypeEngine { /// Compile a BAST document into a [`AlkTypeEngine`]. /// /// This is the expensive operation — it parses the BAST document /// into a typed tree, computes the layout, and (optionally) builds a /// standard `jsonschema::Validator` for the JSON-validation path. /// Call once at load time; use the returned engine for repeated /// operations. /// /// `bast_doc` is the raw BAST JSON `Value` (`{ "$defs": { ... } }`). /// `root_name` selects which `$defs` entry is the top-level type /// (D-BAST-001). The `mode` parameter selects the layout strategy. /// The same document can be compiled in either mode. /// /// `json_schema` is an optional consumer-provided standard JSON /// Schema (Draft 2020-12 or any draft `jsonschema` supports) used by /// [`AlkTypeEngine::validate_json`] / [`AlkTypeEngine::is_valid_json`] /// (D-BAST-007). Pass `None` when JSON validation is not needed; /// calling the JSON-validation methods then returns /// [`AlkTypeError::Schema`]. The JSON Schema is independent of the /// BAST document — BAST describes bytes, not JSON shape. It may be /// authored separately or derived from BAST via future codegen. /// /// The engine retains a clone of the BAST `Value` so that /// [`AlkTypeEngine::sequential_reader`] and /// [`AlkTypeEngine::read_field`] can re-parse the typed tree on /// demand without lifetime entanglement with the caller's `Value`. /// /// # Errors /// /// Returns [`AlkTypeError::Schema`] if the BAST document is malformed /// or the root type is not a struct, propagated from [`BastDoc::new`], /// [`LayoutBuilder::new`], [`OffsetMap::compute`], or /// [`validation::build_validator`] (when `json_schema` is `Some`). pub fn compile( bast_doc: &Value, root_name: &str, mode: LayoutMode, json_schema: Option<&Value>, ) -> Result { crate::bast_meta::validate_bast_doc(bast_doc)?; let doc = BastDoc::new(bast_doc, root_name)?; let root_def = doc.root_def(); let struct_node = match root_def.kind() { BastDefKind::Struct(s) => s, other => { return Err(AlkTypeError::Schema(format!( "AlkTypeEngine::compile requires a struct root, got {kind}", kind = other.alk_kind() ))); } }; let endian = struct_node.endian(); let layout = match mode { LayoutMode::Packed => { let builder = LayoutBuilder::new(bast_doc, root_name)?; Layout::Packed { builder } } LayoutMode::Aligned => { let offset_map = OffsetMap::compute(&doc)?; Layout::Aligned { offset_map } } }; let json_validator = match json_schema { Some(schema) => Some(validation::build_validator(schema)?), None => None, }; Ok(Self { layout, json_validator, endian, bast_doc: bast_doc.clone(), root_name: root_name.to_string(), }) } /// The schema's endianness. pub fn endian(&self) -> Endian { self.endian } /// The layout mode this engine was compiled with. pub fn mode(&self) -> LayoutMode { match self.layout { Layout::Packed { .. } => LayoutMode::Packed, Layout::Aligned { .. } => LayoutMode::Aligned, } } /// Access the aligned offset map. Returns `None` if compiled in /// packed mode. pub fn offset_map(&self) -> Option<&OffsetMap> { match &self.layout { Layout::Aligned { offset_map } => Some(offset_map), Layout::Packed { .. } => None, } } /// Access the layout builder (write-side of packed mode). /// Returns `None` if compiled in aligned mode. pub fn layout_builder(&self) -> Option<&LayoutBuilder> { match &self.layout { Layout::Packed { builder, .. } => Some(builder), Layout::Aligned { .. } => None, } } /// Construct a fresh [`SequentialReader`] for packed-mode reads /// (ADR-101). Each call returns a new reader with the cursor at /// position 0. The consumer owns the reader and calls /// `read_next`/`read_field`/`reset` on it directly. /// /// Returns `None` if compiled in aligned mode. pub fn sequential_reader(&self) -> Option { match &self.layout { Layout::Packed { .. } => { SequentialReader::new(&self.bast_doc, &self.root_name).ok() } Layout::Aligned { .. } => None, } } /// Validate a JSON `Value` against the consumer-provided JSON Schema /// supplied to [`AlkTypeEngine::compile`] (D-BAST-007). /// /// The validator is a standard `jsonschema::Validator` built at /// construction time from a plain JSON Schema document — no custom /// keywords, no BAST involvement. BAST describes bytes, not JSON /// shape; a JSON value is validated by a JSON Schema, which is a /// separate concern. /// /// Returns `Ok(())` if valid, `Err(AlkTypeError::Validation(...))` if /// the instance violates the JSON Schema, or /// `Err(AlkTypeError::Schema(...))` if no JSON Schema was supplied to /// [`AlkTypeEngine::compile`]. /// /// # Errors /// /// - [`AlkTypeError::Validation`] if the instance violates the JSON /// Schema. The payload is the `jsonschema::ValidationError` (D-BAST-009 /// — uniform with the `validate_bytes` path). /// - [`AlkTypeError::Schema`] if `compile` was called with /// `json_schema: None`. pub fn validate_json(&self, instance: &Value) -> Result<(), AlkTypeError> { let validator = self.json_validator.as_ref().ok_or_else(|| { AlkTypeError::Schema( "validate_json requires a JSON Schema supplied to AlkTypeEngine::compile \ (json_schema was None)" .to_string(), ) })?; validator .validate(instance) .map_err(|e| AlkTypeError::Validation(e.to_owned())) } /// Check if a JSON `Value` is valid against the consumer-provided /// JSON Schema supplied to [`AlkTypeEngine::compile`] (D-BAST-007). /// /// Returns `false` if the instance is invalid **or** if no JSON /// Schema was supplied to [`AlkTypeEngine::compile`]. Use /// [`AlkTypeEngine::validate_json`] if the distinction matters. pub fn is_valid_json(&self, instance: &Value) -> bool { self.json_validator .as_ref() .is_some_and(|v| v.is_valid(instance)) } /// Validate a binary buffer against the schema by materializing a /// `serde_json::Value` tree from the bytes (walking the layout engine) /// and then validating that `Value` against the BAST-native validator /// — a recursive walker over the BAST type tree that checks the /// value-domain constraints the materializer does not (integer /// ranges, `maxLength`, timestamp shape, enum index bounds, union /// variant constraints). Decided in D-BAST-006; see /// [bast-format.md §Validation Model](../../docs/architecture/bast-format.md#validation-model). /// /// Dispatches on the engine's layout mode: packed mode walks /// sequentially from offset 0; aligned mode reads at offsets from /// the `OffsetMap`. Both produce the same `Value` form; the /// BAST-native validator is mode-agnostic. /// /// # Errors /// /// - [`AlkTypeError::Access`] if a field cannot be read from the /// buffer (too short, invalid UTF-8, value out of range for the /// target type). Carries the field path. /// - [`AlkTypeError::Offset`] if a field is not found in the /// offset map (aligned mode only). /// - [`AlkTypeError::Validation`] if the materialized `Value` /// violates a value-domain constraint expressed in the BAST /// document. The payload is a `jsonschema::ValidationError::custom` /// (D-BAST-009 — the variant type stays uniform with the /// `validate_json` path). pub fn validate_bytes(&self, buffer: &[u8]) -> Result<(), AlkTypeError> { let doc = BastDoc::new(&self.bast_doc, &self.root_name)?; let value = match &self.layout { Layout::Packed { .. } => materialize::materialize_packed(&doc, buffer)?, Layout::Aligned { offset_map } => { materialize::materialize_aligned(&doc, buffer, offset_map)? } }; bast_validation::validate_value(&doc, &value) } /// Read a field from a buffer at its computed offset (aligned mode). /// /// Looks up the field's byte range in the [`OffsetMap`] and reads the /// appropriate type using the [`crate::data_access`] functions. Works /// for fixed-size primitive kinds and length-prefixed `String`/ /// `Bytes`/`Timestamp` fields. /// /// Returns an error if compiled in packed mode — use /// [`AlkTypeEngine::sequential_reader`] for packed mode. Also /// returns an error for composite kinds (`Struct`, `Union`, `Array`, /// `Record`) — those are better handled via the layout-specific APIs. /// /// # Errors /// /// - [`AlkTypeError::Access`] if compiled in packed mode. /// - [`AlkTypeError::Offset`] if `field_path` is not in the offset map. /// - [`AlkTypeError::Access`] for buffer-too-short or invalid data, /// propagated from [`crate::data_access`]. pub fn read_field<'a>( &self, buffer: &'a [u8], field_path: &str, ) -> Result, AlkTypeError> { let offset_map = match &self.layout { Layout::Aligned { offset_map } => offset_map, Layout::Packed { .. } => { return Err(AlkTypeError::Access { field_path: field_path.to_string(), reason: "read_field is only available in aligned mode; \ use sequential_reader() for packed mode" .to_string(), }); } }; let range = offset_map .get(field_path) .ok_or_else(|| AlkTypeError::Offset { field_path: field_path.to_string(), reason: "field not found in offset map".to_string(), })?; let doc = BastDoc::new(&self.bast_doc, &self.root_name)?; let leaf = lookup_leaf_field(&doc, field_path).ok_or_else(|| AlkTypeError::Offset { field_path: field_path.to_string(), reason: "field schema not found in BAST tree or has no primitive kind".to_string(), })?; let kind = leaf.kind; let endian = leaf.endian; match kind { AlkTypeKind::Int8 => { let v = data_access::read_i8(buffer, range.start, field_path)?; Ok(FieldValue::I8(v)) } AlkTypeKind::Int16 => { let v = data_access::read_i16(buffer, range.start, field_path, endian)?; Ok(FieldValue::I16(v)) } AlkTypeKind::Int32 => { let v = data_access::read_i32(buffer, range.start, field_path, endian)?; Ok(FieldValue::I32(v)) } AlkTypeKind::Int64 => { let v = data_access::read_i64(buffer, range.start, field_path, endian)?; Ok(FieldValue::I64(v)) } AlkTypeKind::Uint8 => { let v = data_access::read_u8(buffer, range.start, field_path)?; Ok(FieldValue::U8(v)) } AlkTypeKind::Uint16 => { let v = data_access::read_u16(buffer, range.start, field_path, endian)?; Ok(FieldValue::U16(v)) } AlkTypeKind::Uint32 => { let v = data_access::read_u32(buffer, range.start, field_path, endian)?; Ok(FieldValue::U32(v)) } AlkTypeKind::Uint64 => { let v = data_access::read_u64(buffer, range.start, field_path, endian)?; Ok(FieldValue::U64(v)) } AlkTypeKind::Float32 => { let v = data_access::read_f32(buffer, range.start, field_path, endian)?; Ok(FieldValue::F32(v)) } AlkTypeKind::Float64 => { let v = data_access::read_f64(buffer, range.start, field_path, endian)?; Ok(FieldValue::F64(v)) } AlkTypeKind::Boolean => { let v = data_access::read_bool(buffer, range.start, field_path)?; Ok(FieldValue::Bool(v)) } AlkTypeKind::Enum => { let v = data_access::read_enum(buffer, range.start, field_path, endian)?; Ok(FieldValue::Enum(v)) } AlkTypeKind::String => { let v = match leaf.encoding { VariableEncoding::OffsetIndirect => { data_access::read_string_indirect(buffer, range.start, field_path, endian)? } VariableEncoding::LengthPrefixed => { data_access::read_string(buffer, range.start, field_path, endian)? } }; Ok(FieldValue::String(v)) } AlkTypeKind::Bytes => { let v = match leaf.encoding { VariableEncoding::OffsetIndirect => { data_access::read_bytes_indirect(buffer, range.start, field_path, endian)? } VariableEncoding::LengthPrefixed => { data_access::read_bytes(buffer, range.start, field_path, endian)? } }; Ok(FieldValue::Bytes(v)) } AlkTypeKind::Timestamp => { let v = data_access::read_string(buffer, range.start, field_path, endian)?; Ok(FieldValue::String(v)) } AlkTypeKind::Struct => Ok(FieldValue::Struct { start: range.start, end: range.end, }), AlkTypeKind::Union | AlkTypeKind::Array | AlkTypeKind::Record => { Err(AlkTypeError::Access { field_path: field_path.to_string(), reason: "read_field does not support composite types; \ use the layout-specific APIs" .to_string(), }) } } } /// Write a field to a buffer at its computed offset (aligned mode). /// /// Looks up the field's byte range in the [`OffsetMap`] and writes the /// appropriate type using the [`crate::data_access`] functions. Works /// for fixed-size primitive kinds and length-prefixed `String`/ /// `Bytes`/`Timestamp` fields. /// /// Returns an error if compiled in packed mode — use /// [`AlkTypeEngine::layout_builder`] for packed mode. Also returns /// an error for composite kinds (`Struct`, `Union`, `Array`, `Record`). /// /// # Errors /// /// - [`AlkTypeError::Access`] if compiled in packed mode. /// - [`AlkTypeError::Offset`] if `field_path` is not in the offset map. /// - [`AlkTypeError::Access`] for buffer-too-short or invalid data, /// propagated from [`crate::data_access`]. pub fn write_field( &self, buffer: &mut [u8], field_path: &str, value: &FieldValue<'_>, ) -> Result<(), AlkTypeError> { let offset_map = match &self.layout { Layout::Aligned { offset_map } => offset_map, Layout::Packed { .. } => { return Err(AlkTypeError::Access { field_path: field_path.to_string(), reason: "write_field is only available in aligned mode; \ use layout_builder() for packed mode" .to_string(), }); } }; let range = offset_map .get(field_path) .ok_or_else(|| AlkTypeError::Offset { field_path: field_path.to_string(), reason: "field not found in offset map".to_string(), })?; let doc = BastDoc::new(&self.bast_doc, &self.root_name)?; let leaf = lookup_leaf_field(&doc, field_path).ok_or_else(|| AlkTypeError::Offset { field_path: field_path.to_string(), reason: "field schema not found in BAST tree or has no primitive kind".to_string(), })?; let endian = leaf.endian; match value { FieldValue::I8(v) => data_access::write_i8(buffer, range.start, *v, field_path), FieldValue::I16(v) => { data_access::write_i16(buffer, range.start, *v, field_path, endian) } FieldValue::I32(v) => { data_access::write_i32(buffer, range.start, *v, field_path, endian) } FieldValue::I64(v) => { data_access::write_i64(buffer, range.start, *v, field_path, endian) } FieldValue::U8(v) => data_access::write_u8(buffer, range.start, *v, field_path), FieldValue::U16(v) => { data_access::write_u16(buffer, range.start, *v, field_path, endian) } FieldValue::U32(v) => { data_access::write_u32(buffer, range.start, *v, field_path, endian) } FieldValue::U64(v) => { data_access::write_u64(buffer, range.start, *v, field_path, endian) } FieldValue::F32(v) => { data_access::write_f32(buffer, range.start, *v, field_path, endian) } FieldValue::F64(v) => { data_access::write_f64(buffer, range.start, *v, field_path, endian) } FieldValue::Bool(v) => data_access::write_bool(buffer, range.start, *v, field_path), FieldValue::Enum(v) => { data_access::write_enum(buffer, range.start, *v, field_path, endian) } FieldValue::String(v) => { if leaf.encoding == VariableEncoding::OffsetIndirect { return Err(AlkTypeError::Access { field_path: field_path.to_string(), reason: "write_field cannot write offset-indirect fields; \ use data_access::write_string_indirect with the \ offset map range and a data offset" .to_string(), }); } data_access::write_string(buffer, range.start, v, field_path, endian)?; Ok(()) } FieldValue::Bytes(v) => { if leaf.encoding == VariableEncoding::OffsetIndirect { return Err(AlkTypeError::Access { field_path: field_path.to_string(), reason: "write_field cannot write offset-indirect fields; \ use data_access::write_bytes_indirect with the \ offset map range and a data offset" .to_string(), }); } data_access::write_bytes(buffer, range.start, v, field_path, endian)?; Ok(()) } FieldValue::Struct { .. } | FieldValue::Union { .. } | FieldValue::Array { .. } => { Err(AlkTypeError::Access { field_path: field_path.to_string(), reason: "write_field does not support composite types; \ use the layout-specific APIs" .to_string(), }) } } } } impl fmt::Debug for AlkTypeEngine { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("AlkTypeEngine") .field("layout", &self.layout) .field("json_validator", &self.json_validator.as_ref().map(|_| "")) .field("endian", &self.endian) .field("root_name", &self.root_name) .finish() } } /// The resolved leaf-field metadata needed by `read_field`/`write_field`: /// the field's kind, its variable-length encoding, and its effective /// endianness (field override, else the enclosing struct's default). struct LeafFieldInfo { kind: AlkTypeKind, encoding: VariableEncoding, endian: Endian, } /// Walk the BAST typed tree to find the leaf field for a dotted field /// path. Returns `None` if any segment is missing or the resolved type is /// a `$ref` that can't be resolved. /// /// Splits `field_path` on `.` and descends into the root struct's /// `fields` at each step, resolving `$ref`s via [`BastDoc::resolve_typeref`]. /// Array element segments (`field[i]`) are not handled here — /// `read_field`/`write_field` only address leaf fields. fn lookup_leaf_field(doc: &BastDoc<'_>, field_path: &str) -> Option { let root_def = doc.root_def(); let mut current_struct: BastStruct<'_> = match root_def.kind() { BastDefKind::Struct(s) => s.clone(), _ => return None, }; let mut struct_endian = current_struct.endian(); let segments: Vec<&str> = field_path.split('.').collect(); let last = segments.len(); for (i, segment) in segments.iter().enumerate() { let field = current_struct.fields().iter().find(|f| f.name() == *segment)?; let ty = field.ty(); let resolved = doc.resolve_typeref(ty).ok()?; let field_endian = field.effective_endian(struct_endian); if i + 1 == last { return Some(LeafFieldInfo { kind: resolved.alk_kind(), encoding: field.encoding(), endian: field_endian, }); } match &resolved { BastType::Struct(s) => { current_struct = s.clone(); struct_endian = s.endian(); } _ => return None, } } None } #[cfg(test)] mod tests { use super::*; use serde_json::json; fn fixed_struct_doc() -> Value { json!({ "$defs": { "S": { "kind": "struct", "endian": "little", "fields": [ { "name": "flag", "kind": "uint8" }, { "name": "id", "kind": "uint32" }, { "name": "score", "kind": "float32" }, { "name": "tag", "kind": "string" } ] } } }) } #[test] fn compile_aligned_builds_offset_map() { let doc = fixed_struct_doc(); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile"); assert_eq!(engine.mode(), LayoutMode::Aligned); assert!(engine.offset_map().is_some()); assert!(engine.layout_builder().is_none()); assert!(engine.sequential_reader().is_none()); } #[test] fn compile_packed_builds_builder_and_reader() { let doc = fixed_struct_doc(); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile"); assert_eq!(engine.mode(), LayoutMode::Packed); assert!(engine.layout_builder().is_some()); assert!(engine.sequential_reader().is_some()); assert!(engine.offset_map().is_none()); } #[test] fn endian_parsed_from_struct() { let doc = json!({ "$defs": { "S": { "kind": "struct", "endian": "big", "fields": [ { "name": "id", "kind": "uint32" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile"); assert_eq!(engine.endian(), Endian::Big); } #[test] fn endian_defaults_to_little() { let doc = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "id", "kind": "uint32" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile"); assert_eq!(engine.endian(), Endian::Little); } #[test] fn read_field_aligned_reads_fixed_fields() { let doc = json!({ "$defs": { "S": { "kind": "struct", "endian": "little", "fields": [ { "name": "flag", "kind": "uint8" }, { "name": "id", "kind": "uint32" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile"); let mut buf = vec![0u8; 8]; buf[0] = 0xAB; buf[4..8].copy_from_slice(&0x01020304u32.to_le_bytes()); assert_eq!( engine.read_field(&buf, "flag").unwrap(), FieldValue::U8(0xAB) ); assert_eq!( engine.read_field(&buf, "id").unwrap(), FieldValue::U32(0x01020304) ); } #[test] fn read_field_aligned_reads_string_length_prefixed() { let doc = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "name", "kind": "string" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile"); let mut buf = vec![0u8; 32]; let len_bytes = 5u32.to_le_bytes(); buf[0..4].copy_from_slice(&len_bytes); buf[4..9].copy_from_slice(b"hello"); assert_eq!( engine.read_field(&buf, "name").unwrap(), FieldValue::String("hello") ); } #[test] fn read_field_returns_access_error_in_packed_mode() { let doc = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "id", "kind": "uint32" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile"); let buf = [0u8; 4]; let err = engine.read_field(&buf, "id").unwrap_err(); assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}"); } #[test] fn read_field_returns_offset_error_for_missing_field() { let doc = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "id", "kind": "uint32" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile"); let buf = [0u8; 4]; let err = engine.read_field(&buf, "missing").unwrap_err(); assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}"); } #[test] fn read_field_returns_error_for_composite_types() { let doc = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "vals", "kind": { "kind": "array", "element": "uint32", "count": 2 } } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile"); let buf = [0u8; 8]; // `vals` itself is not in the offset map (only `vals[0]`/`vals[1]` // are). The lookup fails with `Offset`. Reading an individual // array element returns its kind (`Uint32`), which `read_field` // handles as a leaf — composites aren't addressed via // `read_field`. This test confirms the offset-map lookup path // errors out for the array field path. let err = engine.read_field(&buf, "vals").unwrap_err(); assert!( matches!(err, AlkTypeError::Access { .. } | AlkTypeError::Offset { .. }), "got {err:?}" ); } #[test] fn write_field_aligned_writes_fixed_fields() { let doc = json!({ "$defs": { "S": { "kind": "struct", "endian": "little", "fields": [ { "name": "flag", "kind": "uint8" }, { "name": "id", "kind": "uint32" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile"); let mut buf = vec![0u8; 8]; engine .write_field(&mut buf, "flag", &FieldValue::U8(0xAB)) .unwrap(); engine .write_field(&mut buf, "id", &FieldValue::U32(0x01020304)) .unwrap(); assert_eq!(buf[0], 0xAB); assert_eq!(&buf[4..8], &0x01020304u32.to_le_bytes()); assert_eq!( engine.read_field(&buf, "flag").unwrap(), FieldValue::U8(0xAB) ); assert_eq!( engine.read_field(&buf, "id").unwrap(), FieldValue::U32(0x01020304) ); } #[test] fn write_field_round_trips_string() { let doc = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "name", "kind": "string" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile"); let mut buf = vec![0u8; 32]; engine .write_field(&mut buf, "name", &FieldValue::String("hello")) .unwrap(); assert_eq!( engine.read_field(&buf, "name").unwrap(), FieldValue::String("hello") ); } #[test] fn write_field_returns_access_error_in_packed_mode() { let doc = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "id", "kind": "uint32" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile"); let mut buf = [0u8; 4]; let err = engine .write_field(&mut buf, "id", &FieldValue::U32(1)) .unwrap_err(); assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}"); } #[test] fn write_field_returns_offset_error_for_missing_field() { let doc = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "id", "kind": "uint32" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile"); let mut buf = [0u8; 4]; let err = engine .write_field(&mut buf, "missing", &FieldValue::U32(1)) .unwrap_err(); assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}"); } #[test] fn write_field_returns_error_for_composite_value() { let doc = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "id", "kind": "uint32" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile"); let mut buf = [0u8; 8]; let err = engine .write_field(&mut buf, "id", &FieldValue::Struct { start: 0, end: 4 }) .unwrap_err(); assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}"); } #[test] fn compile_returns_schema_error_for_invalid_top_level() { let doc = json!({ "type": "object", "properties": {} }); let err = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn compile_returns_schema_error_for_non_struct_root() { let doc = json!({ "$defs": { "U": { "kind": "union", "discriminator": { "kind": "byte", "offset": 0, "type": "uint8" }, "mapping": { "1": { "$ref": "#/$defs/A" } } }, "A": { "kind": "struct", "fields": [] } } }); let err = AlkTypeEngine::compile(&doc, "U", LayoutMode::Packed, None).unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn compile_rejects_unknown_endian_annotation() { let doc = json!({ "$defs": { "S": { "kind": "struct", "endian": "middle", "fields": [ { "name": "id", "kind": "uint32" } ] } } }); let err = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn compile_rejects_unknown_encoding_annotation() { let doc = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "blob", "kind": "bytes", "encoding": "sideways" } ] } } }); let err = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn compile_rejects_non_integer_align() { let doc = json!({ "$defs": { "S": { "kind": "struct", "align": "wide", "fields": [ { "name": "id", "kind": "uint32" } ] } } }); let err = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn compile_accepts_inline_struct_field() { let doc = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "inner", "kind": { "kind": "struct", "fields": [ { "name": "x", "kind": "uint16" } ] } } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile"); assert_eq!(engine.mode(), LayoutMode::Packed); } #[test] fn debug_formats_without_panicking() { let doc = fixed_struct_doc(); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile"); let s = format!("{engine:?}"); assert!(s.contains("AlkTypeEngine")); assert!(s.contains("Aligned")); } #[test] fn lookup_leaf_field_walks_dotted_path() { let doc = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "header", "kind": { "kind": "struct", "fields": [ { "name": "magic", "kind": "uint32" }, { "name": "version", "kind": "uint8" } ] } } ] } } }); let d = BastDoc::new(&doc, "S").expect("doc"); assert_eq!(lookup_leaf_field(&d, "header.magic").unwrap().kind, AlkTypeKind::Uint32); assert_eq!(lookup_leaf_field(&d, "header.version").unwrap().kind, AlkTypeKind::Uint8); assert!(lookup_leaf_field(&d, "header.missing").is_none()); assert!(lookup_leaf_field(&d, "missing").is_none()); } #[test] fn read_field_aligned_reads_nested_struct_leaf_fields() { let doc = json!({ "$defs": { "S": { "kind": "struct", "endian": "little", "fields": [ { "name": "header", "kind": { "kind": "struct", "fields": [ { "name": "magic", "kind": "uint32" }, { "name": "version", "kind": "uint8" } ] } }, { "name": "body", "kind": "uint32" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile"); let mut buf = vec![0u8; 12]; buf[0..4].copy_from_slice(&0xDEADBEEFu32.to_le_bytes()); buf[4] = 0x01; buf[8..12].copy_from_slice(&0xCAFEBABEu32.to_le_bytes()); assert_eq!( engine.read_field(&buf, "header.magic").unwrap(), FieldValue::U32(0xDEADBEEF) ); assert_eq!( engine.read_field(&buf, "header.version").unwrap(), FieldValue::U8(0x01) ); assert_eq!( engine.read_field(&buf, "body").unwrap(), FieldValue::U32(0xCAFEBABE) ); } #[test] fn read_field_aligned_reads_bytes_field() { let doc = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "blob", "kind": "bytes" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile"); let mut buf = vec![0u8; 16]; let payload = [0xAA, 0xBB, 0xCC]; let len_bytes = 3u32.to_le_bytes(); buf[0..4].copy_from_slice(&len_bytes); buf[4..7].copy_from_slice(&payload); match engine.read_field(&buf, "blob").unwrap() { FieldValue::Bytes(b) => assert_eq!(b, &payload[..]), other => panic!("expected Bytes, got {other:?}"), } } #[test] fn read_field_aligned_reads_timestamp_as_string() { let doc = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "ts", "kind": "timestamp" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile"); let mut buf = vec![0u8; 32]; let stamp = "2026-07-20T15:30:00Z"; let len_bytes = (stamp.len() as u32).to_le_bytes(); buf[0..4].copy_from_slice(&len_bytes); buf[4..4 + stamp.len()].copy_from_slice(stamp.as_bytes()); match engine.read_field(&buf, "ts").unwrap() { FieldValue::String(s) => assert_eq!(s, stamp), other => panic!("expected String, got {other:?}"), } } #[test] fn read_field_aligned_honors_field_level_endian_override() { let doc = json!({ "$defs": { "S": { "kind": "struct", "endian": "big", "fields": [ { "name": "crc", "kind": "uint32", "endian": "little" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile"); let buf = [0x04u8, 0x03, 0x02, 0x01]; assert_eq!( engine.read_field(&buf, "crc").unwrap(), FieldValue::U32(0x01020304) ); } #[test] fn write_field_aligned_honors_field_level_endian_override() { let doc = json!({ "$defs": { "S": { "kind": "struct", "endian": "big", "fields": [ { "name": "crc", "kind": "uint32", "endian": "little" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile"); let mut buf = [0u8; 4]; engine .write_field(&mut buf, "crc", &FieldValue::U32(0x01020304)) .unwrap(); assert_eq!(buf, [0x04, 0x03, 0x02, 0x01]); } #[test] fn read_field_aligned_reads_offset_indirect_bytes() { let doc = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "blob", "kind": "bytes", "encoding": "offset-indirect" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile"); let mut buf = vec![0u8; 8 + 3]; buf[0..4].copy_from_slice(&8u32.to_le_bytes()); buf[4..8].copy_from_slice(&3u32.to_le_bytes()); buf[8..11].copy_from_slice(&[0xAA, 0xBB, 0xCC]); match engine.read_field(&buf, "blob").unwrap() { FieldValue::Bytes(b) => assert_eq!(b, &[0xAA, 0xBB, 0xCC]), other => panic!("expected Bytes, got {other:?}"), } } #[test] fn read_field_aligned_reads_offset_indirect_string() { let doc = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "name", "kind": "string", "encoding": "offset-indirect" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile"); let mut buf = vec![0u8; 8 + 5]; buf[0..4].copy_from_slice(&8u32.to_le_bytes()); buf[4..8].copy_from_slice(&5u32.to_le_bytes()); buf[8..13].copy_from_slice(b"hello"); match engine.read_field(&buf, "name").unwrap() { FieldValue::String(s) => assert_eq!(s, "hello"), other => panic!("expected String, got {other:?}"), } } #[test] fn write_field_aligned_rejects_offset_indirect() { let doc = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "blob", "kind": "bytes", "encoding": "offset-indirect" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile"); let mut buf = vec![0u8; 16]; let err = engine .write_field(&mut buf, "blob", &FieldValue::Bytes(&[1, 2, 3])) .unwrap_err(); assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}"); } // ----- validate_bytes tests (ADR-010) ----- // // Note: step 4 wires the layout layer to BAST. The `validate_bytes` // value-constraint enforcement (maxLength, enum bounds) is step 5's // concern (the BAST-native validator). These tests cover the // materialization + structural path. fn chunk_header_doc() -> Value { json!({ "$defs": { "ChunkHeader": { "kind": "struct", "endian": "big", "fields": [ { "name": "channel_id", "kind": "uint32" }, { "name": "length", "kind": "uint32" } ] } } }) } #[test] fn validate_bytes_packed_accepts_valid_chunk_header() { let doc = chunk_header_doc(); let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Packed, None).expect("compile"); let buf = [0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 12u8]; assert!(engine.validate_bytes(&buf).is_ok(), "valid header should pass"); } #[test] fn validate_bytes_packed_rejects_short_buffer() { let doc = chunk_header_doc(); let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Packed, None).expect("compile"); let buf = [0u8; 4]; let err = engine.validate_bytes(&buf).unwrap_err(); assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}"); } #[test] fn validate_bytes_aligned_accepts_valid_chunk_header() { let doc = chunk_header_doc(); let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Aligned, None).expect("compile"); let buf = [0u8, 0u8, 0u8, 42u8, 0u8, 0u8, 0u8, 7u8]; assert!(engine.validate_bytes(&buf).is_ok(), "valid header should pass"); } #[test] fn validate_bytes_aligned_rejects_short_buffer() { let doc = chunk_header_doc(); let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Aligned, None).expect("compile"); let buf = [0u8; 6]; let err = engine.validate_bytes(&buf).unwrap_err(); assert!( matches!(err, AlkTypeError::Access { .. } | AlkTypeError::Offset { .. }), "got {err:?}" ); } #[test] fn validate_bytes_packed_materializes_struct_with_mixed_fields() { let doc = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "flag", "kind": "uint8" }, { "name": "id", "kind": "uint32" }, { "name": "name", "kind": "string" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile"); let mut buf = vec![0u8; 16]; buf[0] = 0xAB; buf[1..5].copy_from_slice(&0x01020304u32.to_le_bytes()); buf[5..9].copy_from_slice(&5u32.to_le_bytes()); buf[9..14].copy_from_slice(b"hello"); assert!(engine.validate_bytes(&buf).is_ok(), "valid mixed struct should pass"); } #[test] fn validate_bytes_with_simple_struct_round_trips() { let doc = json!({ "$defs": { "S": { "kind": "struct", "endian": "little", "fields": [ { "name": "channel_id", "kind": "uint32" }, { "name": "length", "kind": "uint32" } ] } } }); let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile"); let mut buf = vec![0u8; 8]; buf[0..4].copy_from_slice(&42u32.to_le_bytes()); buf[4..8].copy_from_slice(&7u32.to_le_bytes()); assert!(engine.validate_bytes(&buf).is_ok()); } // ----- validate_json / is_valid_json tests (step 6, D-BAST-007) ----- fn uint32_struct_bast() -> Value { json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "id", "kind": "uint32" } ] } } }) } fn id_json_schema() -> Value { json!({ "type": "object", "properties": { "id": { "type": "integer", "minimum": 0, "maximum": 4294967295u64 } }, "required": ["id"] }) } #[test] fn validate_json_accepts_valid_instance() { let bast = uint32_struct_bast(); let schema = id_json_schema(); let engine = AlkTypeEngine::compile(&bast, "S", LayoutMode::Packed, Some(&schema)).expect("compile"); assert!(engine.validate_json(&json!({"id": 42})).is_ok()); assert!(engine.is_valid_json(&json!({"id": 42}))); } #[test] fn validate_json_rejects_invalid_instance() { let bast = uint32_struct_bast(); let schema = id_json_schema(); let engine = AlkTypeEngine::compile(&bast, "S", LayoutMode::Packed, Some(&schema)).expect("compile"); let err = engine.validate_json(&json!({"id": -1})).unwrap_err(); assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}"); assert!(!engine.is_valid_json(&json!({"id": -1}))); assert!(!engine.is_valid_json(&json!({"id": "x"}))); assert!(!engine.is_valid_json(&json!({}))); } #[test] fn validate_json_returns_schema_error_when_no_json_schema_supplied() { let bast = uint32_struct_bast(); let engine = AlkTypeEngine::compile(&bast, "S", LayoutMode::Packed, None).expect("compile"); let err = engine.validate_json(&json!({"id": 42})).unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn is_valid_json_returns_false_when_no_json_schema_supplied() { let bast = uint32_struct_bast(); let engine = AlkTypeEngine::compile(&bast, "S", LayoutMode::Packed, None).expect("compile"); assert!(!engine.is_valid_json(&json!({"id": 42}))); } #[test] fn validate_json_independent_of_bast_document() { let bast = uint32_struct_bast(); let schema = json!({ "type": "object", "properties": { "name": { "type": "string", "maxLength": 3 } }, "required": ["name"] }); let engine = AlkTypeEngine::compile(&bast, "S", LayoutMode::Aligned, Some(&schema)).expect("compile"); assert!(engine.validate_json(&json!({"name": "hi"})).is_ok()); assert!(engine.validate_json(&json!({"name": "toolong"})).is_err()); assert!(!engine.is_valid_json(&json!({"name": "toolong"}))); } #[test] fn compile_with_json_schema_returns_schema_error_for_malformed_json_schema() { let bast = uint32_struct_bast(); let bad_schema = json!({"type": "not-a-real-type"}); let err = AlkTypeEngine::compile(&bast, "S", LayoutMode::Packed, Some(&bad_schema)) .unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn validate_json_validates_nested_object_json_schema() { let bast = uint32_struct_bast(); let schema = json!({ "type": "object", "properties": { "packet": { "type": "object", "properties": { "kind": { "type": "string", "enum": ["a", "b"] } }, "required": ["kind"] } }, "required": ["packet"] }); let engine = AlkTypeEngine::compile(&bast, "S", LayoutMode::Packed, Some(&schema)).expect("compile"); assert!(engine.validate_json(&json!({"packet": {"kind": "a"}})).is_ok()); assert!(engine.validate_json(&json!({"packet": {"kind": "c"}})).is_err()); assert!(!engine.is_valid_json(&json!({"packet": {"kind": "c"}}))); } }