Embed BAST v1 meta-schema as BAST_META_SCHEMA
Step 2 of the BAST pivot. Adds the normative BAST meta-schema (Draft 2020-12 JSON Schema) as a public serde_json::Value, embedded at compile time and available for validating BAST document well-formedness. Copied verbatim from docs/architecture/bast-format.md §The Meta-Schema. - New src/bast_meta.rs: BAST_META_SCHEMA static, built lazily via LazyLock (the json! macro allocates, so it can't be a const; parsed once, reused as &'static Value thereafter). - src/lib.rs: pub mod bast_meta + re-export BAST_META_SCHEMA. Additive public surface. The meta-schema validates structure (correct $defs shape, known kind strings, required properties, no additional properties, $ref restricted to #/$defs/<name>). Value-domain constraints (maxLength, enum index bounds) are enforced by the BAST-native validator (step 5), not this meta-schema. Verification: - cargo test --release: 418 tests pass (332 lib + 86 integration); 16 new unit tests exercise the meta-schema against valid and invalid BAST documents (missing $defs, unknown kind, additional properties, byte-discriminator union, enum, array+count, record, $ref, malformed ref, empty enum). - cargo clippy --all-targets -- -D warnings: clean. - cargo build --target wasm32-unknown-unknown --release: clean (LazyLock + serde_json::json! macro are wasm-safe).
This commit is contained in:
1 parent
66ab9d7d93
commit
29134789a9
2 files changed
+451
No files matched your search
@@ -0,0 +1,449 @@
|
||||
//! 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<Value> = 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
|
||||
}
|
||||
]
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
pub mod bast_meta;
|
||||
pub mod builder;
|
||||
pub mod data_access;
|
||||
pub mod engine;
|
||||
@@ -40,6 +41,7 @@ pub mod sequential_reader;
|
||||
pub mod tunion;
|
||||
pub mod validation;
|
||||
|
||||
pub use bast_meta::BAST_META_SCHEMA;
|
||||
pub use builder::{Definitions, Discriminator, Schema};
|
||||
pub use engine::{LayoutMode, AlkTypeEngine};
|
||||
pub use error::AlkTypeError;
|
||||
|
||||
Reference in new issue
Block a user