//! BAST document parser — the typed surface over a BAST (Binary Abstract //! Syntax Tree) document that the layout engines, materializer, and //! BAST-native validator walk. //! //! This is step 3 of the BAST pivot //! ([`docs/plans/bast-implementation.md`](../docs/plans/bast-implementation.md)). //! The format is normative in //! [`docs/architecture/bast-format.md`](../docs/architecture/bast-format.md); //! the POC lives on branch `bast-validator-poc` as `src/bast_poc.rs`. //! //! ## Design //! //! The parser produces a **typed tree** ([`BastDoc`]/[`BastDef`]/[`BastType`]/…) //! that borrows from the source [`serde_json::Value`]. Three consumers //! (layout, materialize, validate) walk the same tree, so a typed view //! pays for itself: each walks matched arms over [`BastType`] instead of //! re-parsing raw JSON at every node. Borrowing (not cloning) keeps the //! parse allocation-free beyond the small typed nodes themselves. //! //! `$ref` resolution is a single hash lookup against `$defs` — BAST refs //! are always full JSON Pointers restricted to `#/$defs/`. Union //! variant `$ref`s are resolved **lazily** by the materializer/validator //! via [`BastDoc::resolve_typeref`]; the parser only records the //! [`BastRef`] target name. //! //! ## Untrusted input //! //! Every path that walks a BAST document returns //! [`AlkTypeError::Schema`] on a malformed //! document, never `panic!`/`unreachable!`/`unwrap` (AGENTS.md §3 — the //! downstream `alkcall` consumer accepts schemas from arbitrary internet //! peers). Overflow-safe arithmetic (`checked_add`, `usize::try_from`) //! is used for any offset/count cast (AGENTS.md §4). use crate::error::AlkTypeError; use crate::schema::{AlkTypeKind, Endian, VariableEncoding}; use serde_json::Value; const DEFS_KEY: &str = "$defs"; const REF_PREFIX: &str = "#/$defs/"; /// A parsed BAST document: the typed view over `{ "$defs": { ... } }` /// plus the chosen root name (D-BAST-001 — the root type name is a /// required `compile()` parameter, not a convention). /// /// Built once via [`BastDoc::new`] from the raw BAST `Value`; the /// downstream `compile()` call (step 4) will construct this and hand it /// to the layout engines. The tree borrows from `root` — keep the /// source `Value` alive for the lifetime of the doc. #[derive(Debug)] pub struct BastDoc<'a> { root: &'a Value, root_name: &'a str, root_def: BastDef<'a>, } impl<'a> BastDoc<'a> { /// Parse a BAST document, selecting `root_name` as the top-level type. /// /// Validates the document structurally as it parses: the `$defs` /// block is present, the root entry exists, and every reachable /// definition parses to a typed [`BastDef`]. A malformed document /// surfaces as [`AlkTypeError::Schema`], never a panic. Only the /// root definition and the definitions it (transitively) references /// are parsed eagerly; orphan `$defs` entries are not checked. /// /// The raw `Value` is also retained so lazy `$ref` resolution /// ([`BastDoc::resolve_typeref`]) can reach any `$defs` entry at access time. pub fn new(root: &'a Value, root_name: &'a str) -> Result { let raw_root_def = Self::lookup_def_raw(root, root_name)?; let root_def = BastDef::parse(raw_root_def, root_name, "")?; let doc = Self { root, root_name, root_def, }; Ok(doc) } /// The root type name this doc was compiled with. pub fn root_name(&self) -> &'a str { self.root_name } /// The parsed root type definition. pub fn root_def(&self) -> &BastDef<'a> { &self.root_def } /// Look up a raw `$defs` entry by name. Returns the raw JSON node. /// /// The single hash lookup that replaces v0.1.0's ref-normalization /// machinery. BAST refs are always `#/$defs/`; /// no external refs, no fragment-only pointers, no bare names. pub fn lookup_def(&self, name: &str) -> Result<&'a Value, AlkTypeError> { Self::lookup_def_raw(self.root, name) } /// Resolve a [`BastType::Ref`] to the [`BastDef`] it names. /// /// Used lazily by the materializer and BAST-native validator (step 5) /// for union variant dispatch and `$ref` fields. The parser does not /// inline refs at construction time — variant refs stay as /// [`BastRef`]s and resolve on demand. pub fn resolve_ref(&self, r: &BastRef<'a>) -> Result, AlkTypeError> { let raw = self.lookup_def(r.name())?; BastDef::parse(raw, r.name(), "") } /// Resolve a [`BastType`] that may be a [`Ref`](BastType::Ref) into /// the concrete [`BastType`] it points at, or return the inline type /// unchanged. /// /// The composite-walkers (struct fields, array elements, record /// values, union mappings) call this to deref one level. Deeper /// `$ref` chains are resolved by recursing through the consumer. pub fn resolve_typeref(&self, ty: &BastType<'a>) -> Result, AlkTypeError> { match ty { BastType::Ref(r) => { let def = self.resolve_ref(r)?; Ok(BastType::from_def(def)) } other => Ok(other.clone()), } } /// Resolve a [`BastType`] that may be a [`Ref`](BastType::Ref) into /// the [`BastDef`] it names, or wrap an inline type in a synthetic /// [`BastDef`]. /// /// Convenient for the validator/materializer, which work in terms of /// [`BastDef`] (struct/union/enum) and need to handle both /// `{ "$ref": "#/$defs/X" }` and inline `{ "kind": "struct", ... }` /// uniformly. pub fn resolve_typeref_as_def( &self, ty: &BastType<'a>, owning_path: &str, ) -> Result, AlkTypeError> { match ty { BastType::Ref(r) => self.resolve_ref(r), BastType::Struct(s) => Ok(BastDef { name: "", kind: BastDefKind::Struct(s.clone()), source: s.source, }), BastType::Union(u) => Ok(BastDef { name: "", kind: BastDefKind::Union(u.clone()), source: u.source, }), BastType::Enum(e) => Ok(BastDef { name: "", kind: BastDefKind::Enum(e.clone()), source: e.source, }), BastType::Primitive(_) | BastType::Array(_) | BastType::Record(_) => { Err(AlkTypeError::Schema(format!( "bast: type at {owning_path} is not a struct/union/enum (got {ty})" ))) } } } fn lookup_def_raw(root: &'a Value, name: &str) -> Result<&'a Value, AlkTypeError> { let defs = root .get(DEFS_KEY) .and_then(Value::as_object) .ok_or_else(|| AlkTypeError::Schema("bast: document missing $defs".to_string()))?; defs.get(name).ok_or_else(|| { AlkTypeError::Schema(format!("bast: $defs has no entry {name:?}")) }) } } /// A named `$defs` entry, parsed into its typed form. /// /// The three composite kinds that can live at the top level: `struct`, /// `union`, `enum` (the meta-schema's `TypeDef.oneOf`). Primitives, /// arrays, and records only appear as field/element/value types /// ([`BastType`]), not as named definitions. #[derive(Debug, Clone)] pub struct BastDef<'a> { name: &'a str, kind: BastDefKind<'a>, source: &'a Value, } impl<'a> BastDef<'a> { /// The `$defs` key this definition was registered under. Empty /// string for synthetic definitions produced by /// [`BastDoc::resolve_typeref_as_def`] from an inline type. pub fn name(&self) -> &'a str { self.name } /// The typed kind — struct, union, or enum. pub fn kind(&self) -> &BastDefKind<'a> { &self.kind } /// The raw JSON node this definition was parsed from. Retained for /// consumers that need properties the typed view doesn't expose yet /// (e.g. the layout engines during the step 4 migration). pub fn source(&self) -> &'a Value { self.source } fn parse(node: &'a Value, name: &'a str, path: &str) -> Result { let kind_str = node .get("kind") .and_then(Value::as_str) .ok_or_else(|| { AlkTypeError::Schema(format!( "bast: def {name:?} at {path} has no `kind` string" )) })?; let alk_kind = AlkTypeKind::from_bast_str(kind_str)?; let kind = match alk_kind { AlkTypeKind::Struct => { BastDefKind::Struct(BastStruct::parse(node, path)?) } AlkTypeKind::Union => { BastDefKind::Union(BastUnion::parse(node, path)?) } AlkTypeKind::Enum => { BastDefKind::Enum(BastEnum::parse(node, path)?) } other => { return Err(AlkTypeError::Schema(format!( "bast: def {name:?} at {path} has top-level kind {other}, but $defs entries must be struct/union/enum" ))) } }; Ok(Self { name, kind, source: node, }) } } /// The typed kind of a [`BastDef`]. #[derive(Debug, Clone)] pub enum BastDefKind<'a> { /// `kind: "struct"` — an ordered list of fields. Struct(BastStruct<'a>), /// `kind: "union"` — discriminator + mapping of variant TypeRefs. Union(BastUnion<'a>), /// `kind: "enum"` — a non-empty list of string values. Enum(BastEnum<'a>), } impl<'a> BastDefKind<'a> { /// The [`AlkTypeKind`] of this definition. pub fn alk_kind(&self) -> AlkTypeKind { match self { BastDefKind::Struct(_) => AlkTypeKind::Struct, BastDefKind::Union(_) => AlkTypeKind::Union, BastDefKind::Enum(_) => AlkTypeKind::Enum, } } } /// `kind: "struct"` — the most common top-level type. /// /// Field order is unambiguous: `fields` is an array, not an object with /// `properties`, so byte order is the array position /// ([bast-format.md §Design Principles](../docs/architecture/bast-format.md#design-principles) /// #4). #[derive(Debug, Clone)] pub struct BastStruct<'a> { endian: Endian, align: Option, fields: Vec>, source: &'a Value, } impl<'a> BastStruct<'a> { /// The struct-level default endianness. Defaults to /// [`Endian::Little`] when absent (ADR-003). pub fn endian(&self) -> Endian { self.endian } /// The struct-level alignment, only meaningful in aligned static /// mode (ADR-002/003). `None` when not declared. pub fn align(&self) -> Option { self.align } /// The ordered field list. Array position is field order. pub fn fields(&self) -> &[BastField<'a>] { &self.fields } /// The raw JSON node this struct was parsed from. pub fn source(&self) -> &'a Value { self.source } fn parse(node: &'a Value, path: &str) -> Result { let endian = parse_endian_opt(node).unwrap_or(Endian::Little); let align = parse_align(node); let raw_fields = node .get("fields") .and_then(Value::as_array) .ok_or_else(|| { AlkTypeError::Schema(format!( "bast: struct at {path} has no `fields` array" )) })?; let mut fields = Vec::new(); for (i, raw_field) in raw_fields.iter().enumerate() { let field_path = format!("{path}.fields[{i}]"); fields.push(BastField::parse(raw_field, &field_path)?); } Ok(Self { endian, align, fields, source: node, }) } } /// A field within a struct or field-name-discriminator union. #[derive(Debug, Clone)] pub struct BastField<'a> { name: &'a str, ty: BastType<'a>, endian: Option, align: Option, encoding: VariableEncoding, max_length: Option, source: &'a Value, } impl<'a> BastField<'a> { /// Construct a synthetic field wrapping `ty` with no annotations. /// Used internally by the materializer for array elements, record /// values, and union variants — these are TypeRefs, not fields, so /// they don't carry field-level annotations. `source` is the /// `BastType`'s source node (or `&Value::Null` for primitives). pub(crate) fn synthetic(ty: BastType<'a>, source: &'a Value) -> Self { Self { name: "", ty, endian: None, align: None, encoding: VariableEncoding::LengthPrefixed, max_length: None, source, } } /// The field name. Guaranteed to match `^[a-zA-Z_][a-zA-Z0-9_]*$` by /// the meta-schema; the parser does not re-check the pattern. pub fn name(&self) -> &'a str { self.name } /// The field's type — a primitive, `$ref`, array, or record. pub fn ty(&self) -> &BastType<'a> { &self.ty } /// Field-level endianness override. `None` means inherit the /// struct/union default. pub fn endian(&self) -> Option { self.endian } /// Field-level alignment override (aligned mode only). `None` means /// inherit the struct default / natural alignment. pub fn align(&self) -> Option { self.align } /// The variable-length encoding strategy. Defaults to /// [`VariableEncoding::LengthPrefixed`] (ADR-003). pub fn encoding(&self) -> VariableEncoding { self.encoding } /// The byte-length cap. Enforced as a validation constraint /// (packed mode) or a fixed-size reservation (aligned mode) per /// ADR-003. pub fn max_length(&self) -> Option { self.max_length } /// The raw JSON node this field was parsed from. pub fn source(&self) -> &'a Value { self.source } /// The effective endianness: this field's override, or the container /// default. pub fn effective_endian(&self, default: Endian) -> Endian { self.endian.unwrap_or(default) } fn parse(node: &'a Value, path: &str) -> Result { let name = node .get("name") .and_then(Value::as_str) .ok_or_else(|| { AlkTypeError::Schema(format!( "bast: field at {path} has no `name` string" )) })?; let raw_kind = node .get("kind") .ok_or_else(|| { AlkTypeError::Schema(format!( "bast: field {name:?} at {path} has no `kind`" )) })?; let ty = BastType::parse(raw_kind, path)?; let endian = parse_endian_opt(node); let align = parse_align(node); let encoding = parse_encoding(node); let max_length = parse_max_length(node); Ok(Self { name, ty, endian, align, encoding, max_length, source: node, }) } } /// `kind: "union"` — a tagged union with a byte-offset or field-name /// discriminator. /// /// Variant `$ref`s are resolved **lazily** by the materializer/validator /// via [`BastDoc::resolve_typeref`] — no compile-time inlining. #[derive(Debug, Clone)] pub struct BastUnion<'a> { endian: Endian, discriminator: BastDiscriminator<'a>, fields: Vec>, mapping: Vec<(&'a str, BastType<'a>)>, source: &'a Value, } impl<'a> BastUnion<'a> { /// The union-level default endianness for variant fields. pub fn endian(&self) -> Endian { self.endian } /// The discriminator — byte-offset or field-name. pub fn discriminator(&self) -> &BastDiscriminator<'a> { &self.discriminator } /// The optional `fields` array. Only valid with field-name /// discriminators (D-BAST-005); empty for byte-offset discriminators. pub fn fields(&self) -> &[BastField<'a>] { &self.fields } /// The `mapping` entries, in document order. Each key is a /// stringified discriminator value; each value is the variant /// [`BastType`] (typically a `$ref`). pub fn mapping(&self) -> &[(&'a str, BastType<'a>)] { &self.mapping } /// Look up the variant [`BastType`] for a discriminator value. pub fn variant_for(&self, key: &str) -> Option<&BastType<'a>> { self.mapping .iter() .find(|(k, _)| *k == key) .map(|(_, v)| v) } /// The raw JSON node this union was parsed from. pub fn source(&self) -> &'a Value { self.source } fn parse(node: &'a Value, path: &str) -> Result { let endian = parse_endian_opt(node).unwrap_or(Endian::Little); let discriminator = BastDiscriminator::parse(node, path)?; let raw_fields = node.get("fields").and_then(Value::as_array); let fields = match raw_fields { Some(arr) => { if !matches!(discriminator, BastDiscriminator::Field { .. }) { return Err(AlkTypeError::Schema(format!( "bast: union at {path} has `fields` but a non-field discriminator (fields are only valid with field-name discriminators, D-BAST-005)" ))); } let mut out = Vec::new(); for (i, raw_field) in arr.iter().enumerate() { let field_path = format!("{path}.fields[{i}]"); out.push(BastField::parse(raw_field, &field_path)?); } out } None => { if matches!(discriminator, BastDiscriminator::Field { .. }) { return Err(AlkTypeError::Schema(format!( "bast: field-name union at {path} has no `fields` array (the discriminator field must be declared)" ))); } Vec::new() } }; let raw_mapping = node .get("mapping") .and_then(Value::as_object) .ok_or_else(|| { AlkTypeError::Schema(format!( "bast: union at {path} has no `mapping` object" )) })?; let mut mapping = Vec::new(); for (key, value) in raw_mapping.iter() { let entry_path = format!("{path}.mapping[{key}]"); let ty = BastType::parse(value, &entry_path)?; mapping.push((key.as_str(), ty)); } if mapping.is_empty() { return Err(AlkTypeError::Schema(format!( "bast: union at {path} has an empty `mapping`" ))); } Ok(Self { endian, discriminator, fields, mapping, source: node, }) } } /// A union discriminator — byte-offset or field-name (ADR-003, D-BAST-005). #[derive(Debug, Clone)] pub enum BastDiscriminator<'a> { /// A fixed-size integer at a known byte offset. Mapping keys are /// stringified integers. Byte { /// Byte position of the discriminator within the union's buffer. offset: usize, /// The discriminator's integer width (uint8/uint16/uint32). disc_type: AlkTypeKind, }, /// A length-prefixed string field within the union. Mapping keys are /// string values matching the field's value. The `fields` array /// declares the discriminator field alongside any shared fields. Field { /// The field name that holds the discriminator value. name: &'a str, }, } impl<'a> BastDiscriminator<'a> { fn parse(union_node: &'a Value, path: &str) -> Result { let disc = union_node.get("discriminator").ok_or_else(|| { AlkTypeError::Schema(format!( "bast: union at {path} has no `discriminator`" )) })?; let obj = disc.as_object().ok_or_else(|| { AlkTypeError::Schema(format!( "bast: union at {path} discriminator must be an object" )) })?; let kind = obj .get("kind") .and_then(Value::as_str) .ok_or_else(|| { AlkTypeError::Schema(format!( "bast: union at {path} discriminator has no `kind`" )) })?; match kind { "byte" => { let offset = parse_usize_field(disc, "offset", path, "discriminator offset")? .unwrap_or(0); let type_str = obj .get("type") .and_then(Value::as_str) .ok_or_else(|| { AlkTypeError::Schema(format!( "bast: byte discriminator at {path} has no `type`" )) })?; let disc_type = AlkTypeKind::from_bast_str(type_str)?; if !matches!( disc_type, AlkTypeKind::Uint8 | AlkTypeKind::Uint16 | AlkTypeKind::Uint32 ) { return Err(AlkTypeError::Schema(format!( "bast: byte discriminator at {path} type {disc_type} is not uint8/uint16/uint32" ))); } Ok(BastDiscriminator::Byte { offset, disc_type }) } "field" => { let name = obj .get("name") .and_then(Value::as_str) .ok_or_else(|| { AlkTypeError::Schema(format!( "bast: field discriminator at {path} has no `name`" )) })?; Ok(BastDiscriminator::Field { name }) } other => Err(AlkTypeError::Schema(format!( "bast: unknown discriminator kind {other:?} at {path}" ))), } } } /// `kind: "enum"` — a non-empty list of string values. /// /// Binary representation is a `u32` index into [`values`](Self::values), /// 0-based, encoded per the struct's endianness. The BAST-native /// validator (step 5) checks the materialized index against /// `values.len()` — **the fix for the v0.1.0 dead constraint** where the /// built-in `enum` keyword checked string membership but the materializer /// emitted a numeric index that never matched. #[derive(Debug, Clone)] pub struct BastEnum<'a> { values: Vec<&'a str>, source: &'a Value, } impl<'a> BastEnum<'a> { /// The declared values, in declaration order. Non-empty (enforced /// by the meta-schema's `minItems: 1` and re-checked here). pub fn values(&self) -> &[&'a str] { &self.values } /// The raw JSON node this enum was parsed from. pub fn source(&self) -> &'a Value { self.source } fn parse(node: &'a Value, path: &str) -> Result { let raw_values = node .get("values") .and_then(Value::as_array) .ok_or_else(|| { AlkTypeError::Schema(format!( "bast: enum at {path} has no `values` array" )) })?; if raw_values.is_empty() { return Err(AlkTypeError::Schema(format!( "bast: enum at {path} has an empty `values` array" ))); } let mut values = Vec::with_capacity(raw_values.len()); for (i, v) in raw_values.iter().enumerate() { let s = v.as_str().ok_or_else(|| { AlkTypeError::Schema(format!( "bast: enum at {path} values[{i}] is not a string" )) })?; values.push(s); } Ok(Self { values, source: node, }) } } /// A type reference — the central mechanism for typing fields, array /// elements, record values, and union variants. /// /// Four 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`]. #[derive(Debug, Clone)] pub enum BastType<'a> { /// A built-in primitive (`"uint32"`, `"string"`, etc.). Primitive(AlkTypeKind), /// A `$ref` to a named `$defs` entry: `{ "$ref": "#/$defs/Name" }`. Ref(BastRef<'a>), /// `kind: "array"` — a fixed-size array (count required in v1, /// D-BAST-004). Array(BastArray<'a>), /// `kind: "record"` — a string-keyed map. Record(BastRecord<'a>), /// An inline `kind: "struct"`. Struct(BastStruct<'a>), /// An inline `kind: "union"`. Union(BastUnion<'a>), /// An inline `kind: "enum"`. Enum(BastEnum<'a>), } impl<'a> BastType<'a> { /// The [`AlkTypeKind`] of this type. For [`Ref`](Self::Ref), this is /// [`AlkTypeKind::Struct`] — the meta-schema only allows /// struct/union/enum as named definitions, so a `$ref` always lands /// on one of those. Consumers that need the concrete kind should /// resolve the ref via [`BastDoc::resolve_typeref`] first. pub fn alk_kind(&self) -> AlkTypeKind { match self { BastType::Primitive(k) => *k, BastType::Ref(_) => AlkTypeKind::Struct, BastType::Array(_) => AlkTypeKind::Array, BastType::Record(_) => AlkTypeKind::Record, BastType::Struct(_) => AlkTypeKind::Struct, BastType::Union(_) => AlkTypeKind::Union, BastType::Enum(_) => AlkTypeKind::Enum, } } fn parse(node: &'a Value, path: &str) -> Result { if let Some(s) = node.as_str() { let k = AlkTypeKind::from_bast_str(s)?; return Ok(BastType::Primitive(k)); } let obj = node.as_object().ok_or_else(|| { AlkTypeError::Schema(format!( "bast: type at {path} is neither a primitive string nor an object: {node}" )) })?; if let Some(ref_path) = obj.get("$ref").and_then(Value::as_str) { let r = BastRef::parse(ref_path, path)?; return Ok(BastType::Ref(r)); } let kind_str = obj .get("kind") .and_then(Value::as_str) .ok_or_else(|| { AlkTypeError::Schema(format!( "bast: type at {path} has no `kind` and no `$ref`" )) })?; let k = AlkTypeKind::from_bast_str(kind_str)?; match k { AlkTypeKind::Array => Ok(BastType::Array(BastArray::parse(node, path)?)), AlkTypeKind::Record => Ok(BastType::Record(BastRecord::parse(node, path)?)), AlkTypeKind::Struct => Ok(BastType::Struct(BastStruct::parse(node, path)?)), AlkTypeKind::Union => Ok(BastType::Union(BastUnion::parse(node, path)?)), AlkTypeKind::Enum => Ok(BastType::Enum(BastEnum::parse(node, path)?)), other => Err(AlkTypeError::Schema(format!( "bast: type at {path} has inline kind {other}, which is only valid as a primitive string or a $defs entry" ))), } } fn from_def(def: BastDef<'a>) -> Self { match def.kind { BastDefKind::Struct(s) => BastType::Struct(s), BastDefKind::Union(u) => BastType::Union(u), BastDefKind::Enum(e) => BastType::Enum(e), } } } /// A `$ref` to a named `$defs` entry, restricted to `#/$defs/`. /// /// The restriction keeps resolution a single hash lookup /// ([`BastDoc::lookup_def`]); the v0.1.0 engine needed a normalization /// pass for TypeBox's bare-name refs, but BAST forbids them. #[derive(Debug, Clone)] pub struct BastRef<'a> { name: &'a str, } impl<'a> BastRef<'a> { /// The referenced `$defs` entry name (e.g. `Read` for /// `#/$defs/Read`). pub fn name(&self) -> &'a str { self.name } fn parse(ref_path: &'a str, path: &str) -> Result { let name = ref_path.strip_prefix(REF_PREFIX).ok_or_else(|| { AlkTypeError::Schema(format!( "bast: $ref at {path} is {ref_path:?}, expected \"#/$defs/\"" )) })?; if name.is_empty() { return Err(AlkTypeError::Schema(format!( "bast: $ref at {path} has an empty name after \"#/$defs/\"" ))); } Ok(Self { name }) } } /// `kind: "array"` — a fixed-size array. /// /// `count` is required in v1 (D-BAST-004, aligning with OQ-001 — arrays /// of variable-length elements without a count are deferred). The /// element type is itself a [`BastType`], so nested `$ref`s work: /// `{ "kind": "array", "element": { "$ref": "#/$defs/Point" }, "count": 4 }`. #[derive(Debug, Clone)] pub struct BastArray<'a> { element: Box>, count: usize, source: &'a Value, } impl<'a> BastArray<'a> { /// The element type. pub fn element(&self) -> &BastType<'a> { &self.element } /// The fixed element count. pub fn count(&self) -> usize { self.count } /// The raw JSON node this array type was parsed from. pub fn source(&self) -> &'a Value { self.source } fn parse(node: &'a Value, path: &str) -> Result { let element_node = node.get("element").ok_or_else(|| { AlkTypeError::Schema(format!( "bast: array at {path} has no `element`" )) })?; let element = BastType::parse(element_node, &format!("{path}.element"))?; let count = parse_usize_field(node, "count", path, "array count")?.ok_or_else(|| { AlkTypeError::Schema(format!( "bast: array at {path} has no `count` (variable-length arrays are not supported in v1, D-BAST-004)" )) })?; Ok(Self { element: Box::new(element), count, source: node, }) } } /// `kind: "record"` — a string-keyed map. /// /// Binary layout: a count-prefixed sequence of `(key, value)` pairs — /// `[count: u32][key_len: u32][key_bytes][value]...` repeated `count` /// times (per [bast-format.md §Record](../docs/architecture/bast-format.md#record)). #[derive(Debug, Clone)] pub struct BastRecord<'a> { values: Box>, source: &'a Value, } impl<'a> BastRecord<'a> { /// The value type. pub fn values(&self) -> &BastType<'a> { &self.values } /// The raw JSON node this record type was parsed from. pub fn source(&self) -> &'a Value { self.source } fn parse(node: &'a Value, path: &str) -> Result { let values_node = node.get("values").ok_or_else(|| { AlkTypeError::Schema(format!( "bast: record at {path} has no `values`" )) })?; let values = BastType::parse(values_node, &format!("{path}.values"))?; Ok(Self { values: Box::new(values), source: node, }) } } // --------------------------------------------------------------------------- // Shared annotation parsers (BAST-property form). // // These read type-level/field-level properties (`endian`, `align`, // `encoding`, `maxLength`) directly off a BAST node. Their *semantics* // are unchanged (ADR-003); only the input location moved from the // v0.1.0 keyword-value objects to BAST type-level properties. They're // `fn`s rather than methods on the typed nodes so the layout engines // can call them on raw `&Value` during the step 4 migration. // --------------------------------------------------------------------------- fn parse_endian_opt(node: &Value) -> Option { match node.as_object().and_then(|o| o.get("endian")).and_then(Value::as_str) { Some("big") => Some(Endian::Big), Some("little") => Some(Endian::Little), _ => None, } } fn parse_align(node: &Value) -> Option { node.as_object() .and_then(|o| o.get("align")) .and_then(Value::as_u64) .and_then(|n| usize::try_from(n).ok()) } fn parse_max_length(node: &Value) -> Option { node.as_object() .and_then(|o| o.get("maxLength")) .and_then(Value::as_u64) .and_then(|n| usize::try_from(n).ok()) } fn parse_encoding(node: &Value) -> VariableEncoding { match node.as_object().and_then(|o| o.get("encoding")).and_then(Value::as_str) { Some("offset-indirect") => VariableEncoding::OffsetIndirect, _ => VariableEncoding::LengthPrefixed, } } fn parse_usize_field( node: &Value, key: &str, path: &str, label: &str, ) -> Result, AlkTypeError> { let obj = match node.as_object() { Some(o) => o, None => return Ok(None), }; match obj.get(key) { None => Ok(None), Some(Value::Null) => Ok(None), Some(v) => { let n = v.as_u64().ok_or_else(|| { AlkTypeError::Schema(format!( "bast: {label} at {path} is not a non-negative integer" )) })?; let n = usize::try_from(n).map_err(|_| { AlkTypeError::Schema(format!( "bast: {label} at {path} (= {n}) overflows usize" )) })?; Ok(Some(n)) } } } impl<'a> std::fmt::Display for BastType<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { BastType::Primitive(k) => write!(f, "{}", k.to_bast_str()), BastType::Ref(r) => write!(f, "$ref:#/$defs/({})", r.name()), BastType::Array(a) => write!(f, "array<{}>[{}]", a.element, a.count), BastType::Record(r) => write!(f, "record<{}>", r.values()), BastType::Struct(_) => write!(f, "struct"), BastType::Union(_) => write!(f, "union"), BastType::Enum(_) => write!(f, "enum"), } } } #[cfg(test)] mod tests { use super::*; use serde_json::json; fn doc_from<'a>(root: &'a Value, root_name: &'a str) -> BastDoc<'a> { BastDoc::new(root, root_name).expect("bast doc") } // ----- BastDoc construction --------------------------------------- #[test] fn new_parses_simple_struct_root() { let root = json!({ "$defs": { "ChunkHeader": { "kind": "struct", "endian": "big", "fields": [ { "name": "channel_id", "kind": "uint32" }, { "name": "length", "kind": "uint32" } ] } } }); let d = BastDoc::new(&root, "ChunkHeader").expect("doc"); assert_eq!(d.root_name(), "ChunkHeader"); let def = d.root_def(); assert!(matches!(def.kind(), BastDefKind::Struct(_))); let s = match def.kind() { BastDefKind::Struct(s) => s, _ => unreachable!(), }; assert_eq!(s.endian(), Endian::Big); assert_eq!(s.fields().len(), 2); assert_eq!(s.fields()[0].name(), "channel_id"); assert!(matches!(s.fields()[0].ty(), BastType::Primitive(AlkTypeKind::Uint32))); } #[test] fn missing_defs_is_schema_error() { let root = json!({ "type": "object" }); let err = BastDoc::new(&root, "X").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn missing_root_def_is_schema_error() { let root = json!({ "$defs": { "Other": { "kind": "struct", "fields": [] } } }); let err = BastDoc::new(&root, "Missing").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn malformed_kind_is_schema_error_not_panic() { let root = json!({ "$defs": { "S": { "kind": "not-a-real-kind" } } }); let err = BastDoc::new(&root, "S").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn top_level_primitive_kind_is_error() { let root = json!({ "$defs": { "S": { "kind": "uint32" } } }); let err = BastDoc::new(&root, "S").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn struct_missing_fields_is_error() { let root = json!({ "$defs": { "S": { "kind": "struct" } } }); let err = BastDoc::new(&root, "S").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn field_missing_name_is_error() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "kind": "uint32" } ] } } }); let err = BastDoc::new(&root, "S").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn field_missing_kind_is_error() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "id" } ] } } }); let err = BastDoc::new(&root, "S").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } // ----- Endianness / annotations ----------------------------------- #[test] fn struct_endian_defaults_to_little() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [] } } }); let d = doc_from(&root, "S"); let s = match d.root_def().kind() { BastDefKind::Struct(s) => s, _ => unreachable!(), }; assert_eq!(s.endian(), Endian::Little); assert_eq!(s.align(), None); } #[test] fn struct_align_parsed() { let root = json!({ "$defs": { "S": { "kind": "struct", "align": 256, "fields": [] } } }); let d = doc_from(&root, "S"); let s = match d.root_def().kind() { BastDefKind::Struct(s) => s, _ => unreachable!(), }; assert_eq!(s.align(), Some(256)); } #[test] fn field_endian_override_and_encoding() { let root = json!({ "$defs": { "S": { "kind": "struct", "endian": "big", "fields": [ { "name": "blob", "kind": "bytes", "encoding": "offset-indirect", "maxLength": 256, "endian": "little" } ] } } }); let d = doc_from(&root, "S"); let s = match d.root_def().kind() { BastDefKind::Struct(s) => s, _ => unreachable!(), }; let f = &s.fields()[0]; assert_eq!(f.name(), "blob"); assert_eq!(f.endian(), Some(Endian::Little)); assert_eq!(f.encoding(), VariableEncoding::OffsetIndirect); assert_eq!(f.max_length(), Some(256)); assert_eq!(f.effective_endian(Endian::Big), Endian::Little); } #[test] fn field_encoding_defaults_to_length_prefixed() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "s", "kind": "string" } ] } } }); let d = doc_from(&root, "S"); let s = match d.root_def().kind() { BastDefKind::Struct(s) => s, _ => unreachable!(), }; assert_eq!(s.fields()[0].encoding(), VariableEncoding::LengthPrefixed); } // ----- TypeRef: primitives, $ref, array, record ------------------- #[test] fn typeref_primitive_round_trips() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "a", "kind": "int8" }, { "name": "b", "kind": "uint64" }, { "name": "c", "kind": "bool" }, { "name": "d", "kind": "timestamp" } ] } } }); let d = doc_from(&root, "S"); let s = match d.root_def().kind() { BastDefKind::Struct(s) => s, _ => unreachable!(), }; assert!(matches!(s.fields()[0].ty(), BastType::Primitive(AlkTypeKind::Int8))); assert!(matches!(s.fields()[1].ty(), BastType::Primitive(AlkTypeKind::Uint64))); assert!(matches!(s.fields()[2].ty(), BastType::Primitive(AlkTypeKind::Boolean))); assert!(matches!(s.fields()[3].ty(), BastType::Primitive(AlkTypeKind::Timestamp))); } #[test] fn typeref_ref_parses_name() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "status", "kind": { "$ref": "#/$defs/StatusCode" } } ] }, "StatusCode": { "kind": "enum", "values": ["Ok", "Error"] } } }); let d = doc_from(&root, "S"); let s = match d.root_def().kind() { BastDefKind::Struct(s) => s, _ => unreachable!(), }; match s.fields()[0].ty() { BastType::Ref(r) => assert_eq!(r.name(), "StatusCode"), other => panic!("expected Ref, got {other:?}"), } } #[test] fn typeref_ref_malformed_is_error() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "x", "kind": { "$ref": "not-a-pointer" } } ] } } }); let err = BastDoc::new(&root, "S").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn typeref_ref_bare_name_is_error() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "x", "kind": { "$ref": "Read" } } ] } } }); let err = BastDoc::new(&root, "S").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn typeref_array_parses_element_and_count() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "pts", "kind": { "kind": "array", "element": "float32", "count": 3 } } ] } } }); let d = doc_from(&root, "S"); let s = match d.root_def().kind() { BastDefKind::Struct(s) => s, _ => unreachable!(), }; match s.fields()[0].ty() { BastType::Array(a) => { assert_eq!(a.count(), 3); assert!(matches!(a.element(), BastType::Primitive(AlkTypeKind::Float32))); } other => panic!("expected Array, got {other:?}"), } } #[test] fn typeref_array_missing_count_is_error() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "pts", "kind": { "kind": "array", "element": "float32" } } ] } } }); let err = BastDoc::new(&root, "S").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn typeref_array_with_ref_element() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "pts", "kind": { "kind": "array", "element": { "$ref": "#/$defs/Point" }, "count": 4 } } ] }, "Point": { "kind": "struct", "fields": [ { "name": "x", "kind": "uint16" }, { "name": "y", "kind": "uint16" } ] } } }); let d = doc_from(&root, "S"); let s = match d.root_def().kind() { BastDefKind::Struct(s) => s, _ => unreachable!(), }; match s.fields()[0].ty() { BastType::Array(a) => { assert_eq!(a.count(), 4); assert!(matches!(a.element(), BastType::Ref(r) if r.name() == "Point")); } other => panic!("expected Array, got {other:?}"), } } #[test] fn typeref_record_parses_values() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "counts", "kind": { "kind": "record", "values": "uint32" } } ] } } }); let d = doc_from(&root, "S"); let s = match d.root_def().kind() { BastDefKind::Struct(s) => s, _ => unreachable!(), }; match s.fields()[0].ty() { BastType::Record(r) => { assert!(matches!(r.values(), BastType::Primitive(AlkTypeKind::Uint32))); } other => panic!("expected Record, got {other:?}"), } } #[test] fn typeref_record_missing_values_is_error() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "counts", "kind": { "kind": "record" } } ] } } }); let err = BastDoc::new(&root, "S").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } // ----- $ref resolution -------------------------------------------- #[test] fn resolve_ref_returns_named_def() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "status", "kind": { "$ref": "#/$defs/StatusCode" } } ] }, "StatusCode": { "kind": "enum", "values": ["Ok", "Error"] } } }); let d = doc_from(&root, "S"); let s = match d.root_def().kind() { BastDefKind::Struct(s) => s, _ => unreachable!(), }; let r = match s.fields()[0].ty() { BastType::Ref(r) => r, _ => unreachable!(), }; let resolved = d.resolve_ref(r).expect("resolve"); match resolved.kind() { BastDefKind::Enum(e) => assert_eq!(e.values(), &["Ok", "Error"]), other => panic!("expected Enum, got {other:?}"), } } #[test] fn resolve_ref_unknown_target_is_error() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "x", "kind": { "$ref": "#/$defs/Ghost" } } ] } } }); let d = doc_from(&root, "S"); let s = match d.root_def().kind() { BastDefKind::Struct(s) => s, _ => unreachable!(), }; let r = match s.fields()[0].ty() { BastType::Ref(r) => r, _ => unreachable!(), }; let err = d.resolve_ref(r).unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn resolve_typeref_derefs_ref_one_level() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "status", "kind": { "$ref": "#/$defs/StatusCode" } } ] }, "StatusCode": { "kind": "enum", "values": ["Ok"] } } }); let d = doc_from(&root, "S"); let s = match d.root_def().kind() { BastDefKind::Struct(s) => s, _ => unreachable!(), }; let resolved = d.resolve_typeref(s.fields()[0].ty()).expect("resolve"); assert!(matches!(resolved, BastType::Enum(_))); } #[test] fn resolve_typeref_passthrough_for_inline_types() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "id", "kind": "uint32" } ] } } }); let d = doc_from(&root, "S"); let s = match d.root_def().kind() { BastDefKind::Struct(s) => s, _ => unreachable!(), }; let resolved = d.resolve_typeref(s.fields()[0].ty()).expect("resolve"); assert!(matches!(resolved, BastType::Primitive(AlkTypeKind::Uint32))); } #[test] fn lookup_def_returns_raw_node() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [] }, "Other": { "kind": "enum", "values": ["X"] } } }); let d = doc_from(&root, "S"); let raw = d.lookup_def("Other").expect("lookup"); assert_eq!(raw["kind"], "enum"); } // ----- Enum -------------------------------------------------------- #[test] fn enum_parses_values_in_order() { let root = json!({ "$defs": { "E": { "kind": "enum", "values": ["Ok", "Error", "Pending"] } } }); let d = doc_from(&root, "E"); let e = match d.root_def().kind() { BastDefKind::Enum(e) => e, _ => unreachable!(), }; assert_eq!(e.values(), &["Ok", "Error", "Pending"]); } #[test] fn enum_empty_values_is_error() { let root = json!({ "$defs": { "E": { "kind": "enum", "values": [] } } }); let err = BastDoc::new(&root, "E").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn enum_missing_values_is_error() { let root = json!({ "$defs": { "E": { "kind": "enum" } } }); let err = BastDoc::new(&root, "E").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn enum_non_string_value_is_error() { let root = json!({ "$defs": { "E": { "kind": "enum", "values": ["Ok", 5] } } }); let err = BastDoc::new(&root, "E").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } // ----- Union: byte discriminator ----------------------------------- #[test] fn union_byte_disc_parses() { let root = json!({ "$defs": { "U": { "kind": "union", "endian": "big", "discriminator": { "kind": "byte", "offset": 0, "type": "uint8" }, "mapping": { "1": { "$ref": "#/$defs/Init" }, "3": { "$ref": "#/$defs/Open" } } }, "Init": { "kind": "struct", "fields": [] }, "Open": { "kind": "struct", "fields": [] } } }); let d = doc_from(&root, "U"); let u = match d.root_def().kind() { BastDefKind::Union(u) => u, _ => unreachable!(), }; assert_eq!(u.endian(), Endian::Big); assert!(matches!(u.discriminator(), BastDiscriminator::Byte { offset: 0, disc_type: AlkTypeKind::Uint8 })); assert_eq!(u.fields().len(), 0); assert_eq!(u.mapping().len(), 2); assert!(u.variant_for("1").is_some()); assert!(u.variant_for("99").is_none()); } #[test] fn union_byte_disc_uint16_type() { let root = json!({ "$defs": { "U": { "kind": "union", "discriminator": { "kind": "byte", "offset": 4, "type": "uint16" }, "mapping": { "10": { "$ref": "#/$defs/V" } } }, "V": { "kind": "struct", "fields": [] } } }); let d = doc_from(&root, "U"); let u = match d.root_def().kind() { BastDefKind::Union(u) => u, _ => unreachable!(), }; assert!(matches!(u.discriminator(), BastDiscriminator::Byte { offset: 4, disc_type: AlkTypeKind::Uint16 })); } #[test] fn union_byte_disc_invalid_type_is_error() { let root = json!({ "$defs": { "U": { "kind": "union", "discriminator": { "kind": "byte", "type": "float32" }, "mapping": { "1": { "$ref": "#/$defs/V" } } }, "V": { "kind": "struct", "fields": [] } } }); let err = BastDoc::new(&root, "U").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn union_byte_disc_missing_type_is_error() { let root = json!({ "$defs": { "U": { "kind": "union", "discriminator": { "kind": "byte", "offset": 0 }, "mapping": { "1": { "$ref": "#/$defs/V" } } }, "V": { "kind": "struct", "fields": [] } } }); let err = BastDoc::new(&root, "U").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn union_missing_discriminator_is_error() { let root = json!({ "$defs": { "U": { "kind": "union", "mapping": { "1": { "$ref": "#/$defs/V" } } }, "V": { "kind": "struct", "fields": [] } } }); let err = BastDoc::new(&root, "U").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn union_missing_mapping_is_error() { let root = json!({ "$defs": { "U": { "kind": "union", "discriminator": { "kind": "byte", "offset": 0, "type": "uint8" } } } }); let err = BastDoc::new(&root, "U").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn union_empty_mapping_is_error() { let root = json!({ "$defs": { "U": { "kind": "union", "discriminator": { "kind": "byte", "offset": 0, "type": "uint8" }, "mapping": {} } } }); let err = BastDoc::new(&root, "U").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn union_unknown_disc_kind_is_error() { let root = json!({ "$defs": { "U": { "kind": "union", "discriminator": { "kind": "magic" }, "mapping": { "1": { "$ref": "#/$defs/V" } } }, "V": { "kind": "struct", "fields": [] } } }); let err = BastDoc::new(&root, "U").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } // ----- Union: field-name discriminator (D-BAST-005) --------------- #[test] fn union_field_disc_parses_with_fields() { let root = json!({ "$defs": { "Event": { "kind": "union", "discriminator": { "kind": "field", "name": "type" }, "fields": [ { "name": "type", "kind": "string" } ], "mapping": { "data": { "$ref": "#/$defs/Data" } } }, "Data": { "kind": "struct", "fields": [] } } }); let d = doc_from(&root, "Event"); let u = match d.root_def().kind() { BastDefKind::Union(u) => u, _ => unreachable!(), }; assert!(matches!(u.discriminator(), BastDiscriminator::Field { name } if *name == "type")); assert_eq!(u.fields().len(), 1); assert_eq!(u.fields()[0].name(), "type"); assert!(matches!(u.variant_for("data"), Some(BastType::Ref(_)))); } #[test] fn union_field_disc_missing_fields_is_error() { let root = json!({ "$defs": { "U": { "kind": "union", "discriminator": { "kind": "field", "name": "type" }, "mapping": { "data": { "$ref": "#/$defs/V" } } }, "V": { "kind": "struct", "fields": [] } } }); let err = BastDoc::new(&root, "U").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn union_field_disc_missing_name_is_error() { let root = json!({ "$defs": { "U": { "kind": "union", "discriminator": { "kind": "field" }, "mapping": { "data": { "$ref": "#/$defs/V" } } }, "V": { "kind": "struct", "fields": [] } } }); let err = BastDoc::new(&root, "U").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn union_byte_disc_with_fields_is_error() { let root = json!({ "$defs": { "U": { "kind": "union", "discriminator": { "kind": "byte", "offset": 0, "type": "uint8" }, "fields": [ { "name": "type", "kind": "string" } ], "mapping": { "1": { "$ref": "#/$defs/V" } } }, "V": { "kind": "struct", "fields": [] } } }); let err = BastDoc::new(&root, "U").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } // ----- Nested composition ------------------------------------------ #[test] fn nested_struct_with_ref_field() { let root = json!({ "$defs": { "Outer": { "kind": "struct", "fields": [ { "name": "magic", "kind": "uint32" }, { "name": "body", "kind": { "$ref": "#/$defs/Inner" } } ] }, "Inner": { "kind": "struct", "fields": [ { "name": "count", "kind": "uint8" } ] } } }); let d = doc_from(&root, "Outer"); let s = match d.root_def().kind() { BastDefKind::Struct(s) => s, _ => unreachable!(), }; assert!(matches!(s.fields()[0].ty(), BastType::Primitive(AlkTypeKind::Uint32))); assert!(matches!(s.fields()[1].ty(), BastType::Ref(r) if r.name() == "Inner")); } #[test] fn inline_struct_field_parses() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "inner", "kind": { "kind": "struct", "fields": [ { "name": "x", "kind": "uint16" } ] } } ] } } }); let d = doc_from(&root, "S"); let s = match d.root_def().kind() { BastDefKind::Struct(s) => s, _ => unreachable!(), }; match s.fields()[0].ty() { BastType::Struct(inner) => assert_eq!(inner.fields().len(), 1), other => panic!("expected inline Struct, got {other:?}"), } } // ----- Display ----------------------------------------------------- #[test] fn bast_type_display_formats() { let prim = BastType::Primitive(AlkTypeKind::Uint32); assert_eq!(format!("{prim}"), "uint32"); let arr = BastType::Array(BastArray { element: Box::new(BastType::Primitive(AlkTypeKind::Float32)), count: 3, source: &Value::Null, }); assert_eq!(format!("{arr}"), "array[3]"); } // ----- Overflow-safe usize parsing --------------------------------- #[test] fn array_count_non_integer_is_error() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "a", "kind": { "kind": "array", "element": "uint8", "count": "three" } } ] } } }); let err = BastDoc::new(&root, "S").unwrap_err(); assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } #[test] fn array_count_large_u64_parses_on_64bit() { let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [ { "name": "a", "kind": { "kind": "array", "element": "uint8", "count": u64::MAX } } ] } } }); let d = BastDoc::new(&root, "S").expect("parses on usize>=u64 targets"); let s = match d.root_def().kind() { BastDefKind::Struct(s) => s, _ => unreachable!(), }; match s.fields()[0].ty() { BastType::Array(a) => assert_eq!(a.count(), usize::try_from(u64::MAX).unwrap()), other => panic!("expected Array, got {other:?}"), } } }