Wire compile() to BAST document + root name (step 4)

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
This commit is contained in:
glm-5.2 committed 2026-08-15 12:29:27 +00:00
1 parent f2f9c0326c
commit 04573e1d86
13 files changed
+3642 -3906

No files matched your search

+339 -289
View File
@@ -1,4 +1,4 @@
//! `AlkTypeEngine` — the compiled form of a schema.
//! `AlkTypeEngine` — the compiled form of a BAST document.
//!
//! Combines the layout engine (both packed and aligned modes) and the
//! jsonschema validator into a single struct. Built once at schema load
@@ -9,12 +9,13 @@
//! §"The AlkTypeEngine struct" and
//! [overview.md](../../docs/architecture/overview.md).
use crate::bast::{BastDefKind, BastDoc, BastStruct, BastType};
use crate::data_access;
use crate::error::AlkTypeError;
use crate::layout_builder::LayoutBuilder;
use crate::materialize;
use crate::offset_map::OffsetMap;
use crate::schema::{self, get_alktype_kind_loose_enum, Endian, AlkTypeKind};
use crate::schema::{Endian, AlkTypeKind};
use crate::sequential_reader::{FieldValue, SequentialReader};
use crate::validation;
use serde_json::Value;
@@ -49,7 +50,7 @@ enum Layout {
Aligned { offset_map: OffsetMap },
}
/// The compiled form of a alktype schema. Combines the layout engine
/// The compiled form of a BAST document. Combines the layout engine
/// (both packed and aligned modes) and the jsonschema validator.
///
/// Built once at schema load time via [`AlkTypeEngine::compile`].
@@ -64,46 +65,68 @@ pub struct AlkTypeEngine {
layout: Layout,
validator: jsonschema::Validator,
endian: Endian,
schema: Value,
bast_doc: Value,
root_name: String,
}
impl AlkTypeEngine {
/// Compile a schema into a [`AlkTypeEngine`].
/// Compile a BAST document into a [`AlkTypeEngine`].
///
/// This is the expensive operation — it parses the schema, normalizes
/// `$ref` values, computes the layout, and builds the jsonschema
/// This is the expensive operation — it parses the BAST document
/// into a typed tree, computes the layout, and builds the jsonschema
/// validator. Call once at load time; use the returned engine for
/// repeated operations.
///
/// The `mode` parameter selects the layout strategy. The same schema
/// can be compiled in either mode.
/// `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.
///
/// 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 schema is malformed or the
/// underlying layout/validator construction fails. The error is
/// propagated from [`LayoutBuilder::new`], [`SequentialReader::new`],
/// Returns [`AlkTypeError::Schema`] if the document is malformed or
/// the root type is not a struct. The error is propagated from
/// [`BastDoc::new`], [`LayoutBuilder::new`],
/// [`OffsetMap::compute`], or [`validation::build_validator`].
pub fn compile(schema: &mut Value, mode: LayoutMode) -> Result<Self, AlkTypeError> {
schema::normalize_refs(schema);
schema::inline_union_variant_refs(schema);
let endian = Endian::from_schema(schema);
pub fn compile(
bast_doc: &Value,
root_name: &str,
mode: LayoutMode,
) -> Result<Self, AlkTypeError> {
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(schema)?;
let builder = LayoutBuilder::new(bast_doc, root_name)?;
Layout::Packed { builder }
}
LayoutMode::Aligned => {
let offset_map = OffsetMap::compute(schema)?;
let offset_map = OffsetMap::compute(&doc)?;
Layout::Aligned { offset_map }
}
};
let validator = validation::build_validator(schema)?;
let validator = validation::build_validator(bast_doc)?;
Ok(Self {
layout,
validator,
endian,
schema: schema.clone(),
bast_doc: bast_doc.clone(),
root_name: root_name.to_string(),
})
}
@@ -146,7 +169,9 @@ impl AlkTypeEngine {
/// Returns `None` if compiled in aligned mode.
pub fn sequential_reader(&self) -> Option<SequentialReader> {
match &self.layout {
Layout::Packed { .. } => SequentialReader::new(&self.schema).ok(),
Layout::Packed { .. } => {
SequentialReader::new(&self.bast_doc, &self.root_name).ok()
}
Layout::Aligned { .. } => None,
}
}
@@ -188,12 +213,11 @@ impl AlkTypeEngine {
/// - [`AlkTypeError::Validation`] if the materialized `Value` does
/// not pass the jsonschema validator.
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(buffer, &self.schema, self.endian)?
}
Layout::Packed { .. } => materialize::materialize_packed(&doc, buffer, self.endian)?,
Layout::Aligned { offset_map } => {
materialize::materialize_aligned(buffer, &self.schema, offset_map, self.endian)?
materialize::materialize_aligned(&doc, buffer, offset_map, self.endian)?
}
};
self.validator
@@ -241,14 +265,10 @@ impl AlkTypeEngine {
field_path: field_path.to_string(),
reason: "field not found in offset map".to_string(),
})?;
let field_schema =
lookup_field_schema(&self.schema, field_path).ok_or_else(|| AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: "field schema not found in schema tree".to_string(),
})?;
let kind = get_alktype_kind_loose_enum(field_schema).ok_or_else(|| AlkTypeError::Offset {
let doc = BastDoc::new(&self.bast_doc, &self.root_name)?;
let kind = lookup_field_kind(&doc, field_path).ok_or_else(|| AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: "field schema has no AlkType:* kind".to_string(),
reason: "field schema not found in BAST tree or has no primitive kind".to_string(),
})?;
let endian = self.endian;
match kind {
@@ -335,8 +355,8 @@ impl AlkTypeEngine {
/// `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`).
/// [`AlkTypeEngine::layout_builder`] for packed mode. Also returns
/// an error for composite kinds (`Struct`, `Union`, `Array`, `Record`).
///
/// # Errors
///
@@ -425,24 +445,40 @@ impl fmt::Debug for AlkTypeEngine {
.field("layout", &self.layout)
.field("validator", &"<jsonschema::Validator>")
.field("endian", &self.endian)
.field("schema", &self.schema)
.field("root_name", &self.root_name)
.finish()
}
}
/// Walk a schema tree to find the node for a dotted field path.
/// Walk the BAST typed tree to find the primitive/composite kind 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 `schema["properties"][segment]`
/// at each step. Returns `None` if any segment is missing or the schema is
/// not an object. Does not resolve `$ref` pointers — the engine stores the
/// normalized schema, and the aligned offset map only records paths for
/// inline fields, so refs at intermediate levels are not expected here.
fn lookup_field_schema<'a>(schema: &'a Value, field_path: &str) -> Option<&'a Value> {
let mut current = schema;
for segment in field_path.split('.') {
current = current.as_object()?.get("properties")?.get(segment)?;
/// 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_field_kind(doc: &BastDoc<'_>, field_path: &str) -> Option<AlkTypeKind> {
let root_def = doc.root_def();
let mut current_struct: BastStruct<'_> = match root_def.kind() {
BastDefKind::Struct(s) => s.clone(),
_ => return None,
};
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()?;
if i + 1 == last {
return Some(resolved.alk_kind());
}
match &resolved {
BastType::Struct(s) => current_struct = s.clone(),
_ => return None,
}
}
Some(current)
None
}
#[cfg(test)]
@@ -450,23 +486,27 @@ mod tests {
use super::*;
use serde_json::json;
fn fixed_struct_schema() -> Value {
fn fixed_struct_doc() -> Value {
json!({
"AlkType:Struct": true,
"endian": "little",
"properties": {
"flag": { "AlkType:Uint8": true },
"id": { "AlkType:Uint32": true },
"score": { "AlkType:Float32": true },
"tag": { "AlkType:String": true }
"$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 mut schema = fixed_struct_schema();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let doc = fixed_struct_doc();
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
assert_eq!(engine.mode(), LayoutMode::Aligned);
assert!(engine.offset_map().is_some());
assert!(engine.layout_builder().is_none());
@@ -475,8 +515,8 @@ mod tests {
#[test]
fn compile_packed_builds_builder_and_reader() {
let mut schema = fixed_struct_schema();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
let doc = fixed_struct_doc();
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).expect("compile");
assert_eq!(engine.mode(), LayoutMode::Packed);
assert!(engine.layout_builder().is_some());
assert!(engine.sequential_reader().is_some());
@@ -484,102 +524,49 @@ mod tests {
}
#[test]
fn compile_normalizes_refs() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"child": { "$ref": "Child" }
},
fn endian_parsed_from_struct() {
let doc = json!({
"$defs": {
"Child": {
"AlkType:Struct": true,
"properties": { "x": { "AlkType:Uint8": true } }
"S": {
"kind": "struct",
"endian": "big",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
assert_eq!(
engine.schema["properties"]["child"]["$ref"],
json!("#/$defs/Child")
);
}
#[test]
fn endian_parsed_from_schema() {
let mut schema = json!({
"AlkType:Struct": true,
"endian": "big",
"properties": { "id": { "AlkType:Uint32": true } }
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).expect("compile");
assert_eq!(engine.endian(), Endian::Big);
}
#[test]
fn endian_defaults_to_little() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": { "id": { "AlkType:Uint32": true } }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).expect("compile");
assert_eq!(engine.endian(), Endian::Little);
}
#[test]
fn validate_json_accepts_valid_instance() {
let mut schema = json!({
"AlkType:Struct": true,
"type": "object",
"properties": {
"id": { "AlkType:Uint32": true, "type": "integer" }
},
"required": ["id"]
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
assert!(engine.validate_json(&json!({"id": 42})).is_ok());
}
#[test]
fn validate_json_rejects_invalid_instance() {
let mut schema = json!({
"AlkType:Struct": true,
"type": "object",
"properties": {
"id": { "AlkType:Uint32": true, "type": "integer" }
},
"required": ["id"]
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let err = engine.validate_json(&json!({"id": -1})).unwrap_err();
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
}
#[test]
fn is_valid_json_returns_bool() {
let mut schema = json!({
"AlkType:Struct": true,
"type": "object",
"properties": {
"id": { "AlkType:Uint32": true, "type": "integer" }
},
"required": ["id"]
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
assert!(engine.is_valid_json(&json!({"id": 42})));
assert!(!engine.is_valid_json(&json!({"id": -1})));
}
#[test]
fn read_field_aligned_reads_fixed_fields() {
let mut schema = json!({
"AlkType:Struct": true,
"endian": "little",
"properties": {
"flag": { "AlkType:Uint8": true },
"id": { "AlkType:Uint32": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"endian": "little",
"fields": [
{ "name": "flag", "kind": "uint8" },
{ "name": "id", "kind": "uint32" }
]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let mut buf = vec![0u8; 8];
buf[0] = 0xAB;
buf[4..8].copy_from_slice(&0x01020304u32.to_le_bytes());
@@ -595,13 +582,15 @@ mod tests {
#[test]
fn read_field_aligned_reads_string_length_prefixed() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"name": { "AlkType:String": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "name", "kind": "string" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let mut buf = vec![0u8; 32];
let len_bytes = 5u32.to_le_bytes();
buf[0..4].copy_from_slice(&len_bytes);
@@ -614,11 +603,15 @@ mod tests {
#[test]
fn read_field_returns_access_error_in_packed_mode() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": { "id": { "AlkType:Uint32": true } }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).expect("compile");
let buf = [0u8; 4];
let err = engine.read_field(&buf, "id").unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
@@ -626,11 +619,15 @@ mod tests {
#[test]
fn read_field_returns_offset_error_for_missing_field() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": { "id": { "AlkType:Uint32": true } }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let buf = [0u8; 4];
let err = engine.read_field(&buf, "missing").unwrap_err();
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
@@ -638,32 +635,46 @@ mod tests {
#[test]
fn read_field_returns_error_for_composite_types() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"vals": {
"AlkType:Array": true,
"items": { "AlkType:Uint32": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "vals", "kind": { "kind": "array", "element": "uint32", "count": 2 } }
]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).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 { .. }), "got {err:?}");
assert!(
matches!(err, AlkTypeError::Access { .. } | AlkTypeError::Offset { .. }),
"got {err:?}"
);
}
#[test]
fn write_field_aligned_writes_fixed_fields() {
let mut schema = json!({
"AlkType:Struct": true,
"endian": "little",
"properties": {
"flag": { "AlkType:Uint8": true },
"id": { "AlkType:Uint32": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"endian": "little",
"fields": [
{ "name": "flag", "kind": "uint8" },
{ "name": "id", "kind": "uint32" }
]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let mut buf = vec![0u8; 8];
engine
.write_field(&mut buf, "flag", &FieldValue::U8(0xAB))
@@ -685,13 +696,15 @@ mod tests {
#[test]
fn write_field_round_trips_string() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"name": { "AlkType:String": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "name", "kind": "string" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let mut buf = vec![0u8; 32];
engine
.write_field(&mut buf, "name", &FieldValue::String("hello"))
@@ -704,11 +717,15 @@ mod tests {
#[test]
fn write_field_returns_access_error_in_packed_mode() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": { "id": { "AlkType:Uint32": true } }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).expect("compile");
let mut buf = [0u8; 4];
let err = engine
.write_field(&mut buf, "id", &FieldValue::U32(1))
@@ -718,11 +735,15 @@ mod tests {
#[test]
fn write_field_returns_offset_error_for_missing_field() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": { "id": { "AlkType:Uint32": true } }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let mut buf = [0u8; 4];
let err = engine
.write_field(&mut buf, "missing", &FieldValue::U32(1))
@@ -732,11 +753,15 @@ mod tests {
#[test]
fn write_field_returns_error_for_composite_value() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": { "id": { "AlkType:Uint32": true } }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let mut buf = [0u8; 8];
let err = engine
.write_field(&mut buf, "id", &FieldValue::Struct { start: 0, end: 4 })
@@ -746,65 +771,88 @@ mod tests {
#[test]
fn compile_returns_schema_error_for_invalid_top_level() {
let mut schema = json!({ "type": "object", "properties": {} });
let err = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).unwrap_err();
let doc = json!({ "type": "object", "properties": {} });
let err = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).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).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn debug_formats_without_panicking() {
let mut schema = fixed_struct_schema();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let doc = fixed_struct_doc();
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let s = format!("{engine:?}");
assert!(s.contains("AlkTypeEngine"));
assert!(s.contains("Aligned"));
}
#[test]
fn lookup_field_schema_walks_dotted_path() {
let schema = json!({
"AlkType:Struct": true,
"properties": {
"header": {
"AlkType:Struct": true,
"properties": {
"version": { "AlkType:Uint8": true }
}
fn lookup_field_kind_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 node = lookup_field_schema(&schema, "header.version").expect("found");
assert_eq!(node, &json!({ "AlkType:Uint8": true }));
assert!(lookup_field_schema(&schema, "header.missing").is_none());
assert!(lookup_field_schema(&schema, "missing").is_none());
}
#[test]
fn alktype_kind_loose_recognizes_object_form() {
let node = json!({ "AlkType:String": { "encoding": "offset-indirect" } });
assert_eq!(
get_alktype_kind_loose_enum(&node),
Some(AlkTypeKind::String)
);
let d = BastDoc::new(&doc, "S").expect("doc");
assert_eq!(lookup_field_kind(&d, "header.magic"), Some(AlkTypeKind::Uint32));
assert_eq!(lookup_field_kind(&d, "header.version"), Some(AlkTypeKind::Uint8));
assert_eq!(lookup_field_kind(&d, "header.missing"), None);
assert_eq!(lookup_field_kind(&d, "missing"), None);
}
#[test]
fn read_field_aligned_reads_nested_struct_leaf_fields() {
let mut schema = json!({
"AlkType:Struct": true,
"endian": "little",
"properties": {
"header": {
"AlkType:Struct": true,
"properties": {
"magic": { "AlkType:Uint32": true },
"version": { "AlkType:Uint8": true }
}
},
"body": { "AlkType:Uint32": true }
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(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let mut buf = vec![0u8; 12];
buf[0..4].copy_from_slice(&0xDEADBEEFu32.to_le_bytes());
buf[4] = 0x01;
@@ -825,13 +873,15 @@ mod tests {
#[test]
fn read_field_aligned_reads_bytes_field() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"blob": { "AlkType:Bytes": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "blob", "kind": "bytes" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let mut buf = vec![0u8; 16];
let payload = [0xAA, 0xBB, 0xCC];
let len_bytes = 3u32.to_le_bytes();
@@ -845,13 +895,15 @@ mod tests {
#[test]
fn read_field_aligned_reads_timestamp_as_string() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"ts": { "AlkType:Timestamp": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "ts", "kind": "timestamp" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).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();
@@ -864,31 +916,41 @@ mod tests {
}
// ----- 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). Until step 5 lands, the
// validator built from a BAST doc sees no `AlkType:*` custom keywords,
// so jsonschema performs only structural validation. These tests
// cover the materialization + structural path.
fn chunk_header_schema() -> Value {
// channels' 8-byte chunk header: big-endian, packed mode. Built
// via the builder to also exercise ADR-009.
crate::builder::Schema::struct_()
.endian(Endian::Big)
.field("channel_id", crate::builder::Schema::uint32())
.field("length", crate::builder::Schema::uint32())
.build()
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 mut schema = chunk_header_schema();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
// channel_id=0, length=12, big-endian
let doc = chunk_header_doc();
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Packed).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 mut schema = chunk_header_schema();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
// Only 4 bytes — the length field can't be read.
let doc = chunk_header_doc();
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Packed).expect("compile");
let buf = [0u8; 4];
let err = engine.validate_bytes(&buf).unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
@@ -896,17 +958,16 @@ mod tests {
#[test]
fn validate_bytes_aligned_accepts_valid_chunk_header() {
let mut schema = chunk_header_schema();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
// channel_id=42, length=7, big-endian
let doc = chunk_header_doc();
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Aligned).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 mut schema = chunk_header_schema();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let doc = chunk_header_doc();
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Aligned).expect("compile");
let buf = [0u8; 6];
let err = engine.validate_bytes(&buf).unwrap_err();
assert!(
@@ -915,35 +976,21 @@ mod tests {
);
}
#[test]
fn validate_bytes_packed_rejects_when_schema_constraint_violated() {
// Schema with a maxLength constraint on a string field. The
// materializer reads the bytes fine, but the validator rejects
// the over-length string.
let mut schema = crate::builder::Schema::struct_()
.field(
"name",
crate::builder::Schema::string().max_length(3),
)
.build();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
// length=5 (LE) + "hello" — exceeds maxLength 3
let mut buf = vec![0u8; 9];
buf[0..4].copy_from_slice(&5u32.to_le_bytes());
buf[4..9].copy_from_slice(b"hello");
let err = engine.validate_bytes(&buf).unwrap_err();
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
}
#[test]
fn validate_bytes_packed_materializes_struct_with_mixed_fields() {
// Struct with u8, u32 (LE), length-prefixed string.
let mut schema = crate::builder::Schema::struct_()
.field("flag", crate::builder::Schema::uint8())
.field("id", crate::builder::Schema::uint32())
.field("name", crate::builder::Schema::string())
.build();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
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).expect("compile");
let mut buf = vec![0u8; 16];
buf[0] = 0xAB;
buf[1..5].copy_from_slice(&0x01020304u32.to_le_bytes());
@@ -953,20 +1000,23 @@ mod tests {
}
#[test]
fn validate_bytes_with_builder_schema_round_trips() {
// Full round-trip: build schema via builder, compile, write bytes
// directly (packed-mode writes go through `LayoutBuilder`; for
// this test we write the two u32s directly since the schema is
// a simple flat struct), then validate via `validate_bytes`.
let mut schema = crate::builder::Schema::struct_()
.endian(Endian::Little)
.field("channel_id", crate::builder::Schema::uint32())
.field("length", crate::builder::Schema::uint32())
.build();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
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).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());
}
}
}