Fix H3: field-disc union wire convention — shared-then-variant (review #006)
Decision (recorded as an ADR-011 addendum): the packed-mode wire layout for a field-name-discriminator TUnion is shared-then-variant — the union's declared `fields` (disc + shared fields) first, then the variant's own fields. Reader and materializer already implemented this; LayoutBuilder was corrected from variant-only layout. Enforcement in BastUnion::parse (the choke point every consumer inherits — union roots at BastDoc::new, referenced unions at resolve_ref): - discriminator field must be declared in `fields` - `fields` must not contain duplicate names - variants must not re-declare shared fields (checked inline and through $ref resolution — parse chain now threads the doc root) - the discriminator field must be the FIRST entry in `fields` (the reader reads the disc at the union start; a later position made it dispatch on the wrong bytes — H3 item 2, probe-verified) Schemas relying on the old variant-only builder convention (variants re-declaring shared fields) are rejected with a clean Schema error naming the convention. Breaking for 0.2.0-era re-declaring schemas; announced with 0.3.x. - L5: FieldValue::Union::variant_start doc now states per-kind semantics (byte-disc: union_start + disc.offset + disc.size; field-disc: after the shared walk). - L6: roundtrip test added (poc_roundtrip.rs) — LayoutBuilder write → SequentialReader read → materialize_packed → validate_bytes over a field-disc union with a second shared field and non-redeclaring variant; pins event.type@0/seq@1/handle@5, total 10. - ADR-011: Status-block addendum recording the convention decision, the no-re-declare rule, and the breaking-constraint note. - Review #006 updated: H3/L5/L6 resolution blocks, resolution log, recommended order. Verified: 488 tests green (410+17+34+15+12, 2 pre-existing ignored), clippy -D warnings clean, wasm32 build green, cargo doc zero warnings.
This commit is contained in:
1 parent
2d166f567b
commit
05a2a42983
7 files changed
+561
-44
No files matched your search
@@ -603,4 +603,111 @@ fn tunion_byte_offset_discriminator_size_lookup() -> Result<(), AlkTypeError> {
|
||||
assert_eq!(tunion::discriminator_size(u16_union)?, 2);
|
||||
assert_eq!(tunion::discriminator_size(u32_union)?, 4);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// L6 (review #006): the single roundtrip test through a field-disc union.
|
||||
// Write with LayoutBuilder → read with SequentialReader → materialize →
|
||||
// validate_bytes on the engine — one schema, all three packed-mode
|
||||
// consumers, so the H3 wire-convention split can never reappear silently.
|
||||
//
|
||||
// Fixture shape: the union's `fields` carry the discriminator (`type`,
|
||||
// uint8) *and* a second shared field (`seq`, uint32); the variant
|
||||
// (`Read`) declares only its own field (`handle`) — it does NOT
|
||||
// re-declare the discriminator or any shared field (forbidden since the
|
||||
// ADR-011 addendum). The disc field is a uint8, so the mapping keys are
|
||||
// stringified integers ("1" = read). The builder lays out
|
||||
// shared-then-variant:
|
||||
// type@0 (1B), seq@1 (4B), handle@5 (4B) — total 9.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn field_disc_union_roundtrip_build_read_materialize_validate() -> Result<(), AlkTypeError> {
|
||||
let root = json!({
|
||||
"$defs": {
|
||||
"S": {
|
||||
"kind": "struct",
|
||||
"endian": "little",
|
||||
"fields": [
|
||||
{ "name": "event", "kind": { "$ref": "#/$defs/Event" } },
|
||||
{ "name": "trailer", "kind": "uint8" }
|
||||
]
|
||||
},
|
||||
"Event": {
|
||||
"kind": "union",
|
||||
"discriminator": { "kind": "field", "name": "type" },
|
||||
"fields": [
|
||||
{ "name": "type", "kind": "uint8" },
|
||||
{ "name": "seq", "kind": "uint32" }
|
||||
],
|
||||
"mapping": {
|
||||
"1": { "$ref": "#/$defs/Read" },
|
||||
"2": { "$ref": "#/$defs/Write" }
|
||||
}
|
||||
},
|
||||
"Read": {
|
||||
"kind": "struct",
|
||||
"fields": [ { "name": "handle", "kind": "uint32" } ]
|
||||
},
|
||||
"Write": {
|
||||
"kind": "struct",
|
||||
"fields": [ { "name": "handle", "kind": "uint32" } ]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// --- Write side: LayoutBuilder (shared-then-variant layout) --------
|
||||
let builder = LayoutBuilder::new(&root, "S")?;
|
||||
let layout = builder.build(&var_sizes(&[("event.__variant", 0)]))?;
|
||||
assert_eq!(layout.total_size(), 9 + 1, "shared(1+4) + variant(4) + trailer(1)");
|
||||
|
||||
let handle_pos = layout.get("event.handle").expect("event.handle");
|
||||
assert_eq!(handle_pos.offset, 5, "variant fields start after shared (type@0, seq@1)");
|
||||
let disc_pos = layout.get("event.type").expect("event.type");
|
||||
assert_eq!(disc_pos.offset, 0);
|
||||
let seq_pos = layout.get("event.seq").expect("event.seq");
|
||||
assert_eq!(seq_pos.offset, 1);
|
||||
|
||||
let mut buffer = vec![0u8; layout.total_size()];
|
||||
data_access::write_u8(&mut buffer, 0, 1, "event.type")?; // mapping key "1"
|
||||
data_access::write_u32(&mut buffer, 1, 77, "event.seq", Endian::Little)?;
|
||||
data_access::write_u32(&mut buffer, 5, 4242, "event.handle", Endian::Little)?;
|
||||
data_access::write_u8(&mut buffer, 9, 55, "trailer")?;
|
||||
|
||||
// --- Engine (packed) + reader + materializer + validator -----------
|
||||
let engine = AlkTypeEngine::compile(&root, "S", LayoutMode::Packed, None)?;
|
||||
|
||||
// validate_bytes (materializer + ValidationPlan) accepts the buffer.
|
||||
engine.validate_bytes(&buffer)?;
|
||||
|
||||
// SequentialReader: the union field reports the disc value and the
|
||||
// variant start (after the shared walk).
|
||||
let mut reader = engine.sequential_reader().expect("packed mode has reader");
|
||||
let (name, value) = reader.read_next(&buffer)?.expect("event");
|
||||
assert_eq!(name, "event");
|
||||
let (disc, variant_start) = match &value {
|
||||
FieldValue::Union { discriminator, variant_start } => (discriminator.clone(), *variant_start),
|
||||
other => panic!("expected Union, got {other:?}"),
|
||||
};
|
||||
assert_eq!(disc, "1", "uint8 disc value 1 stringifies to the mapping key");
|
||||
assert_eq!(variant_start, 5, "variant starts after the shared walk");
|
||||
let (name, value) = reader.read_next(&buffer)?.expect("trailer");
|
||||
assert_eq!(name, "trailer");
|
||||
assert_eq!(value, FieldValue::U8(55));
|
||||
|
||||
// materialize_packed through the engine's plan: shared fields land
|
||||
// in the object, the variant's handle flattens in.
|
||||
let plan = engine_sequential_plan(&root, "S")?;
|
||||
let value = alktype::materialize::materialize_packed(&plan, &buffer)?;
|
||||
assert_eq!(value["event"]["type"], json!(1));
|
||||
assert_eq!(value["event"]["seq"], json!(77));
|
||||
assert_eq!(value["event"]["handle"], json!(4242));
|
||||
assert_eq!(value["event"]["__discriminator"], json!("1"));
|
||||
assert_eq!(value["trailer"], json!(55));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn engine_sequential_plan(root: &serde_json::Value, name: &str) -> Result<std::sync::Arc<ReadPlan>, AlkTypeError> {
|
||||
Ok(std::sync::Arc::new(ReadPlan::compile(root, name)?))
|
||||
}
|
||||
Reference in new issue
Block a user