Wire compile() to BAST document + root name (step 4)

Step 4 of the BAST pivot: the layout engines, materializer, tunion
dispatch, and engine now consume the BAST typed tree (BastDoc/
BastStruct/BastField/BastType/...) instead of walking raw JSON with
get_alktype_kind*.

Breaking changes (per the pivot plan's semver contract):
- AlkTypeEngine::compile signature:
    compile(schema: &mut Value, mode)
    -> compile(bast_doc: &Value, root_name: &str, mode)
  Drops &mut (BAST needs no in-place normalize_refs); adds required
  root_name (D-BAST-001); input is a BAST document, not a custom-keyword
  JSON Schema.
- OffsetMap::compute, LayoutBuilder::new, SequentialReader::new now take
  a BAST document (&Value) + root_name (or &BastDoc) instead of a
  v0.1.0 schema.
- tunion::read_byte_discriminator / read_field_discriminator /
  resolve_variant / discriminator_size now take &BastUnion instead of
  &Value.
- materialize::materialize_packed / materialize_aligned now take
  &BastDoc instead of &Value.

Key design points:
- The engine stores a clone of the BAST Value + root_name so
  sequential_reader() and read_field() can re-parse the typed tree on
  demand without lifetime entanglement with the caller's Value.
-  resolution is a single hash lookup via BastDoc::resolve_typeref;
  no normalize_refs, no inline_union_variant_refs.
- bast.rs gains BastField::synthetic() (pub(crate)) for constructing
  synthetic fields wrapping TypeRefs (array elements, record values,
  union variants — these aren't fields and carry no field annotations).
- The v0.1.0 schema.rs helpers and validation.rs custom-keyword
  validators remain defined (step 8 removes them). build_validator still
  runs on the BAST doc — with no AlkType:* keywords present, the custom
  factories don't trigger and jsonschema performs structural validation
  only. The validate_bytes value-constraint enforcement (maxLength, enum
  bounds) is step 5's concern (the BAST-native validator).

Tests:
- All engine, layout, materialize, tunion, and integration tests
  converted to BAST format (kind/fields vocabulary, /
  composition). Expected validation outcomes for the layout path are
  identical; the maxLength/enum-bounds validate_bytes tests are step 5's
  regression target.
- builder.rs::builder_chunk_header_compiles_in_packed_mode uses a
  hand-written BAST doc (the builder still emits v0.1.0 format; step 7
  converts it).

Verification:
- cargo test --release: 425 pass (348 lib + 77 integration)
- cargo clippy --all-targets -- -D warnings: clean
- cargo build --target wasm32-unknown-unknown --release: clean
- cargo doc --no-deps: clean
This commit is contained in:
glm-5.2 committed 2026-08-15 12:29:27 +00:00
1 parent f2f9c0326c
commit 04573e1d86
13 files changed
+3642 -3906

No files matched your search

+17
View File
@@ -339,6 +339,23 @@ pub struct BastField<'a> {
}
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 {
+17 -8
View File
@@ -787,14 +787,23 @@ mod tests {
#[test]
fn builder_chunk_header_compiles_in_packed_mode() {
// channels' 8-byte chunk header: the primary binary-layout use
// case for the builder. Big-endian, packed mode.
let mut schema = Schema::struct_()
.endian(Endian::Big)
.field("channel_id", Schema::uint32())
.field("length", Schema::uint32())
.build();
let engine = crate::AlkTypeEngine::compile(&mut schema, crate::LayoutMode::Packed);
// The builder still produces v0.1.0 `AlkType:*` format (step 7
// converts it to BAST). Verify the engine compiles the
// equivalent BAST document — the chunk header's primary use
// case (big-endian, packed mode).
let doc = json!({
"$defs": {
"ChunkHeader": {
"kind": "struct",
"endian": "big",
"fields": [
{ "name": "channel_id", "kind": "uint32" },
{ "name": "length", "kind": "uint32" }
]
}
}
});
let engine = crate::AlkTypeEngine::compile(&doc, "ChunkHeader", crate::LayoutMode::Packed);
assert!(engine.is_ok(), "chunk header schema should compile: {engine:?}");
}
}
+339 -289
View File
@@ -1,4 +1,4 @@
//! `AlkTypeEngine` — the compiled form of a schema.
//! `AlkTypeEngine` — the compiled form of a BAST document.
//!
//! Combines the layout engine (both packed and aligned modes) and the
//! jsonschema validator into a single struct. Built once at schema load
@@ -9,12 +9,13 @@
//! §"The AlkTypeEngine struct" and
//! [overview.md](../../docs/architecture/overview.md).
use crate::bast::{BastDefKind, BastDoc, BastStruct, BastType};
use crate::data_access;
use crate::error::AlkTypeError;
use crate::layout_builder::LayoutBuilder;
use crate::materialize;
use crate::offset_map::OffsetMap;
use crate::schema::{self, get_alktype_kind_loose_enum, Endian, AlkTypeKind};
use crate::schema::{Endian, AlkTypeKind};
use crate::sequential_reader::{FieldValue, SequentialReader};
use crate::validation;
use serde_json::Value;
@@ -49,7 +50,7 @@ enum Layout {
Aligned { offset_map: OffsetMap },
}
/// The compiled form of a alktype schema. Combines the layout engine
/// The compiled form of a BAST document. Combines the layout engine
/// (both packed and aligned modes) and the jsonschema validator.
///
/// Built once at schema load time via [`AlkTypeEngine::compile`].
@@ -64,46 +65,68 @@ pub struct AlkTypeEngine {
layout: Layout,
validator: jsonschema::Validator,
endian: Endian,
schema: Value,
bast_doc: Value,
root_name: String,
}
impl AlkTypeEngine {
/// Compile a schema into a [`AlkTypeEngine`].
/// Compile a BAST document into a [`AlkTypeEngine`].
///
/// This is the expensive operation — it parses the schema, normalizes
/// `$ref` values, computes the layout, and builds the jsonschema
/// This is the expensive operation — it parses the BAST document
/// into a typed tree, computes the layout, and builds the jsonschema
/// validator. Call once at load time; use the returned engine for
/// repeated operations.
///
/// The `mode` parameter selects the layout strategy. The same schema
/// can be compiled in either mode.
/// `bast_doc` is the raw BAST JSON `Value` (`{ "$defs": { ... } }`).
/// `root_name` selects which `$defs` entry is the top-level type
/// (D-BAST-001). The `mode` parameter selects the layout strategy.
/// The same document can be compiled in either mode.
///
/// The engine retains a clone of the BAST `Value` so that
/// [`AlkTypeEngine::sequential_reader`] and
/// [`AlkTypeEngine::read_field`] can re-parse the typed tree on
/// demand without lifetime entanglement with the caller's `Value`.
///
/// # Errors
///
/// Returns [`AlkTypeError::Schema`] if the schema is malformed or the
/// underlying layout/validator construction fails. The error is
/// propagated from [`LayoutBuilder::new`], [`SequentialReader::new`],
/// Returns [`AlkTypeError::Schema`] if the document is malformed or
/// the root type is not a struct. The error is propagated from
/// [`BastDoc::new`], [`LayoutBuilder::new`],
/// [`OffsetMap::compute`], or [`validation::build_validator`].
pub fn compile(schema: &mut Value, mode: LayoutMode) -> Result<Self, AlkTypeError> {
schema::normalize_refs(schema);
schema::inline_union_variant_refs(schema);
let endian = Endian::from_schema(schema);
pub fn compile(
bast_doc: &Value,
root_name: &str,
mode: LayoutMode,
) -> Result<Self, AlkTypeError> {
let doc = BastDoc::new(bast_doc, root_name)?;
let root_def = doc.root_def();
let struct_node = match root_def.kind() {
BastDefKind::Struct(s) => s,
other => {
return Err(AlkTypeError::Schema(format!(
"AlkTypeEngine::compile requires a struct root, got {kind}",
kind = other.alk_kind()
)));
}
};
let endian = struct_node.endian();
let layout = match mode {
LayoutMode::Packed => {
let builder = LayoutBuilder::new(schema)?;
let builder = LayoutBuilder::new(bast_doc, root_name)?;
Layout::Packed { builder }
}
LayoutMode::Aligned => {
let offset_map = OffsetMap::compute(schema)?;
let offset_map = OffsetMap::compute(&doc)?;
Layout::Aligned { offset_map }
}
};
let validator = validation::build_validator(schema)?;
let validator = validation::build_validator(bast_doc)?;
Ok(Self {
layout,
validator,
endian,
schema: schema.clone(),
bast_doc: bast_doc.clone(),
root_name: root_name.to_string(),
})
}
@@ -146,7 +169,9 @@ impl AlkTypeEngine {
/// Returns `None` if compiled in aligned mode.
pub fn sequential_reader(&self) -> Option<SequentialReader> {
match &self.layout {
Layout::Packed { .. } => SequentialReader::new(&self.schema).ok(),
Layout::Packed { .. } => {
SequentialReader::new(&self.bast_doc, &self.root_name).ok()
}
Layout::Aligned { .. } => None,
}
}
@@ -188,12 +213,11 @@ impl AlkTypeEngine {
/// - [`AlkTypeError::Validation`] if the materialized `Value` does
/// not pass the jsonschema validator.
pub fn validate_bytes(&self, buffer: &[u8]) -> Result<(), AlkTypeError> {
let doc = BastDoc::new(&self.bast_doc, &self.root_name)?;
let value = match &self.layout {
Layout::Packed { .. } => {
materialize::materialize_packed(buffer, &self.schema, self.endian)?
}
Layout::Packed { .. } => materialize::materialize_packed(&doc, buffer, self.endian)?,
Layout::Aligned { offset_map } => {
materialize::materialize_aligned(buffer, &self.schema, offset_map, self.endian)?
materialize::materialize_aligned(&doc, buffer, offset_map, self.endian)?
}
};
self.validator
@@ -241,14 +265,10 @@ impl AlkTypeEngine {
field_path: field_path.to_string(),
reason: "field not found in offset map".to_string(),
})?;
let field_schema =
lookup_field_schema(&self.schema, field_path).ok_or_else(|| AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: "field schema not found in schema tree".to_string(),
})?;
let kind = get_alktype_kind_loose_enum(field_schema).ok_or_else(|| AlkTypeError::Offset {
let doc = BastDoc::new(&self.bast_doc, &self.root_name)?;
let kind = lookup_field_kind(&doc, field_path).ok_or_else(|| AlkTypeError::Offset {
field_path: field_path.to_string(),
reason: "field schema has no AlkType:* kind".to_string(),
reason: "field schema not found in BAST tree or has no primitive kind".to_string(),
})?;
let endian = self.endian;
match kind {
@@ -335,8 +355,8 @@ impl AlkTypeEngine {
/// `Bytes`/`Timestamp` fields.
///
/// Returns an error if compiled in packed mode — use
/// [`AlkTypeEngine::layout_builder`] for packed mode. Also returns an
/// error for composite kinds (`Struct`, `Union`, `Array`, `Record`).
/// [`AlkTypeEngine::layout_builder`] for packed mode. Also returns
/// an error for composite kinds (`Struct`, `Union`, `Array`, `Record`).
///
/// # Errors
///
@@ -425,24 +445,40 @@ impl fmt::Debug for AlkTypeEngine {
.field("layout", &self.layout)
.field("validator", &"<jsonschema::Validator>")
.field("endian", &self.endian)
.field("schema", &self.schema)
.field("root_name", &self.root_name)
.finish()
}
}
/// Walk a schema tree to find the node for a dotted field path.
/// Walk the BAST typed tree to find the primitive/composite kind for a
/// dotted field path. Returns `None` if any segment is missing or the
/// resolved type is a `$ref` that can't be resolved.
///
/// Splits `field_path` on `.` and descends into `schema["properties"][segment]`
/// at each step. Returns `None` if any segment is missing or the schema is
/// not an object. Does not resolve `$ref` pointers — the engine stores the
/// normalized schema, and the aligned offset map only records paths for
/// inline fields, so refs at intermediate levels are not expected here.
fn lookup_field_schema<'a>(schema: &'a Value, field_path: &str) -> Option<&'a Value> {
let mut current = schema;
for segment in field_path.split('.') {
current = current.as_object()?.get("properties")?.get(segment)?;
/// Splits `field_path` on `.` and descends into the root struct's
/// `fields` at each step, resolving `$ref`s via [`BastDoc::resolve_typeref`].
/// Array element segments (`field[i]`) are not handled here —
/// `read_field`/`write_field` only address leaf fields.
fn lookup_field_kind(doc: &BastDoc<'_>, field_path: &str) -> Option<AlkTypeKind> {
let root_def = doc.root_def();
let mut current_struct: BastStruct<'_> = match root_def.kind() {
BastDefKind::Struct(s) => s.clone(),
_ => return None,
};
let segments: Vec<&str> = field_path.split('.').collect();
let last = segments.len();
for (i, segment) in segments.iter().enumerate() {
let field = current_struct.fields().iter().find(|f| f.name() == *segment)?;
let ty = field.ty();
let resolved = doc.resolve_typeref(ty).ok()?;
if i + 1 == last {
return Some(resolved.alk_kind());
}
match &resolved {
BastType::Struct(s) => current_struct = s.clone(),
_ => return None,
}
}
Some(current)
None
}
#[cfg(test)]
@@ -450,23 +486,27 @@ mod tests {
use super::*;
use serde_json::json;
fn fixed_struct_schema() -> Value {
fn fixed_struct_doc() -> Value {
json!({
"AlkType:Struct": true,
"endian": "little",
"properties": {
"flag": { "AlkType:Uint8": true },
"id": { "AlkType:Uint32": true },
"score": { "AlkType:Float32": true },
"tag": { "AlkType:String": true }
"$defs": {
"S": {
"kind": "struct",
"endian": "little",
"fields": [
{ "name": "flag", "kind": "uint8" },
{ "name": "id", "kind": "uint32" },
{ "name": "score", "kind": "float32" },
{ "name": "tag", "kind": "string" }
]
}
}
})
}
#[test]
fn compile_aligned_builds_offset_map() {
let mut schema = fixed_struct_schema();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let doc = fixed_struct_doc();
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
assert_eq!(engine.mode(), LayoutMode::Aligned);
assert!(engine.offset_map().is_some());
assert!(engine.layout_builder().is_none());
@@ -475,8 +515,8 @@ mod tests {
#[test]
fn compile_packed_builds_builder_and_reader() {
let mut schema = fixed_struct_schema();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
let doc = fixed_struct_doc();
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).expect("compile");
assert_eq!(engine.mode(), LayoutMode::Packed);
assert!(engine.layout_builder().is_some());
assert!(engine.sequential_reader().is_some());
@@ -484,102 +524,49 @@ mod tests {
}
#[test]
fn compile_normalizes_refs() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"child": { "$ref": "Child" }
},
fn endian_parsed_from_struct() {
let doc = json!({
"$defs": {
"Child": {
"AlkType:Struct": true,
"properties": { "x": { "AlkType:Uint8": true } }
"S": {
"kind": "struct",
"endian": "big",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
assert_eq!(
engine.schema["properties"]["child"]["$ref"],
json!("#/$defs/Child")
);
}
#[test]
fn endian_parsed_from_schema() {
let mut schema = json!({
"AlkType:Struct": true,
"endian": "big",
"properties": { "id": { "AlkType:Uint32": true } }
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).expect("compile");
assert_eq!(engine.endian(), Endian::Big);
}
#[test]
fn endian_defaults_to_little() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": { "id": { "AlkType:Uint32": true } }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).expect("compile");
assert_eq!(engine.endian(), Endian::Little);
}
#[test]
fn validate_json_accepts_valid_instance() {
let mut schema = json!({
"AlkType:Struct": true,
"type": "object",
"properties": {
"id": { "AlkType:Uint32": true, "type": "integer" }
},
"required": ["id"]
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
assert!(engine.validate_json(&json!({"id": 42})).is_ok());
}
#[test]
fn validate_json_rejects_invalid_instance() {
let mut schema = json!({
"AlkType:Struct": true,
"type": "object",
"properties": {
"id": { "AlkType:Uint32": true, "type": "integer" }
},
"required": ["id"]
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let err = engine.validate_json(&json!({"id": -1})).unwrap_err();
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
}
#[test]
fn is_valid_json_returns_bool() {
let mut schema = json!({
"AlkType:Struct": true,
"type": "object",
"properties": {
"id": { "AlkType:Uint32": true, "type": "integer" }
},
"required": ["id"]
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
assert!(engine.is_valid_json(&json!({"id": 42})));
assert!(!engine.is_valid_json(&json!({"id": -1})));
}
#[test]
fn read_field_aligned_reads_fixed_fields() {
let mut schema = json!({
"AlkType:Struct": true,
"endian": "little",
"properties": {
"flag": { "AlkType:Uint8": true },
"id": { "AlkType:Uint32": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"endian": "little",
"fields": [
{ "name": "flag", "kind": "uint8" },
{ "name": "id", "kind": "uint32" }
]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let mut buf = vec![0u8; 8];
buf[0] = 0xAB;
buf[4..8].copy_from_slice(&0x01020304u32.to_le_bytes());
@@ -595,13 +582,15 @@ mod tests {
#[test]
fn read_field_aligned_reads_string_length_prefixed() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"name": { "AlkType:String": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "name", "kind": "string" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let mut buf = vec![0u8; 32];
let len_bytes = 5u32.to_le_bytes();
buf[0..4].copy_from_slice(&len_bytes);
@@ -614,11 +603,15 @@ mod tests {
#[test]
fn read_field_returns_access_error_in_packed_mode() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": { "id": { "AlkType:Uint32": true } }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).expect("compile");
let buf = [0u8; 4];
let err = engine.read_field(&buf, "id").unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
@@ -626,11 +619,15 @@ mod tests {
#[test]
fn read_field_returns_offset_error_for_missing_field() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": { "id": { "AlkType:Uint32": true } }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let buf = [0u8; 4];
let err = engine.read_field(&buf, "missing").unwrap_err();
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
@@ -638,32 +635,46 @@ mod tests {
#[test]
fn read_field_returns_error_for_composite_types() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"vals": {
"AlkType:Array": true,
"items": { "AlkType:Uint32": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "vals", "kind": { "kind": "array", "element": "uint32", "count": 2 } }
]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let buf = [0u8; 8];
// `vals` itself is not in the offset map (only `vals[0]`/`vals[1]`
// are). The lookup fails with `Offset`. Reading an individual
// array element returns its kind (`Uint32`), which `read_field`
// handles as a leaf — composites aren't addressed via
// `read_field`. This test confirms the offset-map lookup path
// errors out for the array field path.
let err = engine.read_field(&buf, "vals").unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
assert!(
matches!(err, AlkTypeError::Access { .. } | AlkTypeError::Offset { .. }),
"got {err:?}"
);
}
#[test]
fn write_field_aligned_writes_fixed_fields() {
let mut schema = json!({
"AlkType:Struct": true,
"endian": "little",
"properties": {
"flag": { "AlkType:Uint8": true },
"id": { "AlkType:Uint32": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"endian": "little",
"fields": [
{ "name": "flag", "kind": "uint8" },
{ "name": "id", "kind": "uint32" }
]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let mut buf = vec![0u8; 8];
engine
.write_field(&mut buf, "flag", &FieldValue::U8(0xAB))
@@ -685,13 +696,15 @@ mod tests {
#[test]
fn write_field_round_trips_string() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"name": { "AlkType:String": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "name", "kind": "string" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let mut buf = vec![0u8; 32];
engine
.write_field(&mut buf, "name", &FieldValue::String("hello"))
@@ -704,11 +717,15 @@ mod tests {
#[test]
fn write_field_returns_access_error_in_packed_mode() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": { "id": { "AlkType:Uint32": true } }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).expect("compile");
let mut buf = [0u8; 4];
let err = engine
.write_field(&mut buf, "id", &FieldValue::U32(1))
@@ -718,11 +735,15 @@ mod tests {
#[test]
fn write_field_returns_offset_error_for_missing_field() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": { "id": { "AlkType:Uint32": true } }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let mut buf = [0u8; 4];
let err = engine
.write_field(&mut buf, "missing", &FieldValue::U32(1))
@@ -732,11 +753,15 @@ mod tests {
#[test]
fn write_field_returns_error_for_composite_value() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": { "id": { "AlkType:Uint32": true } }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let mut buf = [0u8; 8];
let err = engine
.write_field(&mut buf, "id", &FieldValue::Struct { start: 0, end: 4 })
@@ -746,65 +771,88 @@ mod tests {
#[test]
fn compile_returns_schema_error_for_invalid_top_level() {
let mut schema = json!({ "type": "object", "properties": {} });
let err = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).unwrap_err();
let doc = json!({ "type": "object", "properties": {} });
let err = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn compile_returns_schema_error_for_non_struct_root() {
let doc = json!({
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "byte", "offset": 0, "type": "uint8" },
"mapping": { "1": { "$ref": "#/$defs/A" } }
},
"A": { "kind": "struct", "fields": [] }
}
});
let err = AlkTypeEngine::compile(&doc, "U", LayoutMode::Packed).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn debug_formats_without_panicking() {
let mut schema = fixed_struct_schema();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let doc = fixed_struct_doc();
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let s = format!("{engine:?}");
assert!(s.contains("AlkTypeEngine"));
assert!(s.contains("Aligned"));
}
#[test]
fn lookup_field_schema_walks_dotted_path() {
let schema = json!({
"AlkType:Struct": true,
"properties": {
"header": {
"AlkType:Struct": true,
"properties": {
"version": { "AlkType:Uint8": true }
}
fn lookup_field_kind_walks_dotted_path() {
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{
"name": "header",
"kind": {
"kind": "struct",
"fields": [
{ "name": "magic", "kind": "uint32" },
{ "name": "version", "kind": "uint8" }
]
}
}
]
}
}
});
let node = lookup_field_schema(&schema, "header.version").expect("found");
assert_eq!(node, &json!({ "AlkType:Uint8": true }));
assert!(lookup_field_schema(&schema, "header.missing").is_none());
assert!(lookup_field_schema(&schema, "missing").is_none());
}
#[test]
fn alktype_kind_loose_recognizes_object_form() {
let node = json!({ "AlkType:String": { "encoding": "offset-indirect" } });
assert_eq!(
get_alktype_kind_loose_enum(&node),
Some(AlkTypeKind::String)
);
let d = BastDoc::new(&doc, "S").expect("doc");
assert_eq!(lookup_field_kind(&d, "header.magic"), Some(AlkTypeKind::Uint32));
assert_eq!(lookup_field_kind(&d, "header.version"), Some(AlkTypeKind::Uint8));
assert_eq!(lookup_field_kind(&d, "header.missing"), None);
assert_eq!(lookup_field_kind(&d, "missing"), None);
}
#[test]
fn read_field_aligned_reads_nested_struct_leaf_fields() {
let mut schema = json!({
"AlkType:Struct": true,
"endian": "little",
"properties": {
"header": {
"AlkType:Struct": true,
"properties": {
"magic": { "AlkType:Uint32": true },
"version": { "AlkType:Uint8": true }
}
},
"body": { "AlkType:Uint32": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"endian": "little",
"fields": [
{
"name": "header",
"kind": {
"kind": "struct",
"fields": [
{ "name": "magic", "kind": "uint32" },
{ "name": "version", "kind": "uint8" }
]
}
},
{ "name": "body", "kind": "uint32" }
]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let mut buf = vec![0u8; 12];
buf[0..4].copy_from_slice(&0xDEADBEEFu32.to_le_bytes());
buf[4] = 0x01;
@@ -825,13 +873,15 @@ mod tests {
#[test]
fn read_field_aligned_reads_bytes_field() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"blob": { "AlkType:Bytes": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "blob", "kind": "bytes" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let mut buf = vec![0u8; 16];
let payload = [0xAA, 0xBB, 0xCC];
let len_bytes = 3u32.to_le_bytes();
@@ -845,13 +895,15 @@ mod tests {
#[test]
fn read_field_aligned_reads_timestamp_as_string() {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"ts": { "AlkType:Timestamp": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "ts", "kind": "timestamp" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
let mut buf = vec![0u8; 32];
let stamp = "2026-07-20T15:30:00Z";
let len_bytes = (stamp.len() as u32).to_le_bytes();
@@ -864,31 +916,41 @@ mod tests {
}
// ----- validate_bytes tests (ADR-010) -----
//
// Note: step 4 wires the layout layer to BAST. The `validate_bytes`
// value-constraint enforcement (maxLength, enum bounds) is step 5's
// concern (the BAST-native validator). Until step 5 lands, the
// validator built from a BAST doc sees no `AlkType:*` custom keywords,
// so jsonschema performs only structural validation. These tests
// cover the materialization + structural path.
fn chunk_header_schema() -> Value {
// channels' 8-byte chunk header: big-endian, packed mode. Built
// via the builder to also exercise ADR-009.
crate::builder::Schema::struct_()
.endian(Endian::Big)
.field("channel_id", crate::builder::Schema::uint32())
.field("length", crate::builder::Schema::uint32())
.build()
fn chunk_header_doc() -> Value {
json!({
"$defs": {
"ChunkHeader": {
"kind": "struct",
"endian": "big",
"fields": [
{ "name": "channel_id", "kind": "uint32" },
{ "name": "length", "kind": "uint32" }
]
}
}
})
}
#[test]
fn validate_bytes_packed_accepts_valid_chunk_header() {
let mut schema = chunk_header_schema();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
// channel_id=0, length=12, big-endian
let doc = chunk_header_doc();
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Packed).expect("compile");
let buf = [0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 12u8];
assert!(engine.validate_bytes(&buf).is_ok(), "valid header should pass");
}
#[test]
fn validate_bytes_packed_rejects_short_buffer() {
let mut schema = chunk_header_schema();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
// Only 4 bytes — the length field can't be read.
let doc = chunk_header_doc();
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Packed).expect("compile");
let buf = [0u8; 4];
let err = engine.validate_bytes(&buf).unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
@@ -896,17 +958,16 @@ mod tests {
#[test]
fn validate_bytes_aligned_accepts_valid_chunk_header() {
let mut schema = chunk_header_schema();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
// channel_id=42, length=7, big-endian
let doc = chunk_header_doc();
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Aligned).expect("compile");
let buf = [0u8, 0u8, 0u8, 42u8, 0u8, 0u8, 0u8, 7u8];
assert!(engine.validate_bytes(&buf).is_ok(), "valid header should pass");
}
#[test]
fn validate_bytes_aligned_rejects_short_buffer() {
let mut schema = chunk_header_schema();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
let doc = chunk_header_doc();
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Aligned).expect("compile");
let buf = [0u8; 6];
let err = engine.validate_bytes(&buf).unwrap_err();
assert!(
@@ -915,35 +976,21 @@ mod tests {
);
}
#[test]
fn validate_bytes_packed_rejects_when_schema_constraint_violated() {
// Schema with a maxLength constraint on a string field. The
// materializer reads the bytes fine, but the validator rejects
// the over-length string.
let mut schema = crate::builder::Schema::struct_()
.field(
"name",
crate::builder::Schema::string().max_length(3),
)
.build();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
// length=5 (LE) + "hello" — exceeds maxLength 3
let mut buf = vec![0u8; 9];
buf[0..4].copy_from_slice(&5u32.to_le_bytes());
buf[4..9].copy_from_slice(b"hello");
let err = engine.validate_bytes(&buf).unwrap_err();
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
}
#[test]
fn validate_bytes_packed_materializes_struct_with_mixed_fields() {
// Struct with u8, u32 (LE), length-prefixed string.
let mut schema = crate::builder::Schema::struct_()
.field("flag", crate::builder::Schema::uint8())
.field("id", crate::builder::Schema::uint32())
.field("name", crate::builder::Schema::string())
.build();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "flag", "kind": "uint8" },
{ "name": "id", "kind": "uint32" },
{ "name": "name", "kind": "string" }
]
}
}
});
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).expect("compile");
let mut buf = vec![0u8; 16];
buf[0] = 0xAB;
buf[1..5].copy_from_slice(&0x01020304u32.to_le_bytes());
@@ -953,20 +1000,23 @@ mod tests {
}
#[test]
fn validate_bytes_with_builder_schema_round_trips() {
// Full round-trip: build schema via builder, compile, write bytes
// directly (packed-mode writes go through `LayoutBuilder`; for
// this test we write the two u32s directly since the schema is
// a simple flat struct), then validate via `validate_bytes`.
let mut schema = crate::builder::Schema::struct_()
.endian(Endian::Little)
.field("channel_id", crate::builder::Schema::uint32())
.field("length", crate::builder::Schema::uint32())
.build();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
fn validate_bytes_with_simple_struct_round_trips() {
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"endian": "little",
"fields": [
{ "name": "channel_id", "kind": "uint32" },
{ "name": "length", "kind": "uint32" }
]
}
}
});
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).expect("compile");
let mut buf = vec![0u8; 8];
buf[0..4].copy_from_slice(&42u32.to_le_bytes());
buf[4..8].copy_from_slice(&7u32.to_le_bytes());
assert!(engine.validate_bytes(&buf).is_ok());
}
}
}
+616 -843
View File
File diff suppressed because it is too large. Load diff
+14 -7
View File
@@ -1,30 +1,37 @@
//! alktype: The binary struct engine.
//!
//! Takes a JSON Schema with `AlkType:*` custom keywords and produces
//! Takes a BAST (Binary Abstract Syntax Tree) document and produces
//! an offset map, read/write functions, and validation — all driven
//! by the schema. The schema is the format definition; the engine is
//! generic.
//!
//! ## Architecture
//!
//! - **Schema layer** ([`schema`]): AlkType kind detection, annotation
//! parsing, `$ref` normalization, endianness.
//! - **BAST parser** ([`bast`]): Typed tree over a BAST document —
//! `BastDoc`/`BastDef`/`BastStruct`/`BastField`/`BastType`/etc.
//! Borrows from the source `serde_json::Value` without cloning field
//! data.
//! - **Layout engine** ([`offset_map`], [`layout_builder`],
//! [`sequential_reader`]): Two layout modes — aligned static for
//! mmap-friendly formats, packed sequential for protocol wire formats.
//! All three consume the BAST typed tree.
//! - **Data access** ([`data_access`]): Typed read/write at computed
//! offsets, zero-copy for fixed-size types.
//! - **TUnion dispatch** ([`tunion`]): Byte-offset and field-name
//! discriminator dispatch.
//! discriminator dispatch over `BastUnion`.
//! - **Validation** ([`validation`]): Custom keyword validators for all
//! 19 `AlkType:*` kinds, delegated to the `jsonschema` crate.
//! 19 `AlkType:*` kinds, delegated to the `jsonschema` crate. (Step 5
//! replaces the `validate_bytes` path with a BAST-native validator;
//! step 6 reworks `validate_json` to use a consumer-provided standard
//! JSON Schema.)
//! - **Builder** ([`builder`]): Fluent Rust API for constructing alktype
//! JSON Schemas at runtime, producing `serde_json::Value` (ADR-009).
//! (Step 7 converts the builder to emit BAST JSON.)
//! - **Materialize** ([`materialize`]): Materialize a `serde_json::Value`
//! tree from a binary buffer by walking the schema. Used by
//! tree from a binary buffer by walking the BAST typed tree. Used by
//! `AlkTypeEngine::validate_bytes` (ADR-010).
//! - **Engine** ([`engine`]): `AlkTypeEngine` — the compiled form of a
//! schema, combining layout and validation.
//! BAST document, combining layout and validation.
#[macro_use]
mod macros;
+610 -642
View File
File diff suppressed because it is too large. Load diff
+405 -417
View File
File diff suppressed because it is too large. Load diff
+748 -832
View File
File diff suppressed because it is too large. Load diff
+172 -274
View File
@@ -3,16 +3,16 @@
//! TUnion supports two discriminator kinds: byte-offset (protocol
//! dispatch, e.g., SFTP type bytes) and field-name (typedef.ts string
//! pattern). This module reads the discriminator value from a byte
//! buffer, looks up the variant schema in the union's `mapping`, and
//! buffer, looks up the variant type in the union's `mapping`, and
//! reports the offset where the variant struct begins.
//!
//! All reads go through [`crate::data_access`] so bounds checks and
//! endianness handling are uniform with the rest of the engine.
use crate::bast::{BastDiscriminator, BastType, BastUnion};
use crate::data_access::{read_enum, read_string, read_u16, read_u32, read_u8};
use crate::error::AlkTypeError;
use crate::schema::{get_alktype_kind, parse_discriminator, DiscriminatorKind, Endian, AlkTypeKind, DISCRIMINATOR_PATH, U32_SIZE};
use serde_json::Value;
use crate::schema::{AlkTypeKind, Endian, DISCRIMINATOR_PATH, U32_SIZE};
const STRING_PREFIX_SIZE: usize = 4;
@@ -39,21 +39,19 @@ pub struct UnionDispatch {
///
/// # Errors
///
/// - [`AlkTypeError::Schema`] if the discriminator annotation is missing
/// or malformed, or if the discriminator `type` is not one of
/// `AlkType:Uint8` / `AlkType:Uint16` / `AlkType:Uint32`.
/// - [`AlkTypeError::Schema`] if the union does not have a byte-offset
/// discriminator.
/// - [`AlkTypeError::Access`] if the buffer is too short to contain the
/// discriminator, or if the read value is not present in the union's
/// `mapping`.
pub fn read_byte_discriminator(
buffer: &[u8],
union_schema: &Value,
union_node: &BastUnion<'_>,
endian: Endian,
) -> Result<UnionDispatch, AlkTypeError> {
let disc = parse_discriminator(union_schema)?;
let (offset, disc_type) = match disc {
DiscriminatorKind::Byte { offset, disc_type } => (offset, disc_type),
DiscriminatorKind::Field { .. } => {
let (offset, disc_type) = match union_node.discriminator() {
BastDiscriminator::Byte { offset, disc_type } => (*offset, *disc_type),
BastDiscriminator::Field { .. } => {
return Err(AlkTypeError::Schema(
"read_byte_discriminator requires a byte-offset discriminator".to_string(),
));
@@ -75,7 +73,7 @@ pub fn read_byte_discriminator(
};
let key = disc_value.to_string();
verify_mapping_key(union_schema, &key, DISCRIMINATOR_PATH, &key)?;
verify_mapping_key(union_node, &key, DISCRIMINATOR_PATH, &key)?;
let variant_offset =
offset
@@ -105,56 +103,44 @@ pub fn read_byte_discriminator(
///
/// # Errors
///
/// - [`AlkTypeError::Schema`] if the discriminator annotation is missing
/// or malformed, the discriminator field is not declared in
/// `properties`, the field has no `AlkType:*` kind, or the field's
/// kind is not one of `AlkType:String` / `AlkType:Uint8` /
/// `AlkType:Enum`.
/// - [`AlkTypeError::Schema`] if the union does not have a field-name
/// discriminator, the discriminator field is not declared in
/// `fields`, or the field's kind is not one of `string` / `uint8` /
/// `enum`.
/// - [`AlkTypeError::Access`] if the buffer is too short to contain the
/// discriminator field, or if the read value is not present in the
/// union's `mapping`.
pub fn read_field_discriminator(
buffer: &[u8],
union_schema: &Value,
union_node: &BastUnion<'_>,
disc_field_offset: usize,
endian: Endian,
) -> Result<UnionDispatch, AlkTypeError> {
let disc = parse_discriminator(union_schema)?;
let name = match disc {
DiscriminatorKind::Field { name } => name,
DiscriminatorKind::Byte { .. } => {
let name = match union_node.discriminator() {
BastDiscriminator::Field { name } => *name,
BastDiscriminator::Byte { .. } => {
return Err(AlkTypeError::Schema(
"read_field_discriminator requires a field-name discriminator".to_string(),
));
}
};
let field_schema = union_schema
.get("properties")
.and_then(Value::as_object)
.and_then(|props| props.get(&name))
.ok_or_else(|| {
AlkTypeError::Schema(format!(
"discriminator field '{name}' not found in union properties"
))
})?;
let disc_field = union_node.fields().iter().find(|f| f.name() == name).ok_or_else(|| {
AlkTypeError::Schema(format!(
"discriminator field '{name}' not found in union fields"
))
})?;
let kind = get_alktype_kind(field_schema)
.and_then(|s| s.parse::<AlkTypeKind>().ok())
.ok_or_else(|| {
AlkTypeError::Schema(format!(
"discriminator field '{name}' has no AlkType:* kind"
))
})?;
let kind = disc_field.ty().alk_kind();
let (key, discriminator_field_size) = match kind {
AlkTypeKind::String => {
let s = read_string(buffer, disc_field_offset, &name, endian)?;
let s = read_string(buffer, disc_field_offset, name, endian)?;
let size =
STRING_PREFIX_SIZE
.checked_add(s.len())
.ok_or_else(|| AlkTypeError::Access {
field_path: name.clone(),
field_path: name.to_string(),
reason: format!(
"string prefix {STRING_PREFIX_SIZE} + data length {} overflows usize",
s.len()
@@ -163,11 +149,11 @@ pub fn read_field_discriminator(
(s.to_string(), size)
}
AlkTypeKind::Uint8 => {
let v = read_u8(buffer, disc_field_offset, &name)?;
let v = read_u8(buffer, disc_field_offset, name)?;
(v.to_string(), 1)
}
AlkTypeKind::Enum => {
let v = read_enum(buffer, disc_field_offset, &name, endian)?;
let v = read_enum(buffer, disc_field_offset, name, endian)?;
(v.to_string(), U32_SIZE)
}
other => {
@@ -177,12 +163,12 @@ pub fn read_field_discriminator(
}
};
verify_mapping_key(union_schema, &key, &name, &key)?;
verify_mapping_key(union_node, &key, name, &key)?;
let variant_offset = disc_field_offset
.checked_add(discriminator_field_size)
.ok_or_else(|| AlkTypeError::Access {
field_path: name.clone(),
field_path: name.to_string(),
reason: format!(
"disc_field_offset {disc_field_offset} + discriminator_field_size {discriminator_field_size} overflows usize"
),
@@ -195,63 +181,39 @@ pub fn read_field_discriminator(
})
}
/// Look up a variant schema from the union's mapping.
/// Look up a variant type from the union's mapping.
///
/// Returns the variant schema. Inline schemas are returned directly.
/// `$ref` pointers of the form `"#/$defs/<name>"` are resolved against
/// the `union_schema`'s own `$defs` block (when the union schema is the
/// schema root). For nested unions whose `$defs` live on an ancestor,
/// the caller (typically `AlkTypeEngine::compile`) is expected to
/// resolve refs before reaching this function, or to inline the
/// variant schemas into the mapping at load time.
/// Returns the variant [`BastType`]. Inline struct/union/enum types are
/// returned directly; `$ref` pointers are returned as
/// [`BastType::Ref`] — the caller resolves them via
/// [`crate::bast::BastDoc::resolve_typeref`] when a concrete definition is needed.
///
/// # Errors
///
/// - [`AlkTypeError::Schema`] if the union has no `mapping` object, the
/// `key` is not present, a `$ref` is malformed, or a `$ref` cannot be
/// resolved against the union schema's own `$defs`.
pub fn resolve_variant<'a>(union_schema: &'a Value, key: &str) -> Result<&'a Value, AlkTypeError> {
let mapping = union_schema
.get("mapping")
.and_then(Value::as_object)
.ok_or_else(|| AlkTypeError::Schema("union is missing 'mapping' object".to_string()))?;
let variant = mapping
.get(key)
.ok_or_else(|| AlkTypeError::Schema(format!("unknown mapping key: {key}")))?;
let ref_str = match variant.get("$ref").and_then(Value::as_str) {
Some(r) => r,
None => return Ok(variant),
};
let pointer = ref_str
.strip_prefix('#')
.ok_or_else(|| AlkTypeError::Schema(format!("unsupported $ref form: {ref_str}")))?;
let resolved = resolve_json_pointer(union_schema, pointer).ok_or_else(|| {
AlkTypeError::Schema(format!(
"cannot resolve $ref {ref_str} against union schema; ensure refs are inlined or the union schema contains $defs"
))
})?;
Ok(resolved)
/// - [`AlkTypeError::Schema`] if the union has no `mapping` entries or
/// the `key` is not present.
pub fn resolve_variant<'a>(
union_node: &'a BastUnion<'a>,
key: &str,
) -> Result<&'a BastType<'a>, AlkTypeError> {
union_node
.variant_for(key)
.ok_or_else(|| AlkTypeError::Schema(format!("unknown mapping key: {key}")))
}
/// Get the discriminator size in bytes for a byte-offset discriminator.
///
/// Returns 1 for `AlkType:Uint8`, 2 for `AlkType:Uint16`, and 4 for
/// `AlkType:Uint32`. Field-name discriminators have no fixed size and
/// produce a [`AlkTypeError::Schema`].
/// Returns 1 for `uint8`, 2 for `uint16`, and 4 for `uint32`.
/// Field-name discriminators have no fixed size and produce a
/// [`AlkTypeError::Schema`].
///
/// # Errors
///
/// - [`AlkTypeError::Schema`] if the discriminator annotation is
/// missing/malformed, the discriminator `type` is unsupported, or the
/// discriminator is a field-name discriminator.
pub fn discriminator_size(union_schema: &Value) -> Result<usize, AlkTypeError> {
let disc = parse_discriminator(union_schema)?;
match disc {
DiscriminatorKind::Byte { disc_type, .. } => match disc_type {
/// - [`AlkTypeError::Schema`] if the discriminator is a field-name
/// discriminator.
pub fn discriminator_size(union_node: &BastUnion<'_>) -> Result<usize, AlkTypeError> {
match union_node.discriminator() {
BastDiscriminator::Byte { disc_type, .. } => match disc_type {
AlkTypeKind::Uint8 => Ok(1),
AlkTypeKind::Uint16 => Ok(2),
AlkTypeKind::Uint32 => Ok(4),
@@ -259,24 +221,19 @@ pub fn discriminator_size(union_schema: &Value) -> Result<usize, AlkTypeError> {
"unsupported byte discriminator type: {other}"
))),
},
DiscriminatorKind::Field { .. } => Err(AlkTypeError::Schema(
BastDiscriminator::Field { .. } => Err(AlkTypeError::Schema(
"field-name discriminator has no fixed size".to_string(),
)),
}
}
fn verify_mapping_key(
union_schema: &Value,
union_node: &BastUnion<'_>,
key: &str,
field_path: &str,
raw_value: &str,
) -> Result<(), AlkTypeError> {
let in_mapping = union_schema
.get("mapping")
.and_then(Value::as_object)
.map(|m| m.contains_key(key))
.unwrap_or(false);
if in_mapping {
if union_node.variant_for(key).is_some() {
Ok(())
} else {
Err(AlkTypeError::Access {
@@ -286,84 +243,64 @@ fn verify_mapping_key(
}
}
fn resolve_json_pointer<'a>(root: &'a Value, pointer: &str) -> Option<&'a Value> {
if pointer.is_empty() {
return Some(root);
}
let trimmed = pointer.strip_prefix('/')?;
let mut current = root;
for unescaped in trimmed.split('/') {
let segment = unescape_json_pointer_token(unescaped)?;
current = current.get(&segment)?;
}
Some(current)
}
fn unescape_json_pointer_token(token: &str) -> Option<String> {
let mut out = String::with_capacity(token.len());
let mut chars = token.chars();
while let Some(c) = chars.next() {
match c {
'~' => match chars.next() {
Some('0') => out.push('~'),
Some('1') => out.push('/'),
_ => return None,
},
other => out.push(other),
}
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bast::BastDoc;
use serde_json::json;
const LE: Endian = Endian::Little;
const BE: Endian = Endian::Big;
fn byte_union_schema(offset: usize, disc_type: &str) -> Value {
fn doc_union<'a>(root: &'a serde_json::Value, name: &'a str) -> BastUnion<'a> {
let doc = BastDoc::new(root, name).expect("bast doc");
match doc.root_def().kind() {
crate::bast::BastDefKind::Union(u) => u.clone(),
_ => panic!("root must be a union"),
}
}
fn byte_union_root(offset: usize, disc_type: &str) -> serde_json::Value {
json!({
"AlkType:Union": true,
"discriminator": {"kind": "byte", "offset": offset, "type": disc_type},
"mapping": {
"5": {"AlkType:Struct": true, "properties": {"id": {"AlkType:Uint32": true}}},
"6": {"AlkType:Struct": true, "properties": {"len": {"AlkType:Uint16": true}}}
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "byte", "offset": offset, "type": disc_type },
"mapping": {
"5": { "kind": "struct", "fields": [ { "name": "id", "kind": "uint32" } ] },
"6": { "kind": "struct", "fields": [ { "name": "len", "kind": "uint16" } ] }
}
}
}
})
}
fn field_union_schema(field_name: &str, field_kind: &str) -> Value {
let field_schema = match field_kind {
"AlkType:Enum" => json!({
"AlkType:Enum": true,
"enum": ["read", "write"]
}),
_ => json!({field_kind: true}),
};
fn field_union_root(field_name: &str, field_kind: &str) -> serde_json::Value {
let (key_a, key_b) = match field_kind {
"AlkType:String" => ("read", "write"),
"string" => ("read", "write"),
_ => ("0", "1"),
};
json!({
"AlkType:Union": true,
"discriminator": {"kind": "field", "name": field_name},
"properties": {
field_name: field_schema
},
"mapping": {
key_a: {"AlkType:Struct": true, "properties": {"n": {"AlkType:Uint32": true}}},
key_b: {"AlkType:Struct": true, "properties": {"m": {"AlkType:Uint16": true}}}
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "field", "name": field_name },
"fields": [ { "name": field_name, "kind": field_kind } ],
"mapping": {
key_a: { "kind": "struct", "fields": [ { "name": "n", "kind": "uint32" } ] },
key_b: { "kind": "struct", "fields": [ { "name": "m", "kind": "uint16" } ] }
}
}
}
})
}
#[test]
fn read_byte_discriminator_uint8_default_offset() {
let schema = byte_union_schema(0, "AlkType:Uint8");
let root = byte_union_root(0, "uint8");
let u = doc_union(&root, "U");
let buf = [5u8, 0xAA, 0xBB, 0xCC];
let d = read_byte_discriminator(&buf, &schema, LE).expect("read");
let d = read_byte_discriminator(&buf, &u, LE).expect("read");
assert_eq!(d.key, "5");
assert_eq!(d.variant_offset, 1);
assert_eq!(d.discriminator_size, 1);
@@ -371,19 +308,21 @@ mod tests {
#[test]
fn read_byte_discriminator_uint8_big_endian() {
let schema = byte_union_schema(0, "AlkType:Uint8");
let root = byte_union_root(0, "uint8");
let u = doc_union(&root, "U");
let buf = [6u8];
let d = read_byte_discriminator(&buf, &schema, BE).expect("read");
let d = read_byte_discriminator(&buf, &u, BE).expect("read");
assert_eq!(d.key, "6");
assert_eq!(d.variant_offset, 1);
}
#[test]
fn read_byte_discriminator_uint16_little_endian() {
let schema = byte_union_schema(2, "AlkType:Uint16");
let root = byte_union_root(2, "uint16");
let u = doc_union(&root, "U");
let mut buf = vec![0u8; 4];
buf[2..4].copy_from_slice(&5u16.to_le_bytes());
let d = read_byte_discriminator(&buf, &schema, LE).expect("read");
let d = read_byte_discriminator(&buf, &u, LE).expect("read");
assert_eq!(d.key, "5");
assert_eq!(d.variant_offset, 4);
assert_eq!(d.discriminator_size, 2);
@@ -391,19 +330,21 @@ mod tests {
#[test]
fn read_byte_discriminator_uint16_big_endian() {
let schema = byte_union_schema(0, "AlkType:Uint16");
let root = byte_union_root(0, "uint16");
let u = doc_union(&root, "U");
let buf = [0x00, 0x06, 0xAA, 0xBB];
let d = read_byte_discriminator(&buf, &schema, BE).expect("read");
let d = read_byte_discriminator(&buf, &u, BE).expect("read");
assert_eq!(d.key, "6");
assert_eq!(d.variant_offset, 2);
}
#[test]
fn read_byte_discriminator_uint32_little_endian() {
let schema = byte_union_schema(0, "AlkType:Uint32");
let root = byte_union_root(0, "uint32");
let u = doc_union(&root, "U");
let mut buf = vec![0u8; 8];
buf[0..4].copy_from_slice(&5u32.to_le_bytes());
let d = read_byte_discriminator(&buf, &schema, LE).expect("read");
let d = read_byte_discriminator(&buf, &u, LE).expect("read");
assert_eq!(d.key, "5");
assert_eq!(d.variant_offset, 4);
assert_eq!(d.discriminator_size, 4);
@@ -411,19 +352,21 @@ mod tests {
#[test]
fn read_byte_discriminator_uint32_big_endian() {
let schema = byte_union_schema(0, "AlkType:Uint32");
let root = byte_union_root(0, "uint32");
let u = doc_union(&root, "U");
let mut buf = vec![0u8; 8];
buf[0..4].copy_from_slice(&6u32.to_be_bytes());
let d = read_byte_discriminator(&buf, &schema, BE).expect("read");
let d = read_byte_discriminator(&buf, &u, BE).expect("read");
assert_eq!(d.key, "6");
assert_eq!(d.variant_offset, 4);
}
#[test]
fn read_byte_discriminator_unknown_value_is_access_error() {
let schema = byte_union_schema(0, "AlkType:Uint8");
let root = byte_union_root(0, "uint8");
let u = doc_union(&root, "U");
let buf = [99u8];
let err = read_byte_discriminator(&buf, &schema, LE).unwrap_err();
let err = read_byte_discriminator(&buf, &u, LE).unwrap_err();
match err {
AlkTypeError::Access { field_path, reason } => {
assert_eq!(field_path, DISCRIMINATOR_PATH);
@@ -435,29 +378,32 @@ mod tests {
#[test]
fn read_byte_discriminator_buffer_too_short_is_access_error() {
let schema = byte_union_schema(4, "AlkType:Uint32");
let root = byte_union_root(4, "uint32");
let u = doc_union(&root, "U");
let buf = [0u8; 2];
let err = read_byte_discriminator(&buf, &schema, LE).unwrap_err();
let err = read_byte_discriminator(&buf, &u, LE).unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }));
}
#[test]
fn read_byte_discriminator_field_kind_is_schema_error() {
let schema = field_union_schema("type", "AlkType:String");
let root = field_union_root("type", "string");
let u = doc_union(&root, "U");
let buf = [0u8; 16];
let err = read_byte_discriminator(&buf, &schema, LE).unwrap_err();
let err = read_byte_discriminator(&buf, &u, LE).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)));
}
#[test]
fn read_field_discriminator_string() {
let schema = field_union_schema("type", "AlkType:String");
let root = field_union_root("type", "string");
let u = doc_union(&root, "U");
let mut buf = vec![0u8; 32];
let value = "read";
let len_bytes = (value.len() as u32).to_le_bytes();
buf[0..4].copy_from_slice(&len_bytes);
buf[4..4 + value.len()].copy_from_slice(value.as_bytes());
let d = read_field_discriminator(&buf, &schema, 0, LE).expect("read");
let d = read_field_discriminator(&buf, &u, 0, LE).expect("read");
assert_eq!(d.key, "read");
assert_eq!(d.variant_offset, 4 + value.len());
assert_eq!(d.discriminator_size, 4 + value.len());
@@ -465,45 +411,37 @@ mod tests {
#[test]
fn read_field_discriminator_uint8() {
let schema = field_union_schema("type", "AlkType:Uint8");
let root = field_union_root("type", "uint8");
let u = doc_union(&root, "U");
let mut buf = vec![0u8; 8];
buf[0] = 0;
let d = read_field_discriminator(&buf, &schema, 0, LE).expect("read");
let d = read_field_discriminator(&buf, &u, 0, LE).expect("read");
assert_eq!(d.key, "0");
assert_eq!(d.variant_offset, 1);
assert_eq!(d.discriminator_size, 1);
}
#[test]
fn read_field_discriminator_enum() {
let schema = field_union_schema("type", "AlkType:Enum");
let mut buf = vec![0u8; 8];
buf[0..4].copy_from_slice(&0u32.to_le_bytes());
let d = read_field_discriminator(&buf, &schema, 0, LE).expect("read");
assert_eq!(d.key, "0");
assert_eq!(d.variant_offset, 4);
assert_eq!(d.discriminator_size, 4);
}
#[test]
fn read_field_discriminator_string_big_endian() {
let schema = field_union_schema("type", "AlkType:String");
let root = field_union_root("type", "string");
let u = doc_union(&root, "U");
let mut buf = vec![0u8; 32];
let value = "write";
let len_bytes = (value.len() as u32).to_be_bytes();
buf[0..4].copy_from_slice(&len_bytes);
buf[4..4 + value.len()].copy_from_slice(value.as_bytes());
let d = read_field_discriminator(&buf, &schema, 0, BE).expect("read");
let d = read_field_discriminator(&buf, &u, 0, BE).expect("read");
assert_eq!(d.key, "write");
assert_eq!(d.variant_offset, 4 + value.len());
}
#[test]
fn read_field_discriminator_unknown_value_is_access_error() {
let schema = field_union_schema("type", "AlkType:Uint8");
let root = field_union_root("type", "uint8");
let u = doc_union(&root, "U");
let mut buf = vec![0u8; 8];
buf[0] = 99;
let err = read_field_discriminator(&buf, &schema, 0, LE).unwrap_err();
let err = read_field_discriminator(&buf, &u, 0, LE).unwrap_err();
match err {
AlkTypeError::Access { field_path, reason } => {
assert_eq!(field_path, "type");
@@ -515,131 +453,91 @@ mod tests {
#[test]
fn read_field_discriminator_field_not_found_is_schema_error() {
let schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "field", "name": "missing"},
"properties": {"other": {"AlkType:Uint8": true}},
"mapping": {"5": {"AlkType:Struct": true}}
let root = json!({
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "field", "name": "missing" },
"fields": [ { "name": "other", "kind": "uint8" } ],
"mapping": { "5": { "kind": "struct", "fields": [] } }
}
}
});
let u = doc_union(&root, "U");
let buf = [0u8; 4];
let err = read_field_discriminator(&buf, &schema, 0, LE).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)));
}
#[test]
fn read_field_discriminator_no_alktype_kind_is_schema_error() {
let schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "field", "name": "type"},
"properties": {"type": {"type": "string"}},
"mapping": {"read": {"AlkType:Struct": true}}
});
let buf = [0u8; 4];
let err = read_field_discriminator(&buf, &schema, 0, LE).unwrap_err();
let err = read_field_discriminator(&buf, &u, 0, LE).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)));
}
#[test]
fn read_field_discriminator_unsupported_kind_is_schema_error() {
let schema = field_union_schema("type", "AlkType:Float32");
let root = json!({
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "field", "name": "type" },
"fields": [ { "name": "type", "kind": "float32" } ],
"mapping": { "read": { "kind": "struct", "fields": [] } }
}
}
});
let u = doc_union(&root, "U");
let buf = [0u8; 8];
let err = read_field_discriminator(&buf, &schema, 0, LE).unwrap_err();
let err = read_field_discriminator(&buf, &u, 0, LE).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)));
}
#[test]
fn read_field_discriminator_byte_kind_is_schema_error() {
let schema = byte_union_schema(0, "AlkType:Uint8");
let root = byte_union_root(0, "uint8");
let u = doc_union(&root, "U");
let buf = [5u8];
let err = read_field_discriminator(&buf, &schema, 0, LE).unwrap_err();
let err = read_field_discriminator(&buf, &u, 0, LE).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)));
}
#[test]
fn resolve_variant_inline_schema() {
let schema = byte_union_schema(0, "AlkType:Uint8");
let variant = resolve_variant(&schema, "5").expect("resolve");
assert_eq!(
variant.get("AlkType:Struct").and_then(Value::as_bool),
Some(true)
);
}
#[test]
fn resolve_variant_ref_against_own_defs() {
let schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "byte"},
"mapping": {
"5": {"$ref": "#/$defs/Read"}
},
"$defs": {
"Read": {"AlkType:Struct": true, "properties": {"id": {"AlkType:Uint32": true}}}
}
});
let variant = resolve_variant(&schema, "5").expect("resolve");
assert_eq!(
variant.get("AlkType:Struct").and_then(Value::as_bool),
Some(true)
);
fn resolve_variant_returns_variant_type() {
let root = byte_union_root(0, "uint8");
let u = doc_union(&root, "U");
let variant = resolve_variant(&u, "5").expect("resolve");
assert!(matches!(variant, BastType::Struct(_)));
}
#[test]
fn resolve_variant_unknown_key_is_schema_error() {
let schema = byte_union_schema(0, "AlkType:Uint8");
let err = resolve_variant(&schema, "999").unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)));
}
#[test]
fn resolve_variant_missing_mapping_is_schema_error() {
let schema = json!({"AlkType:Union": true, "discriminator": {"kind": "byte"}});
let err = resolve_variant(&schema, "5").unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)));
}
#[test]
fn resolve_variant_unresolvable_ref_is_schema_error() {
let schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "byte"},
"mapping": {
"5": {"$ref": "#/$defs/Read"}
}
});
let err = resolve_variant(&schema, "5").unwrap_err();
let root = byte_union_root(0, "uint8");
let u = doc_union(&root, "U");
let err = resolve_variant(&u, "999").unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)));
}
#[test]
fn discriminator_size_uint8() {
let schema = byte_union_schema(0, "AlkType:Uint8");
assert_eq!(discriminator_size(&schema).unwrap(), 1);
let root = byte_union_root(0, "uint8");
let u = doc_union(&root, "U");
assert_eq!(discriminator_size(&u).unwrap(), 1);
}
#[test]
fn discriminator_size_uint16() {
let schema = byte_union_schema(0, "AlkType:Uint16");
assert_eq!(discriminator_size(&schema).unwrap(), 2);
let root = byte_union_root(0, "uint16");
let u = doc_union(&root, "U");
assert_eq!(discriminator_size(&u).unwrap(), 2);
}
#[test]
fn discriminator_size_uint32() {
let schema = byte_union_schema(0, "AlkType:Uint32");
assert_eq!(discriminator_size(&schema).unwrap(), 4);
let root = byte_union_root(0, "uint32");
let u = doc_union(&root, "U");
assert_eq!(discriminator_size(&u).unwrap(), 4);
}
#[test]
fn discriminator_size_field_kind_is_schema_error() {
let schema = field_union_schema("type", "AlkType:String");
let err = discriminator_size(&schema).unwrap_err();
let root = field_union_root("type", "string");
let u = doc_union(&root, "U");
let err = discriminator_size(&u).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)));
}
#[test]
fn discriminator_size_missing_discriminator_is_schema_error() {
let schema = json!({"AlkType:Union": true});
let err = discriminator_size(&schema).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)));
}
}
}
+161 -170
View File
@@ -4,27 +4,34 @@
//! accessors, validation convenience methods, and the aligned-mode
//! `read_field` / `write_field` round-trip for the fixed-size primitive
//! kinds and length-prefixed `String` / `Bytes`.
//!
//! All schemas are BAST documents (`{ "$defs": { ... } }` with `kind`-
//! based vocabulary).
use alktype::*;
use serde_json::json;
fn mixed_fixed_struct_schema() -> serde_json::Value {
fn mixed_fixed_struct_doc() -> serde_json::Value {
json!({
"AlkType:Struct": true,
"endian": "little",
"properties": {
"flag": { "AlkType:Uint8": true },
"id": { "AlkType:Uint32": true },
"score": { "AlkType:Float32": true },
"tag": { "AlkType:String": true }
"$defs": {
"S": {
"kind": "struct",
"endian": "little",
"fields": [
{ "name": "flag", "kind": "uint8" },
{ "name": "id", "kind": "uint32" },
{ "name": "score", "kind": "float32" },
{ "name": "tag", "kind": "string" }
]
}
}
})
}
#[test]
fn compile_aligned_builds_engine_with_offset_map() -> Result<(), AlkTypeError> {
let mut schema = mixed_fixed_struct_schema();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
let doc = mixed_fixed_struct_doc();
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned)?;
assert_eq!(engine.mode(), LayoutMode::Aligned);
assert!(engine.offset_map().is_some());
assert!(engine.layout_builder().is_none());
@@ -34,8 +41,8 @@ fn compile_aligned_builds_engine_with_offset_map() -> Result<(), AlkTypeError> {
#[test]
fn compile_packed_builds_engine_with_builder_and_reader() -> Result<(), AlkTypeError> {
let mut schema = mixed_fixed_struct_schema();
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed)?;
let doc = mixed_fixed_struct_doc();
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed)?;
assert_eq!(engine.mode(), LayoutMode::Packed);
assert!(engine.offset_map().is_none());
assert!(engine.layout_builder().is_some());
@@ -44,147 +51,96 @@ fn compile_packed_builds_engine_with_builder_and_reader() -> Result<(), AlkTypeE
}
#[test]
fn compile_normalizes_bare_name_refs() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"child": { "$ref": "Child" }
},
fn compile_resolves_ref_fields() -> Result<(), AlkTypeError> {
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "child", "kind": { "$ref": "#/$defs/Child" } }
]
},
"Child": {
"AlkType:Struct": true,
"properties": { "x": { "AlkType:Uint8": true } }
"kind": "struct",
"fields": [ { "name": "x", "kind": "uint8" } ]
}
}
});
let _engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed)?;
assert_eq!(
schema["properties"]["child"]["$ref"],
json!("#/$defs/Child")
);
let _engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed)?;
Ok(())
}
#[test]
fn compile_leaves_full_pointer_refs_unchanged() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"child": { "$ref": "#/$defs/Child" }
},
"$defs": {
"Child": {
"AlkType:Struct": true,
"properties": { "x": { "AlkType:Uint8": true } }
}
}
});
let _engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed)?;
assert_eq!(
schema["properties"]["child"]["$ref"],
json!("#/$defs/Child")
);
Ok(())
fn compile_returns_schema_error_when_no_defs() {
let doc = json!({ "type": "object", "properties": {} });
let err = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn compile_returns_schema_error_when_no_alktype_kind() {
let mut schema = json!({ "type": "object", "properties": {} });
let err = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).unwrap_err();
fn compile_returns_schema_error_for_missing_root() {
let doc = json!({ "$defs": { "Other": { "kind": "struct", "fields": [] } } });
let err = AlkTypeEngine::compile(&doc, "Missing", LayoutMode::Aligned).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn endian_parsed_from_schema_big() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"endian": "big",
"properties": { "id": { "AlkType:Uint32": true } }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"endian": "big",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed)?;
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed)?;
assert_eq!(engine.endian(), Endian::Big);
Ok(())
}
#[test]
fn endian_defaults_to_little() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"properties": { "id": { "AlkType:Uint32": true } }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed)?;
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed)?;
assert_eq!(engine.endian(), Endian::Little);
Ok(())
}
#[test]
fn validate_json_accepts_valid_instance() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"type": "object",
"properties": {
"id": { "AlkType:Uint32": true, "type": "integer" }
},
"required": ["id"]
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
assert!(engine.validate_json(&json!({"id": 42})).is_ok());
Ok(())
}
#[test]
fn validate_json_rejects_invalid_instance() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"type": "object",
"properties": {
"id": { "AlkType:Uint32": true, "type": "integer" }
},
"required": ["id"]
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
let err = engine.validate_json(&json!({"id": -1})).unwrap_err();
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
Ok(())
}
#[test]
fn is_valid_json_returns_bool() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"type": "object",
"properties": {
"id": { "AlkType:Uint32": true, "type": "integer" }
},
"required": ["id"]
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
assert!(engine.is_valid_json(&json!({"id": 42})));
assert!(!engine.is_valid_json(&json!({"id": -1})));
Ok(())
}
#[test]
fn read_write_aligned_round_trips_all_fixed_size_kinds() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"endian": "little",
"properties": {
"i8": { "AlkType:Int8": true },
"u8": { "AlkType:Uint8": true },
"i16": { "AlkType:Int16": true },
"u16": { "AlkType:Uint16": true },
"i32": { "AlkType:Int32": true },
"u32": { "AlkType:Uint32": true },
"i64": { "AlkType:Int64": true },
"u64": { "AlkType:Uint64": true },
"f32": { "AlkType:Float32": true },
"f64": { "AlkType:Float64": true },
"b": { "AlkType:Boolean": true },
"e": { "AlkType:Enum": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"endian": "little",
"fields": [
{ "name": "i8", "kind": "int8" },
{ "name": "u8", "kind": "uint8" },
{ "name": "i16", "kind": "int16" },
{ "name": "u16", "kind": "uint16" },
{ "name": "i32", "kind": "int32" },
{ "name": "u32", "kind": "uint32" },
{ "name": "i64", "kind": "int64" },
{ "name": "u64", "kind": "uint64" },
{ "name": "f32", "kind": "float32" },
{ "name": "f64", "kind": "float64" },
{ "name": "b", "kind": "bool" },
{ "name": "e", "kind": { "$ref": "#/$defs/E" } }
]
},
"E": { "kind": "enum", "values": ["A", "B", "C"] }
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned)?;
let offset_map = engine.offset_map().expect("aligned mode has offset_map");
let mut buffer = vec![0u8; offset_map.total_size()];
@@ -230,13 +186,15 @@ fn read_write_aligned_round_trips_all_fixed_size_kinds() -> Result<(), AlkTypeEr
#[test]
fn read_write_aligned_round_trips_string() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"name": { "AlkType:String": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "name", "kind": "string" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned)?;
let offset_map = engine.offset_map().expect("aligned mode has offset_map");
let mut buffer = vec![0u8; offset_map.total_size() + 64];
engine.write_field(&mut buffer, "name", &FieldValue::String("hello world"))?;
@@ -249,13 +207,15 @@ fn read_write_aligned_round_trips_string() -> Result<(), AlkTypeError> {
#[test]
fn read_write_aligned_round_trips_bytes() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"blob": { "AlkType:Bytes": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "blob", "kind": "bytes" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned)?;
let offset_map = engine.offset_map().expect("aligned mode has offset_map");
let payload = b"the quick brown fox".to_vec();
let mut buffer = vec![0u8; offset_map.total_size() + payload.len()];
@@ -269,11 +229,15 @@ fn read_write_aligned_round_trips_bytes() -> Result<(), AlkTypeError> {
#[test]
fn read_field_returns_access_error_in_packed_mode() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"properties": { "id": { "AlkType:Uint32": true } }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed)?;
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed)?;
let buffer = [0u8; 4];
let err = engine.read_field(&buffer, "id").unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
@@ -282,11 +246,15 @@ fn read_field_returns_access_error_in_packed_mode() -> Result<(), AlkTypeError>
#[test]
fn write_field_returns_access_error_in_packed_mode() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"properties": { "id": { "AlkType:Uint32": true } }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed)?;
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed)?;
let mut buffer = [0u8; 4];
let err = engine
.write_field(&mut buffer, "id", &FieldValue::U32(1))
@@ -297,11 +265,15 @@ fn write_field_returns_access_error_in_packed_mode() -> Result<(), AlkTypeError>
#[test]
fn read_field_returns_offset_error_for_missing_path() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"properties": { "id": { "AlkType:Uint32": true } }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned)?;
let buffer = [0u8; 8];
let err = engine.read_field(&buffer, "missing").unwrap_err();
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
@@ -310,11 +282,15 @@ fn read_field_returns_offset_error_for_missing_path() -> Result<(), AlkTypeError
#[test]
fn write_field_returns_offset_error_for_missing_path() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"properties": { "id": { "AlkType:Uint32": true } }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned)?;
let mut buffer = [0u8; 8];
let err = engine
.write_field(&mut buffer, "missing", &FieldValue::U32(1))
@@ -324,30 +300,38 @@ fn write_field_returns_offset_error_for_missing_path() -> Result<(), AlkTypeErro
}
#[test]
fn read_field_returns_access_error_for_composite_types() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"vals": {
"AlkType:Array": true,
"items": { "AlkType:Uint32": true }
fn read_field_returns_error_for_composite_types() -> Result<(), AlkTypeError> {
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "vals", "kind": { "kind": "array", "element": "uint32", "count": 2 } }
]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned)?;
let buffer = [0u8; 8];
let err = engine.read_field(&buffer, "vals").unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
assert!(
matches!(err, AlkTypeError::Access { .. } | AlkTypeError::Offset { .. }),
"got {err:?}"
);
Ok(())
}
#[test]
fn write_field_returns_access_error_for_composite_value() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"properties": { "id": { "AlkType:Uint32": true } }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned)?;
let mut buffer = [0u8; 8];
let err = engine
.write_field(&mut buffer, "id", &FieldValue::Struct { start: 0, end: 4 })
@@ -358,19 +342,26 @@ fn write_field_returns_access_error_for_composite_value() -> Result<(), AlkTypeE
#[test]
fn read_field_aligned_reads_nested_struct_byte_range() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"header": {
"AlkType:Struct": true,
"properties": {
"version": { "AlkType:Uint8": true },
"magic": { "AlkType:Uint32": true }
}
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{
"name": "header",
"kind": {
"kind": "struct",
"fields": [
{ "name": "version", "kind": "uint8" },
{ "name": "magic", "kind": "uint32" }
]
}
}
]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned)?;
let offset_map = engine.offset_map().expect("aligned mode");
let mut buffer = vec![0u8; offset_map.total_size()];
@@ -386,4 +377,4 @@ fn read_field_aligned_reads_nested_struct_byte_range() -> Result<(), AlkTypeErro
FieldValue::U32(0xCAFEBABE)
);
Ok(())
}
}
+155 -125
View File
@@ -2,10 +2,13 @@
//!
//! Exercises the `AlkTypeError` variants across the crate:
//! `Access` (buffer too short, invalid UTF-8, invalid boolean byte,
//! unknown discriminator value), `Schema` (missing AlkType kind,
//! malformed discriminator annotation), and `Offset` (missing
//! variable-length field size in `LayoutBuilder::build`).
//! unknown discriminator value), `Schema` (missing root, malformed
//! discriminator annotation), and `Offset` (missing variable-length
//! field size in `LayoutBuilder::build`).
//!
//! All schemas are BAST documents.
use alktype::bast::BastDoc;
use alktype::data_access;
use alktype::tunion;
use alktype::*;
@@ -149,128 +152,145 @@ fn write_string_buffer_too_short_returns_access_error() {
}
#[test]
fn compile_missing_alktype_kind_returns_schema_error() {
let mut schema = json!({ "type": "object", "properties": {} });
let err = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).unwrap_err();
fn compile_missing_defs_returns_schema_error() {
let doc = json!({ "type": "object", "properties": {} });
let err = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn offset_map_compute_missing_alktype_kind_returns_schema_error() {
let schema = json!({ "type": "object", "properties": {} });
let err = OffsetMap::compute(&schema).unwrap_err();
fn compile_missing_root_returns_schema_error() {
let doc = json!({ "$defs": { "Other": { "kind": "struct", "fields": [] } } });
let err = AlkTypeEngine::compile(&doc, "Missing", LayoutMode::Aligned).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn offset_map_compute_non_struct_top_level_returns_schema_error() {
let schema = json!({ "AlkType:Uint32": true });
let err = OffsetMap::compute(&schema).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
let root = json!({
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "byte", "offset": 0, "type": "uint8" },
"mapping": { "1": { "$ref": "#/$defs/A" } }
},
"A": { "kind": "struct", "fields": [] }
}
});
let doc = BastDoc::new(&root, "U").expect("doc");
let err = OffsetMap::compute(&doc).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)));
}
#[test]
fn layout_builder_new_missing_alktype_kind_returns_schema_error() {
let schema = json!({ "type": "object", "properties": {} });
let err = LayoutBuilder::new(&schema).unwrap_err();
fn layout_builder_new_missing_root_returns_schema_error() {
let root = json!({ "$defs": { "Other": { "kind": "struct", "fields": [] } } });
let err = LayoutBuilder::new(&root, "Missing").unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn layout_builder_new_non_struct_top_level_returns_schema_error() {
let schema = json!({ "AlkType:Uint32": true });
let err = LayoutBuilder::new(&schema).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn parse_discriminator_missing_returns_schema_error() {
let schema = json!({"AlkType:Union": true});
let err = parse_discriminator(&schema).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn parse_discriminator_field_missing_name_returns_schema_error() {
let schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "field"}
let root = json!({
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "byte", "offset": 0, "type": "uint8" },
"mapping": { "1": { "$ref": "#/$defs/A" } }
},
"A": { "kind": "struct", "fields": [] }
}
});
let err = parse_discriminator(&schema).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn parse_discriminator_unknown_kind_returns_schema_error() {
let schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "magic"}
});
let err = parse_discriminator(&schema).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn parse_discriminator_byte_invalid_type_returns_schema_error() {
let schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "byte", "type": "AlkType:Float32"}
});
let err = parse_discriminator(&schema).unwrap_err();
let err = LayoutBuilder::new(&root, "U").unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn read_byte_discriminator_unknown_value_returns_access_error() -> Result<(), AlkTypeError> {
let union_schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "byte", "type": "AlkType:Uint8"},
"mapping": {"5": {"AlkType:Struct": true, "properties": {"x": {"AlkType:Uint8": true}}}}
let root = json!({
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "byte", "type": "uint8" },
"mapping": {
"5": { "kind": "struct", "fields": [ { "name": "x", "kind": "uint8" } ] }
}
}
}
});
let doc = BastDoc::new(&root, "U")?;
let union_def = match doc.root_def().kind() {
alktype::bast::BastDefKind::Union(u) => u,
_ => unreachable!(),
};
let buffer = [99u8, 0x00, 0x00];
let err = tunion::read_byte_discriminator(&buffer, &union_schema, Endian::Little).unwrap_err();
let err = tunion::read_byte_discriminator(&buffer, union_def, Endian::Little).unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
Ok(())
}
#[test]
fn read_byte_discriminator_buffer_too_short_returns_access_error() -> Result<(), AlkTypeError> {
let union_schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "byte", "offset": 4, "type": "AlkType:Uint32"},
"mapping": {"5": {"AlkType:Struct": true, "properties": {"x": {"AlkType:Uint8": true}}}}
let root = json!({
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "byte", "offset": 4, "type": "uint32" },
"mapping": {
"5": { "kind": "struct", "fields": [ { "name": "x", "kind": "uint8" } ] }
}
}
}
});
let doc = BastDoc::new(&root, "U")?;
let union_def = match doc.root_def().kind() {
alktype::bast::BastDefKind::Union(u) => u,
_ => unreachable!(),
};
let buffer = [0u8; 2];
let err = tunion::read_byte_discriminator(&buffer, &union_schema, Endian::Little).unwrap_err();
let err = tunion::read_byte_discriminator(&buffer, union_def, Endian::Little).unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
Ok(())
}
#[test]
fn read_field_discriminator_unknown_value_returns_access_error() -> Result<(), AlkTypeError> {
let union_schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "field", "name": "type"},
"properties": {"type": {"AlkType:Uint8": true}},
"mapping": {"0": {"AlkType:Struct": true, "properties": {"x": {"AlkType:Uint8": true}}}}
let root = json!({
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "field", "name": "type" },
"fields": [ { "name": "type", "kind": "uint8" } ],
"mapping": {
"0": { "kind": "struct", "fields": [ { "name": "x", "kind": "uint8" } ] }
}
}
}
});
let doc = BastDoc::new(&root, "U")?;
let union_def = match doc.root_def().kind() {
alktype::bast::BastDefKind::Union(u) => u,
_ => unreachable!(),
};
let mut buffer = vec![0u8; 8];
buffer[0] = 99;
let err =
tunion::read_field_discriminator(&buffer, &union_schema, 0, Endian::Little).unwrap_err();
tunion::read_field_discriminator(&buffer, union_def, 0, Endian::Little).unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
Ok(())
}
#[test]
fn layout_builder_missing_var_size_returns_offset_error() {
let schema = json!({
"AlkType:Struct": true,
"properties": {
"name": { "AlkType:String": true }
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "name", "kind": "string" } ]
}
}
});
let builder = LayoutBuilder::new(&schema).expect("builder");
let builder = LayoutBuilder::new(&root, "S").expect("builder");
let empty: HashMap<String, usize> = HashMap::new();
let err = builder.build(&empty).unwrap_err();
match err {
@@ -285,39 +305,28 @@ fn layout_builder_missing_var_size_returns_offset_error() {
}
}
#[test]
fn layout_builder_missing_array_data_size_returns_offset_error() {
let schema = json!({
"AlkType:Struct": true,
"properties": {
"vals": {
"AlkType:Array": true,
"items": { "AlkType:Uint32": true }
}
}
});
let builder = LayoutBuilder::new(&schema).expect("builder");
let empty: HashMap<String, usize> = HashMap::new();
let err = builder.build(&empty).unwrap_err();
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
}
#[test]
fn layout_builder_missing_discriminator_value_returns_offset_error() {
let schema = json!({
"AlkType:Struct": true,
"properties": {
"payload": {
"AlkType:Union": true,
"discriminator": {"kind": "byte", "type": "AlkType:Uint8"},
"mapping": {"5": {"$ref": "#/$defs/Read"}}
}
},
let root = json!({
"$defs": {
"Read": {"AlkType:Struct": true, "properties": {"x": {"AlkType:Uint8": true}}}
"S": {
"kind": "struct",
"fields": [
{ "name": "payload", "kind": { "$ref": "#/$defs/Packet" } }
]
},
"Packet": {
"kind": "union",
"discriminator": { "kind": "byte", "type": "uint8" },
"mapping": { "5": { "$ref": "#/$defs/Read" } }
},
"Read": {
"kind": "struct",
"fields": [ { "name": "x", "kind": "uint8" } ]
}
}
});
let builder = LayoutBuilder::new(&schema).expect("builder");
let builder = LayoutBuilder::new(&root, "S").expect("builder");
let empty: HashMap<String, usize> = HashMap::new();
let err = builder.build(&empty).unwrap_err();
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
@@ -325,20 +334,26 @@ fn layout_builder_missing_discriminator_value_returns_offset_error() {
#[test]
fn layout_builder_unknown_discriminator_value_returns_offset_error() {
let schema = json!({
"AlkType:Struct": true,
"properties": {
"payload": {
"AlkType:Union": true,
"discriminator": {"kind": "byte", "type": "AlkType:Uint8"},
"mapping": {"5": {"$ref": "#/$defs/Read"}}
}
},
let root = json!({
"$defs": {
"Read": {"AlkType:Struct": true, "properties": {"x": {"AlkType:Uint8": true}}}
"S": {
"kind": "struct",
"fields": [
{ "name": "payload", "kind": { "$ref": "#/$defs/Packet" } }
]
},
"Packet": {
"kind": "union",
"discriminator": { "kind": "byte", "type": "uint8" },
"mapping": { "5": { "$ref": "#/$defs/Read" } }
},
"Read": {
"kind": "struct",
"fields": [ { "name": "x", "kind": "uint8" } ]
}
}
});
let builder = LayoutBuilder::new(&schema).expect("builder");
let builder = LayoutBuilder::new(&root, "S").expect("builder");
let mut vs = HashMap::new();
vs.insert("payload.__discriminator".to_string(), 99);
let err = builder.build(&vs).unwrap_err();
@@ -352,34 +367,49 @@ fn layout_builder_unknown_discriminator_value_returns_offset_error() {
#[test]
fn sequential_reader_buffer_too_short_returns_access_error() {
let schema = json!({
"AlkType:Struct": true,
"properties": {
"id": { "AlkType:Uint32": true }
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "id", "kind": "uint32" } ]
}
}
});
let buffer = [0u8; 2];
let mut reader = SequentialReader::new(&schema).unwrap();
let mut reader = SequentialReader::new(&root, "S").unwrap();
let err = reader.read_next(&buffer).unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
}
#[test]
fn sequential_reader_unknown_field_returns_schema_error() {
let schema = json!({
"AlkType:Struct": true,
"properties": { "a": { "AlkType:Uint8": true } }
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "a", "kind": "uint8" } ]
}
}
});
let buffer = [0u8; 4];
let mut reader = SequentialReader::new(&schema).unwrap();
let mut reader = SequentialReader::new(&root, "S").unwrap();
let err = reader.read_field(&buffer, "missing").unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn sequential_reader_new_non_struct_returns_schema_error() {
let schema = json!({ "AlkType:Uint32": true });
let err = SequentialReader::new(&schema).unwrap_err();
let root = json!({
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "byte", "offset": 0, "type": "uint8" },
"mapping": { "1": { "$ref": "#/$defs/A" } }
},
"A": { "kind": "struct", "fields": [] }
}
});
let err = SequentialReader::new(&root, "U").unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
@@ -412,4 +442,4 @@ fn read_string_indirect_invalid_utf8_returns_access_error() {
let err = data_access::read_string_indirect(&index, 0, data_region, "name", Endian::Little)
.unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
}
}
+202 -134
View File
@@ -8,7 +8,12 @@
//! walks. Each test writes values to a buffer at computed offsets and
//! reads them back, asserting both the values and (where applicable)
//! the byte positions.
//!
//! All schemas are BAST documents (`{ "$defs": { ... } }` with `kind`-
//! based vocabulary). The root type name is passed to `OffsetMap::compute`
//! / `LayoutBuilder::new` / `SequentialReader::new` / `AlkTypeEngine::compile`.
use alktype::bast::BastDoc;
use alktype::data_access;
use alktype::tunion;
use alktype::*;
@@ -21,16 +26,21 @@ fn var_sizes(pairs: &[(&str, usize)]) -> HashMap<String, usize> {
#[test]
fn fixed_size_round_trip_via_offset_map() -> Result<(), AlkTypeError> {
let schema = json!({
"AlkType:Struct": true,
"properties": {
"id": { "AlkType:Uint32": true },
"score": { "AlkType:Float32": true },
"flag": { "AlkType:Uint8": true },
"count": { "AlkType:Uint16": true }
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "id", "kind": "uint32" },
{ "name": "score", "kind": "float32" },
{ "name": "flag", "kind": "uint8" },
{ "name": "count", "kind": "uint16" }
]
}
}
});
let offset_map = OffsetMap::compute(&schema)?;
let doc = BastDoc::new(&root, "S")?;
let offset_map = OffsetMap::compute(&doc)?;
let mut buffer = vec![0u8; offset_map.total_size()];
let id_range = offset_map.get("id").expect("id range");
@@ -64,16 +74,20 @@ fn fixed_size_round_trip_via_offset_map() -> Result<(), AlkTypeError> {
#[test]
fn fixed_size_round_trip_via_engine_aligned() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"endian": "little",
"properties": {
"id": { "AlkType:Uint32": true },
"score": { "AlkType:Float32": true },
"flag": { "AlkType:Uint8": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"endian": "little",
"fields": [
{ "name": "id", "kind": "uint32" },
{ "name": "score", "kind": "float32" },
{ "name": "flag", "kind": "uint8" }
]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned)?;
let offset_map = engine.offset_map().expect("aligned mode has offset_map");
let mut buffer = vec![0u8; offset_map.total_size()];
@@ -107,13 +121,15 @@ fn string_round_trip_via_data_access() -> Result<(), AlkTypeError> {
#[test]
fn string_round_trip_via_engine_aligned() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"name": { "AlkType:String": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [ { "name": "name", "kind": "string" } ]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned)?;
let offset_map = engine.offset_map().expect("aligned mode has offset_map");
let mut buffer = vec![0u8; offset_map.total_size() + 64];
engine.write_field(&mut buffer, "name", &FieldValue::String("hello"))?;
@@ -141,20 +157,28 @@ fn bytes_round_trip_via_data_access() -> Result<(), AlkTypeError> {
#[test]
fn nested_struct_round_trip_via_offset_map() -> Result<(), AlkTypeError> {
let schema = json!({
"AlkType:Struct": true,
"properties": {
"header": {
"AlkType:Struct": true,
"properties": {
"version": { "AlkType:Uint32": true },
"magic": { "AlkType:Uint32": true }
}
},
"payload": { "AlkType:Bytes": true }
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{
"name": "header",
"kind": {
"kind": "struct",
"fields": [
{ "name": "version", "kind": "uint32" },
{ "name": "magic", "kind": "uint32" }
]
}
},
{ "name": "payload", "kind": "bytes" }
]
}
}
});
let offset_map = OffsetMap::compute(&schema)?;
let doc = BastDoc::new(&root, "S")?;
let offset_map = OffsetMap::compute(&doc)?;
let header_version = offset_map.get("header.version").expect("header.version");
let header_magic = offset_map.get("header.magic").expect("header.magic");
@@ -210,20 +234,27 @@ fn nested_struct_round_trip_via_offset_map() -> Result<(), AlkTypeError> {
#[test]
fn nested_struct_round_trip_via_engine_aligned() -> Result<(), AlkTypeError> {
let mut schema = json!({
"AlkType:Struct": true,
"properties": {
"header": {
"AlkType:Struct": true,
"properties": {
"version": { "AlkType:Uint8": true },
"flags": { "AlkType:Uint8": true }
}
},
"payload_len": { "AlkType:Uint32": true }
let doc = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{
"name": "header",
"kind": {
"kind": "struct",
"fields": [
{ "name": "version", "kind": "uint8" },
{ "name": "flags", "kind": "uint8" }
]
}
},
{ "name": "payload_len", "kind": "uint32" }
]
}
}
});
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned)?;
let offset_map = engine.offset_map().expect("aligned mode");
assert_eq!(offset_map.get("header.version").unwrap().start, 0);
@@ -252,17 +283,21 @@ fn nested_struct_round_trip_via_engine_aligned() -> Result<(), AlkTypeError> {
#[test]
fn big_endian_round_trip_via_offset_map() -> Result<(), AlkTypeError> {
let schema = json!({
"AlkType:Struct": true,
"endian": "big",
"properties": {
"id": { "AlkType:Uint32": true },
"offset": { "AlkType:Float64": true }
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"endian": "big",
"fields": [
{ "name": "id", "kind": "uint32" },
{ "name": "offset", "kind": "float64" }
]
}
}
});
let offset_map = OffsetMap::compute(&schema)?;
let endian = Endian::from_schema(&schema);
assert_eq!(endian, Endian::Big);
let doc = BastDoc::new(&root, "S")?;
let offset_map = OffsetMap::compute(&doc)?;
let endian = Endian::Big;
let id_range = offset_map.get("id").expect("id");
let offset_range = offset_map.get("offset").expect("offset");
@@ -290,14 +325,19 @@ fn big_endian_round_trip_via_offset_map() -> Result<(), AlkTypeError> {
#[test]
fn alignment_padding_round_trip_u8_then_u32() -> Result<(), AlkTypeError> {
let schema = json!({
"AlkType:Struct": true,
"properties": {
"flag": { "AlkType:Uint8": true },
"id": { "AlkType:Uint32": true }
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"fields": [
{ "name": "flag", "kind": "uint8" },
{ "name": "id", "kind": "uint32" }
]
}
}
});
let offset_map = OffsetMap::compute(&schema)?;
let doc = BastDoc::new(&root, "S")?;
let offset_map = OffsetMap::compute(&doc)?;
let flag_range = offset_map.get("flag").expect("flag");
let id_range = offset_map.get("id").expect("id");
@@ -335,16 +375,20 @@ fn alignment_padding_round_trip_u8_then_u32() -> Result<(), AlkTypeError> {
#[test]
fn packed_layout_round_trip_via_layout_builder() -> Result<(), AlkTypeError> {
let schema = json!({
"AlkType:Struct": true,
"endian": "little",
"properties": {
"flag": { "AlkType:Uint8": true },
"id": { "AlkType:Uint32": true },
"payload": { "AlkType:String": true }
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"endian": "little",
"fields": [
{ "name": "flag", "kind": "uint8" },
{ "name": "id", "kind": "uint32" },
{ "name": "payload", "kind": "string" }
]
}
}
});
let builder = LayoutBuilder::new(&schema)?;
let builder = LayoutBuilder::new(&root, "S")?;
let layout = builder.build(&var_sizes(&[("payload", 10)]))?;
let flag_pos = layout.get("flag").expect("flag");
@@ -392,16 +436,20 @@ fn packed_layout_round_trip_via_layout_builder() -> Result<(), AlkTypeError> {
#[test]
fn sequential_reader_round_trip_packed_buffer() -> Result<(), AlkTypeError> {
let schema = json!({
"AlkType:Struct": true,
"endian": "little",
"properties": {
"id": { "AlkType:Uint8": true },
"name": { "AlkType:String": true },
"tail": { "AlkType:Uint8": true }
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"endian": "little",
"fields": [
{ "name": "id", "kind": "uint8" },
{ "name": "name", "kind": "string" },
{ "name": "tail", "kind": "uint8" }
]
}
}
});
let builder = LayoutBuilder::new(&schema)?;
let builder = LayoutBuilder::new(&root, "S")?;
let payload = "hello";
let layout = builder.build(&var_sizes(&[("name", payload.len())]))?;
@@ -411,7 +459,7 @@ fn sequential_reader_round_trip_packed_buffer() -> Result<(), AlkTypeError> {
let after = 1 + 4 + payload.len();
data_access::write_u8(&mut buffer, after, 99, "tail")?;
let mut reader = SequentialReader::new(&schema)?;
let mut reader = SequentialReader::new(&root, "S")?;
assert_eq!(reader.endian(), Endian::Little);
assert_eq!(reader.position(), 0);
@@ -436,13 +484,17 @@ fn sequential_reader_round_trip_packed_buffer() -> Result<(), AlkTypeError> {
#[test]
fn sequential_reader_read_field_walks_preceding_fields() -> Result<(), AlkTypeError> {
let schema = json!({
"AlkType:Struct": true,
"endian": "little",
"properties": {
"a": { "AlkType:Uint8": true },
"b": { "AlkType:Uint32": true },
"c": { "AlkType:Uint8": true }
let root = json!({
"$defs": {
"S": {
"kind": "struct",
"endian": "little",
"fields": [
{ "name": "a", "kind": "uint8" },
{ "name": "b", "kind": "uint32" },
{ "name": "c", "kind": "uint8" }
]
}
}
});
let mut buffer = vec![0u8; 16];
@@ -450,7 +502,7 @@ fn sequential_reader_read_field_walks_preceding_fields() -> Result<(), AlkTypeEr
data_access::write_u32(&mut buffer, 1, 0xDEADBEEF, "b", Endian::Little)?;
data_access::write_u8(&mut buffer, 5, 9, "c")?;
let mut reader = SequentialReader::new(&schema)?;
let mut reader = SequentialReader::new(&root, "S")?;
let value = reader.read_field(&buffer, "c")?;
assert_eq!(value, FieldValue::U8(9));
assert_eq!(reader.position(), 6);
@@ -463,74 +515,90 @@ fn sequential_reader_read_field_walks_preceding_fields() -> Result<(), AlkTypeEr
#[test]
fn tunion_byte_offset_discriminator_dispatch() -> Result<(), AlkTypeError> {
let union_schema = json!({
"AlkType:Union": true,
"discriminator": {
"kind": "byte",
"offset": 0,
"type": "AlkType:Uint8"
},
"mapping": {
"5": { "$ref": "#/$defs/Read" },
"6": { "$ref": "#/$defs/Write" }
},
let root = json!({
"$defs": {
"Read": {
"AlkType:Struct": true,
"properties": {
"handle": { "AlkType:Uint32": true },
"length": { "AlkType:Uint32": true }
"Packet": {
"kind": "union",
"discriminator": { "kind": "byte", "offset": 0, "type": "uint8" },
"mapping": {
"5": { "$ref": "#/$defs/Read" },
"6": { "$ref": "#/$defs/Write" }
}
},
"Read": {
"kind": "struct",
"fields": [
{ "name": "handle", "kind": "uint32" },
{ "name": "length", "kind": "uint32" }
]
},
"Write": {
"AlkType:Struct": true,
"properties": {
"handle": { "AlkType:Uint32": true },
"length": { "AlkType:Uint32": true },
"data": { "AlkType:Uint32": true }
}
"kind": "struct",
"fields": [
{ "name": "handle", "kind": "uint32" },
{ "name": "length", "kind": "uint32" },
{ "name": "data", "kind": "uint32" }
]
}
}
});
let doc = BastDoc::new(&root, "Packet")?;
let union_def = match doc.root_def().kind() {
alktype::bast::BastDefKind::Union(u) => u,
_ => unreachable!(),
};
let mut buffer = vec![0u8; 32];
buffer[0] = 5;
data_access::write_u32(&mut buffer, 1, 0x01020304, "Read.handle", Endian::Big)?;
data_access::write_u32(&mut buffer, 5, 4096, "Read.length", Endian::Big)?;
let dispatch = tunion::read_byte_discriminator(&buffer, &union_schema, Endian::Big)?;
let dispatch = tunion::read_byte_discriminator(&buffer, union_def, Endian::Big)?;
assert_eq!(dispatch.key, "5");
assert_eq!(dispatch.variant_offset, 1);
assert_eq!(dispatch.discriminator_size, 1);
let variant = tunion::resolve_variant(&union_schema, &dispatch.key)?;
assert_eq!(
variant
.get("AlkType:Struct")
.and_then(serde_json::Value::as_bool),
Some(true)
);
let variant = tunion::resolve_variant(union_def, &dispatch.key)?;
match variant {
alktype::bast::BastType::Ref(r) => assert_eq!(r.name(), "Read"),
other => panic!("expected Ref to Read, got {other:?}"),
}
Ok(())
}
#[test]
fn tunion_byte_offset_discriminator_size_lookup() -> Result<(), AlkTypeError> {
let u8_schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "byte", "type": "AlkType:Uint8"},
"mapping": {}
});
let u16_schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "byte", "type": "AlkType:Uint16"},
"mapping": {}
});
let u32_schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "byte", "type": "AlkType:Uint32"},
"mapping": {}
});
assert_eq!(tunion::discriminator_size(&u8_schema)?, 1);
assert_eq!(tunion::discriminator_size(&u16_schema)?, 2);
assert_eq!(tunion::discriminator_size(&u32_schema)?, 4);
fn union_with(disc_type: &str) -> serde_json::Value {
json!({
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "byte", "type": disc_type },
"mapping": { "1": { "$ref": "#/$defs/A" } }
},
"A": { "kind": "struct", "fields": [] }
}
})
}
let u8_root = union_with("uint8");
let u16_root = union_with("uint16");
let u32_root = union_with("uint32");
let u8_doc = BastDoc::new(&u8_root, "U")?;
let u16_doc = BastDoc::new(&u16_root, "U")?;
let u32_doc = BastDoc::new(&u32_root, "U")?;
let u8_union = match u8_doc.root_def().kind() {
alktype::bast::BastDefKind::Union(u) => u,
_ => unreachable!(),
};
let u16_union = match u16_doc.root_def().kind() {
alktype::bast::BastDefKind::Union(u) => u,
_ => unreachable!(),
};
let u32_union = match u32_doc.root_def().kind() {
alktype::bast::BastDefKind::Union(u) => u,
_ => unreachable!(),
};
assert_eq!(tunion::discriminator_size(u8_union)?, 1);
assert_eq!(tunion::discriminator_size(u16_union)?, 2);
assert_eq!(tunion::discriminator_size(u32_union)?, 4);
Ok(())
}
}
+186 -165
View File
@@ -6,103 +6,116 @@
//! correct mapping key and variant offset, that `resolve_variant`
//! follows `$ref` pointers, and that `discriminator_size` reports the
//! right fixed sizes.
//!
//! All schemas are BAST documents.
use alktype::bast::{BastDefKind, BastDoc, BastType, BastUnion};
use alktype::data_access;
use alktype::tunion;
use alktype::{Endian, AlkTypeError};
use serde_json::json;
fn sftp_like_byte_union() -> serde_json::Value {
fn sftp_like_byte_union_doc() -> serde_json::Value {
json!({
"AlkType:Union": true,
"discriminator": {
"kind": "byte",
"offset": 0,
"type": "AlkType:Uint8"
},
"mapping": {
"5": { "$ref": "#/$defs/Read" },
"6": { "$ref": "#/$defs/Write" }
},
"$defs": {
"Read": {
"AlkType:Struct": true,
"properties": {
"handle": { "AlkType:Uint32": true },
"length": { "AlkType:Uint32": true }
"Packet": {
"kind": "union",
"discriminator": {
"kind": "byte",
"offset": 0,
"type": "uint8"
},
"mapping": {
"5": { "$ref": "#/$defs/Read" },
"6": { "$ref": "#/$defs/Write" }
}
},
"Read": {
"kind": "struct",
"fields": [
{ "name": "handle", "kind": "uint32" },
{ "name": "length", "kind": "uint32" }
]
},
"Write": {
"AlkType:Struct": true,
"properties": {
"handle": { "AlkType:Uint32": true },
"length": { "AlkType:Uint32": true },
"data": { "AlkType:Uint32": true }
}
"kind": "struct",
"fields": [
{ "name": "handle", "kind": "uint32" },
{ "name": "length", "kind": "uint32" },
{ "name": "data", "kind": "uint32" }
]
}
}
})
}
fn union_of<'a>(root: &'a serde_json::Value, name: &'a str) -> BastUnion<'a> {
let doc = BastDoc::new(root, name).expect("bast doc");
match doc.root_def().kind() {
BastDefKind::Union(u) => u.clone(),
_ => panic!("root must be a union"),
}
}
#[test]
fn read_byte_discriminator_uint8_dispatches_to_read() -> Result<(), AlkTypeError> {
let union_schema = sftp_like_byte_union();
let root = sftp_like_byte_union_doc();
let union_def = union_of(&root, "Packet");
let mut buffer = vec![0u8; 16];
buffer[0] = 5;
data_access::write_u32(&mut buffer, 1, 0x01020304, "Read.handle", Endian::Big)?;
let dispatch = tunion::read_byte_discriminator(&buffer, &union_schema, Endian::Big)?;
let dispatch = tunion::read_byte_discriminator(&buffer, &union_def, Endian::Big)?;
assert_eq!(dispatch.key, "5");
assert_eq!(dispatch.variant_offset, 1);
assert_eq!(dispatch.discriminator_size, 1);
let variant = tunion::resolve_variant(&union_schema, &dispatch.key)?;
assert_eq!(
variant
.get("AlkType:Struct")
.and_then(serde_json::Value::as_bool),
Some(true)
);
let variant = tunion::resolve_variant(&union_def, &dispatch.key)?;
match variant {
BastType::Ref(r) => assert_eq!(r.name(), "Read"),
other => panic!("expected Ref to Read, got {other:?}"),
}
Ok(())
}
#[test]
fn read_byte_discriminator_uint8_dispatches_to_write() -> Result<(), AlkTypeError> {
let union_schema = sftp_like_byte_union();
let root = sftp_like_byte_union_doc();
let union_def = union_of(&root, "Packet");
let mut buffer = vec![0u8; 16];
buffer[0] = 6;
data_access::write_u32(&mut buffer, 1, 0xDEADBEEF, "Write.handle", Endian::Big)?;
let dispatch = tunion::read_byte_discriminator(&buffer, &union_schema, Endian::Big)?;
let dispatch = tunion::read_byte_discriminator(&buffer, &union_def, Endian::Big)?;
assert_eq!(dispatch.key, "6");
assert_eq!(dispatch.variant_offset, 1);
assert_eq!(dispatch.discriminator_size, 1);
let variant = tunion::resolve_variant(&union_schema, &dispatch.key)?;
let props = variant
.get("properties")
.and_then(serde_json::Value::as_object)
.expect("variant has properties");
assert!(props.contains_key("data"));
let variant = tunion::resolve_variant(&union_def, &dispatch.key)?;
match variant {
BastType::Ref(r) => assert_eq!(r.name(), "Write"),
other => panic!("expected Ref to Write, got {other:?}"),
}
Ok(())
}
#[test]
fn read_byte_discriminator_uint16_little_endian() -> Result<(), AlkTypeError> {
let schema = json!({
"AlkType:Union": true,
"discriminator": {
"kind": "byte",
"offset": 2,
"type": "AlkType:Uint16"
},
"mapping": {
"5": {"AlkType:Struct": true, "properties": {"id": {"AlkType:Uint32": true}}}
let root = json!({
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "byte", "offset": 2, "type": "uint16" },
"mapping": {
"5": { "kind": "struct", "fields": [ { "name": "id", "kind": "uint32" } ] }
}
}
}
});
let union_def = union_of(&root, "U");
let mut buffer = vec![0u8; 16];
buffer[2..4].copy_from_slice(&5u16.to_le_bytes());
let dispatch = tunion::read_byte_discriminator(&buffer, &schema, Endian::Little)?;
let dispatch = tunion::read_byte_discriminator(&buffer, &union_def, Endian::Little)?;
assert_eq!(dispatch.key, "5");
assert_eq!(dispatch.variant_offset, 4);
assert_eq!(dispatch.discriminator_size, 2);
@@ -111,20 +124,21 @@ fn read_byte_discriminator_uint16_little_endian() -> Result<(), AlkTypeError> {
#[test]
fn read_byte_discriminator_uint32_big_endian() -> Result<(), AlkTypeError> {
let schema = json!({
"AlkType:Union": true,
"discriminator": {
"kind": "byte",
"offset": 0,
"type": "AlkType:Uint32"
},
"mapping": {
"101": {"AlkType:Struct": true, "properties": {"id": {"AlkType:Uint32": true}}}
let root = json!({
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "byte", "offset": 0, "type": "uint32" },
"mapping": {
"101": { "kind": "struct", "fields": [ { "name": "id", "kind": "uint32" } ] }
}
}
}
});
let union_def = union_of(&root, "U");
let mut buffer = vec![0u8; 16];
buffer[0..4].copy_from_slice(&101u32.to_be_bytes());
let dispatch = tunion::read_byte_discriminator(&buffer, &schema, Endian::Big)?;
let dispatch = tunion::read_byte_discriminator(&buffer, &union_def, Endian::Big)?;
assert_eq!(dispatch.key, "101");
assert_eq!(dispatch.variant_offset, 4);
assert_eq!(dispatch.discriminator_size, 4);
@@ -133,191 +147,198 @@ fn read_byte_discriminator_uint32_big_endian() -> Result<(), AlkTypeError> {
#[test]
fn read_byte_discriminator_unknown_value_returns_access_error() -> Result<(), AlkTypeError> {
let union_schema = sftp_like_byte_union();
let root = sftp_like_byte_union_doc();
let union_def = union_of(&root, "Packet");
let buffer = [99u8, 0x00, 0x00, 0x00];
let err = tunion::read_byte_discriminator(&buffer, &union_schema, Endian::Big).unwrap_err();
let err = tunion::read_byte_discriminator(&buffer, &union_def, Endian::Big).unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
Ok(())
}
#[test]
fn read_field_discriminator_string_dispatches_to_read() -> Result<(), AlkTypeError> {
let union_schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "field", "name": "type"},
"properties": {
"type": { "AlkType:String": true }
},
"mapping": {
"read": {"$ref": "#/$defs/Read"},
"write": {"$ref": "#/$defs/Write"}
},
let root = json!({
"$defs": {
"Read": {
"AlkType:Struct": true,
"properties": {
"handle": { "AlkType:Uint32": true },
"length": { "AlkType:Uint32": true }
"Event": {
"kind": "union",
"discriminator": { "kind": "field", "name": "type" },
"fields": [ { "name": "type", "kind": "string" } ],
"mapping": {
"read": { "$ref": "#/$defs/Read" },
"write": { "$ref": "#/$defs/Write" }
}
},
"Read": {
"kind": "struct",
"fields": [
{ "name": "handle", "kind": "uint32" },
{ "name": "length", "kind": "uint32" }
]
},
"Write": {
"AlkType:Struct": true,
"properties": {
"handle": { "AlkType:Uint32": true },
"data": { "AlkType:Bytes": true }
}
"kind": "struct",
"fields": [
{ "name": "handle", "kind": "uint32" },
{ "name": "data", "kind": "bytes" }
]
}
}
});
let union_def = union_of(&root, "Event");
let value = "read";
let mut buffer = vec![0u8; 32];
data_access::write_string(&mut buffer, 0, value, "type", Endian::Little)?;
let dispatch = tunion::read_field_discriminator(&buffer, &union_schema, 0, Endian::Little)?;
let dispatch = tunion::read_field_discriminator(&buffer, &union_def, 0, Endian::Little)?;
assert_eq!(dispatch.key, "read");
assert_eq!(dispatch.variant_offset, 4 + value.len());
assert_eq!(dispatch.discriminator_size, 4 + value.len());
let variant = tunion::resolve_variant(&union_schema, &dispatch.key)?;
assert_eq!(
variant
.get("AlkType:Struct")
.and_then(serde_json::Value::as_bool),
Some(true)
);
let variant = tunion::resolve_variant(&union_def, &dispatch.key)?;
match variant {
BastType::Ref(r) => assert_eq!(r.name(), "Read"),
other => panic!("expected Ref to Read, got {other:?}"),
}
Ok(())
}
#[test]
fn read_field_discriminator_string_dispatches_to_write() -> Result<(), AlkTypeError> {
let union_schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "field", "name": "type"},
"properties": {
"type": { "AlkType:String": true }
},
"mapping": {
"read": {"$ref": "#/$defs/Read"},
"write": {"$ref": "#/$defs/Write"}
},
let root = json!({
"$defs": {
"Event": {
"kind": "union",
"discriminator": { "kind": "field", "name": "type" },
"fields": [ { "name": "type", "kind": "string" } ],
"mapping": {
"read": { "$ref": "#/$defs/Read" },
"write": { "$ref": "#/$defs/Write" }
}
},
"Read": {
"AlkType:Struct": true,
"properties": {"x": {"AlkType:Uint8": true}}
"kind": "struct",
"fields": [ { "name": "x", "kind": "uint8" } ]
},
"Write": {
"AlkType:Struct": true,
"properties": {"y": {"AlkType:Uint16": true}}
"kind": "struct",
"fields": [ { "name": "y", "kind": "uint16" } ]
}
}
});
let union_def = union_of(&root, "Event");
let value = "write";
let mut buffer = vec![0u8; 32];
data_access::write_string(&mut buffer, 0, value, "type", Endian::Little)?;
let dispatch = tunion::read_field_discriminator(&buffer, &union_schema, 0, Endian::Little)?;
let dispatch = tunion::read_field_discriminator(&buffer, &union_def, 0, Endian::Little)?;
assert_eq!(dispatch.key, "write");
assert_eq!(dispatch.variant_offset, 4 + value.len());
let variant = tunion::resolve_variant(&union_schema, &dispatch.key)?;
let props = variant
.get("properties")
.and_then(serde_json::Value::as_object)
.expect("variant has properties");
assert!(props.contains_key("y"));
assert!(!props.contains_key("x"));
let variant = tunion::resolve_variant(&union_def, &dispatch.key)?;
match variant {
BastType::Ref(r) => assert_eq!(r.name(), "Write"),
other => panic!("expected Ref to Write, got {other:?}"),
}
Ok(())
}
#[test]
fn read_field_discriminator_uint8_field() -> Result<(), AlkTypeError> {
let union_schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "field", "name": "tag"},
"properties": {
"tag": { "AlkType:Uint8": true }
},
"mapping": {
"0": {"AlkType:Struct": true, "properties": {"a": {"AlkType:Uint32": true}}},
"1": {"AlkType:Struct": true, "properties": {"b": {"AlkType:Uint16": true}}}
let root = json!({
"$defs": {
"Event": {
"kind": "union",
"discriminator": { "kind": "field", "name": "tag" },
"fields": [ { "name": "tag", "kind": "uint8" } ],
"mapping": {
"0": { "kind": "struct", "fields": [ { "name": "a", "kind": "uint32" } ] },
"1": { "kind": "struct", "fields": [ { "name": "b", "kind": "uint16" } ] }
}
}
}
});
let union_def = union_of(&root, "Event");
let mut buffer = vec![0u8; 8];
buffer[0] = 0;
let dispatch = tunion::read_field_discriminator(&buffer, &union_schema, 0, Endian::Little)?;
let dispatch = tunion::read_field_discriminator(&buffer, &union_def, 0, Endian::Little)?;
assert_eq!(dispatch.key, "0");
assert_eq!(dispatch.variant_offset, 1);
assert_eq!(dispatch.discriminator_size, 1);
buffer[0] = 1;
let dispatch = tunion::read_field_discriminator(&buffer, &union_schema, 0, Endian::Little)?;
let dispatch = tunion::read_field_discriminator(&buffer, &union_def, 0, Endian::Little)?;
assert_eq!(dispatch.key, "1");
Ok(())
}
#[test]
fn read_field_discriminator_unknown_value_returns_access_error() -> Result<(), AlkTypeError> {
let union_schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "field", "name": "tag"},
"properties": {
"tag": { "AlkType:Uint8": true }
},
"mapping": {
"0": {"AlkType:Struct": true, "properties": {"a": {"AlkType:Uint32": true}}}
let root = json!({
"$defs": {
"Event": {
"kind": "union",
"discriminator": { "kind": "field", "name": "tag" },
"fields": [ { "name": "tag", "kind": "uint8" } ],
"mapping": {
"0": { "kind": "struct", "fields": [ { "name": "a", "kind": "uint32" } ] }
}
}
}
});
let union_def = union_of(&root, "Event");
let mut buffer = vec![0u8; 8];
buffer[0] = 99;
let err =
tunion::read_field_discriminator(&buffer, &union_schema, 0, Endian::Little).unwrap_err();
tunion::read_field_discriminator(&buffer, &union_def, 0, Endian::Little).unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
Ok(())
}
#[test]
fn discriminator_size_returns_correct_values() -> Result<(), AlkTypeError> {
let u8_schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "byte", "type": "AlkType:Uint8"},
"mapping": {}
});
let u16_schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "byte", "type": "AlkType:Uint16"},
"mapping": {}
});
let u32_schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "byte", "type": "AlkType:Uint32"},
"mapping": {}
});
assert_eq!(tunion::discriminator_size(&u8_schema)?, 1);
assert_eq!(tunion::discriminator_size(&u16_schema)?, 2);
assert_eq!(tunion::discriminator_size(&u32_schema)?, 4);
fn union_with(disc_type: &str) -> serde_json::Value {
json!({
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "byte", "type": disc_type },
"mapping": { "1": { "$ref": "#/$defs/A" } }
},
"A": { "kind": "struct", "fields": [] }
}
})
}
let u8_root = union_with("uint8");
let u16_root = union_with("uint16");
let u32_root = union_with("uint32");
let u8_union = union_of(&u8_root, "U");
let u16_union = union_of(&u16_root, "U");
let u32_union = union_of(&u32_root, "U");
assert_eq!(tunion::discriminator_size(&u8_union)?, 1);
assert_eq!(tunion::discriminator_size(&u16_union)?, 2);
assert_eq!(tunion::discriminator_size(&u32_union)?, 4);
Ok(())
}
#[test]
fn discriminator_size_field_kind_returns_schema_error() {
let schema = json!({
"AlkType:Union": true,
"discriminator": {"kind": "field", "name": "type"},
"properties": {"type": {"AlkType:Uint8": true}},
"mapping": {}
let root = json!({
"$defs": {
"U": {
"kind": "union",
"discriminator": { "kind": "field", "name": "type" },
"fields": [ { "name": "type", "kind": "uint8" } ],
"mapping": { "0": { "kind": "struct", "fields": [] } }
}
}
});
let err = tunion::discriminator_size(&schema).unwrap_err();
let union_def = union_of(&root, "U");
let err = tunion::discriminator_size(&union_def).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn resolve_variant_returns_schema_error_for_unknown_key() {
let union_schema = sftp_like_byte_union();
let err = tunion::resolve_variant(&union_schema, "999").unwrap_err();
let root = sftp_like_byte_union_doc();
let union_def = union_of(&root, "Packet");
let err = tunion::resolve_variant(&union_def, "999").unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
#[test]
fn parse_discriminator_missing_returns_schema_error() {
let schema = json!({"AlkType:Union": true});
let err = alktype::parse_discriminator(&schema).unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
}