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:
1 parent
f2f9c0326c
commit
04573e1d86
13 files changed
+3642
-3906
No files matched your search
+339
-289
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user