//! Fluent Rust builder for constructing BAST documents and standard JSON //! Schemas at runtime. //! //! Produces `serde_json::Value` — the same form [`crate::AlkTypeEngine`] //! consumes. One builder, two output formats (D-BAST-008): //! //! - **BAST JSON** (binary layout) — `Schema::struct_().field(...).build()` //! produces a BAST TypeDef (`{ "kind": "struct", "fields": [...] }`). //! Primitive constructors produce bare TypeRef strings (`"uint32"`). //! Feed to [`crate::AlkTypeEngine::compile`]. //! - **Standard JSON Schema** (JSON validation) — `Schema::object().field(...)` //! produces `{ "type": "object", "properties": {...}, "required": [...] }`. //! No BAST `kind`, no `AlkType:*` keywords. Feed to a standard //! `jsonschema::Validator` (or [`crate::AlkTypeEngine::compile`] with a //! JSON Schema for the `validate_json` path). //! //! The construction API distinguishes BAST kinds from standard JSON Schema //! types via naming conventions (`string()` is the BAST primitive, //! `string_()` is the standard JSON Schema type). Decided in ADR-009; //! BAST output format per D-BAST-008. //! //! The builder is a thin construction layer over the JSON form. There is //! no typed `Schema` enum; `.build()` returns a `Value`. Setters return //! `Self` for chaining. Field order is load-bearing for binary layouts //! (the `fields` array is ordered — no reliance on `serde_json`'s //! `preserve_order` for correctness, per BAST design principle #4). use crate::schema::{AlkTypeKind, Endian, VariableEncoding}; use serde_json::{json, Map, Value}; const REF_PREFIX: &str = "#/$defs/"; /// A schema under construction. One constructor per BAST kind or standard /// JSON Schema type; setters for annotations and constraints; `.build()` /// produces the final [`Value`]. /// /// See `docs/architecture/builder.md` for the full API spec. /// /// # Two output formats /// /// The builder internally tracks which representation a schema is building. /// BAST primitives produce bare TypeRef strings on `.build()`; BAST /// composites (struct/union/enum) and standard JSON Schema types produce /// JSON objects. Annotations ([`endian`](Self::endian), /// [`align`](Self::align), [`encoding`](Self::encoding), /// [`max_length`](Self::max_length)) are stored on the `Schema` and /// placed correctly by `.build()` (struct-level) or /// [`field`](Self::field) (field-level). #[derive(Debug, Clone)] pub struct Schema { repr: Repr, endian: Option, align: Option, encoding: Option, max_length: Option, } /// The internal representation being built. Distinguishes BAST /// primitives (bare strings), BAST composites (objects with `kind`), /// `$ref`s, and standard JSON Schema objects. #[derive(Debug, Clone)] enum Repr { /// A BAST primitive — `.build()` produces the bare string `"uint32"`. Primitive(AlkTypeKind), /// A `$ref` to a named `$defs` entry. Ref(String), /// A BAST struct type (`kind: "struct"`). Struct(StructBuilder), /// A BAST union type (`kind: "union"`). Union(UnionBuilder), /// A BAST enum type (`kind: "enum"`). Enum(Vec), /// A BAST array type (`kind: "array"`). `count` is required for /// valid BAST (D-BAST-004); set via [`.count()`](Schema::count). Array { element: Box, count: Option, }, /// A BAST record type (`kind: "record"`). Record { values: Box, }, /// A standard JSON Schema object (from `object()`, `string_()`, etc.) /// or an object adopted via [`from_value`](Schema::from_value). Standard(Map), /// A non-object value adopted via [`from_value`](Schema::from_value). /// Built as-is. Used when adopting a bare TypeRef string or any /// non-object JSON value for composition. Raw(Value), } /// Internal builder state for a BAST struct. #[derive(Debug, Clone, Default)] struct StructBuilder { fields: Vec, } /// Internal builder state for a BAST union. #[derive(Debug, Clone)] struct UnionBuilder { discriminator: Discriminator, fields: Vec, mapping: Vec<(String, Schema)>, } /// A field under construction within a struct, union, or standard JSON /// Schema object. Stores the field name, the field's type [`Schema`], /// and field-level annotations extracted from that schema by /// [`Schema::build`] / [`Schema::field`]. #[derive(Debug, Clone)] struct FieldBuilder { name: String, schema: Schema, } impl Schema { // ----------------------------------------------------------------- // BAST primitive constructors // ----------------------------------------------------------------- /// BAST `int8`. pub fn int8() -> Self { Self::primitive(AlkTypeKind::Int8) } /// BAST `int16`. pub fn int16() -> Self { Self::primitive(AlkTypeKind::Int16) } /// BAST `int32`. pub fn int32() -> Self { Self::primitive(AlkTypeKind::Int32) } /// BAST `int64`. pub fn int64() -> Self { Self::primitive(AlkTypeKind::Int64) } /// BAST `uint8`. pub fn uint8() -> Self { Self::primitive(AlkTypeKind::Uint8) } /// BAST `uint16`. pub fn uint16() -> Self { Self::primitive(AlkTypeKind::Uint16) } /// BAST `uint32`. pub fn uint32() -> Self { Self::primitive(AlkTypeKind::Uint32) } /// BAST `uint64`. pub fn uint64() -> Self { Self::primitive(AlkTypeKind::Uint64) } /// BAST `float32`. pub fn float32() -> Self { Self::primitive(AlkTypeKind::Float32) } /// BAST `float64`. pub fn float64() -> Self { Self::primitive(AlkTypeKind::Float64) } /// BAST `bool`. pub fn boolean() -> Self { Self::primitive(AlkTypeKind::Boolean) } /// BAST `string` (length-prefixed UTF-8). pub fn string() -> Self { Self::primitive(AlkTypeKind::String) } /// BAST `bytes` (length-prefixed raw bytes). pub fn bytes() -> Self { Self::primitive(AlkTypeKind::Bytes) } /// BAST `timestamp` (length-prefixed RFC 3339 string). pub fn timestamp() -> Self { Self::primitive(AlkTypeKind::Timestamp) } fn primitive(kind: AlkTypeKind) -> Self { Self { repr: Repr::Primitive(kind), endian: None, align: None, encoding: None, max_length: None, } } // ----------------------------------------------------------------- // BAST composite constructors // ----------------------------------------------------------------- /// BAST `struct` (record of fields; add via `.field()`). pub fn struct_() -> Self { Self { repr: Repr::Struct(StructBuilder::default()), endian: None, align: None, encoding: None, max_length: None, } } /// BAST `array` (fixed-size; set count via `.count()`). The element /// type is set at construction time. `count` is required for valid /// BAST (D-BAST-004) — without it, the built value will be rejected /// at `compile` time. pub fn array_of(element: Schema) -> Self { Self { repr: Repr::Array { element: Box::new(element), count: None, }, endian: None, align: None, encoding: None, max_length: None, } } /// BAST `record` (string-keyed map of values). pub fn record_of(value: Schema) -> Self { Self { repr: Repr::Record { values: Box::new(value), }, endian: None, align: None, encoding: None, max_length: None, } } /// BAST `union` (tagged union; add variants via `.mapping()`). /// For field-name discriminators, add the discriminator field and /// any shared fields via `.field()`. pub fn union_(disc: Discriminator) -> Self { Self { repr: Repr::Union(UnionBuilder { discriminator: disc, fields: Vec::new(), mapping: Vec::new(), }), endian: None, align: None, encoding: None, max_length: None, } } /// BAST `enum` with the provided values in declaration order. /// The binary representation is a `u32` index into this list. pub fn enum_of(values: &[&str]) -> Self { Self { repr: Repr::Enum(values.iter().map(|v| (*v).to_string()).collect()), endian: None, align: None, encoding: None, max_length: None, } } // ----------------------------------------------------------------- // Standard JSON Schema type constructors (no BAST `kind`) // ----------------------------------------------------------------- /// Standard `type: "object"`. Fields via `.field()`, required via /// `.required()`. pub fn object() -> Self { Self::with_type("object") } /// Standard `type: "array"`. Items via `.items()`. pub fn array() -> Self { Self::with_type("array") } /// Standard `type: "string"`. pub fn string_() -> Self { Self::with_type("string") } /// Standard `type: "integer"`. pub fn integer() -> Self { Self::with_type("integer") } /// Standard `type: "number"`. pub fn number() -> Self { Self::with_type("number") } /// Standard `type: "boolean"`. pub fn boolean_() -> Self { Self::with_type("boolean") } /// Standard `type: "null"`. pub fn null() -> Self { Self::with_type("null") } /// No type constraint (the `{}` form). pub fn any() -> Self { Self { repr: Repr::Standard(Map::new()), endian: None, align: None, encoding: None, max_length: None, } } fn with_type(type_str: &str) -> Self { let mut object = Map::new(); object.insert("type".to_string(), Value::String(type_str.to_string())); Self { repr: Repr::Standard(object), endian: None, align: None, encoding: None, max_length: None, } } /// Adopt an existing JSON `Value` as a `Schema` for composition. /// Object values are adopted as-is; non-object values (e.g. a bare /// BAST primitive string) are wrapped and built as-is. pub fn from_value(value: Value) -> Self { let repr = match value { Value::Object(map) => Repr::Standard(map), other => Repr::Raw(other), }; Self { repr, endian: None, align: None, encoding: None, max_length: None, } } /// Produce a `{"$ref": "#/$defs/"}` schema. pub fn ref_def(name: &str) -> Self { Self { repr: Repr::Ref(name.to_string()), endian: None, align: None, encoding: None, max_length: None, } } // ----------------------------------------------------------------- // Annotation setters (ADR-003, BAST type-level/field-level properties) // ----------------------------------------------------------------- /// Endianness. On a struct/union, sets the type-level default. On a /// field's type, sets a field-level override (extracted by /// `.field()`). Default little. pub fn endian(mut self, endian: Endian) -> Self { self.endian = Some(endian); self } /// Alignment. On a struct, sets the struct-level default. On a /// field's type, sets a field-level override. Only meaningful in /// aligned static mode (ADR-002/003). pub fn align(mut self, align: usize) -> Self { self.align = Some(align); self } /// Variable-length encoding strategy. Set on a field's type; /// extracted by `.field()` and emitted as a field-level property. /// Default `length-prefixed` (emitted only when explicitly set). pub fn encoding(mut self, encoding: VariableEncoding) -> Self { self.encoding = Some(encoding); self } /// `maxLength` — byte-length cap. On a BAST field's type, extracted /// by `.field()` as a field-level constraint. On a standard JSON /// Schema type, emitted as the standard `maxLength` keyword. /// In aligned mode with a variable-length type, reserves this many /// bytes (strategy 2). In packed mode, validation constraint only. pub fn max_length(mut self, max: usize) -> Self { match &mut self.repr { Repr::Standard(map) => { map.insert("maxLength".to_string(), Value::from(max as u64)); } _ => { self.max_length = Some(max); } } self } /// Set the fixed element count for a BAST array. Required for valid /// BAST (D-BAST-004). No-op on non-array schemas. pub fn count(mut self, count: usize) -> Self { if let Repr::Array { count: c, .. } = &mut self.repr { *c = Some(count); } self } // ----------------------------------------------------------------- // Composite builders // ----------------------------------------------------------------- /// Add a field to a struct, union (field-name discriminator), or /// standard JSON Schema object. /// /// **BAST struct/union:** the field is appended to the ordered /// `fields` array as `{ "name": ..., "kind": , ...annotations }`. /// Field-level annotations (`.endian()`, `.align()`, `.encoding()`, /// `.max_length()` on the field's type) are extracted and emitted /// as field properties. Field order is load-bearing (packed mode /// field order = byte order). /// /// **Standard JSON Schema object:** the field is inserted into /// `properties[name] = field.build()` (standard JSON Schema form). pub fn field(mut self, name: &str, field: Schema) -> Self { match &mut self.repr { Repr::Struct(s) => { s.fields.push(FieldBuilder { name: name.to_string(), schema: field, }); } Repr::Union(u) => { u.fields.push(FieldBuilder { name: name.to_string(), schema: field, }); } Repr::Standard(map) => { let props = map .entry("properties".to_string()) .or_insert_with(|| Value::Object(Map::new())) .as_object_mut(); if let Some(props) = props { props.insert(name.to_string(), field.build()); } } _ => {} } self } /// Mark fields as required (standard JSON Schema `required` keyword). /// Can be called multiple times; required names accumulate. /// Only applies to standard JSON Schema objects; no-op on BAST types /// (BAST structs have all fields present by construction). pub fn required(mut self, names: &[&str]) -> Self { if let Repr::Standard(map) = &mut self.repr { let entry = map .entry("required".to_string()) .or_insert_with(|| Value::Array(Vec::new())); if let Some(arr) = entry.as_array_mut() { for n in names { arr.push(Value::String((*n).to_string())); } } } self } /// Set the items schema for a standard `array` type. No-op on /// non-standard-array schemas. pub fn items(mut self, item: Schema) -> Self { if let Repr::Standard(map) = &mut self.repr { map.insert("items".to_string(), item.build()); } self } /// Set `additionalProperties` for a standard `object` type. No-op on /// non-standard-object schemas. pub fn additional_properties(mut self, props: Schema) -> Self { if let Repr::Standard(map) = &mut self.repr { map.insert("additionalProperties".to_string(), props.build()); } self } /// Add a variant to a BAST union. `disc_value` is the stringified /// discriminator value (mapping key). The variant schema is built /// into a TypeRef. No-op on non-union schemas. pub fn mapping(mut self, disc_value: &str, variant: Schema) -> Self { if let Repr::Union(u) = &mut self.repr { u.mapping.push((disc_value.to_string(), variant)); } self } // ----------------------------------------------------------------- // Constraint setters (standard JSON Schema) // ----------------------------------------------------------------- /// `minimum` (inclusive lower bound for numbers/integers). /// Standard JSON Schema keyword; only applies to standard types. pub fn minimum(mut self, min: f64) -> Self { if let Repr::Standard(map) = &mut self.repr { if let Some(n) = serde_json::Number::from_f64(min) { map.insert("minimum".to_string(), Value::Number(n)); } } self } /// `maximum` (inclusive upper bound for numbers/integers). /// Standard JSON Schema keyword; only applies to standard types. pub fn maximum(mut self, max: f64) -> Self { if let Repr::Standard(map) = &mut self.repr { if let Some(n) = serde_json::Number::from_f64(max) { map.insert("maximum".to_string(), Value::Number(n)); } } self } /// `minLength` (minimum string length). Standard JSON Schema keyword. pub fn min_length(mut self, min: usize) -> Self { if let Repr::Standard(map) = &mut self.repr { map.insert("minLength".to_string(), Value::from(min as u64)); } self } /// `minItems` (minimum array length). Standard JSON Schema keyword. pub fn min_items(mut self, min: usize) -> Self { if let Repr::Standard(map) = &mut self.repr { map.insert("minItems".to_string(), Value::from(min as u64)); } self } /// `maxItems` (maximum array length). Standard JSON Schema keyword. pub fn max_items(mut self, max: usize) -> Self { if let Repr::Standard(map) = &mut self.repr { map.insert("maxItems".to_string(), Value::from(max as u64)); } self } /// `format` (e.g. "date-time", "uri", "email"). Standard JSON Schema /// keyword. pub fn format(mut self, fmt: &str) -> Self { if let Repr::Standard(map) = &mut self.repr { map.insert("format".to_string(), Value::String(fmt.to_string())); } self } /// `title` (human-readable title). Standard JSON Schema keyword. pub fn title(mut self, t: &str) -> Self { if let Repr::Standard(map) = &mut self.repr { map.insert("title".to_string(), Value::String(t.to_string())); } self } /// `description` (human-readable description). Standard JSON Schema /// keyword. pub fn description(mut self, d: &str) -> Self { if let Repr::Standard(map) = &mut self.repr { map.insert("description".to_string(), Value::String(d.to_string())); } self } // ----------------------------------------------------------------- // Build // ----------------------------------------------------------------- /// Produce the final `serde_json::Value`. /// /// For BAST primitives, this is a bare string (`"uint32"`). For /// BAST composites and standard JSON Schema types, this is a JSON /// object. For `$ref`s, this is `{ "$ref": "#/$defs/" }`. pub fn build(self) -> Value { let Schema { repr, endian, align, encoding: _, max_length: _, } = self; match &repr { Repr::Primitive(k) => Value::String(k.to_bast_str().to_string()), Repr::Ref(name) => { json!({ "$ref": format!("{REF_PREFIX}{name}") }) } Repr::Struct(s) => { let mut m = Map::new(); m.insert("kind".to_string(), Value::String("struct".to_string())); if let Some(e) = endian { m.insert("endian".to_string(), endian_value(e)); } if let Some(a) = align { m.insert("align".to_string(), Value::from(a as u64)); } let fields = s.fields.iter().map(build_field_value).collect(); m.insert("fields".to_string(), Value::Array(fields)); Value::Object(m) } Repr::Union(u) => { let mut m = Map::new(); m.insert("kind".to_string(), Value::String("union".to_string())); if let Some(e) = endian { m.insert("endian".to_string(), endian_value(e)); } m.insert("discriminator".to_string(), discriminator_value(&u.discriminator)); if matches!(u.discriminator, Discriminator::Field { .. }) && !u.fields.is_empty() { let fields = u.fields.iter().map(build_field_value).collect(); m.insert("fields".to_string(), Value::Array(fields)); } let mapping = Map::from_iter( u.mapping .iter() .map(|(k, v)| (k.clone(), v.clone().build())), ); m.insert("mapping".to_string(), Value::Object(mapping)); Value::Object(m) } Repr::Enum(values) => { let mut m = Map::new(); m.insert("kind".to_string(), Value::String("enum".to_string())); m.insert( "values".to_string(), Value::Array(values.iter().map(|v| Value::String(v.clone())).collect()), ); Value::Object(m) } Repr::Array { element, count } => { let mut m = Map::new(); m.insert("kind".to_string(), Value::String("array".to_string())); m.insert("element".to_string(), element.clone().build()); if let Some(c) = count { m.insert("count".to_string(), Value::from(*c as u64)); } Value::Object(m) } Repr::Record { values } => { let mut m = Map::new(); m.insert("kind".to_string(), Value::String("record".to_string())); m.insert("values".to_string(), values.clone().build()); Value::Object(m) } Repr::Standard(map) => Value::Object(map.clone()), Repr::Raw(v) => v.clone(), } } } /// Build a single BAST field object from a [`FieldBuilder`], extracting /// field-level annotations from the field's type schema. fn build_field_value(f: &FieldBuilder) -> Value { let mut m = Map::new(); m.insert("name".to_string(), Value::String(f.name.clone())); m.insert("kind".to_string(), f.schema.clone().build()); if let Some(e) = f.schema.endian { m.insert("endian".to_string(), endian_value(e)); } if let Some(a) = f.schema.align { m.insert("align".to_string(), Value::from(a as u64)); } if let Some(enc) = f.schema.encoding { m.insert( "encoding".to_string(), Value::String( match enc { VariableEncoding::LengthPrefixed => "length-prefixed", VariableEncoding::OffsetIndirect => "offset-indirect", } .to_string(), ), ); } if let Some(ml) = f.schema.max_length { m.insert("maxLength".to_string(), Value::from(ml as u64)); } Value::Object(m) } fn endian_value(e: Endian) -> Value { Value::String( match e { Endian::Little => "little", Endian::Big => "big", } .to_string(), ) } fn discriminator_value(disc: &Discriminator) -> Value { match disc { Discriminator::Byte { offset, disc_type } => Value::Object(Map::from_iter([ ("kind".to_string(), Value::String("byte".to_string())), ("offset".to_string(), Value::from(*offset as u64)), ("type".to_string(), Value::String(disc_type.to_bast_str().to_string())), ])), Discriminator::Field { name } => Value::Object(Map::from_iter([ ("kind".to_string(), Value::String("field".to_string())), ("name".to_string(), Value::String(name.clone())), ])), } } /// TUnion discriminator, builder-friendly form. Mirrors /// [`crate::schema::DiscriminatorKind`] but constructed via the builder /// API. #[derive(Debug, Clone)] pub enum Discriminator { /// Byte-offset discriminator. `offset` is the byte position; `disc_type` /// is the AlkType kind of the discriminator (Uint8/Uint16/Uint32). Byte { /// Byte position of the discriminator within the union's buffer. offset: usize, /// The AlkType kind of the discriminator (typically Uint8). disc_type: AlkTypeKind, }, /// Field-name discriminator. `name` is the field holding the /// discriminator value. The discriminator field and any shared /// fields are declared via `.field()` on the union builder. Field { /// The field name that holds the discriminator value. name: String, }, } /// Helper for building named `$defs` that schemas can `$ref` by name. /// /// `define` returns a `Schema` (the `$ref` to the definition), so it can /// be passed directly to `.field()` / `.mapping()` without a separate /// `ref_def` call. Use [`build`](Self::build) to get the `{"$defs": { ... }}` /// object, or [`build_doc`](Self::build_doc) to produce a complete BAST /// document with a named root type. #[derive(Debug, Default)] pub struct Definitions { defs: Map, } impl Definitions { /// Create an empty `Definitions` registry. pub fn new() -> Self { Self::default() } /// Define a named schema. Returns a `Schema` that produces /// `{"$ref": "#/$defs/"}`. pub fn define(&mut self, name: &str, schema: Schema) -> Schema { self.defs.insert(name.to_string(), schema.build()); Schema::ref_def(name) } /// Like `define`, but the schema is an existing `Value` (adopted via /// `Schema::from_value`). pub fn define_value(&mut self, name: &str, value: Value) -> Schema { self.defs.insert(name.to_string(), value); Schema::ref_def(name) } /// Produce the `{"$defs": { ... }}` object. pub fn build(self) -> Value { Value::Object(Map::from_iter([( "$defs".to_string(), Value::Object(self.defs), )])) } /// Build a complete BAST document with `root_name` as the root type. /// /// The root schema is inserted into `$defs` alongside any previously /// defined entries. The resulting `Value` is ready for /// [`crate::AlkTypeEngine::compile`]: /// /// ```rust,ignore /// let mut defs = Definitions::new(); /// defs.define("Read", Schema::struct_().field("id", Schema::uint32())); /// let doc = defs.build_doc("Packet", Schema::struct_() /// .field("payload", Schema::ref_def("Read"))); /// let engine = AlkTypeEngine::compile(&doc, "Packet", LayoutMode::Packed, None)?; /// ``` pub fn build_doc(self, root_name: &str, root: Schema) -> Value { let mut defs = self.defs; defs.insert(root_name.to_string(), root.build()); Value::Object(Map::from_iter([( "$defs".to_string(), Value::Object(defs), )])) } /// Merge the `$defs` into a top-level schema `Value`. Convenience /// for the common case of building a schema that references its /// definitions. If `top` already has a `$defs` object, the definitions /// are merged into it (existing keys with the same name are /// overwritten); otherwise a `$defs` key is inserted. /// /// For BAST documents, prefer [`build_doc`](Self::build_doc) — it /// places the root type inside `$defs` (where BAST requires it). /// This method is useful when merging additional definitions into an /// existing BAST document that already has a `$defs` block. pub fn merge_into(self, top: &mut Value) { if let Some(obj) = top.as_object_mut() { match obj.get_mut("$defs").and_then(Value::as_object_mut) { Some(existing) => existing.extend(self.defs), None => { obj.insert("$defs".to_string(), Value::Object(self.defs)); } } } } } #[cfg(test)] mod tests { use super::*; use serde_json::json; // ----- Primitive constructors --------------------------------------- #[test] fn builder_uint32_produces_bast_string() { assert_eq!(Schema::uint32().build(), json!("uint32")); } #[test] fn builder_all_primitives_produce_bast_strings() { assert_eq!(Schema::int8().build(), json!("int8")); assert_eq!(Schema::int16().build(), json!("int16")); assert_eq!(Schema::int32().build(), json!("int32")); assert_eq!(Schema::int64().build(), json!("int64")); assert_eq!(Schema::uint8().build(), json!("uint8")); assert_eq!(Schema::uint16().build(), json!("uint16")); assert_eq!(Schema::uint32().build(), json!("uint32")); assert_eq!(Schema::uint64().build(), json!("uint64")); assert_eq!(Schema::float32().build(), json!("float32")); assert_eq!(Schema::float64().build(), json!("float64")); assert_eq!(Schema::boolean().build(), json!("bool")); assert_eq!(Schema::string().build(), json!("string")); assert_eq!(Schema::bytes().build(), json!("bytes")); assert_eq!(Schema::timestamp().build(), json!("timestamp")); } // ----- Struct ------------------------------------------------------- #[test] fn builder_struct_with_endian_and_fields() { let s = Schema::struct_() .endian(Endian::Big) .field("channel_id", Schema::uint32()) .field("length", Schema::uint32()) .build(); assert_eq!( s, json!({ "kind": "struct", "endian": "big", "fields": [ { "name": "channel_id", "kind": "uint32" }, { "name": "length", "kind": "uint32" } ] }) ); } #[test] fn builder_struct_with_align() { let s = Schema::struct_().align(256).field("x", Schema::uint8()).build(); assert_eq!(s["align"], json!(256)); assert_eq!(s["kind"], json!("struct")); } #[test] fn builder_field_order_preserved() { let s = Schema::struct_() .field("first", Schema::uint8()) .field("second", Schema::uint16()) .field("third", Schema::uint32()) .build(); let fields = s["fields"].as_array().unwrap(); assert_eq!(fields.len(), 3); assert_eq!(fields[0]["name"], json!("first")); assert_eq!(fields[1]["name"], json!("second")); assert_eq!(fields[2]["name"], json!("third")); } #[test] fn builder_field_with_endian_override() { let s = Schema::struct_() .endian(Endian::Big) .field("crc", Schema::uint32().endian(Endian::Little)) .build(); assert_eq!( s["fields"][0], json!({ "name": "crc", "kind": "uint32", "endian": "little" }) ); } #[test] fn builder_field_with_encoding_and_max_length() { let s = Schema::struct_() .field( "blob", Schema::bytes() .encoding(VariableEncoding::OffsetIndirect) .max_length(256), ) .build(); assert_eq!( s["fields"][0], json!({ "name": "blob", "kind": "bytes", "encoding": "offset-indirect", "maxLength": 256 }) ); } #[test] fn builder_field_encoding_length_prefixed_emitted_explicitly() { let s = Schema::struct_() .field("s", Schema::string().encoding(VariableEncoding::LengthPrefixed)) .build(); assert_eq!( s["fields"][0], json!({ "name": "s", "kind": "string", "encoding": "length-prefixed" }) ); } #[test] fn builder_field_without_annotations_is_minimal() { let s = Schema::struct_().field("id", Schema::uint32()).build(); assert_eq!( s["fields"][0], json!({ "name": "id", "kind": "uint32" }) ); } #[test] fn builder_struct_default_endian_is_omitted() { let s = Schema::struct_().field("x", Schema::uint8()).build(); assert!( s.get("endian").is_none(), "default endian should not be emitted" ); } // ----- Standard JSON Schema object --------------------------------- #[test] fn builder_object_with_required_and_constraints() { let s = Schema::object() .field("path", Schema::string_().max_length(4096)) .field("offset", Schema::integer().minimum(0.0)) .field("length", Schema::integer().minimum(0.0)) .required(&["path"]) .build(); assert_eq!( s, json!({ "type": "object", "properties": { "path": {"type": "string", "maxLength": 4096}, "offset": {"type": "integer", "minimum": 0.0}, "length": {"type": "integer", "minimum": 0.0} }, "required": ["path"] }) ); } #[test] fn builder_required_accumulates_across_calls() { let s = Schema::object() .field("a", Schema::integer()) .field("b", Schema::integer()) .required(&["a"]) .required(&["b"]) .build(); assert_eq!(s["required"], json!(["a", "b"])); } // ----- Union -------------------------------------------------------- #[test] fn builder_union_byte_discriminator_with_mapping() { let s = Schema::union_(Discriminator::Byte { offset: 0, disc_type: AlkTypeKind::Uint8, }) .mapping("5", Schema::ref_def("Read")) .mapping("6", Schema::ref_def("Write")) .build(); assert_eq!( s, json!({ "kind": "union", "discriminator": {"kind": "byte", "offset": 0, "type": "uint8"}, "mapping": { "5": {"$ref": "#/$defs/Read"}, "6": {"$ref": "#/$defs/Write"} } }) ); } #[test] fn builder_union_field_discriminator() { let s = Schema::union_(Discriminator::Field { name: "type".to_string() }) .field("type", Schema::string()) .mapping("read", Schema::ref_def("Read")) .build(); assert_eq!( s["discriminator"], json!({"kind": "field", "name": "type"}) ); assert_eq!( s["fields"], json!([{ "name": "type", "kind": "string" }]) ); assert_eq!( s["mapping"], json!({ "read": {"$ref": "#/$defs/Read"} }) ); } #[test] fn builder_union_byte_disc_no_fields_key() { let s = Schema::union_(Discriminator::Byte { offset: 0, disc_type: AlkTypeKind::Uint8, }) .mapping("1", Schema::ref_def("Init")) .build(); assert!( s.get("fields").is_none(), "byte-offset union should not have a fields key" ); } #[test] fn builder_union_with_endian() { let s = Schema::union_(Discriminator::Byte { offset: 0, disc_type: AlkTypeKind::Uint8, }) .endian(Endian::Big) .mapping("1", Schema::ref_def("Init")) .build(); assert_eq!(s["endian"], json!("big")); } // ----- Enum --------------------------------------------------------- #[test] fn builder_enum_of_with_values() { let s = Schema::enum_of(&["read", "write", "execute"]).build(); assert_eq!( s, json!({ "kind": "enum", "values": ["read", "write", "execute"] }) ); } // ----- Array and Record -------------------------------------------- #[test] fn builder_array_of_with_count() { let arr = Schema::array_of(Schema::uint32()).count(3).build(); assert_eq!( arr, json!({"kind": "array", "element": "uint32", "count": 3}) ); } #[test] fn builder_array_of_without_count_omits_count() { let arr = Schema::array_of(Schema::uint32()).build(); assert_eq!( arr, json!({"kind": "array", "element": "uint32"}) ); } #[test] fn builder_array_of_with_ref_element() { let arr = Schema::array_of(Schema::ref_def("Point")).count(4).build(); assert_eq!( arr, json!({ "kind": "array", "element": {"$ref": "#/$defs/Point"}, "count": 4 }) ); } #[test] fn builder_record_of() { let rec = Schema::record_of(Schema::float32()).build(); assert_eq!( rec, json!({"kind": "record", "values": "float32"}) ); } // ----- $ref and from_value ------------------------------------------ #[test] fn builder_ref_def_produces_json_pointer() { assert_eq!( Schema::ref_def("Read").build(), json!({"$ref": "#/$defs/Read"}) ); } #[test] fn builder_from_value_adopts_existing_schema() { let existing = json!({"type": "integer", "minimum": 0}); let s = Schema::from_value(existing.clone()); assert_eq!(s.build(), existing); } #[test] fn builder_from_value_adopts_bare_string() { let s = Schema::from_value(json!("uint32")); assert_eq!(s.build(), json!("uint32")); } #[test] fn builder_any_is_empty_object() { assert_eq!(Schema::any().build(), json!({})); } // ----- Encoding setter ---------------------------------------------- #[test] fn builder_encoding_on_primitive_does_not_affect_build() { // Annotations on a primitive are for field() to extract; // build() of a bare primitive is just the string. let s = Schema::string().encoding(VariableEncoding::OffsetIndirect).build(); assert_eq!(s, json!("string")); } #[test] fn builder_encoding_offset_indirect_in_field() { let s = Schema::struct_() .field("handle", Schema::string().encoding(VariableEncoding::OffsetIndirect)) .build(); assert_eq!( s["fields"][0], json!({ "name": "handle", "kind": "string", "encoding": "offset-indirect" }) ); } #[test] fn builder_encoding_length_prefixed_in_field() { let s = Schema::struct_() .field("s", Schema::string().encoding(VariableEncoding::LengthPrefixed)) .build(); assert_eq!( s["fields"][0]["encoding"], json!("length-prefixed") ); } #[test] fn builder_encoding_not_set_is_omitted() { let s = Schema::struct_().field("s", Schema::string()).build(); assert!( s["fields"][0].get("encoding").is_none(), "encoding should be omitted when not set" ); } // ----- Definitions -------------------------------------------------- #[test] fn builder_definitions_define_returns_ref() { let mut defs = Definitions::new(); let r = defs.define("FileNotFound", Schema::object().field("path", Schema::string_())); assert_eq!(r.build(), json!({"$ref": "#/$defs/FileNotFound"})); let built = defs.build(); assert_eq!( built["$defs"]["FileNotFound"], json!({"type": "object", "properties": {"path": {"type": "string"}}}) ); } #[test] fn builder_definitions_define_struct_produces_bast() { let mut defs = Definitions::new(); defs.define("Read", Schema::struct_().field("id", Schema::uint32())); let built = defs.build(); assert_eq!( built["$defs"]["Read"], json!({ "kind": "struct", "fields": [{ "name": "id", "kind": "uint32" }] }) ); } #[test] fn builder_build_doc_produces_complete_bast_document() { let mut defs = Definitions::new(); defs.define("Read", Schema::struct_().field("id", Schema::uint32())); let doc = defs.build_doc( "Packet", Schema::struct_().field("payload", Schema::ref_def("Read")), ); assert_eq!( doc, json!({ "$defs": { "Read": { "kind": "struct", "fields": [{ "name": "id", "kind": "uint32" }] }, "Packet": { "kind": "struct", "fields": [ { "name": "payload", "kind": {"$ref": "#/$defs/Read"} } ] } } }) ); } #[test] fn builder_build_doc_compiles() { let mut defs = Definitions::new(); defs.define("Read", Schema::struct_().field("id", Schema::uint32())); let doc = defs.build_doc( "Packet", Schema::struct_() .endian(Endian::Big) .field("payload", Schema::ref_def("Read")), ); let engine = crate::AlkTypeEngine::compile( &doc, "Packet", crate::LayoutMode::Packed, None, ); assert!(engine.is_ok(), "build_doc output should compile: {engine:?}"); } #[test] fn builder_merge_into_inserts_defs_when_absent() { let mut defs = Definitions::new(); defs.define("Read", Schema::struct_().field("id", Schema::uint32())); let mut top = Schema::struct_().field("payload", Schema::ref_def("Read")).build(); defs.merge_into(&mut top); assert!(top["$defs"].is_object(), "$defs should be inserted"); assert_eq!( top["$defs"]["Read"], json!({"kind": "struct", "fields": [{ "name": "id", "kind": "uint32" }]}) ); } #[test] fn builder_merge_into_merges_into_existing_defs() { let mut top = json!({ "$defs": { "Write": {"kind": "struct", "fields": [{ "name": "n", "kind": "uint16" }]} } }); let mut defs = Definitions::new(); defs.define("Read", Schema::struct_().field("id", Schema::uint32())); defs.merge_into(&mut top); assert_eq!( top["$defs"]["Write"], json!({"kind": "struct", "fields": [{ "name": "n", "kind": "uint16" }]}), "existing Write def should be preserved" ); assert_eq!( top["$defs"]["Read"], json!({"kind": "struct", "fields": [{ "name": "id", "kind": "uint32" }]}), "new Read def should be merged in" ); } #[test] fn builder_merge_into_overwrites_duplicate_keys() { let mut top = json!({ "$defs": { "X": {"old": true} } }); let mut defs = Definitions::new(); defs.define_value("X", json!({"new": true})); defs.merge_into(&mut top); assert_eq!(top["$defs"]["X"], json!({"new": true})); } #[test] fn builder_merge_into_is_noop_for_non_object_top() { let mut top = json!("not an object"); let mut defs = Definitions::new(); defs.define("Read", Schema::uint32()); defs.merge_into(&mut top); assert_eq!(top, json!("not an object"), "non-object top should be untouched"); } // ----- End-to-end: builder → compile -------------------------------- #[test] fn builder_chunk_header_compiles_in_packed_mode() { let defs = Definitions::new(); let doc = defs.build_doc( "ChunkHeader", Schema::struct_() .endian(Endian::Big) .field("channel_id", Schema::uint32()) .field("length", Schema::uint32()), ); let engine = crate::AlkTypeEngine::compile( &doc, "ChunkHeader", crate::LayoutMode::Packed, None, ); assert!(engine.is_ok(), "chunk header schema should compile: {engine:?}"); } #[test] fn builder_sftp_style_union_compiles() { let mut defs = Definitions::new(); defs.define("Init", Schema::struct_().field("version", Schema::uint32())); defs.define("Read", Schema::struct_().field("id", Schema::uint32())); let doc = defs.build_doc( "Packet", Schema::struct_().field( "payload", Schema::union_(Discriminator::Byte { offset: 0, disc_type: AlkTypeKind::Uint8, }) .mapping("1", Schema::ref_def("Init")) .mapping("5", Schema::ref_def("Read")), ), ); let engine = crate::AlkTypeEngine::compile( &doc, "Packet", crate::LayoutMode::Packed, None, ); assert!(engine.is_ok(), "sftp-style union should compile: {engine:?}"); } #[test] fn builder_array_with_count_compiles() { let doc = Definitions::new().build_doc( "Mesh", Schema::struct_().field( "vertices", Schema::array_of(Schema::float32()).count(3), ), ); let engine = crate::AlkTypeEngine::compile( &doc, "Mesh", crate::LayoutMode::Packed, None, ); assert!(engine.is_ok(), "array with count should compile: {engine:?}"); } #[test] fn builder_record_compiles() { let doc = Definitions::new().build_doc( "Config", Schema::struct_().field("entries", Schema::record_of(Schema::uint32())), ); let engine = crate::AlkTypeEngine::compile( &doc, "Config", crate::LayoutMode::Packed, None, ); assert!(engine.is_ok(), "record should compile: {engine:?}"); } }