- AlkTypeEngine::compile gains a 4th param json_schema: Option<&Value>. When Some, a standard jsonschema::Validator is built from the consumer-provided JSON Schema and stored for the JSON-validation path. When None, validate_json returns AlkTypeError::Schema and is_valid_json returns false (D-BAST-007). - validate_json / is_valid_json signatures unchanged (per semver contract). Behavior: they now validate against the consumer JSON Schema, not a custom-keyword validator built from the alktype schema. The BAST document is not involved in this path. - build_validator repurposed (deferred decision #2): same signature, now builds a standard jsonschema::Validator with no custom keywords. Behavioral break, not a type break. Re-export kept. - Removed the 19 jsonschema::Keyword implementations and the 4 define_*_validator! macros (dead on the bytes path since step 5, now dead on the JSON path too). The is_rfc3339_timestamp helper lives on in bast_validation.rs (already copied there in step 5). - All compile call sites updated to pass None for json_schema (the layout/read/write/validate_bytes tests don't need JSON validation). - New tests: validate_json accepts/rejects against consumer JSON Schema, returns Schema error when no JSON Schema supplied, is_valid_json false when no schema, independence from BAST doc, malformed JSON Schema build error, nested object JSON Schema. Verification: - cargo test --release: 409 pass (332 lib + 77 integration) - cargo clippy --all-targets -- -D warnings: clean - cargo build --target wasm32-unknown-unknown --release: clean - cargo doc --no-deps: clean
380 lines
13 KiB
Rust
380 lines
13 KiB
Rust
//! Integration tests for the `AlkTypeEngine` public API.
|
|
//!
|
|
//! Exercises the engine across both layout modes, the convenience
|
|
//! 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_doc() -> serde_json::Value {
|
|
json!({
|
|
"$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 doc = mixed_fixed_struct_doc();
|
|
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None)?;
|
|
assert_eq!(engine.mode(), LayoutMode::Aligned);
|
|
assert!(engine.offset_map().is_some());
|
|
assert!(engine.layout_builder().is_none());
|
|
assert!(engine.sequential_reader().is_none());
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn compile_packed_builds_engine_with_builder_and_reader() -> Result<(), AlkTypeError> {
|
|
let doc = mixed_fixed_struct_doc();
|
|
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None)?;
|
|
assert_eq!(engine.mode(), LayoutMode::Packed);
|
|
assert!(engine.offset_map().is_none());
|
|
assert!(engine.layout_builder().is_some());
|
|
assert!(engine.sequential_reader().is_some());
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn compile_resolves_ref_fields() -> Result<(), AlkTypeError> {
|
|
let doc = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{ "name": "child", "kind": { "$ref": "#/$defs/Child" } }
|
|
]
|
|
},
|
|
"Child": {
|
|
"kind": "struct",
|
|
"fields": [ { "name": "x", "kind": "uint8" } ]
|
|
}
|
|
}
|
|
});
|
|
let _engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn compile_returns_schema_error_when_no_defs() {
|
|
let doc = json!({ "type": "object", "properties": {} });
|
|
let err = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).unwrap_err();
|
|
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn compile_returns_schema_error_for_missing_root() {
|
|
let doc = json!({ "$defs": { "Other": { "kind": "struct", "fields": [] } } });
|
|
let err = AlkTypeEngine::compile(&doc, "Missing", LayoutMode::Aligned, None).unwrap_err();
|
|
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn endian_parsed_from_schema_big() -> Result<(), AlkTypeError> {
|
|
let doc = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"endian": "big",
|
|
"fields": [ { "name": "id", "kind": "uint32" } ]
|
|
}
|
|
}
|
|
});
|
|
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None)?;
|
|
assert_eq!(engine.endian(), Endian::Big);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn endian_defaults_to_little() -> Result<(), AlkTypeError> {
|
|
let doc = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [ { "name": "id", "kind": "uint32" } ]
|
|
}
|
|
}
|
|
});
|
|
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None)?;
|
|
assert_eq!(engine.endian(), Endian::Little);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn read_write_aligned_round_trips_all_fixed_size_kinds() -> Result<(), AlkTypeError> {
|
|
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(&doc, "S", LayoutMode::Aligned, None)?;
|
|
let offset_map = engine.offset_map().expect("aligned mode has offset_map");
|
|
let mut buffer = vec![0u8; offset_map.total_size()];
|
|
|
|
engine.write_field(&mut buffer, "i8", &FieldValue::I8(-127))?;
|
|
engine.write_field(&mut buffer, "u8", &FieldValue::U8(0xAB))?;
|
|
engine.write_field(&mut buffer, "i16", &FieldValue::I16(-32000))?;
|
|
engine.write_field(&mut buffer, "u16", &FieldValue::U16(0xBEEF))?;
|
|
engine.write_field(&mut buffer, "i32", &FieldValue::I32(-2_000_000_007))?;
|
|
engine.write_field(&mut buffer, "u32", &FieldValue::U32(0xDEADBEEF))?;
|
|
engine.write_field(&mut buffer, "i64", &FieldValue::I64(-9_000_000_000_000_000_000))?;
|
|
engine.write_field(&mut buffer, "u64", &FieldValue::U64(0x0102030405060708))?;
|
|
engine.write_field(&mut buffer, "f32", &FieldValue::F32(1.5))?;
|
|
engine.write_field(&mut buffer, "f64", &FieldValue::F64(2.5))?;
|
|
engine.write_field(&mut buffer, "b", &FieldValue::Bool(true))?;
|
|
engine.write_field(&mut buffer, "e", &FieldValue::Enum(7))?;
|
|
|
|
assert_eq!(engine.read_field(&buffer, "i8")?, FieldValue::I8(-127));
|
|
assert_eq!(engine.read_field(&buffer, "u8")?, FieldValue::U8(0xAB));
|
|
assert_eq!(engine.read_field(&buffer, "i16")?, FieldValue::I16(-32000));
|
|
assert_eq!(engine.read_field(&buffer, "u16")?, FieldValue::U16(0xBEEF));
|
|
assert_eq!(
|
|
engine.read_field(&buffer, "i32")?,
|
|
FieldValue::I32(-2_000_000_007)
|
|
);
|
|
assert_eq!(
|
|
engine.read_field(&buffer, "u32")?,
|
|
FieldValue::U32(0xDEADBEEF)
|
|
);
|
|
assert_eq!(
|
|
engine.read_field(&buffer, "i64")?,
|
|
FieldValue::I64(-9_000_000_000_000_000_000)
|
|
);
|
|
assert_eq!(
|
|
engine.read_field(&buffer, "u64")?,
|
|
FieldValue::U64(0x0102030405060708)
|
|
);
|
|
assert_eq!(engine.read_field(&buffer, "f32")?, FieldValue::F32(1.5));
|
|
assert_eq!(engine.read_field(&buffer, "f64")?, FieldValue::F64(2.5));
|
|
assert_eq!(engine.read_field(&buffer, "b")?, FieldValue::Bool(true));
|
|
assert_eq!(engine.read_field(&buffer, "e")?, FieldValue::Enum(7));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn read_write_aligned_round_trips_string() -> Result<(), AlkTypeError> {
|
|
let doc = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [ { "name": "name", "kind": "string" } ]
|
|
}
|
|
}
|
|
});
|
|
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None)?;
|
|
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"))?;
|
|
assert_eq!(
|
|
engine.read_field(&buffer, "name")?,
|
|
FieldValue::String("hello world")
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn read_write_aligned_round_trips_bytes() -> Result<(), AlkTypeError> {
|
|
let doc = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [ { "name": "blob", "kind": "bytes" } ]
|
|
}
|
|
}
|
|
});
|
|
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None)?;
|
|
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()];
|
|
engine.write_field(&mut buffer, "blob", &FieldValue::Bytes(&payload))?;
|
|
assert_eq!(
|
|
engine.read_field(&buffer, "blob")?,
|
|
FieldValue::Bytes(&payload)
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn read_field_returns_access_error_in_packed_mode() -> Result<(), AlkTypeError> {
|
|
let doc = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [ { "name": "id", "kind": "uint32" } ]
|
|
}
|
|
}
|
|
});
|
|
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None)?;
|
|
let buffer = [0u8; 4];
|
|
let err = engine.read_field(&buffer, "id").unwrap_err();
|
|
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn write_field_returns_access_error_in_packed_mode() -> Result<(), AlkTypeError> {
|
|
let doc = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [ { "name": "id", "kind": "uint32" } ]
|
|
}
|
|
}
|
|
});
|
|
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None)?;
|
|
let mut buffer = [0u8; 4];
|
|
let err = engine
|
|
.write_field(&mut buffer, "id", &FieldValue::U32(1))
|
|
.unwrap_err();
|
|
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn read_field_returns_offset_error_for_missing_path() -> Result<(), AlkTypeError> {
|
|
let doc = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [ { "name": "id", "kind": "uint32" } ]
|
|
}
|
|
}
|
|
});
|
|
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None)?;
|
|
let buffer = [0u8; 8];
|
|
let err = engine.read_field(&buffer, "missing").unwrap_err();
|
|
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn write_field_returns_offset_error_for_missing_path() -> Result<(), AlkTypeError> {
|
|
let doc = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [ { "name": "id", "kind": "uint32" } ]
|
|
}
|
|
}
|
|
});
|
|
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None)?;
|
|
let mut buffer = [0u8; 8];
|
|
let err = engine
|
|
.write_field(&mut buffer, "missing", &FieldValue::U32(1))
|
|
.unwrap_err();
|
|
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
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(&doc, "S", LayoutMode::Aligned, None)?;
|
|
let buffer = [0u8; 8];
|
|
let err = engine.read_field(&buffer, "vals").unwrap_err();
|
|
assert!(
|
|
matches!(err, AlkTypeError::Access { .. } | AlkTypeError::Offset { .. }),
|
|
"got {err:?}"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn write_field_returns_access_error_for_composite_value() -> Result<(), AlkTypeError> {
|
|
let doc = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [ { "name": "id", "kind": "uint32" } ]
|
|
}
|
|
}
|
|
});
|
|
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None)?;
|
|
let mut buffer = [0u8; 8];
|
|
let err = engine
|
|
.write_field(&mut buffer, "id", &FieldValue::Struct { start: 0, end: 4 })
|
|
.unwrap_err();
|
|
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn read_field_aligned_reads_nested_struct_byte_range() -> Result<(), AlkTypeError> {
|
|
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(&doc, "S", LayoutMode::Aligned, None)?;
|
|
let offset_map = engine.offset_map().expect("aligned mode");
|
|
let mut buffer = vec![0u8; offset_map.total_size()];
|
|
|
|
engine.write_field(&mut buffer, "header.version", &FieldValue::U8(3))?;
|
|
engine.write_field(&mut buffer, "header.magic", &FieldValue::U32(0xCAFEBABE))?;
|
|
|
|
assert_eq!(
|
|
engine.read_field(&buffer, "header.version")?,
|
|
FieldValue::U8(3)
|
|
);
|
|
assert_eq!(
|
|
engine.read_field(&buffer, "header.magic")?,
|
|
FieldValue::U32(0xCAFEBABE)
|
|
);
|
|
Ok(())
|
|
} |