Validate BAST documents against the meta-schema at compile time
Resolves review #003 finding M3. - Add bast_meta::validate_bast_doc and call it from AlkTypeEngine::compile before parsing. Malformed annotations (unknown endian/encoding strings, non-integer align/maxLength, missing required properties) now surface as AlkTypeError::Schema instead of being silently tolerated by the parser. - Align the meta-schema TypeRef with the parser: allow inline struct/union/ enum as TypeRefs (the parser and builder already accepted them; the meta-schema and spec did not). Update bast-format.md TypeRef table and the BastType doc comment to the seven-form vocabulary. Verification: cargo test --release (410 pass), cargo clippy --all-targets -- -D warnings (clean), cargo doc --no-deps (clean), cargo build --target wasm32-unknown-unknown --release (clean).
This commit is contained in:
1 parent
e5c7cc1ca2
commit
230345a867
4 files changed
+155
-6
No files matched your search
+5
-5
@@ -681,12 +681,12 @@ impl<'a> BastEnum<'a> {
|
||||
/// A type reference — the central mechanism for typing fields, array
|
||||
/// elements, record values, and union variants.
|
||||
///
|
||||
/// Four forms, matching the
|
||||
/// Seven forms, matching the
|
||||
/// [TypeRef](../docs/architecture/bast-format.md#typeref) `oneOf`:
|
||||
/// primitive string, `$ref` object, array object, record object.
|
||||
/// Composite kinds (`struct`/`union`/`enum`) only appear inline here when
|
||||
/// a field/element/value is an anonymous composite; named composites go
|
||||
/// through [`BastType::Ref`].
|
||||
/// primitive string, `$ref` object, array object, record object, and
|
||||
/// inline `struct`/`union`/`enum`. Composite kinds (`struct`/`union`/
|
||||
/// `enum`) appear inline here when a field/element/value is an anonymous
|
||||
/// composite; named composites go through [`BastType::Ref`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BastType<'a> {
|
||||
/// A built-in primitive (`"uint32"`, `"string"`, etc.).
|
||||
|
||||
@@ -124,6 +124,18 @@ pub static BAST_META_SCHEMA: LazyLock<Value> = LazyLock::new(|| {
|
||||
},
|
||||
"required": ["kind", "values"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Inline struct type",
|
||||
"$ref": "#/$defs/StructDef"
|
||||
},
|
||||
{
|
||||
"description": "Inline union type",
|
||||
"$ref": "#/$defs/UnionDef"
|
||||
},
|
||||
{
|
||||
"description": "Inline enum type",
|
||||
"$ref": "#/$defs/EnumDef"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -184,6 +196,26 @@ pub static BAST_META_SCHEMA: LazyLock<Value> = LazyLock::new(|| {
|
||||
})
|
||||
});
|
||||
|
||||
/// Validate a BAST document against the meta-schema.
|
||||
///
|
||||
/// Returns `Ok(())` if the document is well-formed, or
|
||||
/// [`crate::error::AlkTypeError::Schema`] with the first validation error
|
||||
/// otherwise.
|
||||
/// This is the structural gate [`crate::AlkTypeEngine::compile`] applies
|
||||
/// before parsing — it catches malformed annotations (unknown `endian`/
|
||||
/// `encoding` strings, non-integer `align`/`maxLength`, missing required
|
||||
/// properties) that the parser would otherwise silently tolerate.
|
||||
pub fn validate_bast_doc(doc: &Value) -> Result<(), crate::error::AlkTypeError> {
|
||||
let validator = jsonschema::options()
|
||||
.build(&BAST_META_SCHEMA)
|
||||
.map_err(|e| {
|
||||
crate::error::AlkTypeError::Schema(format!("BAST meta-schema failed to build: {e}"))
|
||||
})?;
|
||||
validator.validate(doc).map_err(|e| {
|
||||
crate::error::AlkTypeError::Schema(format!("BAST document is not well-formed: {e}"))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::BAST_META_SCHEMA;
|
||||
@@ -446,4 +478,44 @@ mod tests {
|
||||
});
|
||||
assert!(validator.validate(&doc).is_err(), "bare-name ref accepted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn meta_schema_accepts_inline_struct_field() {
|
||||
let validator = jsonschema::options()
|
||||
.build(&BAST_META_SCHEMA)
|
||||
.expect("meta-schema compiles");
|
||||
let doc = serde_json::json!({
|
||||
"$defs": {
|
||||
"S": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{ "name": "inner", "kind": {
|
||||
"kind": "struct",
|
||||
"fields": [ { "name": "x", "kind": "uint16" } ]
|
||||
} }
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
assert!(validator.validate(&doc).is_ok(), "inline struct field rejected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn meta_schema_accepts_inline_struct_in_union_mapping() {
|
||||
let validator = jsonschema::options()
|
||||
.build(&BAST_META_SCHEMA)
|
||||
.expect("meta-schema compiles");
|
||||
let doc = serde_json::json!({
|
||||
"$defs": {
|
||||
"U": {
|
||||
"kind": "union",
|
||||
"discriminator": { "kind": "byte", "offset": 0, "type": "uint8" },
|
||||
"mapping": {
|
||||
"5": { "kind": "struct", "fields": [ { "name": "x", "kind": "uint8" } ] }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
assert!(validator.validate(&doc).is_ok(), "inline struct in union mapping rejected");
|
||||
}
|
||||
}
|
||||
@@ -126,6 +126,7 @@ impl AlkTypeEngine {
|
||||
mode: LayoutMode,
|
||||
json_schema: Option<&Value>,
|
||||
) -> Result<Self, AlkTypeError> {
|
||||
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() {
|
||||
@@ -913,6 +914,71 @@ mod tests {
|
||||
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();
|
||||
|
||||
Reference in new issue
Block a user