//! BAST meta-schema — the standard JSON Schema (Draft 2020-12) that //! validates the *structure* of BAST documents (is a document //! well-formed?). //! //! This is distinct from the BAST-native validator (step 5), which //! validates *binary data* against a BAST document (are the bytes a //! valid instance?). See //! [`docs/architecture/bast-format.md`](../docs/architecture/bast-format.md) //! for the normative spec. //! //! The meta-schema is embedded at compile time via the `serde_json::json!` //! macro — no I/O, no feature flags, wasm-clean. It is also published at //! `https://alk.dev/bast/v1/schema` (the `$id`). Consumers and editors can //! validate BAST documents against it with any standard JSON Schema //! validator: //! //! ```ignore //! jsonschema::options() //! .build(&alktype::BAST_META_SCHEMA)? //! .validate(&bast_doc)?; //! ``` use serde_json::{json, Value}; use std::sync::LazyLock; /// The BAST v1 meta-schema, as a `serde_json::Value`. /// /// A BAST document is valid against this schema iff it is well-formed /// (correct `$defs` shape, known `kind` strings, required properties /// present, no additional properties). Value-domain constraints /// (`maxLength`, enum index bounds, etc.) are enforced by the /// BAST-native validator, not this meta-schema. /// /// Built lazily on first access via `serde_json::json!` (the `json!` /// macro allocates, so it can't be a `const`). The parsed `Value` is /// then reused for every subsequent call — `&'static` via `LazyLock`. pub static BAST_META_SCHEMA: LazyLock = LazyLock::new(|| { json!({ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://alk.dev/bast/v1/schema", "title": "Binary Abstract Syntax Tree (BAST) v1", "description": "Meta-schema for BAST documents. A BAST document describes the binary layout of structured data.", "type": "object", "properties": { "$defs": { "type": "object", "additionalProperties": { "$ref": "#/$defs/TypeDef" } } }, "required": ["$defs"], "$defs": { "TypeDef": { "oneOf": [ { "$ref": "#/$defs/StructDef" }, { "$ref": "#/$defs/UnionDef" }, { "$ref": "#/$defs/EnumDef" } ] }, "StructDef": { "type": "object", "properties": { "kind": { "const": "struct" }, "endian": { "enum": ["little", "big"] }, "align": { "type": "integer", "minimum": 1 }, "fields": { "type": "array", "items": { "$ref": "#/$defs/FieldDef" } } }, "required": ["kind", "fields"], "additionalProperties": false }, "FieldDef": { "type": "object", "properties": { "name": { "type": "string", "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$" }, "kind": { "$ref": "#/$defs/TypeRef" }, "endian": { "enum": ["little", "big"] }, "align": { "type": "integer", "minimum": 1 }, "encoding": { "enum": ["length-prefixed", "offset-indirect"] }, "maxLength": { "type": "integer", "minimum": 0 } }, "required": ["name", "kind"], "additionalProperties": false }, "TypeRef": { "oneOf": [ { "description": "Primitive type", "type": "string", "enum": [ "int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64", "float32", "float64", "bool", "string", "bytes", "timestamp" ] }, { "description": "Reference to a named $defs entry", "type": "object", "properties": { "$ref": { "type": "string", "pattern": "^#/\\$defs/[a-zA-Z_][a-zA-Z0-9_]*$" } }, "required": ["$ref"], "additionalProperties": false }, { "description": "Array type (fixed-size only in v1 — count is required)", "type": "object", "properties": { "kind": { "const": "array" }, "element": { "$ref": "#/$defs/TypeRef" }, "count": { "type": "integer", "minimum": 0 } }, "required": ["kind", "element", "count"], "additionalProperties": false }, { "description": "Record (string-keyed map) type", "type": "object", "properties": { "kind": { "const": "record" }, "values": { "$ref": "#/$defs/TypeRef" } }, "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" } ] }, "UnionDef": { "type": "object", "properties": { "kind": { "const": "union" }, "endian": { "enum": ["little", "big"] }, "fields": { "type": "array", "items": { "$ref": "#/$defs/FieldDef" } }, "discriminator": { "oneOf": [ { "type": "object", "properties": { "kind": { "const": "byte" }, "offset": { "type": "integer", "minimum": 0 }, "type": { "enum": ["uint8", "uint16", "uint32"] } }, "required": ["kind", "offset", "type"], "additionalProperties": false }, { "type": "object", "properties": { "kind": { "const": "field" }, "name": { "type": "string" } }, "required": ["kind", "name"], "additionalProperties": false } ] }, "mapping": { "type": "object", "additionalProperties": { "$ref": "#/$defs/TypeRef" } } }, "required": ["kind", "discriminator", "mapping"], "additionalProperties": false }, "EnumDef": { "type": "object", "properties": { "kind": { "const": "enum" }, "values": { "type": "array", "items": { "type": "string" }, "minItems": 1 } }, "required": ["kind", "values"], "additionalProperties": false } } }) }); /// 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; #[test] fn meta_schema_has_correct_id_and_draft() { assert_eq!( BAST_META_SCHEMA["$schema"], "https://json-schema.org/draft/2020-12/schema" ); assert_eq!( BAST_META_SCHEMA["$id"], "https://alk.dev/bast/v1/schema" ); } #[test] fn meta_schema_requires_defs() { let required = BAST_META_SCHEMA["required"].as_array().expect("required is array"); assert!(required.iter().any(|v| v == "$defs")); } #[test] fn meta_schema_covers_struct_union_enum_type_defs() { let type_def_oneof = BAST_META_SCHEMA["$defs"]["TypeDef"]["oneOf"] .as_array() .expect("TypeDef.oneOf is array"); let refs: Vec<&str> = type_def_oneof .iter() .map(|v| v["$ref"].as_str().expect("ref string")) .collect(); assert!(refs.contains(&"#/$defs/StructDef")); assert!(refs.contains(&"#/$defs/UnionDef")); assert!(refs.contains(&"#/$defs/EnumDef")); } #[test] fn meta_schema_typeref_enumerates_all_14_primitives() { let typeref_oneof = BAST_META_SCHEMA["$defs"]["TypeRef"]["oneOf"] .as_array() .expect("TypeRef.oneOf is array"); let primitive_arm = typeref_oneof .iter() .find(|v| v["description"] == "Primitive type") .expect("primitive arm present"); let primitives = primitive_arm["enum"] .as_array() .expect("primitive enum is array"); let strs: Vec<&str> = primitives.iter().map(|v| v.as_str().unwrap()).collect(); assert_eq!(strs.len(), 14); for expected in [ "int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64", "float32", "float64", "bool", "string", "bytes", "timestamp", ] { assert!(strs.contains(&expected), "missing primitive {expected}"); } } #[test] fn meta_schema_validates_minimal_bast_document() { let validator = jsonschema::options() .build(&BAST_META_SCHEMA) .expect("meta-schema compiles"); let doc = serde_json::json!({ "$defs": { "Point": { "kind": "struct", "fields": [ { "name": "x", "kind": "uint32" }, { "name": "y", "kind": "uint32" } ] } } }); assert!(validator.validate(&doc).is_ok(), "valid doc rejected"); } #[test] fn meta_schema_rejects_missing_defs() { let validator = jsonschema::options() .build(&BAST_META_SCHEMA) .expect("meta-schema compiles"); let doc = serde_json::json!({ "type": "object" }); assert!(validator.validate(&doc).is_err(), "missing $defs accepted"); } #[test] fn meta_schema_rejects_unknown_kind() { let validator = jsonschema::options() .build(&BAST_META_SCHEMA) .expect("meta-schema compiles"); let doc = serde_json::json!({ "$defs": { "Bad": { "kind": "mystery", "fields": [] } } }); assert!(validator.validate(&doc).is_err(), "unknown kind accepted"); } #[test] fn meta_schema_rejects_additional_properties_on_struct() { let validator = jsonschema::options() .build(&BAST_META_SCHEMA) .expect("meta-schema compiles"); let doc = serde_json::json!({ "$defs": { "Bad": { "kind": "struct", "fields": [], "bogus": true } } }); assert!(validator.validate(&doc).is_err(), "additional property accepted"); } #[test] fn meta_schema_accepts_union_with_byte_discriminator() { let validator = jsonschema::options() .build(&BAST_META_SCHEMA) .expect("meta-schema compiles"); let doc = serde_json::json!({ "$defs": { "Msg": { "kind": "union", "discriminator": { "kind": "byte", "offset": 0, "type": "uint8" }, "mapping": { "1": { "$ref": "#/$defs/A" }, "2": { "$ref": "#/$defs/B" } } }, "A": { "kind": "struct", "fields": [] }, "B": { "kind": "struct", "fields": [] } } }); assert!(validator.validate(&doc).is_ok(), "byte-discriminator union rejected"); } #[test] fn meta_schema_accepts_enum() { let validator = jsonschema::options() .build(&BAST_META_SCHEMA) .expect("meta-schema compiles"); let doc = serde_json::json!({ "$defs": { "Status": { "kind": "enum", "values": ["Ok", "Err"] } } }); assert!(validator.validate(&doc).is_ok(), "enum rejected"); } #[test] fn meta_schema_rejects_empty_enum_values() { let validator = jsonschema::options() .build(&BAST_META_SCHEMA) .expect("meta-schema compiles"); let doc = serde_json::json!({ "$defs": { "Bad": { "kind": "enum", "values": [] } } }); assert!(validator.validate(&doc).is_err(), "empty enum accepted"); } #[test] fn meta_schema_accepts_array_with_count() { let validator = jsonschema::options() .build(&BAST_META_SCHEMA) .expect("meta-schema compiles"); let doc = serde_json::json!({ "$defs": { "Vec3": { "kind": "struct", "fields": [ { "name": "data", "kind": { "kind": "array", "element": "float32", "count": 3 } } ] } } }); assert!(validator.validate(&doc).is_ok(), "array with count rejected"); } #[test] fn meta_schema_rejects_array_without_count() { let validator = jsonschema::options() .build(&BAST_META_SCHEMA) .expect("meta-schema compiles"); let doc = serde_json::json!({ "$defs": { "Bad": { "kind": "struct", "fields": [ { "name": "data", "kind": { "kind": "array", "element": "float32" } } ] } } }); assert!(validator.validate(&doc).is_err(), "array without count accepted"); } #[test] fn meta_schema_accepts_record() { let validator = jsonschema::options() .build(&BAST_META_SCHEMA) .expect("meta-schema compiles"); let doc = serde_json::json!({ "$defs": { "Map": { "kind": "struct", "fields": [ { "name": "entries", "kind": { "kind": "record", "values": "string" } } ] } } }); assert!(validator.validate(&doc).is_ok(), "record rejected"); } #[test] fn meta_schema_accepts_ref_object() { let validator = jsonschema::options() .build(&BAST_META_SCHEMA) .expect("meta-schema compiles"); let doc = serde_json::json!({ "$defs": { "Outer": { "kind": "struct", "fields": [ { "name": "inner", "kind": { "$ref": "#/$defs/Inner" } } ] }, "Inner": { "kind": "struct", "fields": [] } } }); assert!(validator.validate(&doc).is_ok(), "ref object rejected"); } #[test] fn meta_schema_rejects_malformed_ref() { let validator = jsonschema::options() .build(&BAST_META_SCHEMA) .expect("meta-schema compiles"); let doc = serde_json::json!({ "$defs": { "Bad": { "kind": "struct", "fields": [ { "name": "inner", "kind": { "$ref": "Inner" } } ] } } }); 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"); } }