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:
1 parent
f2f9c0326c
commit
04573e1d86
13 files changed
+3642
-3906
No files matched your search
+17
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
+14
-7
@@ -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
File diff suppressed because it is too large.
Load diff
+405
-417
File diff suppressed because it is too large.
Load diff
+748
-832
File diff suppressed because it is too large.
Load diff
+172
-274
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user