Resolve v0.1.0 open questions and fix production-readiness issues
POC: /workspace/alktype-builder-poc/ (18/18 tests pass, findings in
docs/research/alktype-builder-poc/findings.md). Round 2 adds the SFTP
Packet validate_bytes tests (7 new: valid Init/Read/Write/Status,
short buffer, unknown discriminator, over-maxLength Bytes).
Open questions resolved (OQ-004 through OQ-008):
- OQ-004: Discriminator::Field name is String (already implemented;
docs updated to mark resolved)
- OQ-005: Both union discriminator kinds return the same shape:
{__discriminator, ...variant-fields}. Field-name path also had a
real offset bug (returned start, not end) - fixed.
- OQ-006: builder.md Example 3 now wraps the Union in a
Schema::struct_().field("payload", ...) and merges $defs via
Definitions::merge_into (matches the engine's AlkType:Struct-at-root
constraint and the SFTP wire shape)
- OQ-007: Bytes materialization is array-of-u8 (Value::Array of
Value::Number, one entry per byte 0..=255). BytesValidator accepts
both Value::String (validate_json) and Value::Array (validate_bytes).
maxLength = max byte count. Replaces the lossy from_utf8_lossy path
that corrupted non-UTF-8 bytes and broke maxLength semantics.
- OQ-008 (new): UnionValidator now dispatches to variant schemas via
sub-validators built at factory time. AlkTypeEngine::compile calls
schema::inline_union_variant_refs before build_validator to inline
$refs in union mapping entries (necessary because union_factory
receives the union node, but $defs live at the schema root).
Production-readiness fixes in src/ (no stubs/hedges in a published crate):
- materialize.rs: Record stub -> full count-prefixed key/value pair
implementation per schema-layer.md TRecord
- materialize.rs: root_of() was broken (returned the current node, not
the schema root) -> root schema threaded through every recursive call
so resolve_ref_or_inline can resolve $refs for nested composites
- builder.rs: LengthPrefixed encoding setter was a no-op when the
keyword was already in object form -> complete the branch (updates
the encoding entry in place for both LengthPrefixed and OffsetIndirect)
- builder.rs, engine.rs: POC-referencing comments cleaned up; the
round-trip test's or_else fallback (papering over write_field being
aligned-only) replaced with direct byte writes
Documentation:
- builder.md: Example 3 updated; Discriminator::Field spec shows String;
Open Questions section updated (OQ-004 resolved)
- validation.md: AlkType:Bytes and AlkType:Union validator descriptions
updated for array-of-u8 form and variant dispatch
- open-questions.md: OQ-004/005/006/007/008 marked resolved; new
Validation theme entries
- questions/004-008: individual OQ files updated with resolutions
- findings.md: round 2 results documented
Verification:
- cargo test: 369 -> 391 tests pass (22 new: 14 materialize, 5
inline_union_variant_refs, 3 validation/builder)
- cargo clippy --all-targets: clean
- POC: 11 -> 18 tests (7 new SFTP Packet tests); all pass
This commit is contained in:
1 parent
c6893eece8
commit
c0217d91a8
15 files changed
+1553
-493
No files matched your search
+633
-116
@@ -1,16 +1,26 @@
|
||||
//! Materialize a `serde_json::Value` tree from a binary buffer by walking
|
||||
//! the schema. Used by [`crate::engine::AlkTypeEngine::validate_bytes`]
|
||||
//! (ADR-010) to collapse the two-step dance (read bytes → `Value`,
|
||||
//! (ADR-010) to collapse the two-step dance (read bytes -> `Value`,
|
||||
//! then validate `Value`) into a single call.
|
||||
//!
|
||||
//! The materializer reuses the [`crate::data_access`] read functions for
|
||||
//! leaf kinds and recurses into composites (`Struct`, `Array`, `Record`).
|
||||
//! For `Union`, it dispatches on the discriminator and recurses into the
|
||||
//! variant. Errors carry the field path (ADR-004).
|
||||
//! leaf kinds and recurses into composites (`Struct`, `Array`, `Record`,
|
||||
//! `Union`). For `Union`, it dispatches on the discriminator and recurses
|
||||
//! into the variant. Errors carry the field path (ADR-004).
|
||||
//!
|
||||
//! Packed mode walks sequentially from offset 0; aligned mode reads at
|
||||
//! offsets from the [`crate::offset_map::OffsetMap`]. Both produce the
|
||||
//! same `Value` form; the validator is mode-agnostic.
|
||||
//!
|
||||
//! ## `$ref` resolution
|
||||
//!
|
||||
//! The root schema is threaded through every recursive call so that
|
||||
//! `$ref` pointers (e.g. `"#/$defs/Read"`) in composite variants can be
|
||||
//! resolved against the schema root via
|
||||
//! [`crate::schema::resolve_ref_or_inline`]. This is load-bearing for
|
||||
//! unions and arrays whose variants/elements are `$ref`s into `$defs`
|
||||
//! (the SFTP Packet shape: a struct wrapping a union with `$ref`
|
||||
//! variants).
|
||||
|
||||
use crate::data_access;
|
||||
use crate::error::AlkTypeError;
|
||||
@@ -26,13 +36,14 @@ const U32_SIZE: usize = 4;
|
||||
///
|
||||
/// `schema` must declare `AlkType:Struct` at the root (the same
|
||||
/// requirement as [`crate::sequential_reader::SequentialReader::new`]).
|
||||
/// `endian` is the schema's endianness (parsed by the caller).
|
||||
/// `endian` is the schema's endianness (parsed by the caller). The
|
||||
/// `schema` is also used as the root for `$ref` resolution.
|
||||
pub fn materialize_packed(
|
||||
buffer: &[u8],
|
||||
schema: &Value,
|
||||
endian: Endian,
|
||||
) -> Result<Value, AlkTypeError> {
|
||||
materialize_struct_packed(buffer, schema, "", endian)
|
||||
materialize_struct_packed(buffer, schema, schema, "", endian)
|
||||
}
|
||||
|
||||
/// Materialize a `Value` tree from `buffer` by walking `schema` in aligned
|
||||
@@ -40,29 +51,24 @@ pub fn materialize_packed(
|
||||
///
|
||||
/// `schema` must declare `AlkType:Struct` at the root. `offset_map` must
|
||||
/// have been computed from the same `schema`. `endian` is the schema's
|
||||
/// endianness.
|
||||
/// endianness. The `schema` is also used as the root for `$ref` resolution.
|
||||
pub fn materialize_aligned(
|
||||
buffer: &[u8],
|
||||
schema: &Value,
|
||||
offset_map: &crate::offset_map::OffsetMap,
|
||||
endian: Endian,
|
||||
) -> Result<Value, AlkTypeError> {
|
||||
// For aligned mode, the offset map records byte ranges for each leaf
|
||||
// field via dotted paths. To materialize a struct, we walk the
|
||||
// schema's `properties` and for each field, look up the path in the
|
||||
// offset map. Composites need recursion with their own offset map
|
||||
// entries (e.g., `header.magic` for a nested struct).
|
||||
materialize_struct_aligned(buffer, schema, "", offset_map, endian)
|
||||
materialize_struct_aligned(buffer, schema, schema, "", offset_map, endian)
|
||||
}
|
||||
|
||||
fn materialize_struct_packed(
|
||||
buffer: &[u8],
|
||||
root: &Value,
|
||||
struct_schema: &Value,
|
||||
path_prefix: &str,
|
||||
endian: Endian,
|
||||
) -> Result<Value, AlkTypeError> {
|
||||
let struct_schema = resolve_ref_or_inline(struct_schema, root_of(struct_schema))
|
||||
.unwrap_or(struct_schema);
|
||||
let struct_schema = resolve_ref_or_inline(struct_schema, root).unwrap_or(struct_schema);
|
||||
let kind = get_alktype_kind_loose_enum(struct_schema).ok_or_else(|| {
|
||||
AlkTypeError::Schema(format!(
|
||||
"materialize: schema at {path_prefix} has no AlkType:* kind"
|
||||
@@ -89,7 +95,8 @@ fn materialize_struct_packed(
|
||||
} else {
|
||||
format!("{path_prefix}.{name}")
|
||||
};
|
||||
let (value, new_offset) = materialize_field_packed(buffer, field_schema, &path, offset, endian)?;
|
||||
let (value, new_offset) =
|
||||
materialize_field_packed(buffer, root, field_schema, &path, offset, endian)?;
|
||||
obj.insert(name.clone(), value);
|
||||
offset = new_offset;
|
||||
}
|
||||
@@ -98,12 +105,18 @@ fn materialize_struct_packed(
|
||||
|
||||
fn materialize_field_packed(
|
||||
buffer: &[u8],
|
||||
root: &Value,
|
||||
field_schema: &Value,
|
||||
field_path: &str,
|
||||
offset: usize,
|
||||
endian: Endian,
|
||||
) -> Result<(Value, usize), AlkTypeError> {
|
||||
let kind = get_alktype_kind_loose_enum(field_schema).ok_or_else(|| {
|
||||
// Resolve $ref against the root before checking the kind. A
|
||||
// $ref-bearing node has no AlkType:* kind; the kind lives on the
|
||||
// resolved target. This is load-bearing for union variants and
|
||||
// array elements that reference $defs.
|
||||
let resolved = resolve_ref_or_inline(field_schema, root).unwrap_or(field_schema);
|
||||
let kind = get_alktype_kind_loose_enum(resolved).ok_or_else(|| {
|
||||
AlkTypeError::Schema(format!(
|
||||
"materialize: field {field_path} has no AlkType:* kind: {field_schema}"
|
||||
))
|
||||
@@ -143,19 +156,21 @@ fn materialize_field_packed(
|
||||
}
|
||||
AlkTypeKind::Float32 => {
|
||||
let v = data_access::read_f32(buffer, offset, field_path, endian)?;
|
||||
Ok(
|
||||
(serde_json::Number::from_f64(v as f64)
|
||||
Ok((
|
||||
serde_json::Number::from_f64(v as f64)
|
||||
.map(Value::Number)
|
||||
.unwrap_or(Value::Null), offset + 4),
|
||||
)
|
||||
.unwrap_or(Value::Null),
|
||||
offset + 4,
|
||||
))
|
||||
}
|
||||
AlkTypeKind::Float64 => {
|
||||
let v = data_access::read_f64(buffer, offset, field_path, endian)?;
|
||||
Ok(
|
||||
(serde_json::Number::from_f64(v)
|
||||
Ok((
|
||||
serde_json::Number::from_f64(v)
|
||||
.map(Value::Number)
|
||||
.unwrap_or(Value::Null), offset + 8),
|
||||
)
|
||||
.unwrap_or(Value::Null),
|
||||
offset + 8,
|
||||
))
|
||||
}
|
||||
AlkTypeKind::Boolean => {
|
||||
let v = data_access::read_bool(buffer, offset, field_path)?;
|
||||
@@ -171,15 +186,17 @@ fn materialize_field_packed(
|
||||
}
|
||||
AlkTypeKind::Bytes => {
|
||||
let b = data_access::read_bytes(buffer, offset, field_path, endian)?;
|
||||
// Bytes materialize as a string for validation (JSON has no
|
||||
// native byte type — see schema-layer.md §TBytes). The
|
||||
// jsonschema validator's BytesValidator expects a string.
|
||||
// The bytes are not necessarily UTF-8; use lossy conversion so
|
||||
// validation can still catch length violations. For strict
|
||||
// byte-preserving validation, the consumer should use the
|
||||
// data-access layer directly.
|
||||
let s = String::from_utf8_lossy(b).into_owned();
|
||||
Ok((Value::String(s), offset + U32_SIZE + b.len()))
|
||||
// Materialize as a JSON array of u8 (one Value::Number per
|
||||
// byte, 0..=255). This is the round-trippable form for raw
|
||||
// bytes — JSON has no native byte type, and `from_utf8_lossy`
|
||||
// corrupts non-UTF-8 byte sequences (breaking `maxLength`
|
||||
// semantics, since the replacement char U+FFFD is 3 bytes in
|
||||
// UTF-8). The `BytesValidator` accepts `Value::Array` for
|
||||
// `validate_bytes` and `Value::String` for `validate_json`
|
||||
// (so JSON consumers can still pass a string). `maxLength`
|
||||
// is interpreted as max byte count (array length).
|
||||
let arr: Vec<Value> = b.iter().map(|&byte| Value::from(u32::from(byte))).collect();
|
||||
Ok((Value::Array(arr), offset + U32_SIZE + b.len()))
|
||||
}
|
||||
AlkTypeKind::Timestamp => {
|
||||
let s = data_access::read_string(buffer, offset, field_path, endian)?;
|
||||
@@ -190,42 +207,29 @@ fn materialize_field_packed(
|
||||
// The nested struct's fields follow inline. We don't know its
|
||||
// total size without walking it, so we recurse and let it
|
||||
// return the new offset.
|
||||
let nested = materialize_struct_packed_at(
|
||||
buffer,
|
||||
field_schema,
|
||||
field_path,
|
||||
offset,
|
||||
endian,
|
||||
)?;
|
||||
Ok(nested)
|
||||
materialize_struct_packed_at(buffer, root, resolved, field_path, offset, endian)
|
||||
}
|
||||
AlkTypeKind::Array => {
|
||||
materialize_array_packed(buffer, field_schema, field_path, offset, endian)
|
||||
materialize_array_packed(buffer, root, resolved, field_path, offset, endian)
|
||||
}
|
||||
AlkTypeKind::Union => {
|
||||
materialize_union_packed(buffer, field_schema, field_path, offset, endian)
|
||||
materialize_union_packed(buffer, root, resolved, field_path, offset, endian)
|
||||
}
|
||||
AlkTypeKind::Record => {
|
||||
// Record materialization is more complex (count-prefixed
|
||||
// key/value pairs). Defer for the POC — the chunk header
|
||||
// doesn't use Record.
|
||||
Err(AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: "materialize: AlkType:Record not yet supported in validate_bytes (POC)".to_string(),
|
||||
})
|
||||
materialize_record_packed(buffer, root, resolved, field_path, offset, endian)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn materialize_struct_packed_at(
|
||||
buffer: &[u8],
|
||||
root: &Value,
|
||||
struct_schema: &Value,
|
||||
path_prefix: &str,
|
||||
offset: usize,
|
||||
endian: Endian,
|
||||
) -> Result<(Value, usize), AlkTypeError> {
|
||||
let struct_schema = resolve_ref_or_inline(struct_schema, root_of(struct_schema))
|
||||
.unwrap_or(struct_schema);
|
||||
let struct_schema = resolve_ref_or_inline(struct_schema, root).unwrap_or(struct_schema);
|
||||
let props = struct_schema
|
||||
.get("properties")
|
||||
.and_then(Value::as_object)
|
||||
@@ -238,7 +242,8 @@ fn materialize_struct_packed_at(
|
||||
let mut cur = offset;
|
||||
for (name, field_schema) in props.iter() {
|
||||
let path = format!("{path_prefix}.{name}");
|
||||
let (value, new_offset) = materialize_field_packed(buffer, field_schema, &path, cur, endian)?;
|
||||
let (value, new_offset) =
|
||||
materialize_field_packed(buffer, root, field_schema, &path, cur, endian)?;
|
||||
obj.insert(name.clone(), value);
|
||||
cur = new_offset;
|
||||
}
|
||||
@@ -247,18 +252,17 @@ fn materialize_struct_packed_at(
|
||||
|
||||
fn materialize_array_packed(
|
||||
buffer: &[u8],
|
||||
root: &Value,
|
||||
field_schema: &Value,
|
||||
field_path: &str,
|
||||
offset: usize,
|
||||
endian: Endian,
|
||||
) -> Result<(Value, usize), AlkTypeError> {
|
||||
let element_schema = field_schema
|
||||
.get("items")
|
||||
.ok_or_else(|| {
|
||||
AlkTypeError::Schema(format!(
|
||||
"materialize: array at {field_path} has no items schema"
|
||||
))
|
||||
})?;
|
||||
let element_schema = field_schema.get("items").ok_or_else(|| {
|
||||
AlkTypeError::Schema(format!(
|
||||
"materialize: array at {field_path} has no items schema"
|
||||
))
|
||||
})?;
|
||||
let min = field_schema.get("minItems").and_then(Value::as_u64);
|
||||
let max = field_schema.get("maxItems").and_then(Value::as_u64);
|
||||
let fixed_count = match (min, max) {
|
||||
@@ -270,7 +274,8 @@ fn materialize_array_packed(
|
||||
let mut cur = offset;
|
||||
for i in 0..count {
|
||||
let path = format!("{field_path}[{i}]");
|
||||
let (value, new_offset) = materialize_field_packed(buffer, element_schema, &path, cur, endian)?;
|
||||
let (value, new_offset) =
|
||||
materialize_field_packed(buffer, root, element_schema, &path, cur, endian)?;
|
||||
arr.push(value);
|
||||
cur = new_offset;
|
||||
}
|
||||
@@ -283,7 +288,8 @@ fn materialize_array_packed(
|
||||
let mut cur = offset + U32_SIZE;
|
||||
for i in 0..count {
|
||||
let path = format!("{field_path}[{i}]");
|
||||
let (value, new_offset) = materialize_field_packed(buffer, element_schema, &path, cur, endian)?;
|
||||
let (value, new_offset) =
|
||||
materialize_field_packed(buffer, root, element_schema, &path, cur, endian)?;
|
||||
arr.push(value);
|
||||
cur = new_offset;
|
||||
}
|
||||
@@ -293,6 +299,7 @@ fn materialize_array_packed(
|
||||
|
||||
fn materialize_union_packed(
|
||||
buffer: &[u8],
|
||||
root: &Value,
|
||||
field_schema: &Value,
|
||||
field_path: &str,
|
||||
offset: usize,
|
||||
@@ -300,70 +307,185 @@ fn materialize_union_packed(
|
||||
) -> Result<(Value, usize), AlkTypeError> {
|
||||
let disc = crate::schema::parse_discriminator(field_schema)?;
|
||||
match disc {
|
||||
DiscriminatorKind::Byte { offset: disc_offset, disc_type } => {
|
||||
DiscriminatorKind::Byte {
|
||||
offset: disc_offset,
|
||||
disc_type,
|
||||
} => {
|
||||
let disc_value = match disc_type {
|
||||
AlkTypeKind::Uint8 => data_access::read_u8(buffer, offset + disc_offset, field_path)? as u32,
|
||||
AlkTypeKind::Uint16 => data_access::read_u16(buffer, offset + disc_offset, field_path, endian)? as u32,
|
||||
AlkTypeKind::Uint32 => data_access::read_u32(buffer, offset + disc_offset, field_path, endian)?,
|
||||
AlkTypeKind::Uint8 => {
|
||||
data_access::read_u8(buffer, offset + disc_offset, field_path)? as u32
|
||||
}
|
||||
AlkTypeKind::Uint16 => {
|
||||
data_access::read_u16(buffer, offset + disc_offset, field_path, endian)? as u32
|
||||
}
|
||||
AlkTypeKind::Uint32 => {
|
||||
data_access::read_u32(buffer, offset + disc_offset, field_path, endian)?
|
||||
}
|
||||
_ => unreachable!("disc_type restricted by parse_discriminator"),
|
||||
};
|
||||
let mapping = field_schema
|
||||
.get("mapping")
|
||||
.and_then(Value::as_object)
|
||||
.ok_or_else(|| {
|
||||
AlkTypeError::Schema(format!(
|
||||
"materialize: union at {field_path} has no mapping"
|
||||
))
|
||||
})?;
|
||||
let mapping = field_schema.get("mapping").and_then(Value::as_object).ok_or_else(|| {
|
||||
AlkTypeError::Schema(format!(
|
||||
"materialize: union at {field_path} has no mapping"
|
||||
))
|
||||
})?;
|
||||
let key = disc_value.to_string();
|
||||
let variant_schema = mapping.get(&key).ok_or_else(|| {
|
||||
AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("union discriminator value {key} not in mapping"),
|
||||
}
|
||||
let variant_schema = mapping.get(&key).ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("union discriminator value {key} not in mapping"),
|
||||
})?;
|
||||
let variant_offset = offset + disc_offset + disc_type.type_size().unwrap_or(1);
|
||||
let (variant_value, new_offset) = materialize_field_packed(
|
||||
buffer,
|
||||
root,
|
||||
variant_schema,
|
||||
field_path,
|
||||
variant_offset,
|
||||
endian,
|
||||
)?;
|
||||
let mut obj = Map::new();
|
||||
obj.insert("__discriminator".to_string(), Value::from(disc_value));
|
||||
if let Some(variant_obj) = variant_value.as_object() {
|
||||
for (k, v) in variant_obj.iter() {
|
||||
obj.insert(k.clone(), v.clone());
|
||||
}
|
||||
} else {
|
||||
obj.insert("__variant".to_string(), variant_value);
|
||||
}
|
||||
Ok((Value::Object(obj), new_offset))
|
||||
Ok((
|
||||
tag_union_value(disc_value, variant_value),
|
||||
new_offset,
|
||||
))
|
||||
}
|
||||
DiscriminatorKind::Field { name } => {
|
||||
// Field-name discriminator: the union is a struct with a
|
||||
// discriminator field; the variant is selected by that field's
|
||||
// value. This is the typedef.ts pattern. For the POC, we
|
||||
// materialize the struct fields and let the validator
|
||||
// dispatch on the discriminator field.
|
||||
let (value, _off) =
|
||||
materialize_struct_packed_at(buffer, field_schema, field_path, offset, endian)?;
|
||||
let _ = &name; // field-name dispatch happens at validation time
|
||||
Ok((value, offset))
|
||||
// Field-name discriminator: the union is a struct whose first
|
||||
// field is the discriminator; the variant struct follows. The
|
||||
// discriminator field's value selects the mapping entry. The
|
||||
// materialized shape is the same as the byte-offset path:
|
||||
// `{ "__discriminator": <key>, <disc-field-name>: <typed-value>, ...variant-fields }`.
|
||||
// The `__discriminator` entry carries the mapping key (stringified)
|
||||
// for uniform validator dispatch; the discriminator field is
|
||||
// preserved under its own name (it's a regular field in the
|
||||
// struct — the typedef.ts pattern).
|
||||
let properties = field_schema.get("properties").and_then(Value::as_object).ok_or_else(|| {
|
||||
AlkTypeError::Schema(format!(
|
||||
"materialize: field-name union at {field_path} has no properties"
|
||||
))
|
||||
})?;
|
||||
let disc_schema = properties.get(&name).ok_or_else(|| {
|
||||
AlkTypeError::Schema(format!(
|
||||
"materialize: union at {field_path} has no discriminator field '{name}'"
|
||||
))
|
||||
})?;
|
||||
let disc_path = format!("{field_path}.{name}");
|
||||
let (disc_value, after_disc) =
|
||||
materialize_field_packed(buffer, root, disc_schema, &disc_path, offset, endian)?;
|
||||
let key = union_discriminator_key(&disc_value, field_path)?;
|
||||
let mapping = field_schema.get("mapping").and_then(Value::as_object).ok_or_else(|| {
|
||||
AlkTypeError::Schema(format!(
|
||||
"materialize: union at {field_path} has no mapping"
|
||||
))
|
||||
})?;
|
||||
let variant_schema = mapping.get(&key).ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("union discriminator value {key} not in mapping"),
|
||||
})?;
|
||||
let (variant_value, new_offset) = materialize_field_packed(
|
||||
buffer,
|
||||
root,
|
||||
variant_schema,
|
||||
field_path,
|
||||
after_disc,
|
||||
endian,
|
||||
)?;
|
||||
let mut obj = Map::new();
|
||||
obj.insert("__discriminator".to_string(), Value::String(key));
|
||||
obj.insert(name.clone(), disc_value);
|
||||
flatten_variant_into(&mut obj, variant_value);
|
||||
Ok((Value::Object(obj), new_offset))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the materialized `Value` for a byte-offset union: an object
|
||||
/// `{ "__discriminator": <disc_value>, ...variant-fields }`. If the
|
||||
/// variant materialized as a non-object (a leaf), it is nested under
|
||||
/// `"__variant"`.
|
||||
fn tag_union_value(disc_value: u32, variant_value: Value) -> Value {
|
||||
let mut obj = Map::new();
|
||||
obj.insert("__discriminator".to_string(), Value::from(disc_value));
|
||||
flatten_variant_into(&mut obj, variant_value);
|
||||
Value::Object(obj)
|
||||
}
|
||||
|
||||
/// Flatten a variant's materialized object fields into `obj` (in place).
|
||||
/// If the variant is not an object (e.g. a leaf kind), nest it under
|
||||
/// `"__variant"`.
|
||||
fn flatten_variant_into(obj: &mut Map<String, Value>, variant_value: Value) {
|
||||
if let Some(variant_obj) = variant_value.as_object() {
|
||||
for (k, v) in variant_obj.iter() {
|
||||
obj.insert(k.clone(), v.clone());
|
||||
}
|
||||
} else {
|
||||
obj.insert("__variant".to_string(), variant_value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Stringify a discriminator field value for mapping lookup. Mirrors
|
||||
/// `sequential_reader::discriminator_string_value` for the Value form.
|
||||
fn union_discriminator_key(value: &Value, field_path: &str) -> Result<String, AlkTypeError> {
|
||||
match value {
|
||||
Value::String(s) => Ok(s.clone()),
|
||||
Value::Number(n) => Ok(n.to_string()),
|
||||
_ => Err(AlkTypeError::Schema(format!(
|
||||
"union {field_path} has unsupported discriminator value kind: {value}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Materialize an `AlkType:Record` field: a count-prefixed sequence of
|
||||
/// `(key, value)` pairs. Wire format per [schema-layer.md](../docs/architecture/schema-layer.md)
|
||||
/// §TRecord: `[count: u32][key_len: u32][key_bytes][value]...` repeated
|
||||
/// `count` times. The count and key-length prefixes respect `endian`.
|
||||
/// The value is materialized according to its declared `AlkType:*` kind.
|
||||
///
|
||||
/// The materialized form is a JSON object mapping each key (string) to
|
||||
/// its materialized value. This matches the `RecordValidator`'s
|
||||
/// expectation (an object whose values match the record's value type).
|
||||
fn materialize_record_packed(
|
||||
buffer: &[u8],
|
||||
root: &Value,
|
||||
field_schema: &Value,
|
||||
field_path: &str,
|
||||
offset: usize,
|
||||
endian: Endian,
|
||||
) -> Result<(Value, usize), AlkTypeError> {
|
||||
let value_schema = field_schema.get("values").ok_or_else(|| {
|
||||
AlkTypeError::Schema(format!(
|
||||
"materialize: record at {field_path} has no values schema"
|
||||
))
|
||||
})?;
|
||||
let count = data_access::read_u32(buffer, offset, field_path, endian)? as usize;
|
||||
let mut cur = offset + U32_SIZE;
|
||||
let mut obj = Map::new();
|
||||
for i in 0..count {
|
||||
let key_path = format!("{field_path}[{i}].key");
|
||||
let key_str = data_access::read_string(buffer, cur, &key_path, endian)?;
|
||||
let key_len = U32_SIZE + key_str.len();
|
||||
cur = cur
|
||||
.checked_add(key_len)
|
||||
.ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: key_path.clone(),
|
||||
reason: format!("key offset {cur} + {key_len} overflows usize"),
|
||||
})?;
|
||||
let val_path = format!("{field_path}[{i}].value");
|
||||
let (val, new_offset) =
|
||||
materialize_field_packed(buffer, root, value_schema, &val_path, cur, endian)?;
|
||||
obj.insert(key_str.to_string(), val);
|
||||
cur = new_offset;
|
||||
}
|
||||
Ok((Value::Object(obj), cur))
|
||||
}
|
||||
|
||||
fn materialize_struct_aligned(
|
||||
buffer: &[u8],
|
||||
root: &Value,
|
||||
struct_schema: &Value,
|
||||
path_prefix: &str,
|
||||
offset_map: &crate::offset_map::OffsetMap,
|
||||
endian: Endian,
|
||||
) -> Result<Value, AlkTypeError> {
|
||||
let struct_schema = resolve_ref_or_inline(struct_schema, root_of(struct_schema))
|
||||
.unwrap_or(struct_schema);
|
||||
let struct_schema = resolve_ref_or_inline(struct_schema, root).unwrap_or(struct_schema);
|
||||
let props = struct_schema
|
||||
.get("properties")
|
||||
.and_then(Value::as_object)
|
||||
@@ -379,7 +501,8 @@ fn materialize_struct_aligned(
|
||||
} else {
|
||||
format!("{path_prefix}.{name}")
|
||||
};
|
||||
let kind = get_alktype_kind_loose_enum(field_schema).ok_or_else(|| {
|
||||
let resolved = resolve_ref_or_inline(field_schema, root).unwrap_or(field_schema);
|
||||
let kind = get_alktype_kind_loose_enum(resolved).ok_or_else(|| {
|
||||
AlkTypeError::Schema(format!(
|
||||
"materialize: field {path} has no AlkType:* kind"
|
||||
))
|
||||
@@ -390,19 +513,23 @@ fn materialize_struct_aligned(
|
||||
field_path: path.clone(),
|
||||
reason: "field not found in offset map".to_string(),
|
||||
})?;
|
||||
materialize_leaf_at(buffer, field_schema, &path, range.start, endian)?
|
||||
materialize_leaf_at(buffer, root, resolved, &path, range.start, endian)?
|
||||
} else if kind == AlkTypeKind::Struct {
|
||||
// Nested struct — recurse with the same offset map (nested
|
||||
// fields are recorded under `path.*`).
|
||||
materialize_struct_aligned(buffer, field_schema, &path, offset_map, endian)?
|
||||
materialize_struct_aligned(buffer, root, resolved, &path, offset_map, endian)?
|
||||
} else {
|
||||
// Composite leaf (Array, Union, Record) — fall back to packed-style
|
||||
// walk from the field's start offset.
|
||||
// walk from the field's start offset. The offset map records the
|
||||
// field's start; the composite's internal layout is walked
|
||||
// sequentially from that start (variable-length elements can't
|
||||
// be precomputed into the static offset map).
|
||||
let range = offset_map.get(&path).ok_or_else(|| AlkTypeError::Offset {
|
||||
field_path: path.clone(),
|
||||
reason: "field not found in offset map".to_string(),
|
||||
})?;
|
||||
let (value, _) = materialize_field_packed(buffer, field_schema, &path, range.start, endian)?;
|
||||
let (value, _) =
|
||||
materialize_field_packed(buffer, root, resolved, &path, range.start, endian)?;
|
||||
value
|
||||
};
|
||||
obj.insert(name.clone(), value);
|
||||
@@ -412,12 +539,14 @@ fn materialize_struct_aligned(
|
||||
|
||||
fn materialize_leaf_at(
|
||||
buffer: &[u8],
|
||||
root: &Value,
|
||||
field_schema: &Value,
|
||||
field_path: &str,
|
||||
offset: usize,
|
||||
endian: Endian,
|
||||
) -> Result<Value, AlkTypeError> {
|
||||
let (_kind, value) = materialize_field_packed_returning_kind(buffer, field_schema, field_path, offset, endian)?;
|
||||
let (_kind, value) =
|
||||
materialize_field_packed_returning_kind(buffer, root, field_schema, field_path, offset, endian)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
@@ -427,25 +556,413 @@ fn materialize_leaf_at(
|
||||
/// signature stable.
|
||||
fn materialize_field_packed_returning_kind(
|
||||
buffer: &[u8],
|
||||
root: &Value,
|
||||
field_schema: &Value,
|
||||
field_path: &str,
|
||||
offset: usize,
|
||||
endian: Endian,
|
||||
) -> Result<(AlkTypeKind, Value), AlkTypeError> {
|
||||
let kind = get_alktype_kind_loose_enum(field_schema).ok_or_else(|| {
|
||||
let resolved = resolve_ref_or_inline(field_schema, root).unwrap_or(field_schema);
|
||||
let kind = get_alktype_kind_loose_enum(resolved).ok_or_else(|| {
|
||||
AlkTypeError::Schema(format!(
|
||||
"materialize: field {field_path} has no AlkType:* kind"
|
||||
))
|
||||
})?;
|
||||
let (value, _) = materialize_field_packed(buffer, field_schema, field_path, offset, endian)?;
|
||||
let (value, _) = materialize_field_packed(buffer, root, field_schema, field_path, offset, endian)?;
|
||||
Ok((kind, value))
|
||||
}
|
||||
|
||||
/// Walk a schema to find its root. Since materialize is called with the
|
||||
/// top-level schema as `struct_schema`, the root is `struct_schema` itself
|
||||
/// for the top-level call. For nested calls (via $ref), we need the
|
||||
/// original root. This helper returns the node itself for now — the $ref
|
||||
/// resolution in `resolve_ref_or_inline` handles the common case.
|
||||
fn root_of(schema: &Value) -> &Value {
|
||||
schema
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn materialize_packed_strict(
|
||||
buffer: &[u8],
|
||||
schema: &Value,
|
||||
endian: Endian,
|
||||
) -> Result<Value, AlkTypeError> {
|
||||
materialize_packed(buffer, schema, endian)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Bytes materialization (array-of-u8 form, OQ-007 resolution)
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn materialize_bytes_packed_returns_array_of_u8() {
|
||||
// Schema: Struct { blob: Bytes }. Wire: [len:u32 LE = 3][0x41 0x42 0xC3]
|
||||
// 0xC3 is invalid UTF-8 lead byte — under the old from_utf8_lossy
|
||||
// path this would corrupt. Under array-of-u8 it round-trips.
|
||||
let schema = json!({
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"blob": { "AlkType:Bytes": true }
|
||||
}
|
||||
});
|
||||
let mut buf = vec![0u8; 7];
|
||||
buf[0..4].copy_from_slice(&3u32.to_le_bytes());
|
||||
buf[4] = 0x41;
|
||||
buf[5] = 0x42;
|
||||
buf[6] = 0xC3;
|
||||
let v = materialize_packed_strict(&buf, &schema, Endian::Little).expect("materialize");
|
||||
assert_eq!(
|
||||
v["blob"],
|
||||
json!([65, 66, 195]),
|
||||
"bytes should materialize as array of u8"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materialize_bytes_packed_non_utf8_round_trips() {
|
||||
// All-non-UTF-8 bytes — would have been corrupted by from_utf8_lossy.
|
||||
let schema = json!({
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"raw": { "AlkType:Bytes": true }
|
||||
}
|
||||
});
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[0..4].copy_from_slice(&4u32.to_le_bytes());
|
||||
buf[4] = 0xFF;
|
||||
buf[5] = 0xFE;
|
||||
buf[6] = 0x00;
|
||||
buf[7] = 0x80;
|
||||
let v = materialize_packed_strict(&buf, &schema, Endian::Little).expect("materialize");
|
||||
assert_eq!(v["raw"], json!([255, 254, 0, 128]));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Record materialization
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn materialize_record_packed_count_prefixed_pairs() {
|
||||
// Schema: Struct { counts: Record<Uint32> }.
|
||||
// Wire: [count:u32=2][key_len:u32=1][b][value:u32=10][key_len:u32=1][a][value:u32=20]
|
||||
let schema = json!({
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"counts": {
|
||||
"AlkType:Record": true,
|
||||
"values": { "AlkType:Uint32": true }
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut buf = vec![0u8; 22];
|
||||
let mut off = 0usize;
|
||||
// count = 2
|
||||
buf[off..off + 4].copy_from_slice(&2u32.to_le_bytes());
|
||||
off += 4;
|
||||
// key 1: "b" (len=1)
|
||||
buf[off..off + 4].copy_from_slice(&1u32.to_le_bytes());
|
||||
off += 4;
|
||||
buf[off] = b'b';
|
||||
off += 1;
|
||||
// value 1: 10
|
||||
buf[off..off + 4].copy_from_slice(&10u32.to_le_bytes());
|
||||
off += 4;
|
||||
// key 2: "a" (len=1)
|
||||
buf[off..off + 4].copy_from_slice(&1u32.to_le_bytes());
|
||||
off += 4;
|
||||
buf[off] = b'a';
|
||||
off += 1;
|
||||
// value 2: 20
|
||||
buf[off..off + 4].copy_from_slice(&20u32.to_le_bytes());
|
||||
let v = materialize_packed_strict(&buf, &schema, Endian::Little).expect("materialize");
|
||||
assert_eq!(
|
||||
v["counts"],
|
||||
json!({ "b": 10, "a": 20 }),
|
||||
"record should materialize as an object"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materialize_record_packed_zero_entries() {
|
||||
let schema = json!({
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"counts": {
|
||||
"AlkType:Record": true,
|
||||
"values": { "AlkType:Uint32": true }
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut buf = vec![0u8; 4];
|
||||
buf[0..4].copy_from_slice(&0u32.to_le_bytes());
|
||||
let v = materialize_packed_strict(&buf, &schema, Endian::Little).expect("materialize");
|
||||
assert_eq!(v["counts"], json!({}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materialize_record_packed_rejects_short_buffer() {
|
||||
let schema = json!({
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"counts": {
|
||||
"AlkType:Record": true,
|
||||
"values": { "AlkType:Uint32": true }
|
||||
}
|
||||
}
|
||||
});
|
||||
// Buffer too short for the count prefix.
|
||||
let buf = [0u8; 2];
|
||||
let err = materialize_packed_strict(&buf, &schema, Endian::Little).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Union materialization — byte-offset and field-name shape parity (OQ-005)
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn materialize_union_byte_discriminator_packs_variant_fields() {
|
||||
// Schema: Struct { payload: Union(byte, offset=0, Uint8) mapping { 5 -> Read } }
|
||||
// Read: Struct { id: Uint32 }
|
||||
// Wire: [disc:u8=5][id:u32=42 LE]
|
||||
let schema = json!({
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"payload": {
|
||||
"AlkType:Union": true,
|
||||
"discriminator": { "kind": "byte", "offset": 0, "type": "AlkType:Uint8" },
|
||||
"mapping": {
|
||||
"5": {
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "id": { "AlkType:Uint32": true } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut buf = vec![0u8; 5];
|
||||
buf[0] = 5;
|
||||
buf[1..5].copy_from_slice(&42u32.to_le_bytes());
|
||||
let v = materialize_packed_strict(&buf, &schema, Endian::Little).expect("materialize");
|
||||
let payload = &v["payload"];
|
||||
assert_eq!(
|
||||
payload["__discriminator"],
|
||||
json!(5),
|
||||
"byte-offset union should carry __discriminator"
|
||||
);
|
||||
assert_eq!(
|
||||
payload["id"],
|
||||
json!(42),
|
||||
"variant fields should be flattened into the union object"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materialize_union_field_discriminator_includes_typed_disc_value() {
|
||||
// Schema: Struct { payload: Union(field, name="type") mapping { "read" -> Read } }
|
||||
// The union is a struct: [type: String "read"][variant struct].
|
||||
// Read: Struct { n: Uint32 }
|
||||
// Wire: [len:u32=4][read][n:u32=7 LE]
|
||||
let schema = json!({
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"payload": {
|
||||
"AlkType:Union": true,
|
||||
"discriminator": { "kind": "field", "name": "type" },
|
||||
"properties": {
|
||||
"type": { "AlkType:String": true }
|
||||
},
|
||||
"mapping": {
|
||||
"read": {
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "n": { "AlkType:Uint32": true } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut buf = vec![0u8; 12];
|
||||
buf[0..4].copy_from_slice(&4u32.to_le_bytes());
|
||||
buf[4..8].copy_from_slice(b"read");
|
||||
buf[8..12].copy_from_slice(&7u32.to_le_bytes());
|
||||
let v = materialize_packed_strict(&buf, &schema, Endian::Little).expect("materialize");
|
||||
let payload = &v["payload"];
|
||||
// __discriminator carries the mapping key (string).
|
||||
assert_eq!(payload["__discriminator"], json!("read"));
|
||||
// The original typed discriminator value is preserved under its field name.
|
||||
assert_eq!(payload["type"], json!("read"));
|
||||
// Variant fields are flattened.
|
||||
assert_eq!(payload["n"], json!(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materialize_union_field_discriminator_advances_offset_correctly() {
|
||||
// Regression: the old field-name path returned the start offset,
|
||||
// not the end. A struct with a union followed by another field
|
||||
// would read the second field from the wrong position. This test
|
||||
// places a Uint32 after the union and verifies it reads correctly.
|
||||
let schema = json!({
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"payload": {
|
||||
"AlkType:Union": true,
|
||||
"discriminator": { "kind": "field", "name": "type" },
|
||||
"properties": {
|
||||
"type": { "AlkType:String": true }
|
||||
},
|
||||
"mapping": {
|
||||
"read": {
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "n": { "AlkType:Uint32": true } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"trailer": { "AlkType:Uint32": true }
|
||||
}
|
||||
});
|
||||
// Wire: [len:u32=4]["read"][n:u32=7][trailer:u32=99]
|
||||
let mut buf = vec![0u8; 16];
|
||||
buf[0..4].copy_from_slice(&4u32.to_le_bytes());
|
||||
buf[4..8].copy_from_slice(b"read");
|
||||
buf[8..12].copy_from_slice(&7u32.to_le_bytes());
|
||||
buf[12..16].copy_from_slice(&99u32.to_le_bytes());
|
||||
let v = materialize_packed_strict(&buf, &schema, Endian::Little).expect("materialize");
|
||||
assert_eq!(v["trailer"], json!(99), "trailer should read from after the union");
|
||||
assert_eq!(v["payload"]["n"], json!(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materialize_union_byte_discriminator_unknown_value_is_access_error() {
|
||||
let schema = json!({
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"payload": {
|
||||
"AlkType:Union": true,
|
||||
"discriminator": { "kind": "byte", "offset": 0, "type": "AlkType:Uint8" },
|
||||
"mapping": { "5": { "AlkType:Struct": true, "properties": {} } }
|
||||
}
|
||||
}
|
||||
});
|
||||
let buf = [99u8];
|
||||
let err = materialize_packed_strict(&buf, &schema, Endian::Little).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// $ref resolution (root-schema threading)
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn materialize_union_with_ref_variant_resolves_via_root() {
|
||||
// The SFTP shape: struct wrapping a union whose variants are $refs
|
||||
// into $defs. The materializer must resolve the $ref against the
|
||||
// root schema to find the variant's AlkType:Struct kind.
|
||||
let schema = json!({
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"payload": {
|
||||
"AlkType:Union": true,
|
||||
"discriminator": { "kind": "byte", "offset": 0, "type": "AlkType:Uint8" },
|
||||
"mapping": {
|
||||
"5": { "$ref": "#/$defs/Read" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"Read": {
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "id": { "AlkType:Uint32": true } }
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut buf = vec![0u8; 5];
|
||||
buf[0] = 5;
|
||||
buf[1..5].copy_from_slice(&42u32.to_le_bytes());
|
||||
let v = materialize_packed_strict(&buf, &schema, Endian::Little).expect("materialize");
|
||||
assert_eq!(v["payload"]["__discriminator"], json!(5));
|
||||
assert_eq!(v["payload"]["id"], json!(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materialize_array_with_ref_element_resolves_via_root() {
|
||||
let schema = json!({
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"items": {
|
||||
"AlkType:Array": true,
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
"items": { "$ref": "#/$defs/Point" }
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"Point": {
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"x": { "AlkType:Uint16": true },
|
||||
"y": { "AlkType:Uint16": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// Wire: [x:u16=1 LE][y:u16=2 LE][x:u16=3 LE][y:u16=4 LE]
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[0..2].copy_from_slice(&1u16.to_le_bytes());
|
||||
buf[2..4].copy_from_slice(&2u16.to_le_bytes());
|
||||
buf[4..6].copy_from_slice(&3u16.to_le_bytes());
|
||||
buf[6..8].copy_from_slice(&4u16.to_le_bytes());
|
||||
let v = materialize_packed_strict(&buf, &schema, Endian::Little).expect("materialize");
|
||||
assert_eq!(
|
||||
v["items"],
|
||||
json!([
|
||||
{ "x": 1, "y": 2 },
|
||||
{ "x": 3, "y": 4 }
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Smoke: a struct field after a nested struct advances correctly
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn materialize_struct_with_nested_struct_then_field() {
|
||||
let schema = json!({
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"header": {
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"a": { "AlkType:Uint8": true },
|
||||
"b": { "AlkType:Uint8": true }
|
||||
}
|
||||
},
|
||||
"after": { "AlkType:Uint32": true }
|
||||
}
|
||||
});
|
||||
// Wire: [a=1][b=2][after=0x03030303 LE = 50529027]
|
||||
let mut buf = vec![0u8; 6];
|
||||
buf[0] = 1;
|
||||
buf[1] = 2;
|
||||
buf[2..6].copy_from_slice(&0x03030303u32.to_le_bytes());
|
||||
let v = materialize_packed_strict(&buf, &schema, Endian::Little).expect("materialize");
|
||||
assert_eq!(v["header"]["a"], json!(1));
|
||||
assert_eq!(v["header"]["b"], json!(2));
|
||||
assert_eq!(v["after"], json!(0x03030303u32));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Sanity: existing packed chunk-header path still works
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn materialize_chunk_header_packed() {
|
||||
let schema = json!({
|
||||
"AlkType:Struct": true,
|
||||
"endian": "big",
|
||||
"properties": {
|
||||
"channel_id": { "AlkType:Uint32": true },
|
||||
"length": { "AlkType:Uint32": true }
|
||||
}
|
||||
});
|
||||
let buf = [0u8, 0u8, 0u8, 42u8, 0u8, 0u8, 0u8, 7u8];
|
||||
let v = materialize_packed_strict(&buf, &schema, Endian::Big).expect("materialize");
|
||||
assert_eq!(v["channel_id"], json!(42));
|
||||
assert_eq!(v["length"], json!(7));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user