Remove the Timestamp kind
The Timestamp kind was a residual from an early research reference. It was byte-identical to String everywhere (length-prefixed UTF-8) and its only distinguishing behavior was a hand-rolled non-strict RFC 3339 check that the docs admitted was incomplete (Feb 31 passes, seconds range unchecked, no leap seconds). JSON-level timestamp validation is jsonschema's job (format: date-time on the validate_json path), not alktype's. Removes the AlkTypeKind::Timestamp variant, its to_bast_str/from_bast_str mapping, the builder's Schema::timestamp() constructor, the validate_timestamp/is_rfc3339_timestamp validator arms, and the materializer/reader/engine timestamp arms. Updates the meta-schema primitive enum (14 -> 13), the spec docs (bast-format.md, schema-layer.md, builder.md, validation.md, overview.md, data-access.md, README.md), and the kind-count references (19 -> 18). Verification: cargo test --release (407 pass), cargo clippy --all-targets -- -D warnings (clean), cargo doc --no-deps (clean), cargo build --target wasm32-unknown-unknown --release (clean).
This commit is contained in:
1 parent
62ed009281
commit
510553d800
17 files changed
+38
-225
No files matched your search
@@ -16,7 +16,7 @@ format definition; the engine is generic.
|
||||
|----------|--------|-------------|
|
||||
| [overview.md](overview.md) | accepted | Crate purpose, "schema is the format" principle, dependencies, consumers, scope boundaries |
|
||||
| [`bast-format.md`](bast-format.md) | accepted | **Normative BAST format specification.** Meta-schema, TypeRef, TypeDef shapes (Struct/Union/Enum/FieldDef), examples, validation model. The format the engine consumes. |
|
||||
| [schema-layer.md](schema-layer.md) | accepted | The BAST parser (`src/bast.rs`) — the typed tree (`BastDoc`/`BastDef`/`BastType`/…) every engine module walks, the 19 BAST kinds, the `AlkTypeKind` enum, and the foundational annotation types. |
|
||||
| [schema-layer.md](schema-layer.md) | accepted | The BAST parser (`src/bast.rs`) — the typed tree (`BastDoc`/`BastDef`/`BastType`/…) every engine module walks, the 18 BAST kinds, the `AlkTypeKind` enum, and the foundational annotation types. |
|
||||
| [layout-engine.md](layout-engine.md) | draft | Offset computation, the two layout modes (packed sequential vs aligned static), alignment, endianness, variable-length handling |
|
||||
| [data-access.md](data-access.md) | draft | Read/write functions, TUnion dispatch, field paths, zero-copy access, length-prefix reading |
|
||||
| [validation.md](validation.md) | accepted | The two-validator model (BAST-native for `validate_bytes`, standard `jsonschema` for `validate_json`), `AlkTypeError`, load-time vs access-time validation, `AlkTypeEngine` as the compiled form of a BAST document (ADR-010, ADR-VAL-SPLIT). |
|
||||
|
||||
@@ -140,7 +140,7 @@ These are different validators for different inputs.
|
||||
"int8", "int16", "int32", "int64",
|
||||
"uint8", "uint16", "uint32", "uint64",
|
||||
"float32", "float64",
|
||||
"bool", "string", "bytes", "timestamp"
|
||||
"bool", "string", "bytes"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -309,7 +309,7 @@ are convenient for one-off nested types.
|
||||
|
||||
### Primitives
|
||||
|
||||
The 14 primitive `kind` strings map to the unchanged `AlkTypeKind`
|
||||
The 13 primitive `kind` strings map to the unchanged `AlkTypeKind`
|
||||
variants (D-BAST-002 — lowercase strings, PascalCase enum variants):
|
||||
|
||||
| BAST kind | `AlkTypeKind` | Rust type | Size | Category |
|
||||
@@ -327,7 +327,6 @@ variants (D-BAST-002 — lowercase strings, PascalCase enum variants):
|
||||
| `bool` | `Boolean` | `bool` (`0x00`=false, `0x01`=true) | 1 | fixed |
|
||||
| `string` | `String` | length-prefixed UTF-8 | variable | variable |
|
||||
| `bytes` | `Bytes` | length-prefixed raw bytes | variable | variable |
|
||||
| `timestamp` | `Timestamp` | length-prefixed RFC 3339 string | variable | variable |
|
||||
|
||||
`int64`/`uint64` are alktype additions (not in TypeBox's `typedef.ts`),
|
||||
required by SFTP `offset: u64` and metatensor `data_offsets`. JSON
|
||||
@@ -457,7 +456,7 @@ override). In little-endian mode, `u32::from_le_bytes`; in big-endian
|
||||
mode, `u32::from_be_bytes`. Ensures SFTP consumers (big-endian) have
|
||||
consistent byte order for field values and length prefixes.
|
||||
|
||||
Applies to all variable-length types: `string`, `bytes`, `timestamp`,
|
||||
Applies to all variable-length types: `string`, `bytes`,
|
||||
`record`, and arrays of variable-length elements.
|
||||
|
||||
## Endianness
|
||||
@@ -541,7 +540,6 @@ BAST type tree that checks exactly these:
|
||||
| Float finiteness (Float32/64) | `validate_float` with `as_f64().is_finite()` |
|
||||
| String `maxLength` (byte length) | `check_string` reads the field-level `maxLength` |
|
||||
| Bytes `maxLength` (array length) | `check_bytes` accepts the `Value::Array` form (the materializer emits bytes as an array of u8) |
|
||||
| RFC 3339 timestamp shape | `validate_timestamp` — same non-strict check as v0.1.0 |
|
||||
| Enum index bounds | `validate_enum` checks `idx < values.len()` — **fixes the v0.1.0 dead constraint** |
|
||||
| Union variant dispatch | `validate_union` reads `__discriminator`, looks up the variant, recurses via `validate_typeref` |
|
||||
| Struct fields | `validate_struct` walks `fields`, requires each declared field present, recurses |
|
||||
|
||||
@@ -33,7 +33,7 @@ constructor used (D-BAST-008):
|
||||
|
||||
The builder covers:
|
||||
|
||||
- All 19 BAST kinds (binary-layout schemas) — see
|
||||
- All 18 BAST kinds (binary-layout schemas) — see
|
||||
[schema-layer.md](schema-layer.md) for the kinds and
|
||||
[`bast-format.md`](bast-format.md) for the format.
|
||||
- All standard JSON Schema keywords needed for operation payload
|
||||
@@ -113,7 +113,7 @@ constraints; `.build()` produces `Value`.
|
||||
#### BAST kind constructors
|
||||
|
||||
One constructor per `AlkTypeKind` variant (see [schema-layer.md](schema-layer.md)
|
||||
§"The 19 BAST Kinds"):
|
||||
§"The 18 BAST Kinds"):
|
||||
|
||||
```rust
|
||||
impl Schema {
|
||||
@@ -135,7 +135,6 @@ impl Schema {
|
||||
// Variable-length kinds
|
||||
pub fn string() -> Self;
|
||||
pub fn bytes() -> Self;
|
||||
pub fn timestamp() -> Self;
|
||||
// Composite kinds
|
||||
pub fn struct_() -> Self; // fields added via .field()
|
||||
pub fn union_(disc: Discriminator) -> Self; // variants via .mapping()
|
||||
@@ -150,7 +149,7 @@ Composite constructors produce the BAST object form.
|
||||
|
||||
**`enum_of`** produces a BAST enum TypeDef (`{ "kind": "enum", "values":
|
||||
[...] }`); declaration order is the index order — see
|
||||
[schema-layer.md](schema-layer.md) §"The 19 BAST Kinds"):
|
||||
[schema-layer.md](schema-layer.md) §"The 18 BAST Kinds"):
|
||||
|
||||
```rust
|
||||
Schema::enum_of(&["read", "write", "execute"]).build()
|
||||
|
||||
@@ -100,7 +100,7 @@ impl SequentialReader {
|
||||
```
|
||||
|
||||
`read_field`/`write_field` on `AlkTypeEngine` work for the fixed-size
|
||||
primitive kinds and the length-prefixed `String`/`Bytes`/`Timestamp`
|
||||
primitive kinds and the length-prefixed `String`/`Bytes`
|
||||
fields. Composite kinds (`Struct`, `Union`, `Array`, `Record`) return a
|
||||
`FieldValue` carrying a layout descriptor (byte range, variant start,
|
||||
or array stride) for the consumer to recurse on — see §"FieldValue" above.
|
||||
|
||||
@@ -27,8 +27,8 @@ three capabilities:
|
||||
3. **Validation** — two validators for two input types:
|
||||
- `validate_bytes(&[u8])` uses the BAST-native validator (a recursive
|
||||
walker over the BAST type tree) to check the value-domain
|
||||
constraints the materializer doesn't (integer ranges, `maxLength`,
|
||||
timestamp shape, enum index bounds, union variant constraints).
|
||||
constraints the materializer doesn't (integer ranges, `maxLength`,
|
||||
enum index bounds, union variant constraints).
|
||||
- `validate_json(&Value)` uses a standard `jsonschema::Validator`
|
||||
compiled from a consumer-provided JSON Schema (BAST is not involved
|
||||
— BAST describes bytes, not JSON shape).
|
||||
@@ -194,7 +194,7 @@ and [ADR-BAST](decisions/bast-bast-format.md).
|
||||
## Architecture (component pointers)
|
||||
|
||||
- **[schema-layer.md](schema-layer.md)** — the BAST parser (the typed
|
||||
surface every engine module walks), the 19 BAST kinds, the
|
||||
surface every engine module walks), the 18 BAST kinds, the
|
||||
`AlkTypeKind` enum, and the foundational annotation types.
|
||||
- **[`bast-format.md`](bast-format.md)** — the normative BAST format
|
||||
specification (meta-schema, TypeRef, examples, validation model).
|
||||
|
||||
@@ -18,7 +18,7 @@ validation model). This document describes the *implementation* — the
|
||||
typed parser in `src/bast.rs` and the foundational `AlkTypeKind` enum
|
||||
in `src/schema.rs` — and points at the format spec for shape details.
|
||||
|
||||
## The 19 BAST Kinds
|
||||
## The 18 BAST Kinds
|
||||
|
||||
BAST uses lowercase `kind` strings (`"uint32"`, `"struct"`, `"union"`,
|
||||
etc.). The engine represents them as the `AlkTypeKind` Rust enum — one
|
||||
@@ -41,7 +41,6 @@ comparison at every field access.
|
||||
| `bool` | `Boolean` | `bool` (`0x00`=false, `0x01`=true) | 1 | fixed |
|
||||
| `string` | `String` | length-prefixed UTF-8 | variable | variable |
|
||||
| `bytes` | `Bytes` | length-prefixed raw bytes | variable | variable |
|
||||
| `timestamp` | `Timestamp` | length-prefixed RFC 3339 string | variable | variable |
|
||||
| `struct` | `Struct` | record of fields | sum of field sizes | composite |
|
||||
| `union` | `Union` | tagged union | discriminator + variant | composite |
|
||||
| `array` | `Array` | repeated element | count × element size | composite |
|
||||
@@ -63,7 +62,7 @@ pub enum AlkTypeKind {
|
||||
Uint8, Uint16, Uint32, Uint64,
|
||||
Float32, Float64,
|
||||
Boolean, Enum,
|
||||
String, Bytes, Timestamp,
|
||||
String, Bytes,
|
||||
Struct, Union, Array, Record,
|
||||
}
|
||||
```
|
||||
@@ -78,7 +77,7 @@ The enum carries the kind's binary-layout metadata as inherent methods:
|
||||
| `natural_alignment(self)` | `usize` | 1 for u8/i8/bool, 2 for u16/i16, 4 for u32/i32/f32/enum, 8 for u64/i64/f64, 4 for variable-length (the u32 length prefix), 1 for struct/union/array |
|
||||
| `is_fixed_size(self)` | `bool` | True for the 12 fixed-size primitive kinds |
|
||||
| `is_composite(self)` | `bool` | True for Struct, Union, Array, Record |
|
||||
| `is_variable_length(self)` | `bool` | True for String, Bytes, Timestamp, Record |
|
||||
| `is_variable_length(self)` | `bool` | True for String, Bytes, Record |
|
||||
| `needs_endian(self)` | `bool` | True for kinds whose read/write takes an `Endian` parameter |
|
||||
|
||||
`AlkTypeKind` implements `Display`, backed by `to_bast_str` so the
|
||||
|
||||
@@ -48,7 +48,6 @@ tree ([`crate::bast::BastDoc`]/[`BastType`]) that checks exactly these:
|
||||
| Float finiteness (Float32/64) | `validate_float` with `as_f64().is_finite()` |
|
||||
| String `maxLength` (byte length) | `check_string` reads the field-level `maxLength` |
|
||||
| Bytes `maxLength` (array length) | `check_bytes` accepts the `Value::Array` form (the materializer emits bytes as an array of u8) |
|
||||
| RFC 3339 timestamp shape | `validate_timestamp` — non-strict check (matching v0.1.0) |
|
||||
| Enum index bounds | `validate_enum` checks `idx < values.len()` — **fixes the v0.1.0 dead constraint** |
|
||||
| Union variant dispatch | `validate_union` reads `__discriminator`, looks up the variant, recurses via `validate_typeref` |
|
||||
| Struct fields | `validate_struct` walks `fields`, requires each declared field present, recurses |
|
||||
|
||||
+2
-2
@@ -1139,7 +1139,7 @@ mod tests {
|
||||
{ "name": "a", "kind": "int8" },
|
||||
{ "name": "b", "kind": "uint64" },
|
||||
{ "name": "c", "kind": "bool" },
|
||||
{ "name": "d", "kind": "timestamp" }
|
||||
{ "name": "d", "kind": "bytes" }
|
||||
] } }
|
||||
});
|
||||
let d = doc_from(&root, "S");
|
||||
@@ -1150,7 +1150,7 @@ mod tests {
|
||||
assert!(matches!(s.fields()[0].ty(), BastType::Primitive(AlkTypeKind::Int8)));
|
||||
assert!(matches!(s.fields()[1].ty(), BastType::Primitive(AlkTypeKind::Uint64)));
|
||||
assert!(matches!(s.fields()[2].ty(), BastType::Primitive(AlkTypeKind::Boolean)));
|
||||
assert!(matches!(s.fields()[3].ty(), BastType::Primitive(AlkTypeKind::Timestamp)));
|
||||
assert!(matches!(s.fields()[3].ty(), BastType::Primitive(AlkTypeKind::Bytes)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+4
-4
@@ -92,7 +92,7 @@ pub static BAST_META_SCHEMA: LazyLock<Value> = LazyLock::new(|| {
|
||||
"int8", "int16", "int32", "int64",
|
||||
"uint8", "uint16", "uint32", "uint64",
|
||||
"float32", "float64",
|
||||
"bool", "string", "bytes", "timestamp"
|
||||
"bool", "string", "bytes"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -253,7 +253,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn meta_schema_typeref_enumerates_all_14_primitives() {
|
||||
fn meta_schema_typeref_enumerates_all_13_primitives() {
|
||||
let typeref_oneof = BAST_META_SCHEMA["$defs"]["TypeRef"]["oneOf"]
|
||||
.as_array()
|
||||
.expect("TypeRef.oneOf is array");
|
||||
@@ -265,12 +265,12 @@ mod tests {
|
||||
.as_array()
|
||||
.expect("primitive enum is array");
|
||||
let strs: Vec<&str> = primitives.iter().map(|v| v.as_str().unwrap()).collect();
|
||||
assert_eq!(strs.len(), 14);
|
||||
assert_eq!(strs.len(), 13);
|
||||
for expected in [
|
||||
"int8", "int16", "int32", "int64",
|
||||
"uint8", "uint16", "uint32", "uint64",
|
||||
"float32", "float64",
|
||||
"bool", "string", "bytes", "timestamp",
|
||||
"bool", "string", "bytes",
|
||||
] {
|
||||
assert!(strs.contains(&expected), "missing primitive {expected}");
|
||||
}
|
||||
|
||||
+1
-69
@@ -29,7 +29,6 @@
|
||||
//! | Float finiteness (Float32/64) | `validate_float` |
|
||||
//! | String `maxLength` (byte length) | `check_string` reads [`BastField::max_length`](crate::bast::BastField::max_length) |
|
||||
//! | Bytes `maxLength` (array length) | `check_bytes` |
|
||||
//! | RFC 3339 timestamp shape | `validate_timestamp` (non-strict, matching v0.1.0) |
|
||||
//! | Enum index bounds | `validate_enum` — **fixes the v0.1.0 dead constraint** |
|
||||
//! | Union variant dispatch | `validate_union` reads `__discriminator`, resolves the variant, recurses |
|
||||
//! | Struct fields | `validate_struct` walks `fields`, requires each declared field present, recurses |
|
||||
@@ -71,7 +70,7 @@ const DISCRIMINATOR_KEY: &str = "__discriminator";
|
||||
/// from the bytes, this walker enforces the value-domain constraints
|
||||
/// expressed in the BAST document. The materializer already guarantees
|
||||
/// structural correctness; the validator only checks what the bytes
|
||||
/// alone can't tell you (integer ranges, `maxLength`, timestamp shape,
|
||||
/// alone can't tell you (integer ranges, `maxLength`,
|
||||
/// enum index bounds, union variant constraints).
|
||||
///
|
||||
/// Returns `Err(AlkTypeError::Validation(...))` on the first violated
|
||||
@@ -121,7 +120,6 @@ fn validate_typeref(
|
||||
BastType::Primitive(AlkTypeKind::Bytes) => {
|
||||
check_bytes(value, path, field.and_then(|f| f.max_length()))
|
||||
}
|
||||
BastType::Primitive(AlkTypeKind::Timestamp) => validate_timestamp(value, path),
|
||||
BastType::Enum(e) => validate_enum(value, path, e),
|
||||
BastType::Struct(s) => validate_struct(doc, s, value, path),
|
||||
BastType::Union(u) => validate_union(doc, u, value, path),
|
||||
@@ -239,13 +237,6 @@ fn check_bytes(value: &Value, path: &str, max_length: Option<usize>) -> Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_timestamp(value: &Value, path: &str) -> Result<(), AlkTypeError> {
|
||||
match value.as_str() {
|
||||
Some(s) if is_rfc3339_timestamp(s) => Ok(()),
|
||||
_ => Err(validation_err(path, "expected an RFC 3339 timestamp string")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum validation on the bytes path: the materializer emits a numeric
|
||||
/// index (`Value::Number`), and the constraint is that the index is
|
||||
/// within the `values` array bounds (`0..len-1`).
|
||||
@@ -381,48 +372,6 @@ fn validation_err(path: &str, reason: impl Into<String>) -> AlkTypeError {
|
||||
AlkTypeError::Validation(jsonschema::ValidationError::custom(msg))
|
||||
}
|
||||
|
||||
/// Non-strict RFC 3339 / ISO 8601 datetime check — same shape as the
|
||||
/// v0.1.0 `TimestampValidator` so behavior is identical. Structural
|
||||
/// sanity, not strict validation (Feb 31 passes; seconds range isn't
|
||||
/// checked; leap seconds aren't handled). Strict RFC 3339 validation is
|
||||
/// the consumer's responsibility if they need it (ADR-003, review #002
|
||||
/// N1).
|
||||
fn is_rfc3339_timestamp(s: &str) -> bool {
|
||||
let parts: Vec<&str> = s.splitn(2, 'T').collect();
|
||||
if parts.len() != 2 {
|
||||
return false;
|
||||
}
|
||||
let date_parts: Vec<&str> = parts[0].split('-').collect();
|
||||
if date_parts.len() != 3 {
|
||||
return false;
|
||||
}
|
||||
let time_part = parts[1];
|
||||
let time_clean = if let Some(pos) = time_part.find(['Z', '+']) {
|
||||
&time_part[..pos]
|
||||
} else if let Some(pos) = time_part.rfind('-') {
|
||||
if pos >= 8 {
|
||||
&time_part[..pos]
|
||||
} else {
|
||||
time_part
|
||||
}
|
||||
} else {
|
||||
time_part
|
||||
};
|
||||
let time_parts: Vec<&str> = time_clean.split(':').collect();
|
||||
if time_parts.len() < 2 || time_parts.len() > 3 {
|
||||
return false;
|
||||
}
|
||||
date_parts[0].parse::<u16>().is_ok_and(|y| y > 0)
|
||||
&& date_parts[1]
|
||||
.parse::<u8>()
|
||||
.is_ok_and(|m| (1..=12).contains(&m))
|
||||
&& date_parts[2]
|
||||
.parse::<u8>()
|
||||
.is_ok_and(|d| (1..=31).contains(&d))
|
||||
&& time_parts[0].parse::<u8>().is_ok_and(|h| h <= 23)
|
||||
&& time_parts[1].parse::<u8>().is_ok_and(|m| m <= 59)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -564,23 +513,6 @@ mod tests {
|
||||
assert!(validate_value(&d, &json!({"blob": [65, 255]})).is_ok());
|
||||
}
|
||||
|
||||
// ----- Timestamp -------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn timestamp_valid_and_invalid() {
|
||||
let root = json!({
|
||||
"$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "ts", "kind": "timestamp" }
|
||||
] } }
|
||||
});
|
||||
let d = doc_from(&root, "S");
|
||||
assert!(materialize_and_validate(&d, &prefixed_str_le("2026-07-20T15:30:00Z")).is_ok());
|
||||
assert!(materialize_and_validate(&d, &prefixed_str_le("2026-07-20T15:30:00")).is_ok());
|
||||
assert!(materialize_and_validate(&d, &prefixed_str_le("2026-07-20T15:30:00+02:00")).is_ok());
|
||||
let err = materialize_and_validate(&d, &prefixed_str_le("not-a-date")).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
// ----- Enum index bounds (the v0.1.0 dead-constraint fix) -------------
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -170,10 +170,6 @@ impl Schema {
|
||||
pub fn bytes() -> Self {
|
||||
Self::primitive(AlkTypeKind::Bytes)
|
||||
}
|
||||
/// BAST `timestamp` (length-prefixed RFC 3339 string).
|
||||
pub fn timestamp() -> Self {
|
||||
Self::primitive(AlkTypeKind::Timestamp)
|
||||
}
|
||||
|
||||
fn primitive(kind: AlkTypeKind) -> Self {
|
||||
Self {
|
||||
@@ -837,7 +833,6 @@ mod tests {
|
||||
assert_eq!(Schema::boolean().build(), json!("bool"));
|
||||
assert_eq!(Schema::string().build(), json!("string"));
|
||||
assert_eq!(Schema::bytes().build(), json!("bytes"));
|
||||
assert_eq!(Schema::timestamp().build(), json!("timestamp"));
|
||||
}
|
||||
|
||||
// ----- Struct -------------------------------------------------------
|
||||
|
||||
+3
-29
@@ -259,7 +259,7 @@ impl AlkTypeEngine {
|
||||
/// and then validating that `Value` against the BAST-native validator
|
||||
/// — a recursive walker over the BAST type tree that checks the
|
||||
/// value-domain constraints the materializer does not (integer
|
||||
/// ranges, `maxLength`, timestamp shape, enum index bounds, union
|
||||
/// ranges, `maxLength`, enum index bounds, union
|
||||
/// variant constraints). Decided in D-BAST-006; see
|
||||
/// [bast-format.md §Validation Model](../../docs/architecture/bast-format.md#validation-model).
|
||||
///
|
||||
@@ -296,7 +296,7 @@ impl AlkTypeEngine {
|
||||
/// Looks up the field's byte range in the [`OffsetMap`] and reads the
|
||||
/// appropriate type using the [`crate::data_access`] functions. Works
|
||||
/// for fixed-size primitive kinds and length-prefixed `String`/
|
||||
/// `Bytes`/`Timestamp` fields.
|
||||
/// `Bytes` fields.
|
||||
///
|
||||
/// Returns an error if compiled in packed mode — use
|
||||
/// [`AlkTypeEngine::sequential_reader`] for packed mode. Also
|
||||
@@ -409,10 +409,6 @@ impl AlkTypeEngine {
|
||||
};
|
||||
Ok(FieldValue::Bytes(v))
|
||||
}
|
||||
AlkTypeKind::Timestamp => {
|
||||
let v = data_access::read_string(buffer, range.start, field_path, endian)?;
|
||||
Ok(FieldValue::String(v))
|
||||
}
|
||||
AlkTypeKind::Struct => Ok(FieldValue::Struct {
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
@@ -433,7 +429,7 @@ impl AlkTypeEngine {
|
||||
/// Looks up the field's byte range in the [`OffsetMap`] and writes the
|
||||
/// appropriate type using the [`crate::data_access`] functions. Works
|
||||
/// for fixed-size primitive kinds and length-prefixed `String`/
|
||||
/// `Bytes`/`Timestamp` fields.
|
||||
/// `Bytes` fields.
|
||||
///
|
||||
/// Returns an error if compiled in packed mode — use
|
||||
/// [`AlkTypeEngine::layout_builder`] for packed mode. Also returns
|
||||
@@ -1080,28 +1076,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_field_aligned_reads_timestamp_as_string() {
|
||||
let doc = json!({
|
||||
"$defs": {
|
||||
"S": {
|
||||
"kind": "struct",
|
||||
"fields": [ { "name": "ts", "kind": "timestamp" } ]
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).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();
|
||||
buf[0..4].copy_from_slice(&len_bytes);
|
||||
buf[4..4 + stamp.len()].copy_from_slice(stamp.as_bytes());
|
||||
match engine.read_field(&buf, "ts").unwrap() {
|
||||
FieldValue::String(s) => assert_eq!(s, stamp),
|
||||
other => panic!("expected String, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_field_aligned_honors_field_level_endian_override() {
|
||||
let doc = json!({
|
||||
|
||||
+3
-27
@@ -15,8 +15,8 @@
|
||||
//! known byte size; the offset advances by the size. No alignment
|
||||
//! padding is inserted (the `u32` at offset 1 is unaligned — correct
|
||||
//! for protocol wire formats).
|
||||
//! - **Variable-length fields** (`string`, `bytes`, `timestamp`,
|
||||
//! `record`): always inline length-prefixed in packed mode. The
|
||||
//! - **Variable-length fields** (`string`, `bytes`, `record`): always
|
||||
//! inline length-prefixed in packed mode. The
|
||||
//! 4-byte length prefix is recorded at the current offset; the offset
|
||||
//! advances by `4 + data_size` where `data_size` comes from `var_sizes`
|
||||
//! keyed by the field's dotted path.
|
||||
@@ -291,7 +291,7 @@ impl<'d> BuildCtx<'d> {
|
||||
}
|
||||
|
||||
/// Compute the layout for a variable-length field (String/Bytes/
|
||||
/// Timestamp/Record). Always inline length-prefixed in packed mode.
|
||||
/// Record). Always inline length-prefixed in packed mode.
|
||||
fn walk_variable(
|
||||
&mut self,
|
||||
field_path: &str,
|
||||
@@ -702,30 +702,6 @@ mod tests {
|
||||
assert_eq!(layout.total_size(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timestamp_field_uses_var_sizes() {
|
||||
let root = json!({
|
||||
"$defs": {
|
||||
"S": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{ "name": "ts", "kind": "timestamp" }
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
let layout = build(&root, "S", &var_sizes(&[("ts", 20)]));
|
||||
assert_eq!(
|
||||
layout.get("ts"),
|
||||
Some(&FieldPosition {
|
||||
offset: 0,
|
||||
size: 4,
|
||||
kind: AlkTypeKind::Timestamp
|
||||
})
|
||||
);
|
||||
assert_eq!(layout.total_size(), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_field_uses_var_sizes() {
|
||||
let root = json!({
|
||||
|
||||
+3
-14
@@ -208,15 +208,6 @@ fn materialize_typeref_packed(
|
||||
let arr: Vec<Value> = b.iter().map(|&byte| Value::from(u32::from(byte))).collect();
|
||||
Ok(Value::Array(arr))
|
||||
}
|
||||
BastType::Primitive(AlkTypeKind::Timestamp) => {
|
||||
let s = data_access::read_string(buffer, *offset, field_path, endian)?;
|
||||
let len = U32_SIZE + s.len();
|
||||
*offset = offset.checked_add(len).ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("timestamp offset {len} overflows usize"),
|
||||
})?;
|
||||
Ok(Value::String(s.to_string()))
|
||||
}
|
||||
BastType::Struct(s) => {
|
||||
materialize_struct_packed(doc, s, field_path, endian, buffer, offset)
|
||||
}
|
||||
@@ -556,7 +547,7 @@ fn materialize_array_aligned(
|
||||
Ok(Value::Array(arr))
|
||||
}
|
||||
|
||||
/// Materialize a variable-length leaf (String/Bytes/Timestamp) in aligned
|
||||
/// Materialize a variable-length leaf (String/Bytes) in aligned
|
||||
/// mode, dispatching on the field's encoding strategy:
|
||||
///
|
||||
/// - `offset-indirect`: an 8-byte `{offset, length}` pair at `start`,
|
||||
@@ -576,8 +567,7 @@ fn materialize_variable_aligned(
|
||||
let start = range.start;
|
||||
match field.encoding() {
|
||||
VariableEncoding::OffsetIndirect => match ty {
|
||||
BastType::Primitive(AlkTypeKind::String)
|
||||
| BastType::Primitive(AlkTypeKind::Timestamp) => {
|
||||
BastType::Primitive(AlkTypeKind::String) => {
|
||||
let s = data_access::read_string_indirect(buffer, start, field_path, endian)?;
|
||||
Ok(Value::String(s.to_string()))
|
||||
}
|
||||
@@ -603,8 +593,7 @@ fn materialize_variable_aligned(
|
||||
.unwrap_or(0);
|
||||
let trimmed = &data[..trimmed_len];
|
||||
match ty {
|
||||
BastType::Primitive(AlkTypeKind::String)
|
||||
| BastType::Primitive(AlkTypeKind::Timestamp) => {
|
||||
BastType::Primitive(AlkTypeKind::String) => {
|
||||
let s = std::str::from_utf8(trimmed).map_err(|e| AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("invalid UTF-8 in maxLength string: {e}"),
|
||||
|
||||
+1
-1
@@ -388,7 +388,7 @@ impl<'d> ComputeCtx<'d> {
|
||||
Ok(FieldLayout { align: array_align })
|
||||
}
|
||||
|
||||
/// Compute the layout for a variable-length field (String/Bytes/Record/Timestamp).
|
||||
/// Compute the layout for a variable-length field (String/Bytes/Record).
|
||||
///
|
||||
/// In aligned static mode, three strategies are supported:
|
||||
/// - `maxLength` reservation: `maxLength` bytes at a fixed offset.
|
||||
|
||||
+7
-24
@@ -16,7 +16,7 @@ use std::fmt;
|
||||
pub(crate) const U32_SIZE: usize = 4;
|
||||
pub(crate) const DISCRIMINATOR_PATH: &str = "__discriminator";
|
||||
|
||||
/// The 19 BAST kinds recognized by the engine.
|
||||
/// The 18 BAST kinds recognized by the engine.
|
||||
///
|
||||
/// Each variant corresponds to a lowercase BAST kind string
|
||||
/// ([`AlkTypeKind::to_bast_str`], e.g. `"uint32"` ↔ `Uint32`). The enum
|
||||
@@ -42,7 +42,6 @@ pub enum AlkTypeKind {
|
||||
Union,
|
||||
Array,
|
||||
Record,
|
||||
Timestamp,
|
||||
}
|
||||
|
||||
impl AlkTypeKind {
|
||||
@@ -60,8 +59,7 @@ impl AlkTypeKind {
|
||||
| AlkTypeKind::Struct
|
||||
| AlkTypeKind::Union
|
||||
| AlkTypeKind::Array
|
||||
| AlkTypeKind::Record
|
||||
| AlkTypeKind::Timestamp => None,
|
||||
| AlkTypeKind::Record => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,8 +77,7 @@ impl AlkTypeKind {
|
||||
AlkTypeKind::Float64 | AlkTypeKind::Int64 | AlkTypeKind::Uint64 => 8,
|
||||
AlkTypeKind::String
|
||||
| AlkTypeKind::Bytes
|
||||
| AlkTypeKind::Record
|
||||
| AlkTypeKind::Timestamp => 4,
|
||||
| AlkTypeKind::Record => 4,
|
||||
AlkTypeKind::Struct | AlkTypeKind::Union | AlkTypeKind::Array => 1,
|
||||
}
|
||||
}
|
||||
@@ -119,7 +116,6 @@ impl AlkTypeKind {
|
||||
| AlkTypeKind::Enum
|
||||
| AlkTypeKind::String
|
||||
| AlkTypeKind::Bytes
|
||||
| AlkTypeKind::Timestamp
|
||||
)
|
||||
}
|
||||
|
||||
@@ -131,13 +127,12 @@ impl AlkTypeKind {
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns `true` for variable-length kinds (String, Bytes, Timestamp, Record).
|
||||
/// Returns `true` for variable-length kinds (String, Bytes, Record).
|
||||
pub fn is_variable_length(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
AlkTypeKind::String
|
||||
| AlkTypeKind::Bytes
|
||||
| AlkTypeKind::Timestamp
|
||||
| AlkTypeKind::Record
|
||||
)
|
||||
}
|
||||
@@ -161,7 +156,6 @@ impl AlkTypeKind {
|
||||
AlkTypeKind::Boolean => "bool",
|
||||
AlkTypeKind::String => "string",
|
||||
AlkTypeKind::Bytes => "bytes",
|
||||
AlkTypeKind::Timestamp => "timestamp",
|
||||
AlkTypeKind::Enum => "enum",
|
||||
AlkTypeKind::Struct => "struct",
|
||||
AlkTypeKind::Union => "union",
|
||||
@@ -189,7 +183,6 @@ impl AlkTypeKind {
|
||||
"bool" => Ok(AlkTypeKind::Boolean),
|
||||
"string" => Ok(AlkTypeKind::String),
|
||||
"bytes" => Ok(AlkTypeKind::Bytes),
|
||||
"timestamp" => Ok(AlkTypeKind::Timestamp),
|
||||
"enum" => Ok(AlkTypeKind::Enum),
|
||||
"struct" => Ok(AlkTypeKind::Struct),
|
||||
"union" => Ok(AlkTypeKind::Union),
|
||||
@@ -255,7 +248,6 @@ mod tests {
|
||||
AlkTypeKind::Union,
|
||||
AlkTypeKind::Array,
|
||||
AlkTypeKind::Record,
|
||||
AlkTypeKind::Timestamp,
|
||||
] {
|
||||
assert_eq!(kind.type_size(), None, "failed for {kind:?}");
|
||||
}
|
||||
@@ -278,7 +270,6 @@ mod tests {
|
||||
assert_eq!(AlkTypeKind::String.natural_alignment(), 4);
|
||||
assert_eq!(AlkTypeKind::Bytes.natural_alignment(), 4);
|
||||
assert_eq!(AlkTypeKind::Record.natural_alignment(), 4);
|
||||
assert_eq!(AlkTypeKind::Timestamp.natural_alignment(), 4);
|
||||
assert_eq!(AlkTypeKind::Struct.natural_alignment(), 1);
|
||||
assert_eq!(AlkTypeKind::Union.natural_alignment(), 1);
|
||||
assert_eq!(AlkTypeKind::Array.natural_alignment(), 1);
|
||||
@@ -309,7 +300,6 @@ mod tests {
|
||||
AlkTypeKind::Union,
|
||||
AlkTypeKind::Array,
|
||||
AlkTypeKind::Record,
|
||||
AlkTypeKind::Timestamp,
|
||||
] {
|
||||
assert!(!kind.is_fixed_size(), "expected variable: {kind:?}");
|
||||
}
|
||||
@@ -329,7 +319,6 @@ mod tests {
|
||||
AlkTypeKind::Enum,
|
||||
AlkTypeKind::String,
|
||||
AlkTypeKind::Bytes,
|
||||
AlkTypeKind::Timestamp,
|
||||
] {
|
||||
assert!(kind.needs_endian(), "expected needs_endian: {kind:?}");
|
||||
}
|
||||
@@ -371,14 +360,13 @@ mod tests {
|
||||
AlkTypeKind::Enum,
|
||||
AlkTypeKind::String,
|
||||
AlkTypeKind::Bytes,
|
||||
AlkTypeKind::Timestamp,
|
||||
] {
|
||||
assert!(!kind.is_composite(), "expected not composite: {kind:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_bast_str_covers_all_19_variants() {
|
||||
fn to_bast_str_covers_all_18_variants() {
|
||||
let cases: &[(AlkTypeKind, &str)] = &[
|
||||
(AlkTypeKind::Int8, "int8"),
|
||||
(AlkTypeKind::Int16, "int16"),
|
||||
@@ -393,14 +381,13 @@ mod tests {
|
||||
(AlkTypeKind::Boolean, "bool"),
|
||||
(AlkTypeKind::String, "string"),
|
||||
(AlkTypeKind::Bytes, "bytes"),
|
||||
(AlkTypeKind::Timestamp, "timestamp"),
|
||||
(AlkTypeKind::Enum, "enum"),
|
||||
(AlkTypeKind::Struct, "struct"),
|
||||
(AlkTypeKind::Union, "union"),
|
||||
(AlkTypeKind::Array, "array"),
|
||||
(AlkTypeKind::Record, "record"),
|
||||
];
|
||||
assert_eq!(cases.len(), 19);
|
||||
assert_eq!(cases.len(), 18);
|
||||
for (kind, expected) in cases {
|
||||
assert_eq!(kind.to_bast_str(), *expected);
|
||||
}
|
||||
@@ -422,7 +409,6 @@ mod tests {
|
||||
AlkTypeKind::Boolean,
|
||||
AlkTypeKind::String,
|
||||
AlkTypeKind::Bytes,
|
||||
AlkTypeKind::Timestamp,
|
||||
AlkTypeKind::Enum,
|
||||
AlkTypeKind::Struct,
|
||||
AlkTypeKind::Union,
|
||||
@@ -449,10 +435,6 @@ mod tests {
|
||||
AlkTypeKind::from_bast_str("struct"),
|
||||
Ok(AlkTypeKind::Struct)
|
||||
));
|
||||
assert!(matches!(
|
||||
AlkTypeKind::from_bast_str("timestamp"),
|
||||
Ok(AlkTypeKind::Timestamp)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -460,6 +442,7 @@ mod tests {
|
||||
assert!(AlkTypeKind::from_bast_str("uint128").is_err());
|
||||
assert!(AlkTypeKind::from_bast_str("Uint32").is_err());
|
||||
assert!(AlkTypeKind::from_bast_str("AlkType:Uint32").is_err());
|
||||
assert!(AlkTypeKind::from_bast_str("timestamp").is_err());
|
||||
assert!(AlkTypeKind::from_bast_str("").is_err());
|
||||
}
|
||||
}
|
||||
@@ -359,11 +359,6 @@ fn read_field_value<'a>(
|
||||
let total = U32_SIZE + b.len();
|
||||
Ok((FieldValue::Bytes(b), offset + total))
|
||||
}
|
||||
BastType::Primitive(AlkTypeKind::Timestamp) => {
|
||||
let s = data_access::read_string(buffer, offset, field_path, endian)?;
|
||||
let total = U32_SIZE + s.len();
|
||||
Ok((FieldValue::String(s), offset + total))
|
||||
}
|
||||
BastType::Struct(s) => {
|
||||
let size = walk_struct_size(doc, s, buffer, offset, endian)?;
|
||||
let end = offset
|
||||
@@ -745,11 +740,6 @@ fn read_primitive_value<'a>(
|
||||
let total = U32_SIZE + b.len();
|
||||
Ok((FieldValue::Bytes(b), offset + total))
|
||||
}
|
||||
AlkTypeKind::Timestamp => {
|
||||
let s = data_access::read_string(buffer, offset, field_path, endian)?;
|
||||
let total = U32_SIZE + s.len();
|
||||
Ok((FieldValue::String(s), offset + total))
|
||||
}
|
||||
other => Err(AlkTypeError::Schema(format!(
|
||||
"{other} is not a primitive readable by read_primitive_value"
|
||||
))),
|
||||
@@ -1398,27 +1388,6 @@ mod tests {
|
||||
assert_eq!(reader.position(), pos);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timestamp_field_reads_as_length_prefixed_string() {
|
||||
let root = json!({
|
||||
"$defs": {
|
||||
"S": {
|
||||
"kind": "struct",
|
||||
"fields": [ { "name": "ts", "kind": "timestamp" } ]
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut buf = vec![0u8; 64];
|
||||
let stamp = "2026-07-20T15:30:00Z";
|
||||
let written = write_string(&mut buf, 0, stamp, LE);
|
||||
|
||||
let mut reader = reader(&root, "S");
|
||||
let (name, value) = reader.read_next(&buf).unwrap().unwrap();
|
||||
assert_eq!(name, "ts");
|
||||
assert_eq!(value, FieldValue::String(stamp));
|
||||
assert_eq!(reader.position(), written);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_field_walks_entries() {
|
||||
let root = json!({
|
||||
|
||||
Reference in new issue
Block a user