Resolve N3: maxLength is string/bytes-only (review #006)
- Parse gate in BastField::parse: maxLength on any kind other than
string/bytes is a clean Schema error (records, arrays, inline
structs, refs, union shared fields all covered; the choke point
needs no ref-following since $defs entries are struct/union/enum)
- Meta-schema FieldDef: if kind in {string, bytes} else maxLength
forbidden — the published alk.dev/bast/v1 contract matches the
parser (N2 dual-layer pattern)
- M5's compute-side record maxLength arm became unreachable and was
deleted (offset-indirect arm stays); the two superseded M5
maxLength tests rewritten as the n3_* parse-rejection family
- ADR-006 remedy message tailored per kind: for records both
annotated remedies are dead ends, so the error text points at the
last-position fix only
- Docs aligned: bast-format.md (FieldDef meta-schema + FieldDef/
Variable-Length Encoding prose), layout-engine.md (Strategy 2 +
ADR-006 paragraph), schema-layer.md, ADR-003 §2/§3a amended,
builder .max_length() doc
- Review #006: N3 resolved (all findings now closed), M5 update
note, test-count bookkeeping note (in-session probes vs static
counts), status lines flipped to fully resolved
Verified: 547 tests green + 2 ignored doctests in BOTH release and
default profiles (a stale debug artifact from an earlier session
masked one H3 roundtrip test in debug; clean rebuild passes both),
clippy -D warnings clean, cargo doc --no-deps zero warnings, wasm
build green.
This commit is contained in:
1 parent
0857ea1c23
commit
bb28ba3006
9 files changed
+352
-95
No files matched your search
+142
-2
@@ -393,7 +393,9 @@ impl BastField {
|
||||
|
||||
/// The byte-length cap. Enforced as a validation constraint
|
||||
/// (packed mode) or a fixed-size reservation (aligned mode) per
|
||||
/// ADR-003.
|
||||
/// ADR-003. Only accepted on `string` and `bytes` fields (review
|
||||
/// #006 N3: on any other kind the annotation was silently
|
||||
/// unenforced by `validate_bytes`; rejected at parse now).
|
||||
pub fn max_length(&self) -> Option<usize> {
|
||||
self.max_length
|
||||
}
|
||||
@@ -426,10 +428,22 @@ impl BastField {
|
||||
))
|
||||
})?;
|
||||
let ty = BastType::parse(raw_kind, path, doc_root)?;
|
||||
let max_length = parse_max_length(node);
|
||||
if max_length.is_some() && !matches!(
|
||||
&ty,
|
||||
BastType::Primitive(AlkTypeKind::String) | BastType::Primitive(AlkTypeKind::Bytes)
|
||||
) {
|
||||
return Err(AlkTypeError::Schema(format!(
|
||||
"bast: field {name:?} at {path} declares `maxLength` but its kind \
|
||||
({ty}) is not a variable-length primitive — `maxLength` is only \
|
||||
honored on `string` and `bytes` fields (review #006 N3: on any \
|
||||
other kind the annotation was silently unenforced by \
|
||||
validate_bytes)"
|
||||
)));
|
||||
}
|
||||
let endian = parse_endian_opt(node);
|
||||
let align = parse_align(node, path)?;
|
||||
let encoding = parse_encoding(node);
|
||||
let max_length = parse_max_length(node);
|
||||
Ok(Self {
|
||||
name: name.to_string(),
|
||||
ty,
|
||||
@@ -1240,6 +1254,132 @@ mod tests {
|
||||
assert_eq!(s.fields()[0].encoding(), VariableEncoding::LengthPrefixed);
|
||||
}
|
||||
|
||||
// ----- N3: maxLength is string/bytes-only --------------------------
|
||||
|
||||
#[test]
|
||||
fn n3_max_length_on_record_field_rejected_at_parse() {
|
||||
// N3's probe shape: on a record field the annotation did
|
||||
// nothing — validate_bytes never bounded the entry region by
|
||||
// it. Rejected at parse now, mirroring M5's aligned-mode
|
||||
// posture.
|
||||
let root = json!({
|
||||
"$defs": {
|
||||
"S": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{ "name": "counts", "kind": { "kind": "record", "values": "uint16" }, "maxLength": 8 }
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
let err = BastDoc::new(&root, "S").unwrap_err();
|
||||
match err {
|
||||
AlkTypeError::Schema(msg) => {
|
||||
assert!(msg.contains("maxLength"), "msg: {msg}");
|
||||
assert!(msg.contains("counts"), "msg: {msg}");
|
||||
assert!(msg.contains("string"), "msg: {msg}");
|
||||
}
|
||||
other => panic!("expected Schema, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn n3_max_length_on_fixed_primitive_rejected_at_parse() {
|
||||
let root = json!({
|
||||
"$defs": {
|
||||
"S": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{ "name": "id", "kind": "uint32", "maxLength": 8 }
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
let err = BastDoc::new(&root, "S").unwrap_err();
|
||||
match err {
|
||||
AlkTypeError::Schema(msg) => {
|
||||
assert!(msg.contains("maxLength"), "msg: {msg}");
|
||||
assert!(msg.contains("id"), "msg: {msg}");
|
||||
}
|
||||
other => panic!("expected Schema, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn n3_max_length_on_array_field_rejected_at_parse() {
|
||||
let root = json!({
|
||||
"$defs": {
|
||||
"S": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{ "name": "tags", "kind": { "kind": "array", "element": "string", "count": 4 }, "maxLength": 16 }
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
let err = BastDoc::new(&root, "S").unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn n3_max_length_on_struct_kind_rejected_at_parse() {
|
||||
let root = json!({
|
||||
"$defs": {
|
||||
"S": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{ "name": "nested", "kind": { "kind": "struct", "fields": [ { "name": "a", "kind": "uint8" } ] }, "maxLength": 8 }
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
let err = BastDoc::new(&root, "S").unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn n3_max_length_on_string_and_bytes_still_accepted() {
|
||||
let root = json!({
|
||||
"$defs": {
|
||||
"S": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{ "name": "name", "kind": "string", "maxLength": 256 },
|
||||
{ "name": "blob", "kind": "bytes", "maxLength": 64 }
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
let d = doc_from(&root, "S");
|
||||
let s = match d.root_def().kind() {
|
||||
BastDefKind::Struct(s) => s,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
assert_eq!(s.fields()[0].max_length(), Some(256));
|
||||
assert_eq!(s.fields()[1].max_length(), Some(64));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn n3_max_length_on_union_field_disc_shared_field_rejected_at_parse() {
|
||||
// The union's `fields` parse through the same BastField::parse
|
||||
// choke point, so the gate applies there too.
|
||||
let root = json!({
|
||||
"$defs": {
|
||||
"U": {
|
||||
"kind": "union",
|
||||
"discriminator": { "kind": "field", "name": "type" },
|
||||
"fields": [
|
||||
{ "name": "type", "kind": "uint8" },
|
||||
{ "name": "payload", "kind": { "kind": "record", "values": "uint8" }, "maxLength": 16 }
|
||||
],
|
||||
"mapping": { "1": { "kind": "struct", "fields": [] } }
|
||||
}
|
||||
}
|
||||
});
|
||||
let err = BastDoc::new(&root, "U").unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
// ----- TypeRef: primitives, $ref, array, record -------------------
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user