Rebrand TypeDef to AlkType in code, keyword strings, and docs
Rename all 19 JSON Schema custom keyword strings from "TypeDef:*"
to "AlkType:*" (e.g., "TypeDef:Struct" -> "AlkType:Struct")
across source, tests, and docs. This is a breaking change to the
schema format itself — existing schemas using the old keywords
must be updated.
Rename the Rust identifiers:
- TypedefEngine -> AlkTypeEngine
- TypedefError -> AlkTypeError
- TypeDefKind -> AlkTypeKind
- TYPEDEF_PREFIX -> ALKTYPE_PREFIX
- get_typedef_kind{,_loose,_loose_enum,_enum} ->
get_alktype_kind{,_loose,_loose_enum,_enum}
Update error message strings ("unknown TypeDef kind" ->
"unknown AlkType kind"), 11 test function names containing
typedef_kind/to_typedef_error, and doc-comment prose ("TypeDef
kind" -> "AlkType kind", "typedef engine" -> "alktype
engine", "typedef schema" -> "alktype schema"). Fix the broken
docs/architecture/crates/typedef/ path references in source doc
comments to point at docs/architecture/ directly. Rebrand the
typedef:annotation test fixture and the "not-a-typedef" test
string to their alktype equivalents.
Update ~20 generic "typedef" prose references in the architecture
docs ("typedef is the binary struct engine", "use typedef",
"typedef limitation", "replaced by typedef", etc.) to alktype.
Rename TypedefEngine in the ADR-007 code example to AlkTypeEngine.
Preserve as provenance per the prior prose-rebrand decision:
typedef.ts references (external TypeBox source file),
docs/research/alknet-typedef/findings.md research citations,
/workspace/alknet-typedef-poc/ POC path, and the
"alknet-typedef:" research section headers in findings.
Build, 295 tests, and clippy all pass clean.
This commit is contained in:
@@ -3,7 +3,7 @@ name = "alktype"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Binary struct engine: takes a JSON Schema with TypeDef:* custom keywords and produces an offset map, read/write functions, and validation"
|
||||
description = "Binary struct engine: takes a JSON Schema with AlkType:* custom keywords and produces an offset map, read/write functions, and validation"
|
||||
repository = "https://git.alk.dev/alkdev/alktype"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -6,7 +6,7 @@ last_updated: 2026-07-22
|
||||
# alktype
|
||||
|
||||
The binary struct engine: a small Rust crate that takes a JSON Schema
|
||||
with `TypeDef:*` custom keywords and produces an offset map, read/write
|
||||
with `AlkType:*` custom keywords and produces an offset map, read/write
|
||||
functions, and validation — all driven by the schema. The schema is the
|
||||
format definition; the engine is generic.
|
||||
|
||||
@@ -15,10 +15,10 @@ format definition; the engine is generic.
|
||||
| Document | Status | Description |
|
||||
|----------|--------|-------------|
|
||||
| [overview.md](overview.md) | draft | Crate purpose, "schema is the format" principle, dependencies, consumers, scope boundaries |
|
||||
| [schema-layer.md](schema-layer.md) | draft | The 19 `TypeDef:*` kinds, jsonschema custom keyword integration, TypeBox interop, schema annotations |
|
||||
| [schema-layer.md](schema-layer.md) | draft | The 19 `AlkType:*` kinds, jsonschema custom keyword integration, TypeBox interop, schema annotations |
|
||||
| [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) | draft | Custom keyword validators for all 19 `TypeDef:*` kinds, `TypedefError`, load-time vs access-time validation, `TypedefEngine` |
|
||||
| [validation.md](validation.md) | draft | Custom keyword validators for all 19 `AlkType:*` kinds, `AlkTypeError`, load-time vs access-time validation, `AlkTypeEngine` |
|
||||
|
||||
## Applicable ADRs
|
||||
|
||||
@@ -27,7 +27,7 @@ format definition; the engine is generic.
|
||||
| [001](decisions/001-alktype-purpose-scope-jsonschema-engine.md) | Purpose, Scope, and the jsonschema Engine | What the crate is/isn't; why jsonschema not a custom engine; "schema is the format" principle; scope boundaries |
|
||||
| [002](decisions/002-two-layout-modes-packed-vs-aligned.md) | Two Layout Modes — Packed Sequential vs Aligned Static | The most important architectural finding; when to use each mode; `LayoutBuilder`/`SequentialReader` vs `OffsetMap` |
|
||||
| [003](decisions/003-schema-annotations.md) | Schema Annotations — Endianness, Alignment, Encoding, TUnion Discriminators | Concrete JSON shapes for all schema-level annotations |
|
||||
| [004](decisions/004-error-handling-validation-strategy.md) | Error Handling and Validation Strategy | `TypedefError` enum; load-time build, access-time check; field-path-carrying errors |
|
||||
| [004](decisions/004-error-handling-validation-strategy.md) | Error Handling and Validation Strategy | `AlkTypeError` enum; load-time build, access-time check; field-path-carrying errors |
|
||||
| [005](decisions/005-int64-uint64-first-class-kinds.md) | Int64/Uint64 as First-Class Kinds | 64-bit integers (SFTP offsets, metatensor data_offsets); JSON precision caveat |
|
||||
| [006](decisions/006-reject-non-final-inline-length-prefixed-in-aligned-mode.md) | Reject Non-Final Inline Length-Prefixed Variable Fields in Aligned Mode | Prevents silent data corruption (inline variable data clobbering subsequent fields) |
|
||||
| [007](decisions/007-packed-mode-read-factory.md) | Packed-Mode Read API — Engine as SequentialReader Factory | `engine.sequential_reader()` returns an owned reader, not a reference |
|
||||
@@ -43,7 +43,7 @@ format definition; the engine is generic.
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
1. **The schema is the format.** A JSON Schema with `TypeDef:*` custom
|
||||
1. **The schema is the format.** A JSON Schema with `AlkType:*` custom
|
||||
keywords is both the validation spec and the layout spec. No separate
|
||||
format definition, no separate parser, no separate validator. One
|
||||
schema, three uses: validate, compute offsets, access data. See
|
||||
@@ -88,11 +88,11 @@ format definition; the engine is generic.
|
||||
[validation.md](validation.md) and
|
||||
[ADR-004](decisions/004-error-handling-validation-strategy.md).
|
||||
|
||||
8. **Not a serialization framework.** The typedef engine is not a
|
||||
8. **Not a serialization framework.** The alktype engine is not a
|
||||
general-purpose serde replacement. It operates on raw byte buffers at
|
||||
computed offsets — no intermediate `Value` tree, no reflection, no
|
||||
dynamic dispatch per field. For JSON data, use serde. For binary data
|
||||
with a known schema, use typedef. See [overview.md](overview.md) and
|
||||
with a known schema, use alktype. See [overview.md](overview.md) and
|
||||
[ADR-001](decisions/001-alktype-purpose-scope-jsonschema-engine.md).
|
||||
|
||||
## References
|
||||
@@ -106,5 +106,5 @@ format definition; the engine is generic.
|
||||
schema kinds (619 lines)
|
||||
- `/workspace/jsonschema/` — the jsonschema crate (v0.46.5, Draft 2020-12)
|
||||
- `/workspace/alknet-typedef-poc/` — the POC code (disposable)
|
||||
- `/workspace/@alkimiadev/typebox-rs/` — prior attempt, replaced by typedef
|
||||
- `/workspace/@alkimiadev/typebox-rs/` — prior attempt, replaced by alktype
|
||||
- `/workspace/@alkimiadev/alktype/` — prior attempt (the @alkimiadev/alktype prototype, a handler-registry pattern; not to be confused with this crate, which reuses the name but is backed by the `jsonschema` crate)
|
||||
|
||||
@@ -8,7 +8,7 @@ last_updated: 2026-07-22
|
||||
The data access layer: read/write functions, TUnion dispatch, field paths,
|
||||
zero-copy access for fixed-size types, and length-prefix reading for
|
||||
variable-length types. This is the consumer-facing API — given a compiled
|
||||
`TypedefEngine` and a byte buffer, read and write fields at
|
||||
`AlkTypeEngine` and a byte buffer, read and write fields at
|
||||
schema-computed offsets.
|
||||
|
||||
This document covers two layers:
|
||||
@@ -16,11 +16,11 @@ This document covers two layers:
|
||||
- **Primitive read/write functions** in the `data_access` module —
|
||||
typed reads/writes at a caller-provided offset. These are the building
|
||||
blocks used by the layout types (`OffsetMap`, `LayoutBuilder`,
|
||||
`SequentialReader`) and the `TypedefEngine`. Each operates on a raw
|
||||
byte buffer at a known offset and returns a `TypedefError::Access`
|
||||
`SequentialReader`) and the `AlkTypeEngine`. Each operates on a raw
|
||||
byte buffer at a known offset and returns a `AlkTypeError::Access`
|
||||
carrying the field path on bounds or encoding failures.
|
||||
- **The `FieldValue` enum and the higher-level APIs** —
|
||||
`TypedefEngine::read_field`/`write_field` (aligned mode) and
|
||||
`AlkTypeEngine::read_field`/`write_field` (aligned mode) and
|
||||
`SequentialReader::read_next`/`read_field` (packed mode) — which look
|
||||
up a field's offset via the layout and dispatch to the primitive
|
||||
functions, returning a unified `FieldValue<'a>`.
|
||||
@@ -55,7 +55,7 @@ types, signalling the consumer must walk each element sequentially.
|
||||
|
||||
## Read/Write Model
|
||||
|
||||
The typedef engine operates on raw byte buffers (`&[u8]` for reading,
|
||||
The alktype engine operates on raw byte buffers (`&[u8]` for reading,
|
||||
`&mut [u8]` for writing). There is no intermediate `Value` tree, no
|
||||
reflection, no dynamic dispatch per field. The engine uses the offset map
|
||||
(or `LayoutBuilder`/`SequentialReader`) to locate fields, then performs
|
||||
@@ -63,21 +63,21 @@ typed access at the computed positions.
|
||||
|
||||
### Higher-level read/write
|
||||
|
||||
The `TypedefEngine` and `SequentialReader` provide the primary
|
||||
The `AlkTypeEngine` and `SequentialReader` provide the primary
|
||||
consumer-facing read/write APIs. They look up a field's offset via the
|
||||
layout and dispatch to the primitive `data_access` functions, returning
|
||||
`FieldValue` (read) or accepting `&FieldValue` (write).
|
||||
|
||||
```rust
|
||||
impl TypedefEngine {
|
||||
impl AlkTypeEngine {
|
||||
// Aligned mode: looks up the field's ByteRange in the OffsetMap,
|
||||
// dispatches to the right data_access function by TypeDefKind.
|
||||
// Returns TypedefError::Access if compiled in packed mode
|
||||
// dispatches to the right data_access function by AlkTypeKind.
|
||||
// Returns AlkTypeError::Access if compiled in packed mode
|
||||
// (use sequential_reader() for packed mode).
|
||||
pub fn read_field<'a>(&self, buffer: &'a [u8], field_path: &str)
|
||||
-> Result<FieldValue<'a>, TypedefError>;
|
||||
-> Result<FieldValue<'a>, AlkTypeError>;
|
||||
pub fn write_field(&self, buffer: &mut [u8], field_path: &str,
|
||||
value: &FieldValue<'_>) -> Result<(), TypedefError>;
|
||||
value: &FieldValue<'_>) -> Result<(), AlkTypeError>;
|
||||
|
||||
// Packed mode: returns an owned fresh SequentialReader (ADR-007).
|
||||
// Each call returns a new reader with the cursor at position 0.
|
||||
@@ -90,16 +90,16 @@ impl SequentialReader {
|
||||
// prefixes to find each field's position. read_field walks all
|
||||
// preceding fields to reach the target.
|
||||
pub fn read_next<'a>(&mut self, buffer: &'a [u8])
|
||||
-> Result<Option<(String, FieldValue<'a>)>, TypedefError>;
|
||||
-> Result<Option<(String, FieldValue<'a>)>, AlkTypeError>;
|
||||
pub fn read_field<'a>(&mut self, buffer: &'a [u8], field_path: &str)
|
||||
-> Result<FieldValue<'a>, TypedefError>;
|
||||
-> Result<FieldValue<'a>, AlkTypeError>;
|
||||
pub fn reset(&mut self);
|
||||
pub fn position(&self) -> usize;
|
||||
pub fn endian(&self) -> Endian;
|
||||
}
|
||||
```
|
||||
|
||||
`read_field`/`write_field` on `TypedefEngine` work for the fixed-size
|
||||
`read_field`/`write_field` on `AlkTypeEngine` work for the fixed-size
|
||||
primitive kinds and the length-prefixed `String`/`Bytes`/`Timestamp`
|
||||
fields. Composite kinds (`Struct`, `Union`, `Array`, `Record`) return a
|
||||
`FieldValue` carrying a layout descriptor (byte range, variant start,
|
||||
@@ -115,7 +115,7 @@ which the builder consumes at `build` time.
|
||||
|
||||
The `data_access` module exposes typed read/write functions for each
|
||||
primitive kind. Each takes `field_path: &str` for error attribution
|
||||
(produces a `TypedefError::Access` carrying the path on bounds or
|
||||
(produces a `AlkTypeError::Access` carrying the path on bounds or
|
||||
encoding failures) and, for multi-byte types, an `Endian` parameter.
|
||||
|
||||
### Fixed-size types
|
||||
@@ -126,7 +126,7 @@ accessed via zero-copy reads of N bytes at the offset:
|
||||
```rust
|
||||
// Read a u32 at a known offset, applying endianness. Bounds-checked.
|
||||
fn read_u32(buffer: &[u8], offset: usize, field_path: &str, endian: Endian)
|
||||
-> Result<u32, TypedefError> {
|
||||
-> Result<u32, AlkTypeError> {
|
||||
let bytes: [u8; 4] = read_array(buffer, offset, field_path)?;
|
||||
Ok(match endian {
|
||||
Endian::Little => u32::from_le_bytes(bytes),
|
||||
@@ -136,7 +136,7 @@ fn read_u32(buffer: &[u8], offset: usize, field_path: &str, endian: Endian)
|
||||
|
||||
// Write a u32 at a known offset, applying endianness. Bounds-checked.
|
||||
fn write_u32(buffer: &mut [u8], offset: usize, value: u32,
|
||||
field_path: &str, endian: Endian) -> Result<(), TypedefError> {
|
||||
field_path: &str, endian: Endian) -> Result<(), AlkTypeError> {
|
||||
let bytes = match endian {
|
||||
Endian::Little => value.to_le_bytes(),
|
||||
Endian::Big => value.to_be_bytes(),
|
||||
@@ -148,7 +148,7 @@ fn write_u32(buffer: &mut [u8], offset: usize, value: u32,
|
||||
The engine applies endianness at access time based on the schema's
|
||||
`"endian"` annotation (ADR-003). The offset computation is
|
||||
endian-agnostic. The `read_array`/`write_array` helpers perform the
|
||||
bounds check and produce `TypedefError::Access` with the field path on
|
||||
bounds check and produce `AlkTypeError::Access` with the field path on
|
||||
failure.
|
||||
|
||||
### TEnum access
|
||||
@@ -158,7 +158,7 @@ to the `u32` primitives, applying the schema's endianness:
|
||||
|
||||
```rust
|
||||
pub fn read_enum(buffer: &[u8], offset: usize, field_path: &str, endian: Endian)
|
||||
-> Result<u32, TypedefError> {
|
||||
-> Result<u32, AlkTypeError> {
|
||||
read_u32(buffer, offset, field_path, endian)
|
||||
}
|
||||
```
|
||||
@@ -179,11 +179,11 @@ attribution and `endian` for the length prefix:
|
||||
```rust
|
||||
// Read a length-prefixed string, borrowing from the buffer.
|
||||
fn read_string<'a>(buffer: &'a [u8], offset: usize,
|
||||
field_path: &str, endian: Endian) -> Result<&'a str, TypedefError>;
|
||||
field_path: &str, endian: Endian) -> Result<&'a str, AlkTypeError>;
|
||||
|
||||
// Write a length-prefixed string. Returns total bytes written (4 + data.len()).
|
||||
fn write_string(buffer: &mut [u8], offset: usize, value: &str,
|
||||
field_path: &str, endian: Endian) -> Result<usize, TypedefError>;
|
||||
field_path: &str, endian: Endian) -> Result<usize, AlkTypeError>;
|
||||
|
||||
// read_bytes / write_bytes have the same shape — raw bytes, no UTF-8 check.
|
||||
```
|
||||
@@ -210,10 +210,10 @@ bytes live in a separate `data_region`:
|
||||
```rust
|
||||
fn read_string_indirect<'a>(buffer: &'a [u8], offset: usize,
|
||||
data_region: &'a [u8], field_path: &str,
|
||||
endian: Endian) -> Result<&'a str, TypedefError>;
|
||||
endian: Endian) -> Result<&'a str, AlkTypeError>;
|
||||
fn read_bytes_indirect<'a>(buffer: &'a [u8], offset: usize,
|
||||
data_region: &'a [u8], field_path: &str,
|
||||
endian: Endian) -> Result<&'a [u8], TypedefError>;
|
||||
endian: Endian) -> Result<&'a [u8], AlkTypeError>;
|
||||
```
|
||||
|
||||
The field is a struct `{offset: u32, length: u32}` at a known position
|
||||
@@ -247,14 +247,14 @@ fresh `SequentialReader` scoped to the variant).
|
||||
|
||||
```rust
|
||||
/// Read the discriminator value from a byte-offset TUnion. The discriminator
|
||||
/// is a fixed-size integer (TypeDef:Uint8/Uint16/Uint32) at a known byte
|
||||
/// is a fixed-size integer (AlkType:Uint8/Uint16/Uint32) at a known byte
|
||||
/// offset. Returns the mapping key (stringified integer) and the variant
|
||||
/// struct offset.
|
||||
pub fn read_byte_discriminator(
|
||||
buffer: &[u8],
|
||||
union_schema: &Value,
|
||||
endian: Endian,
|
||||
) -> Result<UnionDispatch, TypedefError>;
|
||||
) -> Result<UnionDispatch, AlkTypeError>;
|
||||
```
|
||||
|
||||
This is the SFTP `Packet` enum pattern — byte 0 is the type byte, bytes
|
||||
@@ -268,14 +268,14 @@ starts at `offset + discriminator_size`.
|
||||
/// Read the discriminator value from a field-name TUnion. The
|
||||
/// discriminator is a named field within the struct — the consumer
|
||||
/// provides the field's computed offset (from the OffsetMap or
|
||||
/// LayoutBuilder). Supports TypeDef:String, Uint8, and Enum discriminator
|
||||
/// LayoutBuilder). Supports AlkType:String, Uint8, and Enum discriminator
|
||||
/// fields.
|
||||
pub fn read_field_discriminator(
|
||||
buffer: &[u8],
|
||||
union_schema: &Value,
|
||||
disc_field_offset: usize,
|
||||
endian: Endian,
|
||||
) -> Result<UnionDispatch, TypedefError>;
|
||||
) -> Result<UnionDispatch, AlkTypeError>;
|
||||
```
|
||||
|
||||
The discriminator is a named field within the struct. Its offset is
|
||||
@@ -291,12 +291,12 @@ the variant's fields starting at the end of the discriminator field.
|
||||
/// are returned directly. $ref pointers of the form "#/$defs/<name>"
|
||||
/// are resolved against the union schema's own $defs block.
|
||||
pub fn resolve_variant<'a>(union_schema: &'a Value, key: &str)
|
||||
-> Result<&'a Value, TypedefError>;
|
||||
-> Result<&'a Value, AlkTypeError>;
|
||||
|
||||
/// Get the discriminator's byte size (1/2/4 for Uint8/16/32) for a
|
||||
/// byte-offset TUnion. Field-name discriminators have no fixed size
|
||||
/// and produce a TypedefError::Schema.
|
||||
pub fn discriminator_size(union_schema: &Value) -> Result<usize, TypedefError>;
|
||||
/// and produce a AlkTypeError::Schema.
|
||||
pub fn discriminator_size(union_schema: &Value) -> Result<usize, AlkTypeError>;
|
||||
```
|
||||
|
||||
### TUnion in the layout engines
|
||||
@@ -318,13 +318,13 @@ variant before recursing.
|
||||
Fields are addressed by dotted paths: `"header.version"`, `"payload.data"`.
|
||||
Both `OffsetMap` and `PackedLayout` store fully-qualified paths (nested
|
||||
struct fields appear under their parent's path prefix). The higher-level
|
||||
APIs (`TypedefEngine::read_field`/`write_field`, `SequentialReader::read_field`)
|
||||
APIs (`AlkTypeEngine::read_field`/`write_field`, `SequentialReader::read_field`)
|
||||
accept a field path, look up the byte range/position in the layout, and
|
||||
dispatch to the primitive `data_access` function for the field's kind.
|
||||
|
||||
For aligned-mode access, `TypedefEngine::read_field(&buffer, "header.version")`
|
||||
For aligned-mode access, `AlkTypeEngine::read_field(&buffer, "header.version")`
|
||||
returns `FieldValue` — it looks up the `ByteRange` in the `OffsetMap`, finds
|
||||
the field's `TypeDef:*` kind in the schema, and calls the matching
|
||||
the field's `AlkType:*` kind in the schema, and calls the matching
|
||||
`data_access::read_*` function. `write_field` is the mirror. Composite
|
||||
kinds (`Struct`, `Union`, `Array`, `Record`) return a `FieldValue`
|
||||
carrying a layout descriptor; the consumer recurses with a fresh reader
|
||||
|
||||
@@ -18,7 +18,7 @@ bytes at computed offsets.
|
||||
(`Read { id: u32, handle: String, offset: u64, len: u32 }`). The wire
|
||||
format is `[length: u32][type: u8][payload]` where payload is the
|
||||
struct's serde bytes. The `Packet` enum dispatches on the type byte —
|
||||
a tagged union of structs. Under the typedef lens, each packet is a
|
||||
a tagged union of structs. Under the alktype lens, each packet is a
|
||||
`TStruct`; the `Packet` enum is a `TUnion` with a byte-offset
|
||||
discriminator.
|
||||
|
||||
@@ -27,7 +27,7 @@ bytes at computed offsets.
|
||||
compute byte offsets for each field so the consumer can read tensor
|
||||
data at known positions without parsing.
|
||||
|
||||
The common pattern: **a JSON Schema with `TypeDef:*` custom keywords
|
||||
The common pattern: **a JSON Schema with `AlkType:*` custom keywords
|
||||
describes the shape of binary data; the binary data is the struct's bytes
|
||||
at computed offsets.** The schema is the format definition; the engine is
|
||||
generic.
|
||||
@@ -51,16 +51,16 @@ validation.
|
||||
The call-channels-unification research
|
||||
(`docs/research/call-channels-unification/findings.md` §"alknet-typedef:
|
||||
JSON Schema as the binary struct engine") identified the convergence and
|
||||
bumped typedef up in the timeline. The POC
|
||||
bumped alktype up in the timeline. The POC
|
||||
(`docs/research/alknet-typedef/findings.md`, 26 tests passing) validated
|
||||
the approach: a ~1,900-line Rust crate that takes a JSON Schema with
|
||||
`TypeDef:*` custom keywords and produces an offset map, read/write
|
||||
`AlkType:*` custom keywords and produces an offset map, read/write
|
||||
functions, and validation — all driven by the schema.
|
||||
|
||||
## Decision
|
||||
|
||||
**alktype is a small Rust crate that takes a JSON Schema with
|
||||
`TypeDef:*` custom keywords and produces three capabilities:**
|
||||
`AlkType:*` custom keywords and produces three capabilities:**
|
||||
|
||||
1. **An offset map** — walks the schema, computes byte offsets for each
|
||||
field based on type sizes, field order, and alignment.
|
||||
@@ -79,8 +79,8 @@ functions, and validation — all driven by the schema.
|
||||
— a recursive walk of the schema JSON that computes byte positions for
|
||||
each field. The custom keyword implementations are ~10 lines each.
|
||||
|
||||
**The schema is the format.** A JSON Schema with `TypeDef:Float32`,
|
||||
`TypeDef:Struct`, `TypeDef:Union` etc. is both the validation spec and
|
||||
**The schema is the format.** A JSON Schema with `AlkType:Float32`,
|
||||
`AlkType:Struct`, `AlkType:Union` etc. is both the validation spec and
|
||||
the layout spec. No separate format definition, no separate parser, no
|
||||
separate validator. One schema, three uses: validate, compute offsets,
|
||||
access data.
|
||||
@@ -88,8 +88,8 @@ access data.
|
||||
**The crate depends on `jsonschema` and `serde_json` (with
|
||||
`preserve_order`).** No tokio, no platform deps. Compiles to
|
||||
`wasm32-unknown-unknown` for browser use. The `jsonschema` crate's
|
||||
`with_keyword("TypeDef:Float32", factory)` API is the integration point
|
||||
for custom type kinds — each `TypeDef:*` kind maps to a custom keyword
|
||||
`with_keyword("AlkType:Float32", factory)` API is the integration point
|
||||
for custom type kinds — each `AlkType:*` kind maps to a custom keyword
|
||||
validator in Rust. Same semantics as TypeBox's `TypeRegistry.Set`, same
|
||||
JSON Schema wire format.
|
||||
|
||||
@@ -113,7 +113,7 @@ be added as a feature gate later — the engine's core (offset computation,
|
||||
adding a variant to the schema JSON, not writing a new Rust struct +
|
||||
serde impl. The engine is generic; the schema is the configuration.
|
||||
- **WASM-clean.** `serde_json` + `jsonschema` + byte manipulation. No
|
||||
tokio, no platform deps. The same typedef schemas work in browser,
|
||||
tokio, no platform deps. The same alktype schemas work in browser,
|
||||
Node, Python (via `wasmtime-py`), Go (via `wazero`), and any other
|
||||
WASM host.
|
||||
- **TypeBox interop.** TypeBox modules render to standard JSON Schema
|
||||
@@ -135,27 +135,27 @@ be added as a feature gate later — the engine's core (offset computation,
|
||||
load-bearing for binary layouts. The `preserve_order` feature adds a
|
||||
small compile-time cost.
|
||||
- **Schema authoring is external.** Schemas are authored in TypeBox (JS)
|
||||
or hand-written JSON. The typedef engine consumes schemas; it does not
|
||||
or hand-written JSON. The alktype engine consumes schemas; it does not
|
||||
generate them. A builder API is deferred (OQ-003).
|
||||
|
||||
## Scope Boundaries (What This Is Not)
|
||||
|
||||
- **Not metatensor.** typedef is the binary struct *engine*. Metatensor
|
||||
- **Not metatensor.** alktype is the binary struct *engine*. Metatensor
|
||||
is a *format* (8-byte header + JSON header + binary data) that uses the
|
||||
typedef engine for its offset computation and tensor access.
|
||||
alktype engine for its offset computation and tensor access.
|
||||
- **Not a Value system.** TypeBox's `Value.Diff`, `Value.Migrate`,
|
||||
`Value.Convert` — schema evolution — is out of scope for v1. The engine
|
||||
should not do anything that explicitly blocks adding a Value system
|
||||
later.
|
||||
- **Not a code generator.** typebox-rs's `codegen/` module is a separate
|
||||
concern. The typedef engine consumes schemas; it does not generate them.
|
||||
- **Not a schema builder.** The typedef engine does not provide a fluent
|
||||
concern. The alktype engine consumes schemas; it does not generate them.
|
||||
- **Not a schema builder.** The alktype engine does not provide a fluent
|
||||
API for constructing schemas. Schemas are plain JSON.
|
||||
- **Not a serialization framework.** The typedef engine is not a
|
||||
- **Not a serialization framework.** The alktype engine is not a
|
||||
general-purpose serde replacement. It operates on raw byte buffers at
|
||||
computed offsets — no intermediate `Value` tree, no reflection, no
|
||||
dynamic dispatch per field. For JSON data, use serde. For binary data
|
||||
with a known schema, use typedef.
|
||||
with a known schema, use alktype.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ tensor data).
|
||||
|
||||
## Decision
|
||||
|
||||
**The typedef engine supports two layout modes, selected by the consumer
|
||||
**The alktype engine supports two layout modes, selected by the consumer
|
||||
at engine construction time:**
|
||||
|
||||
### Mode 1: Packed sequential (`LayoutBuilder` / `SequentialReader`)
|
||||
|
||||
@@ -5,7 +5,7 @@ Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The typedef engine needs concrete JSON shapes for schema-level
|
||||
The alktype engine needs concrete JSON shapes for schema-level
|
||||
annotations that control binary layout behavior. The POCs validated the
|
||||
semantics; this ADR pins the shapes.
|
||||
|
||||
@@ -28,7 +28,7 @@ Four annotation categories need concrete shapes:
|
||||
|
||||
```json
|
||||
{
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"endian": "big",
|
||||
"properties": { ... }
|
||||
}
|
||||
@@ -49,11 +49,11 @@ struct-level.**
|
||||
|
||||
```json
|
||||
{
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"align": 256,
|
||||
"properties": {
|
||||
"header": { "TypeDef:Struct": true, "properties": { ... } },
|
||||
"weight": { "TypeDef:Float32": true, "align": 16 }
|
||||
"header": { "AlkType:Struct": true, "properties": { ... } },
|
||||
"weight": { "AlkType:Float32": true, "align": 16 }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -76,16 +76,16 @@ annotation and the standard JSON Schema `maxLength` keyword.**
|
||||
|
||||
```json
|
||||
// Strategy 1: Inline length-prefixing (default, shorthand)
|
||||
{ "TypeDef:String": true }
|
||||
{ "AlkType:String": true }
|
||||
|
||||
// Strategy 1: Explicit inline length-prefixing
|
||||
{ "TypeDef:String": { "encoding": "length-prefixed" } }
|
||||
{ "AlkType:String": { "encoding": "length-prefixed" } }
|
||||
|
||||
// Strategy 2: Fixed-size reservation (uses standard maxLength)
|
||||
{ "TypeDef:String": true, "maxLength": 256 }
|
||||
{ "AlkType:String": true, "maxLength": 256 }
|
||||
|
||||
// Strategy 3: Offset indirection (opt-in)
|
||||
{ "TypeDef:String": { "encoding": "offset-indirect" } }
|
||||
{ "AlkType:String": { "encoding": "offset-indirect" } }
|
||||
```
|
||||
|
||||
**Strategy 1: Inline length-prefixing (default).** The field's fixed
|
||||
@@ -126,27 +126,27 @@ reserving worst-case space.
|
||||
- `true` is a shorthand for the default (length-prefixed). This keeps
|
||||
the common case concise and the override explicit.
|
||||
- The `encoding` annotation and `maxLength` apply to all variable-length
|
||||
types: `TypeDef:String`, `TypeDef:Bytes`, `TypeDef:Array`,
|
||||
`TypeDef:Record`, `TypeDef:Timestamp`.
|
||||
types: `AlkType:String`, `AlkType:Bytes`, `AlkType:Array`,
|
||||
`AlkType:Record`, `AlkType:Timestamp`.
|
||||
|
||||
### 3a. TRecord value type
|
||||
|
||||
`TypeDef:Record` is a string-keyed map. The value type is declared via
|
||||
`AlkType:Record` is a string-keyed map. The value type is declared via
|
||||
the `"values"` property in the schema:
|
||||
|
||||
```json
|
||||
{
|
||||
"TypeDef:Record": true,
|
||||
"values": { "TypeDef:Float32": true }
|
||||
"AlkType:Record": true,
|
||||
"values": { "AlkType:Float32": true }
|
||||
}
|
||||
```
|
||||
|
||||
- `"values"` is a schema object declaring the `TypeDef:*` kind of all
|
||||
- `"values"` is a schema object declaring the `AlkType:*` kind of all
|
||||
values in the record. All values share the same type.
|
||||
- The binary layout is a count-prefixed sequence of `(key, value)` pairs:
|
||||
`[count: u32][key_len: u32][key_bytes][value]...` repeated `count`
|
||||
times. Each key is a length-prefixed UTF-8 string. Each value is
|
||||
encoded according to its declared `TypeDef:*` kind — a `Record<Uint32>`
|
||||
encoded according to its declared `AlkType:*` kind — a `Record<Uint32>`
|
||||
value is 4 raw bytes; a `Record<String>` value is itself a
|
||||
length-prefixed string; a `Record<Struct>` value is the struct's
|
||||
fields laid out inline. There is **no separate `value_len` prefix** —
|
||||
@@ -165,11 +165,11 @@ field-name (typedef.ts pattern).**
|
||||
|
||||
```json
|
||||
{
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {
|
||||
"kind": "byte",
|
||||
"offset": 0,
|
||||
"type": "TypeDef:Uint8"
|
||||
"type": "AlkType:Uint8"
|
||||
},
|
||||
"mapping": {
|
||||
"1": { "$ref": "#/$defs/Init" },
|
||||
@@ -184,8 +184,8 @@ field-name (typedef.ts pattern).**
|
||||
- The discriminator is a fixed-size integer at a known byte offset.
|
||||
- `"offset"` is the byte position of the discriminator within the union's
|
||||
buffer.
|
||||
- `"type"` is the `TypeDef:*` kind of the discriminator (typically
|
||||
`TypeDef:Uint8` for protocol type bytes).
|
||||
- `"type"` is the `AlkType:*` kind of the discriminator (typically
|
||||
`AlkType:Uint8` for protocol type bytes).
|
||||
- The mapping keys are stringified integers (`"1"`, `"5"`, `"101"`).
|
||||
The engine parses the key to match the discriminator value.
|
||||
- The variant struct starts at `offset + discriminator_size`.
|
||||
@@ -196,7 +196,7 @@ field-name (typedef.ts pattern).**
|
||||
|
||||
```json
|
||||
{
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {
|
||||
"kind": "field",
|
||||
"name": "type"
|
||||
@@ -240,8 +240,8 @@ event types). Both work.
|
||||
|
||||
### Negative
|
||||
|
||||
- **Keyword value shape change.** `"TypeDef:String": true` (boolean) and
|
||||
`"TypeDef:String": { "encoding": "length-prefixed" }` (object) are both
|
||||
- **Keyword value shape change.** `"AlkType:String": true` (boolean) and
|
||||
`"AlkType:String": { "encoding": "length-prefixed" }` (object) are both
|
||||
valid. The engine must handle both shapes. This is a minor parsing
|
||||
concern — the POC already handles it.
|
||||
- **Alignment annotations are mode-specific.** Alignment is only
|
||||
|
||||
@@ -5,11 +5,11 @@ Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The typedef engine operates in three phases, each with distinct error
|
||||
The alktype engine operates in three phases, each with distinct error
|
||||
conditions:
|
||||
|
||||
1. **Schema parsing** — invalid JSON, missing required keywords, unknown
|
||||
`TypeDef:*` kinds, malformed annotations.
|
||||
`AlkType:*` kinds, malformed annotations.
|
||||
2. **Offset computation** — field not found, type not supported for
|
||||
offset computation, recursive schema depth exceeded.
|
||||
3. **Read/write** — buffer too short, invalid UTF-8, value out of range
|
||||
@@ -23,12 +23,12 @@ time (validate each buffer).
|
||||
|
||||
## Decision
|
||||
|
||||
### Error type: `TypedefError`
|
||||
### Error type: `AlkTypeError`
|
||||
|
||||
A single `TypedefError` enum with variants for each error category:
|
||||
A single `AlkTypeError` enum with variants for each error category:
|
||||
|
||||
```rust
|
||||
pub enum TypedefError {
|
||||
pub enum AlkTypeError {
|
||||
/// Schema parsing errors.
|
||||
Schema(String),
|
||||
/// Offset computation errors.
|
||||
@@ -41,39 +41,39 @@ pub enum TypedefError {
|
||||
```
|
||||
|
||||
- `Schema` — for invalid JSON, missing required keywords, unknown
|
||||
`TypeDef:*` kinds. The error message describes the problem.
|
||||
`AlkType:*` kinds. The error message describes the problem.
|
||||
- `Offset` — for field-not-found, unsupported type for offset
|
||||
computation, etc. Carries the field path for debugging.
|
||||
- `Access` — for buffer-too-short, invalid UTF-8, value out of range.
|
||||
Carries the field path for debugging.
|
||||
- `Validation` — wraps `jsonschema`'s `ValidationError`. The
|
||||
`jsonschema` crate already provides rich error messages with schema
|
||||
paths; the typedef engine does not re-wrap or re-interpret them.
|
||||
paths; the alktype engine does not re-wrap or re-interpret them.
|
||||
|
||||
The `Validation` variant uses `ValidationError<'static>` because the
|
||||
validator is built once at schema load time and lives for the lifetime
|
||||
of the `TypedefEngine`. The `'static` lifetime is correct — the validator
|
||||
of the `AlkTypeEngine`. The `'static` lifetime is correct — the validator
|
||||
owns its schema reference.
|
||||
|
||||
### Validation timing: load-time build, access-time check
|
||||
|
||||
The jsonschema validator is built once at schema load time
|
||||
(`validator_for(&schema)?`) and then called repeatedly
|
||||
(`validator.is_valid(&instance)`). The typedef engine follows the same
|
||||
(`validator.is_valid(&instance)`). The alktype engine follows the same
|
||||
pattern:
|
||||
|
||||
1. **Load time:** Parse the schema JSON, build the offset map (or
|
||||
`LayoutBuilder`/`SequentialReader`), build the jsonschema validator.
|
||||
This is the `TypedefEngine::compile(schema: &Value) -> Result<Self,
|
||||
TypedefError>` constructor.
|
||||
This is the `AlkTypeEngine::compile(schema: &Value) -> Result<Self,
|
||||
AlkTypeError>` constructor.
|
||||
2. **Access time:** Use the compiled engine for repeated read/write
|
||||
operations. Validation is opt-in per operation — the consumer calls
|
||||
`engine.validate(buffer)` when validation is desired.
|
||||
|
||||
The `TypedefEngine` struct is the compiled form of a schema:
|
||||
The `AlkTypeEngine` struct is the compiled form of a schema:
|
||||
|
||||
```rust
|
||||
pub struct TypedefEngine {
|
||||
pub struct AlkTypeEngine {
|
||||
offset_map: OffsetMap, // or LayoutBuilder/SequentialReader
|
||||
validator: jsonschema::Validator, // compiled once at load time
|
||||
}
|
||||
@@ -81,21 +81,21 @@ pub struct TypedefEngine {
|
||||
|
||||
### Custom keyword validators
|
||||
|
||||
Each `TypeDef:*` kind gets a `Keyword` implementation registered via
|
||||
Each `AlkType:*` kind gets a `Keyword` implementation registered via
|
||||
`jsonschema::options().with_keyword(...)`. The validators check:
|
||||
|
||||
- **Numeric types** (`TypeDef:Float32`, `TypeDef:Int8`, etc.): range
|
||||
- **Numeric types** (`AlkType:Float32`, `AlkType:Int8`, etc.): range
|
||||
constraints (Int8: -128..127, Uint8: 0..255, etc.), finiteness for
|
||||
floats.
|
||||
- **`TypeDef:String`**: UTF-8 validity.
|
||||
- **`TypeDef:Struct`**: field presence and types (delegated to
|
||||
- **`AlkType:String`**: UTF-8 validity.
|
||||
- **`AlkType:Struct`**: field presence and types (delegated to
|
||||
jsonschema's structural validation — the custom keyword only needs to
|
||||
validate that the struct's fields match their declared `TypeDef:*`
|
||||
validate that the struct's fields match their declared `AlkType:*`
|
||||
kinds).
|
||||
- **`TypeDef:Union`**: discriminator value membership in the mapping.
|
||||
- **`TypeDef:Array`**: element type conformance.
|
||||
- **`TypeDef:Boolean`**: value is `true` or `false`.
|
||||
- **`TypeDef:Timestamp`**: RFC 3339 string format (the internet profile of ISO 8601).
|
||||
- **`AlkType:Union`**: discriminator value membership in the mapping.
|
||||
- **`AlkType:Array`**: element type conformance.
|
||||
- **`AlkType:Boolean`**: value is `true` or `false`.
|
||||
- **`AlkType:Timestamp`**: RFC 3339 string format (the internet profile of ISO 8601).
|
||||
|
||||
The `jsonschema` crate handles all the structural validation (object
|
||||
properties, required fields, array items, enum values) — the custom
|
||||
@@ -108,7 +108,7 @@ Read/write errors include the field path for debugging:
|
||||
|
||||
```rust
|
||||
// Example: reading a u32 from a buffer that's too short
|
||||
Err(TypedefError::Access {
|
||||
Err(AlkTypeError::Access {
|
||||
field_path: "header.version".to_string(),
|
||||
reason: "buffer too short: need 4 bytes at offset 12, have 2".to_string(),
|
||||
})
|
||||
@@ -121,7 +121,7 @@ you exactly which field failed and why.
|
||||
|
||||
### Positive
|
||||
|
||||
- **Single error type.** Consumers handle one `TypedefError` enum, not
|
||||
- **Single error type.** Consumers handle one `AlkTypeError` enum, not
|
||||
multiple error types from different engine phases.
|
||||
- **Field-path-carrying errors.** Read/write errors include the field
|
||||
path, making binary format debugging tractable.
|
||||
|
||||
@@ -5,17 +5,17 @@ Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The typedef engine's kind set (ADR-001, ADR-003) tops out at 32-bit
|
||||
The alktype engine's kind set (ADR-001, ADR-003) tops out at 32-bit
|
||||
integers. The POC included `u64` read/write primitives, and the
|
||||
call-channels-unification research's own SFTP schema example uses
|
||||
`"TypeDef:Uint64"` for the `offset` field (`Read`/`Write` packets have
|
||||
`"AlkType:Uint64"` for the `offset` field (`Read`/`Write` packets have
|
||||
`offset: u64`). Metatensor/safetensors `data_offsets` are also `u64`.
|
||||
|
||||
A `TypeDef:Uint64` variant was added to the `TypeDefKind` enum during
|
||||
A `AlkType:Uint64` variant was added to the `AlkTypeKind` enum during
|
||||
implementation (the task decomposition correctly identified the gap),
|
||||
but without an ADR the addition was half-finished: `type_size()`
|
||||
returned `None`, the layout engines couldn't compute offsets for it, and
|
||||
the validator didn't register a `TypeDef:Uint64` keyword. The variant
|
||||
the validator didn't register a `AlkType:Uint64` keyword. The variant
|
||||
was then removed (commit `14d9cf2`) on the grounds that it was
|
||||
unintended and a latent panic — but the underlying gap is real: SFTP and
|
||||
metatensor, the two primary POC targets, both require 64-bit integers.
|
||||
@@ -28,11 +28,11 @@ is 8 raw bytes, and `from_le_bytes`/`from_be_bytes` work correctly for
|
||||
the full `u64`/`i64` range. The validation concern is handled by
|
||||
accepting integer-form JSON values (the `jsonschema` crate's
|
||||
`as_i64`/`as_u64` methods handle the common range; values past 2^53 are
|
||||
a JSON representation limitation, not a typedef limitation).
|
||||
a JSON representation limitation, not a alktype limitation).
|
||||
|
||||
## Decision
|
||||
|
||||
**Add `TypeDef:Int64` and `TypeDef:Uint64` as first-class kinds.**
|
||||
**Add `AlkType:Int64` and `AlkType:Uint64` as first-class kinds.**
|
||||
|
||||
Both are fixed-size (8 bytes), with natural alignment 8. They follow
|
||||
the schema's endianness annotation like all other fixed-size types.
|
||||
@@ -43,20 +43,20 @@ Read/write is via `data_access::read_i64`/`write_i64`/`read_u64`/
|
||||
|
||||
| Kind | TypeBox key | Rust type | Size | Alignment |
|
||||
|------|-------------|-----------|------|-----------|
|
||||
| `TInt64` | `TypeDef:Int64` | `i64` | 8 | 8 |
|
||||
| `TUint64` | `TypeDef:Uint64` | `u64` | 8 | 8 |
|
||||
| `TInt64` | `AlkType:Int64` | `i64` | 8 | 8 |
|
||||
| `TUint64` | `AlkType:Uint64` | `u64` | 8 | 8 |
|
||||
|
||||
### Validation
|
||||
|
||||
The custom keyword validators check:
|
||||
- `TypeDef:Int64`: value must be an integer in `i64::MIN..=i64::MAX`
|
||||
- `AlkType:Int64`: value must be an integer in `i64::MIN..=i64::MAX`
|
||||
(`-9223372036854775808` to `9223372036854775807`).
|
||||
- `TypeDef:Uint64`: value must be a non-negative integer in
|
||||
- `AlkType:Uint64`: value must be a non-negative integer in
|
||||
`0..=u64::MAX` (`0` to `18446744073709551615`).
|
||||
|
||||
The `jsonschema` crate's `as_i64`/`as_u64` handle the common range.
|
||||
JSON numbers past 2^53 lose precision in the JSON representation —
|
||||
this is a JSON limitation, not a typedef limitation. The binary
|
||||
this is a JSON limitation, not a alktype limitation. The binary
|
||||
representation (8 raw bytes) is always exact. A consumer that needs
|
||||
to validate the full 64-bit range from JSON should provide the value
|
||||
as a JSON integer (which `serde_json` preserves for values up to
|
||||
@@ -66,13 +66,13 @@ enabled, or when the value fits in `i64`/`u64` without the feature).
|
||||
### `FieldValue` additions
|
||||
|
||||
`FieldValue::I64(i64)` and `FieldValue::U64(u64)` are added to the
|
||||
unified return type. The `SequentialReader`, `TypedefEngine::read_field`,
|
||||
and `TypedefEngine::write_field` dispatch on the new kinds.
|
||||
unified return type. The `SequentialReader`, `AlkTypeEngine::read_field`,
|
||||
and `AlkTypeEngine::write_field` dispatch on the new kinds.
|
||||
|
||||
### Kind count
|
||||
|
||||
The engine now has **19** first-class kinds (17 + Int64 + Uint64).
|
||||
`TypeDefKind::is_fixed_size()` returns `true` for both new kinds.
|
||||
`AlkTypeKind::is_fixed_size()` returns `true` for both new kinds.
|
||||
`type_size()` returns `Some(8)`. `natural_alignment()` returns `8`.
|
||||
`needs_endian()` returns `true`.
|
||||
|
||||
@@ -82,8 +82,8 @@ The engine now has **19** first-class kinds (17 + Int64 + Uint64).
|
||||
|
||||
- **Unblocks the two primary POC targets.** SFTP `Read`/`Write` packets
|
||||
(`offset: u64`) and metatensor `data_offsets` (`u64`) are now
|
||||
expressible in typedef schemas.
|
||||
- **Completes the half-finished addition.** The `TypeDefKind` enum,
|
||||
expressible in alktype schemas.
|
||||
- **Completes the half-finished addition.** The `AlkTypeKind` enum,
|
||||
`data_access` primitives, and `FieldValue` variants for 64-bit
|
||||
integers now have matching layout, validator, and engine support.
|
||||
- **No new design surface.** Int64/Uint64 are fixed-size types that
|
||||
@@ -94,7 +94,7 @@ The engine now has **19** first-class kinds (17 + Int64 + Uint64).
|
||||
|
||||
- **JSON precision caveat.** Values past 2^53 lose precision in the
|
||||
JSON representation (not in the binary representation). This is a
|
||||
JSON limitation, not a typedef limitation, but it means the
|
||||
JSON limitation, not a alktype limitation, but it means the
|
||||
validation layer cannot perfectly round-trip the full 64-bit range
|
||||
through JSON `Number` without `arbitrary_precision`. In practice,
|
||||
SFTP offsets and tensor data offsets are well within 2^53.
|
||||
@@ -105,7 +105,7 @@ The engine now has **19** first-class kinds (17 + Int64 + Uint64).
|
||||
## References
|
||||
|
||||
- `docs/research/call-channels-unification/findings.md` §"russh-sftp" —
|
||||
the SFTP schema with `"offset": { "TypeDef:Uint64": true }`
|
||||
the SFTP schema with `"offset": { "AlkType:Uint64": true }`
|
||||
- `docs/research/alknet-typedef/findings.md` §"POC 1" — the POC included
|
||||
u64 read/write
|
||||
- [ADR-001](001-alktype-purpose-scope-jsonschema-engine.md) —
|
||||
|
||||
@@ -17,7 +17,7 @@ indirection)."
|
||||
|
||||
The implementation has a bug: `OffsetMap::compute` reserves only 4 bytes
|
||||
for an inline length-prefixed variable field (the length prefix), but
|
||||
`TypedefEngine::write_field` for a `String`/`Bytes` field calls
|
||||
`AlkTypeEngine::write_field` for a `String`/`Bytes` field calls
|
||||
`data_access::write_string` at `range.start`, which writes
|
||||
`[4-byte length][data]` inline — clobbering every subsequent field. The
|
||||
`read_field` path has the mirror behavior (reads inline), so the engine
|
||||
@@ -45,12 +45,12 @@ length-prefixing.
|
||||
**Reject non-final inline length-prefixed variable fields in aligned
|
||||
static mode at `OffsetMap::compute` time.**
|
||||
|
||||
A variable-length field (`TypeDef:String`, `TypeDef:Bytes`,
|
||||
`TypeDef:Timestamp`, `TypeDef:Record`) in aligned static mode that uses
|
||||
A variable-length field (`AlkType:String`, `AlkType:Bytes`,
|
||||
`AlkType:Timestamp`, `AlkType:Record`) in aligned static mode that uses
|
||||
the default inline length-prefixing strategy (no `maxLength`, no
|
||||
`offset-indirect`) must be the last field in its struct. If a non-final
|
||||
inline length-prefixed variable field is encountered,
|
||||
`OffsetMap::compute` returns `TypedefError::Offset` with a message
|
||||
`OffsetMap::compute` returns `AlkTypeError::Offset` with a message
|
||||
explaining that non-final variable fields in aligned mode require
|
||||
`maxLength` (fixed-size reservation) or `"encoding": "offset-indirect"`
|
||||
(offset indirection).
|
||||
|
||||
@@ -5,7 +5,7 @@ Accepted
|
||||
|
||||
## Context
|
||||
|
||||
`TypedefEngine` stores a `SequentialReader` inside its `Layout::Packed`
|
||||
`AlkTypeEngine` stores a `SequentialReader` inside its `Layout::Packed`
|
||||
variant. The engine exposes it via
|
||||
`engine.sequential_reader() -> Option<&SequentialReader>`.
|
||||
|
||||
@@ -43,7 +43,7 @@ Three options were considered:
|
||||
an owned fresh reader, reconstructed from the stored schema.
|
||||
|
||||
```rust
|
||||
impl TypedefEngine {
|
||||
impl AlkTypeEngine {
|
||||
/// Construct a fresh SequentialReader for packed-mode reads.
|
||||
/// Returns None if compiled in aligned mode.
|
||||
pub fn sequential_reader(&self) -> Option<SequentialReader>;
|
||||
|
||||
@@ -12,7 +12,7 @@ aligned mode has three implementation problems:
|
||||
1. **No variant field offsets.** Only the `__discriminator` byte range
|
||||
is recorded in the `OffsetMap`. Variant field offsets are not
|
||||
available anywhere in aligned mode — the consumer must recompute
|
||||
them by hand. This makes `TypedefEngine::read_field` on a union
|
||||
them by hand. This makes `AlkTypeEngine::read_field` on a union
|
||||
variant field impossible.
|
||||
|
||||
2. **`find_discriminator_field` takes the first variant's offset.** For
|
||||
@@ -49,8 +49,8 @@ worse than rejecting it clearly.
|
||||
|
||||
**Reject `TUnion` in aligned static mode for v1.**
|
||||
|
||||
`OffsetMap::compute` returns `TypedefError::Offset` when it encounters
|
||||
a `TypeDef:Union` field, with a message explaining that unions are not
|
||||
`OffsetMap::compute` returns `AlkTypeError::Offset` when it encounters
|
||||
a `AlkType:Union` field, with a message explaining that unions are not
|
||||
supported in aligned mode and the consumer should use packed mode (or
|
||||
restructure as a struct with an explicit discriminator field).
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ protocols.
|
||||
|
||||
**Components:**
|
||||
|
||||
- **`LayoutBuilder`** — constructed via `LayoutBuilder::new(schema)` (requires `TypeDef:Struct` at the top level), then `builder.build(&var_sizes) -> Result<PackedLayout, TypedefError>` where `var_sizes: &HashMap<String, usize>` maps variable-length field paths (and TUnion discriminator/variant keys) to their actual byte sizes. Used at write time when the consumer knows the data sizes upfront. The builder computes positions only; the consumer writes data via the [`data_access`](data-access.md) functions at the computed positions.
|
||||
- **`SequentialReader`** — constructed via `SequentialReader::new(schema)`, then driven by `reader.read_next(&buffer) -> Result<Option<(String, FieldValue)>, TypedefError>` until `Ok(None)`, or `reader.read_field(&buffer, path)` to seek a single field (which walks all preceding fields to reach the target). `reader.reset()` rewinds to the start. Used at read time when the consumer is parsing an incoming frame.
|
||||
- **`LayoutBuilder`** — constructed via `LayoutBuilder::new(schema)` (requires `AlkType:Struct` at the top level), then `builder.build(&var_sizes) -> Result<PackedLayout, AlkTypeError>` where `var_sizes: &HashMap<String, usize>` maps variable-length field paths (and TUnion discriminator/variant keys) to their actual byte sizes. Used at write time when the consumer knows the data sizes upfront. The builder computes positions only; the consumer writes data via the [`data_access`](data-access.md) functions at the computed positions.
|
||||
- **`SequentialReader`** — constructed via `SequentialReader::new(schema)`, then driven by `reader.read_next(&buffer) -> Result<Option<(String, FieldValue)>, AlkTypeError>` until `Ok(None)`, or `reader.read_field(&buffer, path)` to seek a single field (which walks all preceding fields to reach the target). `reader.reset()` rewinds to the start. Used at read time when the consumer is parsing an incoming frame.
|
||||
|
||||
**How it works:**
|
||||
|
||||
@@ -69,7 +69,7 @@ and safetensors.
|
||||
|
||||
**Component:**
|
||||
|
||||
- **`OffsetMap`** — constructed via `OffsetMap::compute(schema) -> Result<Self, TypedefError>` (requires `TypeDef:Struct` at the top level). Walks the schema once, computes fixed byte positions for each field based on type sizes and alignment. The output is a flat table of `(field_path, byte_range)` pairs (see [Public Types](#public-types)). Used for both read and write at known offsets.
|
||||
- **`OffsetMap`** — constructed via `OffsetMap::compute(schema) -> Result<Self, AlkTypeError>` (requires `AlkType:Struct` at the top level). Walks the schema once, computes fixed byte positions for each field based on type sizes and alignment. The output is a flat table of `(field_path, byte_range)` pairs (see [Public Types](#public-types)). Used for both read and write at known offsets.
|
||||
|
||||
**How it works:**
|
||||
|
||||
@@ -109,7 +109,7 @@ length, then slices the separate data region.
|
||||
Inline length-prefixed variable fields in aligned mode are only allowed
|
||||
as the **last field** in their struct. A non-final inline
|
||||
length-prefixed variable field is rejected at `OffsetMap::compute` time
|
||||
with a `TypedefError::Offset` — the `OffsetMap` reserves only 4 bytes
|
||||
with a `AlkTypeError::Offset` — the `OffsetMap` reserves only 4 bytes
|
||||
(the length prefix), but `data_access::write_string` writes prefix +
|
||||
data inline, which would clobber subsequent fields. Non-final variable
|
||||
fields must use `maxLength` (fixed-size reservation) or
|
||||
@@ -125,7 +125,7 @@ padding is inserted between fields.
|
||||
### Fixed-size types
|
||||
|
||||
For each fixed-size type, the algorithm:
|
||||
1. Determines the type's byte size from the `TypeDef:*` kind.
|
||||
1. Determines the type's byte size from the `AlkType:*` kind.
|
||||
2. In aligned mode: inserts padding to satisfy the type's alignment
|
||||
(or the field's `align` annotation, or the struct's `align` default).
|
||||
3. Records the field's `(start, end)` range.
|
||||
@@ -141,7 +141,7 @@ its total size.
|
||||
|
||||
**`TUnion`:** TUnion is supported in packed sequential mode only. In
|
||||
aligned static mode, `OffsetMap::compute` rejects `TUnion` fields with
|
||||
`TypedefError::Offset` — see
|
||||
`AlkTypeError::Offset` — see
|
||||
[ADR-008](decisions/008-reject-tunion-in-aligned-mode.md). Unions
|
||||
are the protocol dispatch pattern (SFTP type bytes, call protocol event
|
||||
types); mmap-friendly formats use structs and arrays, not tagged unions.
|
||||
@@ -170,7 +170,7 @@ alignment padding in aligned mode). Element `i` starts at
|
||||
|
||||
### Variable-length types
|
||||
|
||||
The typedef engine supports three strategies for variable-length types
|
||||
The alktype engine supports three strategies for variable-length types
|
||||
(see [schema-layer.md](schema-layer.md) §Variable-length types and
|
||||
[ADR-003](decisions/003-schema-annotations.md) §3 for the full
|
||||
annotation shapes).
|
||||
@@ -221,7 +221,7 @@ and byte-swaps accordingly. All fixed-size types — including `TEnum`
|
||||
## Mode Selection
|
||||
|
||||
The consumer selects the mode at engine construction time via the
|
||||
`LayoutMode` enum, passed to `TypedefEngine::compile`:
|
||||
`LayoutMode` enum, passed to `AlkTypeEngine::compile`:
|
||||
|
||||
```rust
|
||||
pub enum LayoutMode {
|
||||
@@ -246,7 +246,7 @@ frames) and a `LayoutBuilder` (for constructing outgoing frames). A schema
|
||||
describing a metatensor layout can be consumed by an `OffsetMap` (for
|
||||
mmap access).
|
||||
|
||||
`TypedefEngine` exposes mode-appropriate accessors: `engine.offset_map()`
|
||||
`AlkTypeEngine` exposes mode-appropriate accessors: `engine.offset_map()`
|
||||
returns `Some(&OffsetMap)` in aligned mode and `None` in packed mode;
|
||||
`engine.layout_builder()` returns `Some(&LayoutBuilder)` in packed mode
|
||||
and `None` in aligned mode. `engine.sequential_reader()` returns
|
||||
@@ -254,7 +254,7 @@ and `None` in aligned mode. `engine.sequential_reader()` returns
|
||||
reader has mutable cursor state that the consumer owns; see
|
||||
[ADR-007](decisions/007-packed-mode-read-factory.md)) in packed
|
||||
mode and `None` in aligned mode. See [validation.md](validation.md)
|
||||
§"The TypedefEngine struct" for the engine API.
|
||||
§"The AlkTypeEngine struct" for the engine API.
|
||||
|
||||
## Public Types
|
||||
|
||||
@@ -282,14 +282,14 @@ provides `len()` and `is_empty()`.
|
||||
pub struct FieldPosition {
|
||||
pub offset: usize,
|
||||
pub size: usize,
|
||||
pub kind: TypeDefKind,
|
||||
pub kind: AlkTypeKind,
|
||||
}
|
||||
```
|
||||
|
||||
A field's computed position in a packed layout, produced by
|
||||
`LayoutBuilder::build`. For variable-length fields, `size` is `4` (the
|
||||
length prefix); for fixed-size fields, `size` is the type's byte size.
|
||||
`kind` records the field's `TypeDef:*` kind so the consumer can dispatch
|
||||
`kind` records the field's `AlkType:*` kind so the consumer can dispatch
|
||||
to the correct `data_access` read/write function.
|
||||
|
||||
### `PackedLayout` (packed mode)
|
||||
@@ -317,14 +317,14 @@ A flat table of `(field_path, byte_range)` pairs computed from a schema.
|
||||
|
||||
```rust
|
||||
impl OffsetMap {
|
||||
pub fn compute(schema: &Value) -> Result<Self, TypedefError>;
|
||||
pub fn compute(schema: &Value) -> Result<Self, AlkTypeError>;
|
||||
pub fn get(&self, field_path: &str) -> Option<&ByteRange>;
|
||||
pub fn total_size(&self) -> usize;
|
||||
pub fn iter(&self) -> impl Iterator<Item = &(String, ByteRange)>;
|
||||
}
|
||||
```
|
||||
|
||||
`compute` requires a `TypeDef:Struct` at the top level. `total_size`
|
||||
`compute` requires a `AlkType:Struct` at the top level. `total_size`
|
||||
includes trailing alignment padding. `iter` yields fields in insertion
|
||||
order (schema `properties` order, nested struct fields appearing inline).
|
||||
|
||||
@@ -354,7 +354,7 @@ See [open-questions.md](open-questions.md) for full details.
|
||||
the two layout modes decision
|
||||
- [ADR-003](decisions/003-schema-annotations.md) — schema
|
||||
annotations
|
||||
- [schema-layer.md](schema-layer.md) — the 17 TypeDef kinds and their
|
||||
- [schema-layer.md](schema-layer.md) — the 17 AlkType kinds and their
|
||||
byte sizes
|
||||
- [data-access.md](data-access.md) — read/write functions that use the
|
||||
computed offsets
|
||||
|
||||
@@ -6,7 +6,7 @@ last_updated: 2026-07-22
|
||||
# alktype — Overview
|
||||
|
||||
The binary struct engine: a small Rust crate that takes a JSON Schema
|
||||
with `TypeDef:*` custom keywords and produces an offset map, read/write
|
||||
with `AlkType:*` custom keywords and produces an offset map, read/write
|
||||
functions, and validation — all driven by the schema. The schema is the
|
||||
format definition; the engine is generic.
|
||||
|
||||
@@ -17,8 +17,8 @@ Component details are in the sibling documents.
|
||||
## What
|
||||
|
||||
`alktype` is a library crate that consumes JSON Schemas annotated
|
||||
with `TypeDef:*` custom keywords (the same kinds defined in TypeBox's
|
||||
`typedef.ts`, plus `TypeDef:Bytes`, `TypeDef:Int64`, and `TypeDef:Uint64`
|
||||
with `AlkType:*` custom keywords (the same kinds defined in TypeBox's
|
||||
`typedef.ts`, plus `AlkType:Bytes`, `AlkType:Int64`, and `AlkType:Uint64`
|
||||
as alktype additions) and produces three capabilities:
|
||||
|
||||
1. **An offset map** — walks the schema, computes byte offsets for each
|
||||
@@ -52,8 +52,8 @@ engine with different schemas.
|
||||
|
||||
The guiding insight:
|
||||
|
||||
> **The schema is the format.** A JSON Schema with `TypeDef:Float32`,
|
||||
> `TypeDef:Struct`, `TypeDef:Union` etc. is both the validation spec and
|
||||
> **The schema is the format.** A JSON Schema with `AlkType:Float32`,
|
||||
> `AlkType:Struct`, `AlkType:Union` etc. is both the validation spec and
|
||||
> the layout spec. No separate format definition, no separate parser, no
|
||||
> separate validator. One schema, three uses: validate, compute offsets,
|
||||
> access data.
|
||||
@@ -67,15 +67,15 @@ the binary data is the struct's bytes at computed offsets.
|
||||
The crate was bumped up in the timeline when the call-channels-unification
|
||||
research surfaced that channels, TTY, and the binary call protocol are
|
||||
all variations on the same wire-format family — `[discriminant][length][payload]`.
|
||||
The typedef engine makes the "channels is call with a binary data plane"
|
||||
The alktype engine makes the "channels is call with a binary data plane"
|
||||
unification concrete: the binary data plane's wire format is the call
|
||||
protocol's own schema system, just binary-encoded. The `channel_open`
|
||||
marker says "use binary framing"; the typedef engine says "here's how to
|
||||
marker says "use binary framing"; the alktype engine says "here's how to
|
||||
read/write the binary payload."
|
||||
|
||||
## The "Schema Is the Format" Principle
|
||||
|
||||
A JSON Schema with `TypeDef:*` custom keywords serves three roles
|
||||
A JSON Schema with `AlkType:*` custom keywords serves three roles
|
||||
simultaneously:
|
||||
|
||||
| Role | Mechanism | When |
|
||||
@@ -123,7 +123,7 @@ schema JSON determines the order of fields in the binary struct.
|
||||
|
||||
The russh-sftp case is the most instructive and the highest-value POC
|
||||
target. The `Packet` enum's `TryFrom<&mut Bytes>` impl is a hand-written
|
||||
dispatch on a type byte followed by serde deserialization. Under typedef,
|
||||
dispatch on a type byte followed by serde deserialization. Under alktype,
|
||||
the dispatch is `TUnion` with a byte-offset discriminator — the schema
|
||||
says "byte 0 is the discriminator, bytes 1..N are the variant struct."
|
||||
The engine reads the discriminator, looks up the variant schema, computes
|
||||
@@ -133,28 +133,28 @@ offsets, reads fields. Same result, no per-packet-type code.
|
||||
|
||||
These boundaries are decided in [ADR-001](decisions/001-alktype-purpose-scope-jsonschema-engine.md).
|
||||
|
||||
- **Not metatensor.** typedef is the binary struct *engine*. Metatensor
|
||||
- **Not metatensor.** alktype is the binary struct *engine*. Metatensor
|
||||
is a *format* (8-byte header + JSON header + binary data) that uses the
|
||||
typedef engine for its offset computation and tensor access.
|
||||
alktype engine for its offset computation and tensor access.
|
||||
- **Not a Value system.** TypeBox's `Value.Diff`, `Value.Migrate`,
|
||||
`Value.Convert` — schema evolution — is out of scope for v1. The engine
|
||||
should not do anything that explicitly blocks adding a Value system
|
||||
later.
|
||||
- **Not a code generator.** typebox-rs's `codegen/` module is a separate
|
||||
concern. The typedef engine consumes schemas; it does not generate them.
|
||||
- **Not a schema builder.** The typedef engine does not provide a fluent
|
||||
concern. The alktype engine consumes schemas; it does not generate them.
|
||||
- **Not a schema builder.** The alktype engine does not provide a fluent
|
||||
API for constructing schemas. Schemas are plain JSON — authored in
|
||||
TypeBox, generated by ujsx components, or hand-written. A builder API
|
||||
is deferred (OQ-003).
|
||||
- **Not a serialization framework.** The typedef engine is not a
|
||||
- **Not a serialization framework.** The alktype engine is not a
|
||||
general-purpose serde replacement. It operates on raw byte buffers at
|
||||
computed offsets — no intermediate `Value` tree, no reflection, no
|
||||
dynamic dispatch per field. For JSON data, use serde. For binary data
|
||||
with a known schema, use typedef.
|
||||
with a known schema, use alktype.
|
||||
|
||||
## Architecture (component pointers)
|
||||
|
||||
- **[schema-layer.md](schema-layer.md)** — the 19 `TypeDef:*` kinds,
|
||||
- **[schema-layer.md](schema-layer.md)** — the 19 `AlkType:*` kinds,
|
||||
jsonschema custom keyword integration, TypeBox interop, schema
|
||||
annotations (endianness, alignment, encoding, TUnion discriminators).
|
||||
- **[layout-engine.md](layout-engine.md)** — offset computation, the two
|
||||
@@ -164,8 +164,8 @@ These boundaries are decided in [ADR-001](decisions/001-alktype-purpose-scope-js
|
||||
dispatch, field paths, zero-copy access for fixed-size types,
|
||||
length-prefix reading for variable-length types.
|
||||
- **[validation.md](validation.md)** — custom keyword validators for all
|
||||
19 `TypeDef:*` kinds, `TypedefError`, load-time vs access-time
|
||||
validation, `TypedefEngine` as the compiled form of a schema.
|
||||
19 `AlkType:*` kinds, `AlkTypeError`, load-time vs access-time
|
||||
validation, `AlkTypeEngine` as the compiled form of a schema.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
@@ -174,7 +174,7 @@ These boundaries are decided in [ADR-001](decisions/001-alktype-purpose-scope-js
|
||||
| Purpose, scope, and the jsonschema engine | [ADR-001](decisions/001-alktype-purpose-scope-jsonschema-engine.md) | What the crate is/isn't; why jsonschema not a custom engine; "schema is the format" principle; scope boundaries |
|
||||
| Two layout modes | [ADR-002](decisions/002-two-layout-modes-packed-vs-aligned.md) | Packed sequential (`LayoutBuilder`/`SequentialReader`) for protocols; aligned static (`OffsetMap`) for mmap formats |
|
||||
| Schema annotations | [ADR-003](decisions/003-schema-annotations.md) | Endianness (schema-level, default LE), alignment (struct + field-level), encoding (length-prefixed vs offset-indirect), TUnion discriminators (byte-offset vs field-name) |
|
||||
| Error handling and validation | [ADR-004](decisions/004-error-handling-validation-strategy.md) | `TypedefError` enum; load-time build, access-time check; field-path-carrying errors; jsonschema `ValidationError` wrapping |
|
||||
| Error handling and validation | [ADR-004](decisions/004-error-handling-validation-strategy.md) | `AlkTypeError` enum; load-time build, access-time check; field-path-carrying errors; jsonschema `ValidationError` wrapping |
|
||||
| Int64/Uint64 kinds | [ADR-005](decisions/005-int64-uint64-first-class-kinds.md) | 64-bit integers as first-class kinds (SFTP offsets, metatensor data_offsets) |
|
||||
| Non-final inline variable fields | [ADR-006](decisions/006-reject-non-final-inline-length-prefixed-in-aligned-mode.md) | Rejected in aligned mode (would clobber subsequent fields) |
|
||||
| Packed-mode read factory | [ADR-007](decisions/007-packed-mode-read-factory.md) | `engine.sequential_reader()` returns an owned fresh reader |
|
||||
@@ -199,5 +199,5 @@ See [open-questions.md](open-questions.md) for full details.
|
||||
schema kinds (619 lines)
|
||||
- `/workspace/jsonschema/` — the jsonschema crate (v0.46.5, Draft 2020-12)
|
||||
- `/workspace/alknet-typedef-poc/` — the POC code (disposable)
|
||||
- `/workspace/@alkimiadev/typebox-rs/` — prior attempt, replaced by typedef
|
||||
- `/workspace/@alkimiadev/typebox-rs/` — prior attempt, replaced by alktype
|
||||
- `/workspace/@alkimiadev/alktype/` — prior attempt (the @alkimiadev/alktype prototype; not to be confused with this crate, which reuses the name but is backed by the `jsonschema` crate)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
changing the existing JSON-consumption path)
|
||||
- **Priority**: medium
|
||||
- **Impacts**: Blocks programmatic schema construction in Rust without a
|
||||
JS toolchain. Any consumer that wants to build typedef schemas at
|
||||
JS toolchain. Any consumer that wants to build alktype schemas at
|
||||
runtime from Rust code (rather than loading pre-authored JSON) must
|
||||
construct the JSON manually or depend on TypeBox. Does NOT block any
|
||||
current consumer — all v1 consumers (SFTP, metatensor, binary call
|
||||
|
||||
@@ -5,12 +5,12 @@ last_updated: 2026-07-22
|
||||
|
||||
# alktype — Schema Layer
|
||||
|
||||
The schema layer: the 19 `TypeDef:*` custom type kinds, their mapping to
|
||||
The schema layer: the 19 `AlkType:*` custom type kinds, their mapping to
|
||||
Rust types and byte sizes, the `jsonschema` custom keyword integration,
|
||||
TypeBox interop, and the concrete JSON shapes for schema-level
|
||||
annotations.
|
||||
|
||||
## The 19 TypeDef Kinds
|
||||
## The 19 AlkType Kinds
|
||||
|
||||
These are the custom schema kinds defined in TypeBox's `typedef.ts`
|
||||
(`/workspace/@alkdev/typebox/example/typedef/typedef.ts`, 619 lines) and
|
||||
@@ -20,42 +20,42 @@ encoding strategy (for variable-length types).
|
||||
|
||||
| Kind | TypeBox key | Rust type | Size | Category |
|
||||
|------|-------------|-----------|------|----------|
|
||||
| `TFloat32` | `TypeDef:Float32` | `f32` | 4 | fixed |
|
||||
| `TFloat64` | `TypeDef:Float64` | `f64` | 8 | fixed |
|
||||
| `TInt8` | `TypeDef:Int8` | `i8` | 1 | fixed |
|
||||
| `TInt16` | `TypeDef:Int16` | `i16` | 2 | fixed |
|
||||
| `TInt32` | `TypeDef:Int32` | `i32` | 4 | fixed |
|
||||
| `TInt64` | `TypeDef:Int64` | `i64` | 8 | fixed |
|
||||
| `TUint8` | `TypeDef:Uint8` | `u8` | 1 | fixed |
|
||||
| `TUint16` | `TypeDef:Uint16` | `u16` | 2 | fixed |
|
||||
| `TUint32` | `TypeDef:Uint32` | `u32` | 4 | fixed |
|
||||
| `TUint64` | `TypeDef:Uint64` | `u64` | 8 | fixed |
|
||||
| `TBoolean` | `TypeDef:Boolean` | `bool` (0x00=false, 0x01=true) | 1 | fixed |
|
||||
| `TString` | `TypeDef:String` | length-prefixed UTF-8 | variable | variable |
|
||||
| `TBytes` | `TypeDef:Bytes` | length-prefixed raw bytes | variable | variable |
|
||||
| `TStruct` | `TypeDef:Struct` | record of fields | sum of field sizes | composite |
|
||||
| `TUnion` | `TypeDef:Union` | tagged union | discriminator + variant | composite |
|
||||
| `TArray` | `TypeDef:Array` | repeated element | count × element size | composite |
|
||||
| `TEnum` | `TypeDef:Enum` | u32 index into enum values | 4 (fixed) | fixed |
|
||||
| `TRecord` | `TypeDef:Record` | count-prefixed sequence of (key, value) pairs | variable | variable |
|
||||
| `TTimestamp` | `TypeDef:Timestamp` | length-prefixed RFC 3339 string | variable | variable |
|
||||
| `TFloat32` | `AlkType:Float32` | `f32` | 4 | fixed |
|
||||
| `TFloat64` | `AlkType:Float64` | `f64` | 8 | fixed |
|
||||
| `TInt8` | `AlkType:Int8` | `i8` | 1 | fixed |
|
||||
| `TInt16` | `AlkType:Int16` | `i16` | 2 | fixed |
|
||||
| `TInt32` | `AlkType:Int32` | `i32` | 4 | fixed |
|
||||
| `TInt64` | `AlkType:Int64` | `i64` | 8 | fixed |
|
||||
| `TUint8` | `AlkType:Uint8` | `u8` | 1 | fixed |
|
||||
| `TUint16` | `AlkType:Uint16` | `u16` | 2 | fixed |
|
||||
| `TUint32` | `AlkType:Uint32` | `u32` | 4 | fixed |
|
||||
| `TUint64` | `AlkType:Uint64` | `u64` | 8 | fixed |
|
||||
| `TBoolean` | `AlkType:Boolean` | `bool` (0x00=false, 0x01=true) | 1 | fixed |
|
||||
| `TString` | `AlkType:String` | length-prefixed UTF-8 | variable | variable |
|
||||
| `TBytes` | `AlkType:Bytes` | length-prefixed raw bytes | variable | variable |
|
||||
| `TStruct` | `AlkType:Struct` | record of fields | sum of field sizes | composite |
|
||||
| `TUnion` | `AlkType:Union` | tagged union | discriminator + variant | composite |
|
||||
| `TArray` | `AlkType:Array` | repeated element | count × element size | composite |
|
||||
| `TEnum` | `AlkType:Enum` | u32 index into enum values | 4 (fixed) | fixed |
|
||||
| `TRecord` | `AlkType:Record` | count-prefixed sequence of (key, value) pairs | variable | variable |
|
||||
| `TTimestamp` | `AlkType:Timestamp` | length-prefixed RFC 3339 string | variable | variable |
|
||||
|
||||
`TypeDef:Int64` and `TypeDef:Uint64` are alktype additions —
|
||||
`AlkType:Int64` and `AlkType:Uint64` are alktype additions —
|
||||
TypeBox's `typedef.ts` tops out at 32-bit integers. They are required by
|
||||
the primary POC targets: SFTP `Read`/`Write` packets have `offset: u64`,
|
||||
and metatensor `data_offsets` are `u64`. See
|
||||
[ADR-005](decisions/005-int64-uint64-first-class-kinds.md).
|
||||
|
||||
### The `TypeDefKind` enum
|
||||
### The `AlkTypeKind` enum
|
||||
|
||||
The engine represents the 19 kinds as a Rust enum — `TypeDefKind` — with
|
||||
one variant per kind (`TypeDefKind::Float32`, `TypeDefKind::Struct`, etc.).
|
||||
The engine represents the 19 kinds as a Rust enum — `AlkTypeKind` — with
|
||||
one variant per kind (`AlkTypeKind::Float32`, `AlkTypeKind::Struct`, etc.).
|
||||
The enum provides compile-time exhaustiveness checking and integer
|
||||
discriminant dispatch (a jump table) instead of string comparison at
|
||||
every field access. It is `pub` and re-exported from the crate root.
|
||||
|
||||
```rust
|
||||
pub enum TypeDefKind {
|
||||
pub enum AlkTypeKind {
|
||||
Int8, Int16, Int32, Int64,
|
||||
Uint8, Uint16, Uint32, Uint64,
|
||||
Float32, Float64,
|
||||
@@ -69,7 +69,7 @@ The enum carries the kind's binary-layout metadata as inherent methods:
|
||||
|
||||
| Method | Returns | Notes |
|
||||
|--------|---------|-------|
|
||||
| `as_str(self)` | `&'static str` | The JSON Schema keyword, e.g. `"TypeDef:Uint8"` |
|
||||
| `as_str(self)` | `&'static str` | The JSON Schema keyword, e.g. `"AlkType:Uint8"` |
|
||||
| `type_size(self)` | `Option<usize>` | `Some(N)` for fixed-size kinds; `None` for variable/composite |
|
||||
| `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 |
|
||||
@@ -77,9 +77,9 @@ The enum carries the kind's binary-layout metadata as inherent methods:
|
||||
| `is_variable_length(self)` | `bool` | True for String, Bytes, Timestamp, Record |
|
||||
| `needs_endian(self)` | `bool` | True for kinds whose read/write takes an `Endian` parameter |
|
||||
|
||||
`TypeDefKind` implements `Display` (renders the keyword string) and
|
||||
`AlkTypeKind` implements `Display` (renders the keyword string) and
|
||||
`FromStr` (parses the keyword string back into the variant, returning
|
||||
`TypedefError::Schema` for unknown kinds). The layout engines and the
|
||||
`AlkTypeError::Schema` for unknown kinds). The layout engines and the
|
||||
validator dispatch on the enum, not on strings.
|
||||
|
||||
### Fixed-size types
|
||||
@@ -90,17 +90,17 @@ computation uses these sizes directly. Read/write is zero-copy pointer
|
||||
cast for these types.
|
||||
|
||||
**`TBoolean` byte representation:** `0x00` = false, `0x01` = true. Other
|
||||
values are invalid and produce a `TypedefError::Access` on read.
|
||||
values are invalid and produce a `AlkTypeError::Access` on read.
|
||||
|
||||
**`TEnum` binary representation:** A `u32` index into the enum's declared
|
||||
values, in declaration order. The first declared value is index 0, the
|
||||
second is index 1, etc. The enum's values are declared via the standard
|
||||
JSON Schema `"enum"` keyword (e.g., `"enum": ["read", "write", "execute"]`).
|
||||
The `TypeDef:Enum` custom keyword signals that the type is an enum for
|
||||
The `AlkType:Enum` custom keyword signals that the type is an enum for
|
||||
layout purposes; the built-in `enum` keyword provides the value list.
|
||||
|
||||
**Design note:** TypeBox's `TEnum` is a string enum (variable-length). The
|
||||
typedef engine uses a `u32` index instead — a deliberate deviation from
|
||||
alktype engine uses a `u32` index instead — a deliberate deviation from
|
||||
TypeBox fidelity in favor of binary efficiency. Most enums have a small
|
||||
number of variants (e.g., the call protocol's 5 event types); a `u32`
|
||||
index is compact, fixed-size, and sufficient for any realistic enum. The
|
||||
@@ -113,7 +113,7 @@ all other fixed-size types. In little-endian mode the index is
|
||||
### Variable-length types
|
||||
|
||||
`TString`, `TBytes`, `TRecord`, and `TTimestamp` have variable byte sizes.
|
||||
The typedef engine supports three strategies for handling variable-length
|
||||
The alktype engine supports three strategies for handling variable-length
|
||||
types in binary layouts, selected by the `encoding` annotation and the
|
||||
standard JSON Schema `maxLength` keyword:
|
||||
|
||||
@@ -169,7 +169,7 @@ consistent byte order for both field values and length prefixes.
|
||||
**`TBytes`:** Raw bytes — no UTF-8 constraint. The payload is `&[u8]`.
|
||||
Otherwise identical to `TString` in layout (same three strategies).
|
||||
|
||||
**Design note:** `TypeDef:Bytes` is an alktype addition — it does
|
||||
**Design note:** `AlkType:Bytes` is an alktype addition — it does
|
||||
not exist in TypeBox's `typedef.ts` (which defines 16 kinds). It is
|
||||
included because raw byte arrays are a common binary protocol primitive
|
||||
(SFTP data payloads, channels payloads, tensor data) and are semantically
|
||||
@@ -178,11 +178,11 @@ bytes with no encoding (not base64, not hex). In the JSON representation
|
||||
(for validation), TBytes is a string (JSON has no native byte type).
|
||||
|
||||
**`TRecord`:** A string-keyed map. The value type is declared via the
|
||||
schema's `"values"` property (e.g., `"values": { "TypeDef:Float32": true }`).
|
||||
schema's `"values"` property (e.g., `"values": { "AlkType:Float32": true }`).
|
||||
Binary layout is a count-prefixed sequence of `(key, value)` pairs:
|
||||
`[count: u32][key_len: u32][key_bytes][value]...` repeated `count` times.
|
||||
The count is the number of entries. Each key is a length-prefixed UTF-8
|
||||
string. Each value is encoded according to its declared `TypeDef:*` kind
|
||||
string. Each value is encoded according to its declared `AlkType:*` kind
|
||||
— a `Record<Uint32>` value is 4 raw bytes; a `Record<String>` value is
|
||||
itself a length-prefixed string; a `Record<Struct>` value is the struct's
|
||||
fields laid out inline. There is **no separate `value_len` prefix** —
|
||||
@@ -222,25 +222,25 @@ computation recurses into their properties.
|
||||
The `schema` module exposes the foundational types and functions every
|
||||
other module depends on. These are re-exported from the crate root.
|
||||
|
||||
### `get_typedef_kind` vs `get_typedef_kind_loose`
|
||||
### `get_alktype_kind` vs `get_alktype_kind_loose`
|
||||
|
||||
The engine recognizes a `TypeDef:*` kind on a schema node two ways,
|
||||
The engine recognizes a `AlkType:*` kind on a schema node two ways,
|
||||
because the keyword value may be either a boolean (`true`) or an
|
||||
annotation object (`{ "encoding": "..." }`):
|
||||
|
||||
| Function | Recognizes | Returns |
|
||||
|----------|------------|---------|
|
||||
| `get_typedef_kind(node) -> Option<&str>` | Boolean form only (`{ "TypeDef:String": true }`) | The keyword string, e.g. `"TypeDef:String"` |
|
||||
| `get_typedef_kind_loose(node) -> Option<&str>` | Boolean form **and** object form | The keyword string |
|
||||
| `get_typedef_kind_enum(node) -> Option<TypeDefKind>` | Boolean form only | The parsed enum variant |
|
||||
| `get_typedef_kind_loose_enum(node) -> Option<TypeDefKind>` | Boolean form **and** object form | The parsed enum variant |
|
||||
| `get_alktype_kind(node) -> Option<&str>` | Boolean form only (`{ "AlkType:String": true }`) | The keyword string, e.g. `"AlkType:String"` |
|
||||
| `get_alktype_kind_loose(node) -> Option<&str>` | Boolean form **and** object form | The keyword string |
|
||||
| `get_alktype_kind_enum(node) -> Option<AlkTypeKind>` | Boolean form only | The parsed enum variant |
|
||||
| `get_alktype_kind_loose_enum(node) -> Option<AlkTypeKind>` | Boolean form **and** object form | The parsed enum variant |
|
||||
|
||||
The boolean-form-only functions are used by the validator factories
|
||||
(which reject the object form as a schema error) and the top-level
|
||||
kind-check in `OffsetMap::compute` / `LayoutBuilder::new` / `SequentialReader::new`
|
||||
(which require `TypeDef:Struct` at the root). The "loose" variants are
|
||||
(which require `AlkType:Struct` at the root). The "loose" variants are
|
||||
used by the layout engines during field traversal, so that a variable-
|
||||
length field with an `encoding` annotation (`{ "TypeDef:String":
|
||||
length field with an `encoding` annotation (`{ "AlkType:String":
|
||||
{ "encoding": "offset-indirect" } }`) is still recognized as a `String`.
|
||||
|
||||
### Annotation parsers
|
||||
@@ -254,7 +254,7 @@ Each schema-level annotation has a dedicated parser that reads it from a
|
||||
| `parse_align(node) -> Option<usize>` | `"align"` | `None` |
|
||||
| `parse_max_length(node) -> Option<usize>` | `"maxLength"` | `None` |
|
||||
| `parse_encoding(keyword_value) -> VariableEncoding` | `"encoding"` (within the keyword's value object) | `VariableEncoding::LengthPrefixed` |
|
||||
| `parse_discriminator(node) -> Result<DiscriminatorKind, TypedefError>` | `"discriminator"` | (required — returns `TypedefError::Schema` if absent) |
|
||||
| `parse_discriminator(node) -> Result<DiscriminatorKind, AlkTypeError>` | `"discriminator"` | (required — returns `AlkTypeError::Schema` if absent) |
|
||||
|
||||
### Public enums
|
||||
|
||||
@@ -262,13 +262,13 @@ Each schema-level annotation has a dedicated parser that reads it from a
|
||||
pub enum Endian { Little, Big }
|
||||
pub enum VariableEncoding { LengthPrefixed, OffsetIndirect }
|
||||
pub enum DiscriminatorKind {
|
||||
Byte { offset: usize, disc_type: TypeDefKind },
|
||||
Byte { offset: usize, disc_type: AlkTypeKind },
|
||||
Field { name: String },
|
||||
}
|
||||
```
|
||||
|
||||
`DiscriminatorKind::Byte` carries the byte position (`offset`) and the
|
||||
discriminator's `TypeDef:*` kind (`disc_type`, restricted to `Uint8`/
|
||||
discriminator's `AlkType:*` kind (`disc_type`, restricted to `Uint8`/
|
||||
`Uint16`/`Uint32`). `DiscriminatorKind::Field` carries the discriminator
|
||||
field's name. See [data-access.md](data-access.md) §"TUnion Dispatch" for
|
||||
how these drive dispatch.
|
||||
@@ -277,7 +277,7 @@ how these drive dispatch.
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `normalize_refs(schema: &mut Value)` | Walks the schema; rewrites every `"$ref"` whose value is a bare name (no `#` prefix) to `"#/$defs/<name>"`. Idempotent. Runs once at `TypedefEngine::compile` time. |
|
||||
| `normalize_refs(schema: &mut Value)` | Walks the schema; rewrites every `"$ref"` whose value is a bare name (no `#` prefix) to `"#/$defs/<name>"`. Idempotent. Runs once at `AlkTypeEngine::compile` time. |
|
||||
| `resolve_ref(root, ref_path) -> Option<&Value>` | Resolves a JSON Pointer `$ref` (e.g. `"#/$defs/Read"`) against the root schema. |
|
||||
| `resolve_ref_or_inline(node, root) -> Option<&Value>` | If `node` has a `"$ref"`, resolves it against `root`; otherwise returns `node` itself (it's an inline schema). |
|
||||
|
||||
@@ -288,22 +288,22 @@ on every `$ref`-bearing node they encounter during traversal.
|
||||
## jsonschema Custom Keyword Integration
|
||||
|
||||
The `jsonschema` crate (v0.46.5, Draft 2020-12) supports custom keywords
|
||||
via the `with_keyword` API. Each `TypeDef:*` kind is registered as a
|
||||
via the `with_keyword` API. Each `AlkType:*` kind is registered as a
|
||||
custom keyword:
|
||||
|
||||
```rust
|
||||
let validator = jsonschema::options()
|
||||
.with_keyword("TypeDef:Float32", factory)
|
||||
.with_keyword("TypeDef:Int32", factory)
|
||||
.with_keyword("TypeDef:Struct", factory)
|
||||
.with_keyword("AlkType:Float32", factory)
|
||||
.with_keyword("AlkType:Int32", factory)
|
||||
.with_keyword("AlkType:Struct", factory)
|
||||
// ... all 17 kinds
|
||||
.build(&schema)?;
|
||||
```
|
||||
|
||||
The factory closure receives the parent schema object, the keyword's
|
||||
value, and the schema path — enabling cross-keyword awareness. The
|
||||
`TypeDef:Struct` validator, for example, inspects the parent's
|
||||
`properties` to validate each field against its declared `TypeDef:*` kind.
|
||||
`AlkType:Struct` validator, for example, inspects the parent's
|
||||
`properties` to validate each field against its declared `AlkType:*` kind.
|
||||
|
||||
Each custom keyword implementation is ~10 lines. The `jsonschema` crate
|
||||
handles all structural validation (object properties, required fields,
|
||||
@@ -313,7 +313,7 @@ validator implementations.
|
||||
|
||||
This is the same pattern as TypeBox's `TypeRegistry.Set` on the JS side.
|
||||
Same semantics, different language, same JSON Schema wire format. A
|
||||
TypeBox schema serialized to JSON feeds into the typedef engine after a
|
||||
TypeBox schema serialized to JSON feeds into the alktype engine after a
|
||||
single pre-processing step: normalizing `$ref` values (see below).
|
||||
|
||||
## TypeBox Interop
|
||||
@@ -330,8 +330,8 @@ const TensorRef = Type.Object({
|
||||
```
|
||||
|
||||
serialized to JSON is a standard JSON Schema with `type: "object"`,
|
||||
`properties`, and `required`. That JSON feeds into the typedef engine
|
||||
after `$ref` normalization. The `TypeDef:*` custom keywords are added by
|
||||
`properties`, and `required`. That JSON feeds into the alktype engine
|
||||
after `$ref` normalization. The `AlkType:*` custom keywords are added by
|
||||
TypeBox's `TypeRegistry.Set` — they appear in the serialized JSON as
|
||||
additional properties on the schema object.
|
||||
|
||||
@@ -340,11 +340,11 @@ additional properties on the schema object.
|
||||
TypeBox generates bare-name `$ref` values (e.g., `"$ref": "Read"`),
|
||||
referencing sibling definitions within the same `$defs` block. The
|
||||
`jsonschema` crate requires full JSON Pointer paths (e.g.,
|
||||
`"$ref": "#/$defs/Read"`). The typedef engine normalizes TypeBox-style
|
||||
`"$ref": "#/$defs/Read"`). The alktype engine normalizes TypeBox-style
|
||||
refs at schema load time via [`normalize_refs`](#ref-resolution-and-normalization)
|
||||
— a ~20-line recursive walk that rewrites every bare-name `"$ref"` to
|
||||
`"#/$defs/<name>"`. The normalization is idempotent — full JSON Pointer
|
||||
refs pass through unchanged. It runs once at `TypedefEngine::compile`
|
||||
refs pass through unchanged. It runs once at `AlkTypeEngine::compile`
|
||||
time, before the schema is passed to `jsonschema` or the offset
|
||||
computation.
|
||||
|
||||
@@ -353,7 +353,7 @@ with `Resource 'Read' is not present in a registry`. Full JSON Pointer
|
||||
refs (`#/$defs/Read`) resolve correctly. The normalization step bridges
|
||||
the gap between TypeBox's output and jsonschema's input.
|
||||
|
||||
The typedef engine does not depend on TypeBox or any JS toolchain. It
|
||||
The alktype engine does not depend on TypeBox or any JS toolchain. It
|
||||
consumes JSON — whether that JSON was authored in TypeBox, generated by
|
||||
a ujsx component, or hand-written. The schema is the interface.
|
||||
|
||||
@@ -367,7 +367,7 @@ decided in [ADR-003](decisions/003-schema-annotations.md).
|
||||
Schema-level annotation with a default of little-endian:
|
||||
|
||||
```json
|
||||
{ "TypeDef:Struct": true, "endian": "big", "properties": { ... } }
|
||||
{ "AlkType:Struct": true, "endian": "big", "properties": { ... } }
|
||||
```
|
||||
|
||||
- `"endian": "little"` (default) — read/write in little-endian byte order.
|
||||
@@ -380,10 +380,10 @@ Both struct-level and field-level, with field-level overriding:
|
||||
|
||||
```json
|
||||
{
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"align": 256,
|
||||
"properties": {
|
||||
"weight": { "TypeDef:Float32": true, "align": 16 }
|
||||
"weight": { "AlkType:Float32": true, "align": 16 }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -398,23 +398,23 @@ Both struct-level and field-level, with field-level overriding:
|
||||
|
||||
### Variable-length encoding
|
||||
|
||||
The typedef engine supports three strategies for variable-length types
|
||||
The alktype engine supports three strategies for variable-length types
|
||||
(see §Variable-length types above for full details). The strategy is
|
||||
selected by the `encoding` annotation and the standard JSON Schema
|
||||
`maxLength` keyword:
|
||||
|
||||
```json
|
||||
// Strategy 1: Inline length-prefixing (default, shorthand)
|
||||
{ "TypeDef:String": true }
|
||||
{ "AlkType:String": true }
|
||||
|
||||
// Strategy 1: Explicit inline length-prefixing
|
||||
{ "TypeDef:String": { "encoding": "length-prefixed" } }
|
||||
{ "AlkType:String": { "encoding": "length-prefixed" } }
|
||||
|
||||
// Strategy 2: Fixed-size reservation (uses standard maxLength)
|
||||
{ "TypeDef:String": true, "maxLength": 256 }
|
||||
{ "AlkType:String": true, "maxLength": 256 }
|
||||
|
||||
// Strategy 3: Offset indirection (opt-in)
|
||||
{ "TypeDef:String": { "encoding": "offset-indirect" } }
|
||||
{ "AlkType:String": { "encoding": "offset-indirect" } }
|
||||
```
|
||||
|
||||
- `"encoding": "length-prefixed"` (default) — 4-byte length prefix at
|
||||
@@ -428,8 +428,8 @@ selected by the `encoding` annotation and the standard JSON Schema
|
||||
`{offset: u32, length: u32}` pointing into a separate data region.
|
||||
The consumer provides the data region separately. Used by metatensor
|
||||
blob tensors.
|
||||
- Applies to all variable-length types: `TypeDef:String`, `TypeDef:Bytes`,
|
||||
`TypeDef:Array`, `TypeDef:Record`, `TypeDef:Timestamp`.
|
||||
- Applies to all variable-length types: `AlkType:String`, `AlkType:Bytes`,
|
||||
`AlkType:Array`, `AlkType:Record`, `AlkType:Timestamp`.
|
||||
|
||||
### TUnion discriminators
|
||||
|
||||
@@ -440,11 +440,11 @@ Two discriminator kinds: byte-offset (protocol dispatch) and field-name
|
||||
|
||||
```json
|
||||
{
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {
|
||||
"kind": "byte",
|
||||
"offset": 0,
|
||||
"type": "TypeDef:Uint8"
|
||||
"type": "AlkType:Uint8"
|
||||
},
|
||||
"mapping": {
|
||||
"5": { "$ref": "#/$defs/Read" },
|
||||
@@ -455,8 +455,8 @@ Two discriminator kinds: byte-offset (protocol dispatch) and field-name
|
||||
```
|
||||
|
||||
- `"offset"` — byte position of the discriminator.
|
||||
- `"type"` — the `TypeDef:*` kind of the discriminator (typically
|
||||
`TypeDef:Uint8`).
|
||||
- `"type"` — the `AlkType:*` kind of the discriminator (typically
|
||||
`AlkType:Uint8`).
|
||||
- Mapping keys are stringified integers. The variant struct starts at
|
||||
`offset + discriminator_size`.
|
||||
|
||||
@@ -464,7 +464,7 @@ Two discriminator kinds: byte-offset (protocol dispatch) and field-name
|
||||
|
||||
```json
|
||||
{
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {
|
||||
"kind": "field",
|
||||
"name": "type"
|
||||
|
||||
@@ -5,22 +5,22 @@ last_updated: 2026-07-22
|
||||
|
||||
# alktype — Validation
|
||||
|
||||
The validation layer: custom keyword validators for all 19 `TypeDef:*`
|
||||
kinds, the `TypedefError` enum, load-time vs access-time validation
|
||||
strategy, and the `TypedefEngine` as the compiled form of a schema.
|
||||
The validation layer: custom keyword validators for all 19 `AlkType:*`
|
||||
kinds, the `AlkTypeError` enum, load-time vs access-time validation
|
||||
strategy, and the `AlkTypeEngine` as the compiled form of a schema.
|
||||
|
||||
## Validation Strategy
|
||||
|
||||
Validation is delegated to the `jsonschema` crate (v0.46.5, Draft
|
||||
2020-12). The typedef engine does not implement its own validation —
|
||||
it registers custom keyword validators for each `TypeDef:*` kind and
|
||||
2020-12). The alktype engine does not implement its own validation —
|
||||
it registers custom keyword validators for each `AlkType:*` kind and
|
||||
lets `jsonschema` handle the structural validation (object properties,
|
||||
required fields, array items, enum values).
|
||||
|
||||
The strategy is decided in [ADR-004](decisions/004-error-handling-validation-strategy.md):
|
||||
|
||||
1. **Load time:** Parse the schema JSON, build the layout engine, build the
|
||||
jsonschema validator. This is the `TypedefEngine::compile(schema)` constructor.
|
||||
jsonschema validator. This is the `AlkTypeEngine::compile(schema)` constructor.
|
||||
2. **Access time:** Use the compiled engine for repeated read/write
|
||||
operations. Validation is opt-in per operation.
|
||||
|
||||
@@ -37,7 +37,7 @@ the correct separation of concerns:
|
||||
- **Binary access validation** (data access layer): the read/write
|
||||
functions perform type-level validation at access time — range checks
|
||||
for integers, UTF-8 validity for strings, buffer bounds checking.
|
||||
These return `TypedefError::Access` with field paths.
|
||||
These return `AlkTypeError::Access` with field paths.
|
||||
|
||||
The "schema is the format" principle means the same schema describes
|
||||
both the JSON shape and the binary layout. The jsonschema validator
|
||||
@@ -47,13 +47,13 @@ buffer into a `Value` tree via the data access layer, then validates
|
||||
that `Value` against the jsonschema validator. This is a two-step
|
||||
process, not a single `validate(buffer)` call.
|
||||
|
||||
### The `TypedefEngine` struct
|
||||
### The `AlkTypeEngine` struct
|
||||
|
||||
The `TypedefEngine` is the compiled form of a schema. It supports both
|
||||
The `AlkTypeEngine` is the compiled form of a schema. It supports both
|
||||
layout modes (ADR-002) via an internal `Layout` enum:
|
||||
|
||||
```rust
|
||||
pub struct TypedefEngine {
|
||||
pub struct AlkTypeEngine {
|
||||
layout: Layout, // packed or aligned (private enum)
|
||||
validator: jsonschema::Validator, // compiled once at load time
|
||||
endian: Endian, // parsed from the schema's "endian" annotation
|
||||
@@ -72,8 +72,8 @@ The consumer selects the mode at construction time via `LayoutMode`
|
||||
enum is private — the engine exposes mode-appropriate accessors instead:
|
||||
|
||||
```rust
|
||||
impl TypedefEngine {
|
||||
pub fn compile(schema: &mut Value, mode: LayoutMode) -> Result<Self, TypedefError>;
|
||||
impl AlkTypeEngine {
|
||||
pub fn compile(schema: &mut Value, mode: LayoutMode) -> Result<Self, AlkTypeError>;
|
||||
pub fn mode(&self) -> LayoutMode;
|
||||
pub fn endian(&self) -> Endian;
|
||||
pub fn offset_map(&self) -> Option<&OffsetMap>; // Some in aligned mode
|
||||
@@ -94,39 +94,39 @@ The `SequentialReader` (read-side) is not stored — it has mutable cursor
|
||||
state that the consumer owns, so `sequential_reader()` constructs a fresh
|
||||
reader on each call (ADR-007).
|
||||
|
||||
The `read_field`/`write_field` methods on `TypedefEngine` are the
|
||||
The `read_field`/`write_field` methods on `AlkTypeEngine` are the
|
||||
aligned-mode data-access API — see [data-access.md](data-access.md)
|
||||
§"Higher-level read/write".
|
||||
|
||||
## Custom Keyword Validators
|
||||
|
||||
Each `TypeDef:*` kind gets a `Keyword` implementation registered via
|
||||
Each `AlkType:*` kind gets a `Keyword` implementation registered via
|
||||
`jsonschema::options().with_keyword(...)`. The validators check leaf
|
||||
type constraints; `jsonschema` handles all structural validation.
|
||||
|
||||
### Numeric type validators
|
||||
|
||||
**`TypeDef:Float32` / `TypeDef:Float64`:**
|
||||
**`AlkType:Float32` / `AlkType:Float64`:**
|
||||
- Value must be a finite number.
|
||||
- For `Float32`: value must be representable as `f32` (no precision loss
|
||||
beyond `f32`'s mantissa).
|
||||
|
||||
**`TypeDef:Int8` / `TypeDef:Int16` / `TypeDef:Int32`:**
|
||||
**`AlkType:Int8` / `AlkType:Int16` / `AlkType:Int32`:**
|
||||
- Value must be an integer within the type's range.
|
||||
- Int8: -128..127, Int16: -32768..32767, Int32: -2147483648..2147483647.
|
||||
|
||||
**`TypeDef:Uint8` / `TypeDef:Uint16` / `TypeDef:Uint32`:**
|
||||
**`AlkType:Uint8` / `AlkType:Uint16` / `AlkType:Uint32`:**
|
||||
- Value must be a non-negative integer within the type's range.
|
||||
- Uint8: 0..255, Uint16: 0..65535, Uint32: 0..4294967295.
|
||||
|
||||
### String and binary validators
|
||||
|
||||
**`TypeDef:String`:**
|
||||
**`AlkType:String`:**
|
||||
- Value must be a valid UTF-8 string.
|
||||
- If `maxLength` is specified in the schema, the string's byte length
|
||||
must not exceed it.
|
||||
|
||||
**`TypeDef:Bytes`:**
|
||||
**`AlkType:Bytes`:**
|
||||
- Value must be a string (JSON represents binary data as a string — JSON
|
||||
has no native byte type).
|
||||
- If `maxLength` is specified, the byte length must not exceed it.
|
||||
@@ -135,34 +135,34 @@ type constraints; `jsonschema` handles all structural validation.
|
||||
validation) uses a string; the binary representation (for data access)
|
||||
uses `&[u8]` directly.
|
||||
|
||||
**`TypeDef:Enum`:**
|
||||
- The `TypeDef:Enum` custom keyword signals that the type is an enum for
|
||||
**`AlkType:Enum`:**
|
||||
- The `AlkType:Enum` custom keyword signals that the type is an enum for
|
||||
*layout* purposes (the engine needs to know it's a fixed-size u32 index,
|
||||
not a variable-length string). The built-in `enum` keyword provides the
|
||||
value list and handles value-membership validation. The custom keyword
|
||||
validator is a no-op beyond the built-in check — it exists solely for
|
||||
the layout engine to recognize the type.
|
||||
|
||||
**`TypeDef:Timestamp`:**
|
||||
**`AlkType:Timestamp`:**
|
||||
- Value must be a valid RFC 3339 timestamp string (the internet profile
|
||||
of ISO 8601, e.g., `"2026-07-20T15:30:00Z"`).
|
||||
|
||||
### Composite type validators
|
||||
|
||||
**`TypeDef:Struct`:**
|
||||
**`AlkType:Struct`:**
|
||||
- Value must be an object.
|
||||
- Each property must match its declared `TypeDef:*` kind.
|
||||
- Each property must match its declared `AlkType:*` kind.
|
||||
- Required fields must be present.
|
||||
- The `jsonschema` crate's built-in `properties` and `required` keywords
|
||||
handle the structural checks — the custom keyword only needs to
|
||||
validate that each field's value matches its `TypeDef:*` kind.
|
||||
validate that each field's value matches its `AlkType:*` kind.
|
||||
|
||||
**`TypeDef:Union`:**
|
||||
**`AlkType:Union`:**
|
||||
- The discriminator value must be one of the mapping keys.
|
||||
- The variant struct must match the declared schema for that discriminator
|
||||
value.
|
||||
|
||||
**`TypeDef:Array`:**
|
||||
**`AlkType:Array`:**
|
||||
- Value must be an array.
|
||||
- Each element must match the array's declared element type.
|
||||
- If `minItems`/`maxItems` is specified, the array length must be within
|
||||
@@ -170,19 +170,19 @@ type constraints; `jsonschema` handles all structural validation.
|
||||
|
||||
### Other validators
|
||||
|
||||
**`TypeDef:Boolean`:**
|
||||
**`AlkType:Boolean`:**
|
||||
- Value must be `true` or `false`.
|
||||
|
||||
**`TypeDef:Record`:**
|
||||
**`AlkType:Record`:**
|
||||
- Value must be an object.
|
||||
- All values must match the record's declared value type (specified via
|
||||
the `"values"` property in the schema, e.g.,
|
||||
`"values": { "TypeDef:Float32": true }`).
|
||||
`"values": { "AlkType:Float32": true }`).
|
||||
|
||||
### Validator implementation pattern
|
||||
|
||||
Each custom keyword implementation is ~10 lines. Example for
|
||||
`TypeDef:Float32`:
|
||||
`AlkType:Float32`:
|
||||
|
||||
```rust
|
||||
struct Float32Validator;
|
||||
@@ -204,7 +204,7 @@ Registration:
|
||||
|
||||
```rust
|
||||
let validator = jsonschema::options()
|
||||
.with_keyword("TypeDef:Float32", |parent, value, path| {
|
||||
.with_keyword("AlkType:Float32", |parent, value, path| {
|
||||
Ok(Box::new(Float32Validator))
|
||||
})
|
||||
.build(&schema)?;
|
||||
@@ -212,18 +212,18 @@ let validator = jsonschema::options()
|
||||
|
||||
The factory closure receives the parent schema object, the keyword's
|
||||
value, and the schema path. This enables cross-keyword awareness — for
|
||||
example, a `TypeDef:Struct` validator can inspect the parent's
|
||||
`properties` to validate each field against its declared `TypeDef:*` kind.
|
||||
example, a `AlkType:Struct` validator can inspect the parent's
|
||||
`properties` to validate each field against its declared `AlkType:*` kind.
|
||||
|
||||
## TypedefError
|
||||
## AlkTypeError
|
||||
|
||||
A single `TypedefError` enum covers all error conditions across the
|
||||
A single `AlkTypeError` enum covers all error conditions across the
|
||||
engine's three phases (schema parsing, offset computation, read/write)
|
||||
plus validation. Decided in [ADR-004](decisions/004-error-handling-validation-strategy.md).
|
||||
|
||||
```rust
|
||||
pub enum TypedefError {
|
||||
/// Schema parsing errors (invalid JSON, missing keywords, unknown TypeDef kinds).
|
||||
pub enum AlkTypeError {
|
||||
/// Schema parsing errors (invalid JSON, missing keywords, unknown AlkType kinds).
|
||||
Schema(String),
|
||||
/// Offset computation errors (field not found, unsupported type).
|
||||
Offset { field_path: String, reason: String },
|
||||
@@ -234,8 +234,8 @@ pub enum TypedefError {
|
||||
}
|
||||
```
|
||||
|
||||
- **`Schema`** — for errors during `TypedefEngine::compile()`. Invalid
|
||||
JSON, missing required keywords, unknown `TypeDef:*` kinds.
|
||||
- **`Schema`** — for errors during `AlkTypeEngine::compile()`. Invalid
|
||||
JSON, missing required keywords, unknown `AlkType:*` kinds.
|
||||
- **`Offset`** — for errors during offset computation. Field not found
|
||||
in the schema, type not supported for offset computation, recursive
|
||||
depth exceeded. Carries the field path.
|
||||
@@ -244,14 +244,14 @@ pub enum TypedefError {
|
||||
Carries the field path.
|
||||
- **`Validation`** — wraps `jsonschema`'s `ValidationError`. The
|
||||
`'static` lifetime is correct — the validator owns its schema reference
|
||||
and lives for the lifetime of the `TypedefEngine`.
|
||||
and lives for the lifetime of the `AlkTypeEngine`.
|
||||
|
||||
### Field-path-carrying errors
|
||||
|
||||
Read/write and offset errors include the field path for debugging:
|
||||
|
||||
```rust
|
||||
Err(TypedefError::Access {
|
||||
Err(AlkTypeError::Access {
|
||||
field_path: "header.version".to_string(),
|
||||
reason: "buffer too short: need 4 bytes at offset 12, have 2".to_string(),
|
||||
})
|
||||
@@ -262,7 +262,7 @@ you exactly which field failed and why.
|
||||
|
||||
## Validation Timing
|
||||
|
||||
### Load time: `TypedefEngine::compile()`
|
||||
### Load time: `AlkTypeEngine::compile()`
|
||||
|
||||
The expensive work happens once at schema load time:
|
||||
1. Normalize `$ref` values in the schema (`normalize_refs`).
|
||||
@@ -270,7 +270,7 @@ The expensive work happens once at schema load time:
|
||||
3. Compute the layout (`LayoutBuilder`/`SequentialReader` for packed, `OffsetMap` for aligned).
|
||||
4. Build the jsonschema validator (`jsonschema::options().with_keyword(...).build(&schema)?`).
|
||||
|
||||
The result is a `TypedefEngine` that can be used for repeated operations.
|
||||
The result is a `AlkTypeEngine` that can be used for repeated operations.
|
||||
|
||||
### Access time: `engine.validate_json(&Value)` / `engine.is_valid_json(&Value)`
|
||||
|
||||
@@ -281,7 +281,7 @@ validator is already compiled — these are fast checks against the
|
||||
compiled validator.
|
||||
|
||||
```rust
|
||||
pub fn validate_json(&self, instance: &Value) -> Result<(), TypedefError>;
|
||||
pub fn validate_json(&self, instance: &Value) -> Result<(), AlkTypeError>;
|
||||
pub fn is_valid_json(&self, instance: &Value) -> bool;
|
||||
```
|
||||
|
||||
@@ -314,12 +314,12 @@ representation first, then access the binary buffer.
|
||||
|
||||
| Decision | ADR | Summary |
|
||||
|----------|-----|---------|
|
||||
| Error handling and validation | [ADR-004](decisions/004-error-handling-validation-strategy.md) | `TypedefError` enum; load-time build, access-time check; field-path-carrying errors; jsonschema `ValidationError` wrapping |
|
||||
| Error handling and validation | [ADR-004](decisions/004-error-handling-validation-strategy.md) | `AlkTypeError` enum; load-time build, access-time check; field-path-carrying errors; jsonschema `ValidationError` wrapping |
|
||||
| Purpose and scope | [ADR-001](decisions/001-alktype-purpose-scope-jsonschema-engine.md) | Why jsonschema not a custom engine |
|
||||
|
||||
## Open Questions
|
||||
|
||||
None specific to validation. The three typedef OQs (OQ-001, OQ-002,
|
||||
None specific to validation. The three alktype OQs (OQ-001, OQ-002,
|
||||
OQ-003) are about layout, platform support, and schema construction —
|
||||
not validation.
|
||||
|
||||
@@ -329,7 +329,7 @@ not validation.
|
||||
custom keyword validators for all 17 kinds
|
||||
- [ADR-004](decisions/004-error-handling-validation-strategy.md) —
|
||||
error handling and validation strategy
|
||||
- [schema-layer.md](schema-layer.md) — the 17 TypeDef kinds that the
|
||||
- [schema-layer.md](schema-layer.md) — the 17 AlkType kinds that the
|
||||
validators check
|
||||
- [data-access.md](data-access.md) — read/write functions that operate
|
||||
on the same buffers
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
//! Data access layer: primitive read/write functions for all 17 TypeDef
|
||||
//! Data access layer: primitive read/write functions for all 17 AlkType
|
||||
//! kinds with endianness support, bounds checking, and zero-copy access.
|
||||
//!
|
||||
//! These are the building blocks used by the layout types ([`crate::offset_map`],
|
||||
//! [`crate::layout_builder`], [`crate::sequential_reader`]) and the
|
||||
//! [`crate::engine::TypedefEngine`]. Each function operates on a raw byte
|
||||
//! buffer at a caller-provided offset and returns a [`TypedefError::Access`]
|
||||
//! [`crate::engine::AlkTypeEngine`]. Each function operates on a raw byte
|
||||
//! buffer at a caller-provided offset and returns a [`AlkTypeError::Access`]
|
||||
//! carrying the field path on bounds or encoding failures.
|
||||
//!
|
||||
//! # Conventions
|
||||
//!
|
||||
//! - All multi-byte types respect the [`Endian`] parameter passed by the caller.
|
||||
//! - Bounds checks ensure `buffer.len() >= offset + size`; failures produce
|
||||
//! [`TypedefError::Access`] with a descriptive `reason`.
|
||||
//! [`AlkTypeError::Access`] with a descriptive `reason`.
|
||||
//! - Read functions for variable-length types return slices borrowing from
|
||||
//! the input buffer — no allocation.
|
||||
//! - No `unwrap()` / `expect()` on fallible operations.
|
||||
|
||||
use crate::error::TypedefError;
|
||||
use crate::error::AlkTypeError;
|
||||
use crate::schema::Endian;
|
||||
|
||||
const U32_SIZE: usize = 4;
|
||||
@@ -26,9 +26,9 @@ fn check_bounds(
|
||||
start: usize,
|
||||
end: usize,
|
||||
field_path: &str,
|
||||
) -> Result<(), TypedefError> {
|
||||
) -> Result<(), AlkTypeError> {
|
||||
if end < start || buffer_len < end {
|
||||
return Err(TypedefError::Access {
|
||||
return Err(AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!(
|
||||
"buffer bounds check failed: need bytes [{start}..{end}), buffer has {buffer_len}"
|
||||
@@ -38,8 +38,8 @@ fn check_bounds(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn access_err(field_path: &str, reason: impl Into<String>) -> TypedefError {
|
||||
TypedefError::Access {
|
||||
fn access_err(field_path: &str, reason: impl Into<String>) -> AlkTypeError {
|
||||
AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: reason.into(),
|
||||
}
|
||||
@@ -49,7 +49,7 @@ pub(crate) fn read_array<const N: usize>(
|
||||
buffer: &[u8],
|
||||
offset: usize,
|
||||
field_path: &str,
|
||||
) -> Result<[u8; N], TypedefError> {
|
||||
) -> Result<[u8; N], AlkTypeError> {
|
||||
let end = offset.checked_add(N).ok_or_else(|| {
|
||||
access_err(
|
||||
field_path,
|
||||
@@ -79,7 +79,7 @@ pub(crate) fn write_array<const N: usize>(
|
||||
offset: usize,
|
||||
bytes: [u8; N],
|
||||
field_path: &str,
|
||||
) -> Result<(), TypedefError> {
|
||||
) -> Result<(), AlkTypeError> {
|
||||
let end = offset.checked_add(N).ok_or_else(|| {
|
||||
access_err(
|
||||
field_path,
|
||||
@@ -129,9 +129,9 @@ define_read_write_endian!(f64, read_f64, write_f64, 8);
|
||||
/// Read a `bool` at `offset` from `buffer`.
|
||||
///
|
||||
/// `0x00` decodes to `false`, `0x01` decodes to `true`. Any other byte value
|
||||
/// produces [`TypedefError::Access`] with a reason of the form
|
||||
/// produces [`AlkTypeError::Access`] with a reason of the form
|
||||
/// `"invalid boolean byte 0x02 at offset {offset}"`.
|
||||
pub fn read_bool(buffer: &[u8], offset: usize, field_path: &str) -> Result<bool, TypedefError> {
|
||||
pub fn read_bool(buffer: &[u8], offset: usize, field_path: &str) -> Result<bool, AlkTypeError> {
|
||||
let bytes: [u8; 1] = read_array(buffer, offset, field_path)?;
|
||||
match bytes[0] {
|
||||
0x00 => Ok(false),
|
||||
@@ -151,7 +151,7 @@ pub fn write_bool(
|
||||
offset: usize,
|
||||
value: bool,
|
||||
field_path: &str,
|
||||
) -> Result<(), TypedefError> {
|
||||
) -> Result<(), AlkTypeError> {
|
||||
write_array(
|
||||
buffer,
|
||||
offset,
|
||||
@@ -168,7 +168,7 @@ pub fn read_enum(
|
||||
offset: usize,
|
||||
field_path: &str,
|
||||
endian: Endian,
|
||||
) -> Result<u32, TypedefError> {
|
||||
) -> Result<u32, AlkTypeError> {
|
||||
read_u32(buffer, offset, field_path, endian)
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ pub fn write_enum(
|
||||
value: u32,
|
||||
field_path: &str,
|
||||
endian: Endian,
|
||||
) -> Result<(), TypedefError> {
|
||||
) -> Result<(), AlkTypeError> {
|
||||
write_u32(buffer, offset, value, field_path, endian)
|
||||
}
|
||||
|
||||
@@ -191,13 +191,13 @@ pub fn write_enum(
|
||||
///
|
||||
/// Wire format: `[length: u32][UTF-8 bytes]`. The length prefix respects
|
||||
/// `endian`. Returns a `&'a str` that borrows from the input buffer — no
|
||||
/// allocation. Invalid UTF-8 produces [`TypedefError::Access`].
|
||||
/// allocation. Invalid UTF-8 produces [`AlkTypeError::Access`].
|
||||
pub fn read_string<'a>(
|
||||
buffer: &'a [u8],
|
||||
offset: usize,
|
||||
field_path: &str,
|
||||
endian: Endian,
|
||||
) -> Result<&'a str, TypedefError> {
|
||||
) -> Result<&'a str, AlkTypeError> {
|
||||
let bytes = read_bytes(buffer, offset, field_path, endian)?;
|
||||
std::str::from_utf8(bytes).map_err(|e| {
|
||||
access_err(
|
||||
@@ -216,7 +216,7 @@ pub fn read_bytes<'a>(
|
||||
offset: usize,
|
||||
field_path: &str,
|
||||
endian: Endian,
|
||||
) -> Result<&'a [u8], TypedefError> {
|
||||
) -> Result<&'a [u8], AlkTypeError> {
|
||||
let len_bytes: [u8; U32_SIZE] = read_array(buffer, offset, field_path)?;
|
||||
let len = u32_from(len_bytes, endian) as usize;
|
||||
let data_start = offset.checked_add(U32_SIZE).ok_or_else(|| {
|
||||
@@ -246,7 +246,7 @@ pub fn write_string(
|
||||
value: &str,
|
||||
field_path: &str,
|
||||
endian: Endian,
|
||||
) -> Result<usize, TypedefError> {
|
||||
) -> Result<usize, AlkTypeError> {
|
||||
write_bytes(buffer, offset, value.as_bytes(), field_path, endian)
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ pub fn write_bytes(
|
||||
value: &[u8],
|
||||
field_path: &str,
|
||||
endian: Endian,
|
||||
) -> Result<usize, TypedefError> {
|
||||
) -> Result<usize, AlkTypeError> {
|
||||
let data_len = value.len();
|
||||
let total = U32_SIZE.checked_add(data_len).ok_or_else(|| {
|
||||
access_err(
|
||||
@@ -297,14 +297,14 @@ pub fn write_bytes(
|
||||
/// `{ data_offset: u32, data_length: u32 }` (endian-aware). The actual UTF-8
|
||||
/// bytes live in `data_region[data_offset..data_offset+data_length]`. Returns
|
||||
/// a `&'a str` borrowing from `data_region`. Invalid UTF-8 produces
|
||||
/// [`TypedefError::Access`].
|
||||
/// [`AlkTypeError::Access`].
|
||||
pub fn read_string_indirect<'a>(
|
||||
buffer: &'a [u8],
|
||||
offset: usize,
|
||||
data_region: &'a [u8],
|
||||
field_path: &str,
|
||||
endian: Endian,
|
||||
) -> Result<&'a str, TypedefError> {
|
||||
) -> Result<&'a str, AlkTypeError> {
|
||||
let bytes = read_bytes_indirect(buffer, offset, data_region, field_path, endian)?;
|
||||
std::str::from_utf8(bytes).map_err(|e| {
|
||||
access_err(
|
||||
@@ -325,7 +325,7 @@ pub fn read_bytes_indirect<'a>(
|
||||
data_region: &'a [u8],
|
||||
field_path: &str,
|
||||
endian: Endian,
|
||||
) -> Result<&'a [u8], TypedefError> {
|
||||
) -> Result<&'a [u8], AlkTypeError> {
|
||||
let struct_end = offset
|
||||
.checked_add(8)
|
||||
.ok_or_else(|| access_err(field_path, format!("offset {offset} + 8 overflows usize")))?;
|
||||
@@ -476,7 +476,7 @@ mod tests {
|
||||
let buf = [0x02u8];
|
||||
let err = read_bool(&buf, 0, "f").unwrap_err();
|
||||
match err {
|
||||
TypedefError::Access { field_path, reason } => {
|
||||
AlkTypeError::Access { field_path, reason } => {
|
||||
assert_eq!(field_path, "f");
|
||||
assert!(reason.contains("0x02"), "reason: {reason}");
|
||||
assert!(reason.contains("offset 0"), "reason: {reason}");
|
||||
@@ -500,7 +500,7 @@ mod tests {
|
||||
let buf = [0u8; 2];
|
||||
let err = read_u32(&buf, 0, "header.id", LE).unwrap_err();
|
||||
match err {
|
||||
TypedefError::Access { field_path, reason } => {
|
||||
AlkTypeError::Access { field_path, reason } => {
|
||||
assert_eq!(field_path, "header.id");
|
||||
assert!(reason.contains("bounds"), "reason: {reason}");
|
||||
}
|
||||
@@ -512,7 +512,7 @@ mod tests {
|
||||
fn write_bounds_failure_returns_access_error() {
|
||||
let mut buf = [0u8; 2];
|
||||
let err = write_u32(&mut buf, 0, 1, "header.id", LE).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }));
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -548,14 +548,14 @@ mod tests {
|
||||
let mut buf = vec![0u8; 16];
|
||||
write_bytes(&mut buf, 0, &[0xFF, 0xFE, 0xFD], "name", LE).unwrap();
|
||||
let err = read_string(&buf, 0, "name", LE).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }));
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_string_bounds_failure_on_prefix() {
|
||||
let buf = [0u8; 2];
|
||||
let err = read_string(&buf, 0, "name", LE).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }));
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -565,14 +565,14 @@ mod tests {
|
||||
let len_bytes = (100u32).to_le_bytes();
|
||||
buf[0..4].copy_from_slice(&len_bytes);
|
||||
let err = read_string(&buf, 0, "name", LE).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }));
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_string_bounds_failure() {
|
||||
let mut buf = vec![0u8; 4];
|
||||
let err = write_string(&mut buf, 0, "hello", "name", LE).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }));
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -600,7 +600,7 @@ mod tests {
|
||||
let buf = [0u8; 4];
|
||||
let data_region = b"anything";
|
||||
let err = read_bytes_indirect(&buf, 0, data_region, "blob", LE).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }));
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -610,7 +610,7 @@ mod tests {
|
||||
write_u32(&mut buf, 4, 10, "idx.len", LE).unwrap();
|
||||
let data_region = b"too short";
|
||||
let err = read_bytes_indirect(&buf, 0, data_region, "blob", LE).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }));
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
274
src/engine.rs
274
src/engine.rs
@@ -1,19 +1,19 @@
|
||||
//! `TypedefEngine` — the compiled form of a schema.
|
||||
//! `AlkTypeEngine` — the compiled form of a schema.
|
||||
//!
|
||||
//! Combines the layout engine (both packed and aligned modes) and the
|
||||
//! jsonschema validator into a single struct. Built once at schema load
|
||||
//! time via [`TypedefEngine::compile`]. Used for repeated read/write/
|
||||
//! time via [`AlkTypeEngine::compile`]. Used for repeated read/write/
|
||||
//! validate operations at access time.
|
||||
//!
|
||||
//! See [validation.md](../../docs/architecture/crates/typedef/validation.md)
|
||||
//! §"The TypedefEngine struct" and
|
||||
//! [overview.md](../../docs/architecture/crates/typedef/overview.md).
|
||||
//! See [validation.md](../../docs/architecture/validation.md)
|
||||
//! §"The AlkTypeEngine struct" and
|
||||
//! [overview.md](../../docs/architecture/overview.md).
|
||||
|
||||
use crate::data_access;
|
||||
use crate::error::TypedefError;
|
||||
use crate::error::AlkTypeError;
|
||||
use crate::layout_builder::LayoutBuilder;
|
||||
use crate::offset_map::OffsetMap;
|
||||
use crate::schema::{self, get_typedef_kind_loose_enum, Endian, TypeDefKind};
|
||||
use crate::schema::{self, get_alktype_kind_loose_enum, Endian, AlkTypeKind};
|
||||
use crate::sequential_reader::{FieldValue, SequentialReader};
|
||||
use crate::validation;
|
||||
use serde_json::Value;
|
||||
@@ -32,7 +32,7 @@ pub enum LayoutMode {
|
||||
///
|
||||
/// Carries the layout-specific handles needed for read/write access in
|
||||
/// the selected mode. The consumer chooses the mode at construction time
|
||||
/// via [`TypedefEngine::compile`]; the engine then exposes only the
|
||||
/// via [`AlkTypeEngine::compile`]; the engine then exposes only the
|
||||
/// APIs that make sense for that mode.
|
||||
#[derive(Debug)]
|
||||
enum Layout {
|
||||
@@ -48,26 +48,26 @@ enum Layout {
|
||||
Aligned { offset_map: OffsetMap },
|
||||
}
|
||||
|
||||
/// The compiled form of a typedef schema. Combines the layout engine
|
||||
/// The compiled form of a alktype schema. Combines the layout engine
|
||||
/// (both packed and aligned modes) and the jsonschema validator.
|
||||
///
|
||||
/// Built once at schema load time via [`TypedefEngine::compile`].
|
||||
/// Built once at schema load time via [`AlkTypeEngine::compile`].
|
||||
/// Used for repeated read/write/validate operations at access time.
|
||||
///
|
||||
/// The consumer selects the layout mode at construction time. The engine
|
||||
/// then exposes mode-appropriate accessors: [`TypedefEngine::offset_map`]
|
||||
/// for aligned mode, [`TypedefEngine::layout_builder`] and
|
||||
/// [`TypedefEngine::sequential_reader`] for packed mode. The
|
||||
/// then exposes mode-appropriate accessors: [`AlkTypeEngine::offset_map`]
|
||||
/// for aligned mode, [`AlkTypeEngine::layout_builder`] and
|
||||
/// [`AlkTypeEngine::sequential_reader`] for packed mode. The
|
||||
/// jsonschema validator is mode-agnostic and always available.
|
||||
pub struct TypedefEngine {
|
||||
pub struct AlkTypeEngine {
|
||||
layout: Layout,
|
||||
validator: jsonschema::Validator,
|
||||
endian: Endian,
|
||||
schema: Value,
|
||||
}
|
||||
|
||||
impl TypedefEngine {
|
||||
/// Compile a schema into a [`TypedefEngine`].
|
||||
impl AlkTypeEngine {
|
||||
/// Compile a schema into a [`AlkTypeEngine`].
|
||||
///
|
||||
/// This is the expensive operation — it parses the schema, normalizes
|
||||
/// `$ref` values, computes the layout, and builds the jsonschema
|
||||
@@ -79,11 +79,11 @@ impl TypedefEngine {
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`TypedefError::Schema`] if the schema is malformed or the
|
||||
/// Returns [`AlkTypeError::Schema`] if the schema is malformed or the
|
||||
/// underlying layout/validator construction fails. The error is
|
||||
/// propagated from [`LayoutBuilder::new`], [`SequentialReader::new`],
|
||||
/// [`OffsetMap::compute`], or [`validation::build_validator`].
|
||||
pub fn compile(schema: &mut Value, mode: LayoutMode) -> Result<Self, TypedefError> {
|
||||
pub fn compile(schema: &mut Value, mode: LayoutMode) -> Result<Self, AlkTypeError> {
|
||||
schema::normalize_refs(schema);
|
||||
let endian = Endian::from_schema(schema);
|
||||
let layout = match mode {
|
||||
@@ -152,12 +152,12 @@ impl TypedefEngine {
|
||||
/// Validate a JSON value against the schema. The jsonschema validator
|
||||
/// is already compiled — this is a fast check.
|
||||
///
|
||||
/// Returns `Ok(())` if valid, `Err(TypedefError::Validation(...))` if
|
||||
/// Returns `Ok(())` if valid, `Err(AlkTypeError::Validation(...))` if
|
||||
/// invalid.
|
||||
pub fn validate_json(&self, instance: &Value) -> Result<(), TypedefError> {
|
||||
pub fn validate_json(&self, instance: &Value) -> Result<(), AlkTypeError> {
|
||||
self.validator
|
||||
.validate(instance)
|
||||
.map_err(|e| TypedefError::Validation(e.to_owned()))
|
||||
.map_err(|e| AlkTypeError::Validation(e.to_owned()))
|
||||
}
|
||||
|
||||
/// Check if a JSON value is valid against the schema.
|
||||
@@ -173,25 +173,25 @@ impl TypedefEngine {
|
||||
/// `Bytes`/`Timestamp` fields.
|
||||
///
|
||||
/// Returns an error if compiled in packed mode — use
|
||||
/// [`TypedefEngine::sequential_reader`] for packed mode. Also
|
||||
/// [`AlkTypeEngine::sequential_reader`] for packed mode. Also
|
||||
/// returns an error for composite kinds (`Struct`, `Union`, `Array`,
|
||||
/// `Record`) — those are better handled via the layout-specific APIs.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`TypedefError::Access`] if compiled in packed mode.
|
||||
/// - [`TypedefError::Offset`] if `field_path` is not in the offset map.
|
||||
/// - [`TypedefError::Access`] for buffer-too-short or invalid data,
|
||||
/// - [`AlkTypeError::Access`] if compiled in packed mode.
|
||||
/// - [`AlkTypeError::Offset`] if `field_path` is not in the offset map.
|
||||
/// - [`AlkTypeError::Access`] for buffer-too-short or invalid data,
|
||||
/// propagated from [`crate::data_access`].
|
||||
pub fn read_field<'a>(
|
||||
&self,
|
||||
buffer: &'a [u8],
|
||||
field_path: &str,
|
||||
) -> Result<FieldValue<'a>, TypedefError> {
|
||||
) -> Result<FieldValue<'a>, AlkTypeError> {
|
||||
let offset_map = match &self.layout {
|
||||
Layout::Aligned { offset_map } => offset_map,
|
||||
Layout::Packed { .. } => {
|
||||
return Err(TypedefError::Access {
|
||||
return Err(AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: "read_field is only available in aligned mode; \
|
||||
use sequential_reader() for packed mode"
|
||||
@@ -201,87 +201,87 @@ impl TypedefEngine {
|
||||
};
|
||||
let range = offset_map
|
||||
.get(field_path)
|
||||
.ok_or_else(|| TypedefError::Offset {
|
||||
.ok_or_else(|| AlkTypeError::Offset {
|
||||
field_path: field_path.to_string(),
|
||||
reason: "field not found in offset map".to_string(),
|
||||
})?;
|
||||
let field_schema =
|
||||
lookup_field_schema(&self.schema, field_path).ok_or_else(|| TypedefError::Offset {
|
||||
lookup_field_schema(&self.schema, field_path).ok_or_else(|| AlkTypeError::Offset {
|
||||
field_path: field_path.to_string(),
|
||||
reason: "field schema not found in schema tree".to_string(),
|
||||
})?;
|
||||
let kind = get_typedef_kind_loose_enum(field_schema).ok_or_else(|| TypedefError::Offset {
|
||||
let kind = get_alktype_kind_loose_enum(field_schema).ok_or_else(|| AlkTypeError::Offset {
|
||||
field_path: field_path.to_string(),
|
||||
reason: "field schema has no TypeDef:* kind".to_string(),
|
||||
reason: "field schema has no AlkType:* kind".to_string(),
|
||||
})?;
|
||||
let endian = self.endian;
|
||||
match kind {
|
||||
TypeDefKind::Int8 => {
|
||||
AlkTypeKind::Int8 => {
|
||||
let v = data_access::read_i8(buffer, range.start, field_path)?;
|
||||
Ok(FieldValue::I8(v))
|
||||
}
|
||||
TypeDefKind::Int16 => {
|
||||
AlkTypeKind::Int16 => {
|
||||
let v = data_access::read_i16(buffer, range.start, field_path, endian)?;
|
||||
Ok(FieldValue::I16(v))
|
||||
}
|
||||
TypeDefKind::Int32 => {
|
||||
AlkTypeKind::Int32 => {
|
||||
let v = data_access::read_i32(buffer, range.start, field_path, endian)?;
|
||||
Ok(FieldValue::I32(v))
|
||||
}
|
||||
TypeDefKind::Int64 => {
|
||||
AlkTypeKind::Int64 => {
|
||||
let v = data_access::read_i64(buffer, range.start, field_path, endian)?;
|
||||
Ok(FieldValue::I64(v))
|
||||
}
|
||||
TypeDefKind::Uint8 => {
|
||||
AlkTypeKind::Uint8 => {
|
||||
let v = data_access::read_u8(buffer, range.start, field_path)?;
|
||||
Ok(FieldValue::U8(v))
|
||||
}
|
||||
TypeDefKind::Uint16 => {
|
||||
AlkTypeKind::Uint16 => {
|
||||
let v = data_access::read_u16(buffer, range.start, field_path, endian)?;
|
||||
Ok(FieldValue::U16(v))
|
||||
}
|
||||
TypeDefKind::Uint32 => {
|
||||
AlkTypeKind::Uint32 => {
|
||||
let v = data_access::read_u32(buffer, range.start, field_path, endian)?;
|
||||
Ok(FieldValue::U32(v))
|
||||
}
|
||||
TypeDefKind::Uint64 => {
|
||||
AlkTypeKind::Uint64 => {
|
||||
let v = data_access::read_u64(buffer, range.start, field_path, endian)?;
|
||||
Ok(FieldValue::U64(v))
|
||||
}
|
||||
TypeDefKind::Float32 => {
|
||||
AlkTypeKind::Float32 => {
|
||||
let v = data_access::read_f32(buffer, range.start, field_path, endian)?;
|
||||
Ok(FieldValue::F32(v))
|
||||
}
|
||||
TypeDefKind::Float64 => {
|
||||
AlkTypeKind::Float64 => {
|
||||
let v = data_access::read_f64(buffer, range.start, field_path, endian)?;
|
||||
Ok(FieldValue::F64(v))
|
||||
}
|
||||
TypeDefKind::Boolean => {
|
||||
AlkTypeKind::Boolean => {
|
||||
let v = data_access::read_bool(buffer, range.start, field_path)?;
|
||||
Ok(FieldValue::Bool(v))
|
||||
}
|
||||
TypeDefKind::Enum => {
|
||||
AlkTypeKind::Enum => {
|
||||
let v = data_access::read_enum(buffer, range.start, field_path, endian)?;
|
||||
Ok(FieldValue::Enum(v))
|
||||
}
|
||||
TypeDefKind::String => {
|
||||
AlkTypeKind::String => {
|
||||
let v = data_access::read_string(buffer, range.start, field_path, endian)?;
|
||||
Ok(FieldValue::String(v))
|
||||
}
|
||||
TypeDefKind::Bytes => {
|
||||
AlkTypeKind::Bytes => {
|
||||
let v = data_access::read_bytes(buffer, range.start, field_path, endian)?;
|
||||
Ok(FieldValue::Bytes(v))
|
||||
}
|
||||
TypeDefKind::Timestamp => {
|
||||
AlkTypeKind::Timestamp => {
|
||||
let v = data_access::read_string(buffer, range.start, field_path, endian)?;
|
||||
Ok(FieldValue::String(v))
|
||||
}
|
||||
TypeDefKind::Struct => Ok(FieldValue::Struct {
|
||||
AlkTypeKind::Struct => Ok(FieldValue::Struct {
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
}),
|
||||
TypeDefKind::Union | TypeDefKind::Array | TypeDefKind::Record => {
|
||||
Err(TypedefError::Access {
|
||||
AlkTypeKind::Union | AlkTypeKind::Array | AlkTypeKind::Record => {
|
||||
Err(AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: "read_field does not support composite types; \
|
||||
use the layout-specific APIs"
|
||||
@@ -299,25 +299,25 @@ impl TypedefEngine {
|
||||
/// `Bytes`/`Timestamp` fields.
|
||||
///
|
||||
/// Returns an error if compiled in packed mode — use
|
||||
/// [`TypedefEngine::layout_builder`] for packed mode. Also returns an
|
||||
/// [`AlkTypeEngine::layout_builder`] for packed mode. Also returns an
|
||||
/// error for composite kinds (`Struct`, `Union`, `Array`, `Record`).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`TypedefError::Access`] if compiled in packed mode.
|
||||
/// - [`TypedefError::Offset`] if `field_path` is not in the offset map.
|
||||
/// - [`TypedefError::Access`] for buffer-too-short or invalid data,
|
||||
/// - [`AlkTypeError::Access`] if compiled in packed mode.
|
||||
/// - [`AlkTypeError::Offset`] if `field_path` is not in the offset map.
|
||||
/// - [`AlkTypeError::Access`] for buffer-too-short or invalid data,
|
||||
/// propagated from [`crate::data_access`].
|
||||
pub fn write_field(
|
||||
&self,
|
||||
buffer: &mut [u8],
|
||||
field_path: &str,
|
||||
value: &FieldValue<'_>,
|
||||
) -> Result<(), TypedefError> {
|
||||
) -> Result<(), AlkTypeError> {
|
||||
let offset_map = match &self.layout {
|
||||
Layout::Aligned { offset_map } => offset_map,
|
||||
Layout::Packed { .. } => {
|
||||
return Err(TypedefError::Access {
|
||||
return Err(AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: "write_field is only available in aligned mode; \
|
||||
use layout_builder() for packed mode"
|
||||
@@ -327,7 +327,7 @@ impl TypedefEngine {
|
||||
};
|
||||
let range = offset_map
|
||||
.get(field_path)
|
||||
.ok_or_else(|| TypedefError::Offset {
|
||||
.ok_or_else(|| AlkTypeError::Offset {
|
||||
field_path: field_path.to_string(),
|
||||
reason: "field not found in offset map".to_string(),
|
||||
})?;
|
||||
@@ -372,7 +372,7 @@ impl TypedefEngine {
|
||||
Ok(())
|
||||
}
|
||||
FieldValue::Struct { .. } | FieldValue::Union { .. } | FieldValue::Array { .. } => {
|
||||
Err(TypedefError::Access {
|
||||
Err(AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: "write_field does not support composite types; \
|
||||
use the layout-specific APIs"
|
||||
@@ -383,9 +383,9 @@ impl TypedefEngine {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for TypedefEngine {
|
||||
impl fmt::Debug for AlkTypeEngine {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("TypedefEngine")
|
||||
f.debug_struct("AlkTypeEngine")
|
||||
.field("layout", &self.layout)
|
||||
.field("validator", &"<jsonschema::Validator>")
|
||||
.field("endian", &self.endian)
|
||||
@@ -416,13 +416,13 @@ mod tests {
|
||||
|
||||
fn fixed_struct_schema() -> Value {
|
||||
json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"endian": "little",
|
||||
"properties": {
|
||||
"flag": { "TypeDef:Uint8": true },
|
||||
"id": { "TypeDef:Uint32": true },
|
||||
"score": { "TypeDef:Float32": true },
|
||||
"tag": { "TypeDef:String": true }
|
||||
"flag": { "AlkType:Uint8": true },
|
||||
"id": { "AlkType:Uint32": true },
|
||||
"score": { "AlkType:Float32": true },
|
||||
"tag": { "AlkType:String": true }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -430,7 +430,7 @@ mod tests {
|
||||
#[test]
|
||||
fn compile_aligned_builds_offset_map() {
|
||||
let mut schema = fixed_struct_schema();
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
assert_eq!(engine.mode(), LayoutMode::Aligned);
|
||||
assert!(engine.offset_map().is_some());
|
||||
assert!(engine.layout_builder().is_none());
|
||||
@@ -440,7 +440,7 @@ mod tests {
|
||||
#[test]
|
||||
fn compile_packed_builds_builder_and_reader() {
|
||||
let mut schema = fixed_struct_schema();
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
|
||||
assert_eq!(engine.mode(), LayoutMode::Packed);
|
||||
assert!(engine.layout_builder().is_some());
|
||||
assert!(engine.sequential_reader().is_some());
|
||||
@@ -450,18 +450,18 @@ mod tests {
|
||||
#[test]
|
||||
fn compile_normalizes_refs() {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"child": { "$ref": "Child" }
|
||||
},
|
||||
"$defs": {
|
||||
"Child": {
|
||||
"TypeDef:Struct": true,
|
||||
"properties": { "x": { "TypeDef:Uint8": true } }
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "x": { "AlkType:Uint8": true } }
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
|
||||
assert_eq!(
|
||||
engine.schema["properties"]["child"]["$ref"],
|
||||
json!("#/$defs/Child")
|
||||
@@ -471,64 +471,64 @@ mod tests {
|
||||
#[test]
|
||||
fn endian_parsed_from_schema() {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"endian": "big",
|
||||
"properties": { "id": { "TypeDef:Uint32": true } }
|
||||
"properties": { "id": { "AlkType:Uint32": true } }
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
|
||||
assert_eq!(engine.endian(), Endian::Big);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endian_defaults_to_little() {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"properties": { "id": { "TypeDef:Uint32": true } }
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "id": { "AlkType:Uint32": true } }
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
|
||||
assert_eq!(engine.endian(), Endian::Little);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_json_accepts_valid_instance() {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "TypeDef:Uint32": true, "type": "integer" }
|
||||
"id": { "AlkType:Uint32": true, "type": "integer" }
|
||||
},
|
||||
"required": ["id"]
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
assert!(engine.validate_json(&json!({"id": 42})).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_json_rejects_invalid_instance() {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "TypeDef:Uint32": true, "type": "integer" }
|
||||
"id": { "AlkType:Uint32": true, "type": "integer" }
|
||||
},
|
||||
"required": ["id"]
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let err = engine.validate_json(&json!({"id": -1})).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Validation(_)), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_valid_json_returns_bool() {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "TypeDef:Uint32": true, "type": "integer" }
|
||||
"id": { "AlkType:Uint32": true, "type": "integer" }
|
||||
},
|
||||
"required": ["id"]
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
assert!(engine.is_valid_json(&json!({"id": 42})));
|
||||
assert!(!engine.is_valid_json(&json!({"id": -1})));
|
||||
}
|
||||
@@ -536,14 +536,14 @@ mod tests {
|
||||
#[test]
|
||||
fn read_field_aligned_reads_fixed_fields() {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"endian": "little",
|
||||
"properties": {
|
||||
"flag": { "TypeDef:Uint8": true },
|
||||
"id": { "TypeDef:Uint32": true }
|
||||
"flag": { "AlkType:Uint8": true },
|
||||
"id": { "AlkType:Uint32": true }
|
||||
}
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[0] = 0xAB;
|
||||
buf[4..8].copy_from_slice(&0x01020304u32.to_le_bytes());
|
||||
@@ -560,12 +560,12 @@ mod tests {
|
||||
#[test]
|
||||
fn read_field_aligned_reads_string_length_prefixed() {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"name": { "TypeDef:String": true }
|
||||
"name": { "AlkType:String": true }
|
||||
}
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let mut buf = vec![0u8; 32];
|
||||
let len_bytes = 5u32.to_le_bytes();
|
||||
buf[0..4].copy_from_slice(&len_bytes);
|
||||
@@ -579,55 +579,55 @@ mod tests {
|
||||
#[test]
|
||||
fn read_field_returns_access_error_in_packed_mode() {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"properties": { "id": { "TypeDef:Uint32": true } }
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "id": { "AlkType:Uint32": true } }
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
|
||||
let buf = [0u8; 4];
|
||||
let err = engine.read_field(&buf, "id").unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_field_returns_offset_error_for_missing_field() {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"properties": { "id": { "TypeDef:Uint32": true } }
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "id": { "AlkType:Uint32": true } }
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let buf = [0u8; 4];
|
||||
let err = engine.read_field(&buf, "missing").unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Offset { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_field_returns_error_for_composite_types() {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"vals": {
|
||||
"TypeDef:Array": true,
|
||||
"items": { "TypeDef:Uint32": true }
|
||||
"AlkType:Array": true,
|
||||
"items": { "AlkType:Uint32": true }
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let buf = [0u8; 8];
|
||||
let err = engine.read_field(&buf, "vals").unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_field_aligned_writes_fixed_fields() {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"endian": "little",
|
||||
"properties": {
|
||||
"flag": { "TypeDef:Uint8": true },
|
||||
"id": { "TypeDef:Uint32": true }
|
||||
"flag": { "AlkType:Uint8": true },
|
||||
"id": { "AlkType:Uint32": true }
|
||||
}
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let mut buf = vec![0u8; 8];
|
||||
engine
|
||||
.write_field(&mut buf, "flag", &FieldValue::U8(0xAB))
|
||||
@@ -650,12 +650,12 @@ mod tests {
|
||||
#[test]
|
||||
fn write_field_round_trips_string() {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"name": { "TypeDef:String": true }
|
||||
"name": { "AlkType:String": true }
|
||||
}
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let mut buf = vec![0u8; 32];
|
||||
engine
|
||||
.write_field(&mut buf, "name", &FieldValue::String("hello"))
|
||||
@@ -669,86 +669,86 @@ mod tests {
|
||||
#[test]
|
||||
fn write_field_returns_access_error_in_packed_mode() {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"properties": { "id": { "TypeDef:Uint32": true } }
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "id": { "AlkType:Uint32": true } }
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).expect("compile");
|
||||
let mut buf = [0u8; 4];
|
||||
let err = engine
|
||||
.write_field(&mut buf, "id", &FieldValue::U32(1))
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_field_returns_offset_error_for_missing_field() {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"properties": { "id": { "TypeDef:Uint32": true } }
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "id": { "AlkType:Uint32": true } }
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let mut buf = [0u8; 4];
|
||||
let err = engine
|
||||
.write_field(&mut buf, "missing", &FieldValue::U32(1))
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Offset { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_field_returns_error_for_composite_value() {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"properties": { "id": { "TypeDef:Uint32": true } }
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "id": { "AlkType:Uint32": true } }
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let mut buf = [0u8; 8];
|
||||
let err = engine
|
||||
.write_field(&mut buf, "id", &FieldValue::Struct { start: 0, end: 4 })
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compile_returns_schema_error_for_invalid_top_level() {
|
||||
let mut schema = json!({ "type": "object", "properties": {} });
|
||||
let err = TypedefEngine::compile(&mut schema, LayoutMode::Packed).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}");
|
||||
let err = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_formats_without_panicking() {
|
||||
let mut schema = fixed_struct_schema();
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).expect("compile");
|
||||
let s = format!("{engine:?}");
|
||||
assert!(s.contains("TypedefEngine"));
|
||||
assert!(s.contains("AlkTypeEngine"));
|
||||
assert!(s.contains("Aligned"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_field_schema_walks_dotted_path() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"header": {
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"version": { "TypeDef:Uint8": true }
|
||||
"version": { "AlkType:Uint8": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let node = lookup_field_schema(&schema, "header.version").expect("found");
|
||||
assert_eq!(node, &json!({ "TypeDef:Uint8": true }));
|
||||
assert_eq!(node, &json!({ "AlkType:Uint8": true }));
|
||||
assert!(lookup_field_schema(&schema, "header.missing").is_none());
|
||||
assert!(lookup_field_schema(&schema, "missing").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typedef_kind_loose_recognizes_object_form() {
|
||||
let node = json!({ "TypeDef:String": { "encoding": "offset-indirect" } });
|
||||
fn alktype_kind_loose_recognizes_object_form() {
|
||||
let node = json!({ "AlkType:String": { "encoding": "offset-indirect" } });
|
||||
assert_eq!(
|
||||
get_typedef_kind_loose_enum(&node),
|
||||
Some(TypeDefKind::String)
|
||||
get_alktype_kind_loose_enum(&node),
|
||||
Some(AlkTypeKind::String)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
24
src/error.rs
24
src/error.rs
@@ -1,16 +1,16 @@
|
||||
//! Error types for the typedef engine.
|
||||
//! Error types for the alktype engine.
|
||||
//!
|
||||
//! Decided in ADR-098: a single `TypedefError` enum covers all error
|
||||
//! Decided in ADR-098: a single `AlkTypeError` enum covers all error
|
||||
//! conditions across the engine's three phases (schema parsing, offset
|
||||
//! computation, read/write) plus validation.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// Errors produced by the typedef engine across all phases.
|
||||
/// Errors produced by the alktype engine across all phases.
|
||||
#[derive(Debug)]
|
||||
pub enum TypedefError {
|
||||
pub enum AlkTypeError {
|
||||
/// Schema parsing errors — invalid JSON, missing required keywords,
|
||||
/// unknown `TypeDef:*` kinds, malformed annotations.
|
||||
/// unknown `AlkType:*` kinds, malformed annotations.
|
||||
Schema(String),
|
||||
|
||||
/// Offset computation errors — field not found, type not supported
|
||||
@@ -23,23 +23,23 @@ pub enum TypedefError {
|
||||
|
||||
/// Validation errors — delegated to the `jsonschema` crate.
|
||||
/// The `'static` lifetime is correct: the validator owns its schema
|
||||
/// reference and lives for the lifetime of the `TypedefEngine`.
|
||||
/// reference and lives for the lifetime of the `AlkTypeEngine`.
|
||||
Validation(jsonschema::ValidationError<'static>),
|
||||
}
|
||||
|
||||
impl fmt::Display for TypedefError {
|
||||
impl fmt::Display for AlkTypeError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
TypedefError::Schema(msg) => write!(f, "schema error: {msg}"),
|
||||
TypedefError::Offset { field_path, reason } => {
|
||||
AlkTypeError::Schema(msg) => write!(f, "schema error: {msg}"),
|
||||
AlkTypeError::Offset { field_path, reason } => {
|
||||
write!(f, "offset error at {field_path}: {reason}")
|
||||
}
|
||||
TypedefError::Access { field_path, reason } => {
|
||||
AlkTypeError::Access { field_path, reason } => {
|
||||
write!(f, "access error at {field_path}: {reason}")
|
||||
}
|
||||
TypedefError::Validation(inner) => write!(f, "validation error: {inner}"),
|
||||
AlkTypeError::Validation(inner) => write!(f, "validation error: {inner}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TypedefError {}
|
||||
impl std::error::Error for AlkTypeError {}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
16
src/lib.rs
16
src/lib.rs
@@ -1,13 +1,13 @@
|
||||
//! alktype: The binary struct engine.
|
||||
//!
|
||||
//! Takes a JSON Schema with `TypeDef:*` custom keywords and produces
|
||||
//! Takes a JSON Schema with `AlkType:*` custom keywords and produces
|
||||
//! an offset map, read/write functions, and validation — all driven
|
||||
//! by the schema. The schema is the format definition; the engine is
|
||||
//! generic.
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! - **Schema layer** ([`schema`]): TypeDef kind detection, annotation
|
||||
//! - **Schema layer** ([`schema`]): AlkType kind detection, annotation
|
||||
//! parsing, `$ref` normalization, endianness.
|
||||
//! - **Layout engine** ([`offset_map`], [`layout_builder`],
|
||||
//! [`sequential_reader`]): Two layout modes — aligned static for
|
||||
@@ -17,8 +17,8 @@
|
||||
//! - **TUnion dispatch** ([`tunion`]): Byte-offset and field-name
|
||||
//! discriminator dispatch.
|
||||
//! - **Validation** ([`validation`]): Custom keyword validators for all
|
||||
//! 17 `TypeDef:*` kinds, delegated to the `jsonschema` crate.
|
||||
//! - **Engine** ([`engine`]): `TypedefEngine` — the compiled form of a
|
||||
//! 17 `AlkType:*` kinds, delegated to the `jsonschema` crate.
|
||||
//! - **Engine** ([`engine`]): `AlkTypeEngine` — the compiled form of a
|
||||
//! schema, combining layout and validation.
|
||||
|
||||
#[macro_use]
|
||||
@@ -33,14 +33,14 @@ pub mod sequential_reader;
|
||||
pub mod tunion;
|
||||
pub mod validation;
|
||||
|
||||
pub use engine::{LayoutMode, TypedefEngine};
|
||||
pub use error::TypedefError;
|
||||
pub use engine::{LayoutMode, AlkTypeEngine};
|
||||
pub use error::AlkTypeError;
|
||||
pub use layout_builder::{FieldPosition, LayoutBuilder, PackedLayout};
|
||||
pub use offset_map::{ByteRange, OffsetMap};
|
||||
pub use schema::{
|
||||
get_typedef_kind_loose, get_typedef_kind_loose_enum, normalize_refs, parse_align,
|
||||
get_alktype_kind_loose, get_alktype_kind_loose_enum, normalize_refs, parse_align,
|
||||
parse_discriminator, parse_encoding, parse_endian, parse_max_length, resolve_ref,
|
||||
resolve_ref_or_inline, DiscriminatorKind, Endian, TypeDefKind, VariableEncoding,
|
||||
resolve_ref_or_inline, DiscriminatorKind, Endian, AlkTypeKind, VariableEncoding,
|
||||
};
|
||||
pub use sequential_reader::{FieldValue, SequentialReader};
|
||||
pub use tunion::UnionDispatch;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Macros for generating repetitive code across the 17 TypeDef kinds.
|
||||
//! Macros for generating repetitive code across the 17 AlkType kinds.
|
||||
//!
|
||||
//! These macros eliminate boilerplate in validation, data access, and
|
||||
//! dispatch. Each macro takes a compact specification and generates the
|
||||
@@ -187,7 +187,7 @@ macro_rules! define_read_write_endian {
|
||||
offset: usize,
|
||||
field_path: &str,
|
||||
endian: $crate::Endian,
|
||||
) -> Result<$rust_ty, $crate::TypedefError> {
|
||||
) -> Result<$rust_ty, $crate::AlkTypeError> {
|
||||
let bytes: [u8; $size] = $crate::data_access::read_array(buffer, offset, field_path)?;
|
||||
Ok(match endian {
|
||||
$crate::Endian::Little => <$rust_ty>::from_le_bytes(bytes),
|
||||
@@ -206,7 +206,7 @@ macro_rules! define_read_write_endian {
|
||||
value: $rust_ty,
|
||||
field_path: &str,
|
||||
endian: $crate::Endian,
|
||||
) -> Result<(), $crate::TypedefError> {
|
||||
) -> Result<(), $crate::AlkTypeError> {
|
||||
let bytes = match endian {
|
||||
$crate::Endian::Little => value.to_le_bytes(),
|
||||
$crate::Endian::Big => value.to_be_bytes(),
|
||||
@@ -229,7 +229,7 @@ macro_rules! define_read_write_ne {
|
||||
buffer: &[u8],
|
||||
offset: usize,
|
||||
field_path: &str,
|
||||
) -> Result<$rust_ty, $crate::TypedefError> {
|
||||
) -> Result<$rust_ty, $crate::AlkTypeError> {
|
||||
let bytes: [u8; $size] = $crate::data_access::read_array(buffer, offset, field_path)?;
|
||||
Ok($read_expr(bytes))
|
||||
}
|
||||
@@ -244,7 +244,7 @@ macro_rules! define_read_write_ne {
|
||||
offset: usize,
|
||||
value: $rust_ty,
|
||||
field_path: &str,
|
||||
) -> Result<(), $crate::TypedefError> {
|
||||
) -> Result<(), $crate::AlkTypeError> {
|
||||
$crate::data_access::write_array(buffer, offset, value.to_ne_bytes(), field_path)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
//! to satisfy the field's alignment requirement (natural alignment by
|
||||
//! default, overridable via the `"align"` annotation).
|
||||
|
||||
use crate::error::TypedefError;
|
||||
use crate::error::AlkTypeError;
|
||||
use crate::schema::{
|
||||
get_typedef_kind, get_typedef_kind_loose_enum, parse_align, parse_encoding,
|
||||
parse_max_length, resolve_ref_or_inline, TypeDefKind, VariableEncoding,
|
||||
get_alktype_kind, get_alktype_kind_loose_enum, parse_align, parse_encoding,
|
||||
parse_max_length, resolve_ref_or_inline, AlkTypeKind, VariableEncoding,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -67,25 +67,25 @@ impl OffsetMap {
|
||||
///
|
||||
/// Walks the schema recursively, computing byte positions for each
|
||||
/// field based on type sizes, field order, and alignment. The
|
||||
/// top-level schema must be a `TypeDef:Struct`.
|
||||
/// top-level schema must be a `AlkType:Struct`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`TypedefError::Schema`] if the top-level schema is not a
|
||||
/// `TypeDef:Struct` or has no `TypeDef:*` kind, or if the schema is
|
||||
/// Returns [`AlkTypeError::Schema`] if the top-level schema is not a
|
||||
/// `AlkType:Struct` or has no `AlkType:*` kind, or if the schema is
|
||||
/// malformed (missing `properties`, unknown kind, etc.).
|
||||
///
|
||||
/// Returns [`TypedefError::Offset`] for unsupported type combinations
|
||||
/// Returns [`AlkTypeError::Offset`] for unsupported type combinations
|
||||
/// encountered during the walk.
|
||||
pub fn compute(schema: &Value) -> Result<Self, TypedefError> {
|
||||
let kind = get_typedef_kind(schema)
|
||||
.and_then(|s| s.parse::<TypeDefKind>().ok())
|
||||
pub fn compute(schema: &Value) -> Result<Self, AlkTypeError> {
|
||||
let kind = get_alktype_kind(schema)
|
||||
.and_then(|s| s.parse::<AlkTypeKind>().ok())
|
||||
.ok_or_else(|| {
|
||||
TypedefError::Schema("top-level schema has no TypeDef:* kind".to_string())
|
||||
AlkTypeError::Schema("top-level schema has no AlkType:* kind".to_string())
|
||||
})?;
|
||||
if kind != TypeDefKind::Struct {
|
||||
return Err(TypedefError::Schema(format!(
|
||||
"OffsetMap::compute requires a TypeDef:Struct at the top level, got {kind}"
|
||||
if kind != AlkTypeKind::Struct {
|
||||
return Err(AlkTypeError::Schema(format!(
|
||||
"OffsetMap::compute requires a AlkType:Struct at the top level, got {kind}"
|
||||
)));
|
||||
}
|
||||
let mut ctx = ComputeCtx {
|
||||
@@ -145,7 +145,7 @@ struct FieldLayout {
|
||||
}
|
||||
|
||||
impl<'a> ComputeCtx<'a> {
|
||||
/// Recurse into a `TypeDef:Struct`, appending `(field_path, ByteRange)`
|
||||
/// Recurse into a `AlkType:Struct`, appending `(field_path, ByteRange)`
|
||||
/// pairs to `self.fields` and advancing `self.offset`.
|
||||
///
|
||||
/// Returns `(total_size, alignment)` where `total_size` includes
|
||||
@@ -163,15 +163,15 @@ impl<'a> ComputeCtx<'a> {
|
||||
struct_schema: &Value,
|
||||
prefix: &str,
|
||||
parent_struct_align: usize,
|
||||
) -> Result<(usize, usize), TypedefError> {
|
||||
) -> Result<(usize, usize), AlkTypeError> {
|
||||
let obj = struct_schema
|
||||
.as_object()
|
||||
.ok_or_else(|| TypedefError::Schema("struct schema is not an object".to_string()))?;
|
||||
.ok_or_else(|| AlkTypeError::Schema("struct schema is not an object".to_string()))?;
|
||||
let properties = obj
|
||||
.get("properties")
|
||||
.and_then(|v| v.as_object())
|
||||
.ok_or_else(|| {
|
||||
TypedefError::Schema("struct schema has no 'properties' object".to_string())
|
||||
AlkTypeError::Schema("struct schema has no 'properties' object".to_string())
|
||||
})?;
|
||||
|
||||
let struct_default_align = parse_align(struct_schema).unwrap_or(parent_struct_align);
|
||||
@@ -194,13 +194,13 @@ impl<'a> ComputeCtx<'a> {
|
||||
// data_access::write_string writes prefix+data inline — clobbering
|
||||
// subsequent fields. Only allowed as the last field in the struct.
|
||||
if i < field_count - 1 {
|
||||
if let Some(kind) = get_typedef_kind_loose_enum(field_schema) {
|
||||
if let Some(kind) = get_alktype_kind_loose_enum(field_schema) {
|
||||
if kind.is_variable_length() {
|
||||
let keyword_value = field_schema
|
||||
.as_object()
|
||||
.and_then(|o| {
|
||||
o.keys()
|
||||
.find(|k| k.starts_with("TypeDef:"))
|
||||
.find(|k| k.starts_with("AlkType:"))
|
||||
.and_then(|k| o.get(k))
|
||||
})
|
||||
.cloned()
|
||||
@@ -210,7 +210,7 @@ impl<'a> ComputeCtx<'a> {
|
||||
let is_inline_length_prefixed =
|
||||
encoding == VariableEncoding::LengthPrefixed && max_length.is_none();
|
||||
if is_inline_length_prefixed {
|
||||
return Err(TypedefError::Offset {
|
||||
return Err(AlkTypeError::Offset {
|
||||
field_path: field_path.clone(),
|
||||
reason: format!(
|
||||
"non-final inline length-prefixed variable field \
|
||||
@@ -244,17 +244,17 @@ impl<'a> ComputeCtx<'a> {
|
||||
field_schema: &Value,
|
||||
field_path: &str,
|
||||
struct_default_align: usize,
|
||||
) -> Result<FieldLayout, TypedefError> {
|
||||
let kind = get_typedef_kind_loose_enum(field_schema).ok_or_else(|| TypedefError::Offset {
|
||||
) -> Result<FieldLayout, AlkTypeError> {
|
||||
let kind = get_alktype_kind_loose_enum(field_schema).ok_or_else(|| AlkTypeError::Offset {
|
||||
field_path: field_path.to_string(),
|
||||
reason: "field schema has no TypeDef:* kind".to_string(),
|
||||
reason: "field schema has no AlkType:* kind".to_string(),
|
||||
})?;
|
||||
|
||||
match kind {
|
||||
TypeDefKind::Struct => {
|
||||
AlkTypeKind::Struct => {
|
||||
self.compute_struct_field(field_schema, field_path, struct_default_align)
|
||||
}
|
||||
TypeDefKind::Union => Err(TypedefError::Offset {
|
||||
AlkTypeKind::Union => Err(AlkTypeError::Offset {
|
||||
field_path: field_path.to_string(),
|
||||
reason: "TUnion is not supported in aligned static mode (ADR-102). \
|
||||
Unions are the protocol dispatch pattern — use packed sequential \
|
||||
@@ -262,31 +262,31 @@ impl<'a> ComputeCtx<'a> {
|
||||
a struct with an explicit discriminator field."
|
||||
.to_string(),
|
||||
}),
|
||||
TypeDefKind::Array => {
|
||||
AlkTypeKind::Array => {
|
||||
self.compute_array_field(field_schema, field_path, struct_default_align)
|
||||
}
|
||||
TypeDefKind::String
|
||||
| TypeDefKind::Bytes
|
||||
| TypeDefKind::Record
|
||||
| TypeDefKind::Timestamp => {
|
||||
AlkTypeKind::String
|
||||
| AlkTypeKind::Bytes
|
||||
| AlkTypeKind::Record
|
||||
| AlkTypeKind::Timestamp => {
|
||||
self.compute_variable_field(field_schema, field_path, struct_default_align)
|
||||
}
|
||||
k if k.is_fixed_size() => {
|
||||
self.compute_fixed_field(k, field_schema, field_path, struct_default_align)
|
||||
}
|
||||
_ => unreachable!("all TypeDefKind variants are covered above"),
|
||||
_ => unreachable!("all AlkTypeKind variants are covered above"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the layout for a fixed-size primitive field.
|
||||
fn compute_fixed_field(
|
||||
&mut self,
|
||||
kind: TypeDefKind,
|
||||
kind: AlkTypeKind,
|
||||
field_schema: &Value,
|
||||
field_path: &str,
|
||||
struct_default_align: usize,
|
||||
) -> Result<FieldLayout, TypedefError> {
|
||||
let size = kind.type_size().ok_or_else(|| TypedefError::Offset {
|
||||
) -> Result<FieldLayout, AlkTypeError> {
|
||||
let size = kind.type_size().ok_or_else(|| AlkTypeError::Offset {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("type_size returned None for fixed kind {kind}"),
|
||||
})?;
|
||||
@@ -299,7 +299,7 @@ impl<'a> ComputeCtx<'a> {
|
||||
Ok(FieldLayout { align })
|
||||
}
|
||||
|
||||
/// Compute the layout for a nested `TypeDef:Struct` field.
|
||||
/// Compute the layout for a nested `AlkType:Struct` field.
|
||||
///
|
||||
/// Probes the nested struct's layout at a temporary offset of 0 to
|
||||
/// determine its total size and alignment, aligns the parent offset,
|
||||
@@ -309,7 +309,7 @@ impl<'a> ComputeCtx<'a> {
|
||||
field_schema: &Value,
|
||||
field_path: &str,
|
||||
struct_default_align: usize,
|
||||
) -> Result<FieldLayout, TypedefError> {
|
||||
) -> Result<FieldLayout, AlkTypeError> {
|
||||
let inner_parent_align = parse_align(field_schema).unwrap_or(struct_default_align);
|
||||
let mut probe = ComputeCtx {
|
||||
root: self.root,
|
||||
@@ -335,37 +335,37 @@ impl<'a> ComputeCtx<'a> {
|
||||
Ok(FieldLayout { align })
|
||||
}
|
||||
|
||||
/// Compute the layout for a `TypeDef:Array` field.
|
||||
/// Compute the layout for a `AlkType:Array` field.
|
||||
fn compute_array_field(
|
||||
&mut self,
|
||||
field_schema: &Value,
|
||||
field_path: &str,
|
||||
struct_default_align: usize,
|
||||
) -> Result<FieldLayout, TypedefError> {
|
||||
) -> Result<FieldLayout, AlkTypeError> {
|
||||
let obj = field_schema
|
||||
.as_object()
|
||||
.ok_or_else(|| TypedefError::Offset {
|
||||
.ok_or_else(|| AlkTypeError::Offset {
|
||||
field_path: field_path.to_string(),
|
||||
reason: "array schema is not an object".to_string(),
|
||||
})?;
|
||||
|
||||
let items = obj.get("items").ok_or_else(|| TypedefError::Offset {
|
||||
let items = obj.get("items").ok_or_else(|| AlkTypeError::Offset {
|
||||
field_path: field_path.to_string(),
|
||||
reason: "TArray is missing 'items'".to_string(),
|
||||
})?;
|
||||
let element_schema =
|
||||
resolve_ref_or_inline(items, self.root).ok_or_else(|| TypedefError::Offset {
|
||||
resolve_ref_or_inline(items, self.root).ok_or_else(|| AlkTypeError::Offset {
|
||||
field_path: field_path.to_string(),
|
||||
reason: "could not resolve TArray items schema".to_string(),
|
||||
})?;
|
||||
let elem_kind = get_typedef_kind(element_schema)
|
||||
.and_then(|s| s.parse::<TypeDefKind>().ok())
|
||||
.ok_or_else(|| TypedefError::Offset {
|
||||
let elem_kind = get_alktype_kind(element_schema)
|
||||
.and_then(|s| s.parse::<AlkTypeKind>().ok())
|
||||
.ok_or_else(|| AlkTypeError::Offset {
|
||||
field_path: field_path.to_string(),
|
||||
reason: "TArray element schema has no TypeDef:* kind".to_string(),
|
||||
reason: "TArray element schema has no AlkType:* kind".to_string(),
|
||||
})?;
|
||||
if !elem_kind.is_fixed_size() {
|
||||
return Err(TypedefError::Offset {
|
||||
return Err(AlkTypeError::Offset {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!(
|
||||
"TArray of variable-length element kind {elem_kind} is not supported (OQ-069)"
|
||||
@@ -373,7 +373,7 @@ impl<'a> ComputeCtx<'a> {
|
||||
});
|
||||
}
|
||||
|
||||
let elem_size = elem_kind.type_size().ok_or_else(|| TypedefError::Offset {
|
||||
let elem_size = elem_kind.type_size().ok_or_else(|| AlkTypeError::Offset {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("element kind {elem_kind} has no fixed size"),
|
||||
})?;
|
||||
@@ -431,12 +431,12 @@ impl<'a> ComputeCtx<'a> {
|
||||
field_schema: &Value,
|
||||
field_path: &str,
|
||||
struct_default_align: usize,
|
||||
) -> Result<FieldLayout, TypedefError> {
|
||||
) -> Result<FieldLayout, AlkTypeError> {
|
||||
let keyword_value = field_schema
|
||||
.as_object()
|
||||
.and_then(|o| {
|
||||
o.keys()
|
||||
.find(|k| k.starts_with("TypeDef:"))
|
||||
.find(|k| k.starts_with("AlkType:"))
|
||||
.and_then(|k| o.get(k))
|
||||
})
|
||||
.cloned()
|
||||
@@ -510,10 +510,10 @@ mod tests {
|
||||
#[test]
|
||||
fn simple_fixed_fields_natural_alignment() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"flag": { "TypeDef:Uint8": true },
|
||||
"id": { "TypeDef:Uint32": true }
|
||||
"flag": { "AlkType:Uint8": true },
|
||||
"id": { "AlkType:Uint32": true }
|
||||
}
|
||||
});
|
||||
let m = map(&schema);
|
||||
@@ -525,10 +525,10 @@ mod tests {
|
||||
#[test]
|
||||
fn u8_then_u32_three_bytes_padding() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"a": { "TypeDef:Uint8": true },
|
||||
"b": { "TypeDef:Uint32": true }
|
||||
"a": { "AlkType:Uint8": true },
|
||||
"b": { "AlkType:Uint32": true }
|
||||
}
|
||||
});
|
||||
let m = map(&schema);
|
||||
@@ -539,16 +539,16 @@ mod tests {
|
||||
#[test]
|
||||
fn nested_struct_dotted_paths() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"header": {
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"magic": { "TypeDef:Uint32": true },
|
||||
"version": { "TypeDef:Uint8": true }
|
||||
"magic": { "AlkType:Uint32": true },
|
||||
"version": { "AlkType:Uint8": true }
|
||||
}
|
||||
},
|
||||
"body": { "TypeDef:Uint32": true }
|
||||
"body": { "AlkType:Uint32": true }
|
||||
}
|
||||
});
|
||||
let m = map(&schema);
|
||||
@@ -564,11 +564,11 @@ mod tests {
|
||||
#[test]
|
||||
fn array_fixed_count_element_offsets() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"vals": {
|
||||
"TypeDef:Array": true,
|
||||
"items": { "TypeDef:Uint32": true },
|
||||
"AlkType:Array": true,
|
||||
"items": { "AlkType:Uint32": true },
|
||||
"minItems": 3,
|
||||
"maxItems": 3
|
||||
}
|
||||
@@ -584,11 +584,11 @@ mod tests {
|
||||
#[test]
|
||||
fn array_variable_count_length_prefix() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"vals": {
|
||||
"TypeDef:Array": true,
|
||||
"items": { "TypeDef:Uint32": true }
|
||||
"AlkType:Array": true,
|
||||
"items": { "AlkType:Uint32": true }
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -600,10 +600,10 @@ mod tests {
|
||||
#[test]
|
||||
fn variable_string_length_prefix_at_known_offset() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"id": { "TypeDef:Uint32": true },
|
||||
"name": { "TypeDef:String": true }
|
||||
"id": { "AlkType:Uint32": true },
|
||||
"name": { "AlkType:String": true }
|
||||
}
|
||||
});
|
||||
let m = map(&schema);
|
||||
@@ -615,10 +615,10 @@ mod tests {
|
||||
#[test]
|
||||
fn variable_string_max_length_reservation() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"id": { "TypeDef:Uint32": true },
|
||||
"name": { "TypeDef:String": true, "maxLength": 256 }
|
||||
"id": { "AlkType:Uint32": true },
|
||||
"name": { "AlkType:String": true, "maxLength": 256 }
|
||||
}
|
||||
});
|
||||
let m = map(&schema);
|
||||
@@ -630,10 +630,10 @@ mod tests {
|
||||
#[test]
|
||||
fn variable_string_offset_indirect_eight_bytes() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"id": { "TypeDef:Uint32": true },
|
||||
"blob": { "TypeDef:String": { "encoding": "offset-indirect" } }
|
||||
"id": { "AlkType:Uint32": true },
|
||||
"blob": { "AlkType:String": { "encoding": "offset-indirect" } }
|
||||
}
|
||||
});
|
||||
let m = map(&schema);
|
||||
@@ -645,14 +645,14 @@ mod tests {
|
||||
#[test]
|
||||
fn union_byte_discriminator_rejected_in_aligned_mode() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"payload": {
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {
|
||||
"kind": "byte",
|
||||
"offset": 0,
|
||||
"type": "TypeDef:Uint8"
|
||||
"type": "AlkType:Uint8"
|
||||
},
|
||||
"mapping": {
|
||||
"5": { "$ref": "#/$defs/Read" },
|
||||
@@ -662,26 +662,26 @@ mod tests {
|
||||
},
|
||||
"$defs": {
|
||||
"Read": {
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"handle": { "TypeDef:Uint32": true },
|
||||
"length": { "TypeDef:Uint32": true }
|
||||
"handle": { "AlkType:Uint32": true },
|
||||
"length": { "AlkType:Uint32": true }
|
||||
}
|
||||
},
|
||||
"Write": {
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"handle": { "TypeDef:Uint32": true },
|
||||
"length": { "TypeDef:Uint32": true },
|
||||
"data": { "TypeDef:Uint32": true }
|
||||
"handle": { "AlkType:Uint32": true },
|
||||
"length": { "AlkType:Uint32": true },
|
||||
"data": { "AlkType:Uint32": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let err = OffsetMap::compute(&schema).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Offset { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
|
||||
let reason = match err {
|
||||
TypedefError::Offset { reason, .. } => reason,
|
||||
AlkTypeError::Offset { reason, .. } => reason,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
assert!(reason.contains("ADR-102"), "reason: {reason}");
|
||||
@@ -690,10 +690,10 @@ mod tests {
|
||||
#[test]
|
||||
fn union_field_name_discriminator_rejected_in_aligned_mode() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"event": {
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": { "kind": "field", "name": "type" },
|
||||
"mapping": {
|
||||
"read": { "$ref": "#/$defs/Read" },
|
||||
@@ -703,38 +703,38 @@ mod tests {
|
||||
},
|
||||
"$defs": {
|
||||
"Read": {
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"type": { "TypeDef:Uint8": true },
|
||||
"handle": { "TypeDef:Uint32": true }
|
||||
"type": { "AlkType:Uint8": true },
|
||||
"handle": { "AlkType:Uint32": true }
|
||||
}
|
||||
},
|
||||
"Write": {
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"type": { "TypeDef:Uint8": true },
|
||||
"handle": { "TypeDef:Uint32": true },
|
||||
"length": { "TypeDef:Uint32": true }
|
||||
"type": { "AlkType:Uint8": true },
|
||||
"handle": { "AlkType:Uint32": true },
|
||||
"length": { "AlkType:Uint32": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let err = OffsetMap::compute(&schema).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Offset { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_final_inline_string_rejected_in_aligned_mode() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"name": { "TypeDef:String": true },
|
||||
"id": { "TypeDef:Uint32": true }
|
||||
"name": { "AlkType:String": true },
|
||||
"id": { "AlkType:Uint32": true }
|
||||
}
|
||||
});
|
||||
let err = OffsetMap::compute(&schema).unwrap_err();
|
||||
match err {
|
||||
TypedefError::Offset { field_path, reason } => {
|
||||
AlkTypeError::Offset { field_path, reason } => {
|
||||
assert_eq!(field_path, "name");
|
||||
assert!(reason.contains("ADR-100"), "reason: {reason}");
|
||||
}
|
||||
@@ -745,10 +745,10 @@ mod tests {
|
||||
#[test]
|
||||
fn final_inline_string_allowed_in_aligned_mode() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"id": { "TypeDef:Uint32": true },
|
||||
"name": { "TypeDef:String": true }
|
||||
"id": { "AlkType:Uint32": true },
|
||||
"name": { "AlkType:String": true }
|
||||
}
|
||||
});
|
||||
let m = map(&schema);
|
||||
@@ -759,10 +759,10 @@ mod tests {
|
||||
#[test]
|
||||
fn non_final_maxlength_string_allowed_in_aligned_mode() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"name": { "TypeDef:String": true, "maxLength": 256 },
|
||||
"id": { "TypeDef:Uint32": true }
|
||||
"name": { "AlkType:String": true, "maxLength": 256 },
|
||||
"id": { "AlkType:Uint32": true }
|
||||
}
|
||||
});
|
||||
let m = map(&schema);
|
||||
@@ -773,10 +773,10 @@ mod tests {
|
||||
#[test]
|
||||
fn non_final_offset_indirect_string_allowed_in_aligned_mode() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"blob": { "TypeDef:String": { "encoding": "offset-indirect" } },
|
||||
"id": { "TypeDef:Uint32": true }
|
||||
"blob": { "AlkType:String": { "encoding": "offset-indirect" } },
|
||||
"id": { "AlkType:Uint32": true }
|
||||
}
|
||||
});
|
||||
let m = map(&schema);
|
||||
@@ -787,10 +787,10 @@ mod tests {
|
||||
#[test]
|
||||
fn struct_level_align_rounds_up_total() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"align": 16,
|
||||
"properties": {
|
||||
"flag": { "TypeDef:Uint8": true }
|
||||
"flag": { "AlkType:Uint8": true }
|
||||
}
|
||||
});
|
||||
let m = map(&schema);
|
||||
@@ -801,12 +801,12 @@ mod tests {
|
||||
#[test]
|
||||
fn field_level_align_overrides_struct_default() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"align": 1,
|
||||
"properties": {
|
||||
"tag": { "TypeDef:Uint8": true },
|
||||
"flag": { "TypeDef:Uint8": true, "align": 16 },
|
||||
"id": { "TypeDef:Uint32": true }
|
||||
"tag": { "AlkType:Uint8": true },
|
||||
"flag": { "AlkType:Uint8": true, "align": 16 },
|
||||
"id": { "AlkType:Uint32": true }
|
||||
}
|
||||
});
|
||||
let m = map(&schema);
|
||||
@@ -819,11 +819,11 @@ mod tests {
|
||||
#[test]
|
||||
fn field_align_smaller_than_struct_default() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"align": 8,
|
||||
"properties": {
|
||||
"a": { "TypeDef:Uint8": true },
|
||||
"b": { "TypeDef:Uint32": true, "align": 1 }
|
||||
"a": { "AlkType:Uint8": true },
|
||||
"b": { "AlkType:Uint32": true, "align": 1 }
|
||||
}
|
||||
});
|
||||
let m = map(&schema);
|
||||
@@ -835,10 +835,10 @@ mod tests {
|
||||
#[test]
|
||||
fn iter_returns_all_paths_in_order() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"a": { "TypeDef:Uint8": true },
|
||||
"b": { "TypeDef:Uint32": true }
|
||||
"a": { "AlkType:Uint8": true },
|
||||
"b": { "AlkType:Uint32": true }
|
||||
}
|
||||
});
|
||||
let m = map(&schema);
|
||||
@@ -849,16 +849,16 @@ mod tests {
|
||||
#[test]
|
||||
fn compute_rejects_non_struct_top_level() {
|
||||
let schema =
|
||||
json!({ "TypeDef:Union": true, "discriminator": { "kind": "byte" }, "mapping": {} });
|
||||
json!({ "AlkType:Union": true, "discriminator": { "kind": "byte" }, "mapping": {} });
|
||||
let err = OffsetMap::compute(&schema).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)));
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_rejects_missing_typedef_kind() {
|
||||
fn compute_rejects_missing_alktype_kind() {
|
||||
let schema = json!({ "type": "object", "properties": {} });
|
||||
let err = OffsetMap::compute(&schema).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)));
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
444
src/schema.rs
444
src/schema.rs
@@ -1,36 +1,36 @@
|
||||
//! Schema layer: TypeDef kind detection, annotation parsing, `$ref`
|
||||
//! Schema layer: AlkType kind detection, annotation parsing, `$ref`
|
||||
//! normalization, and the `Endian` enum.
|
||||
//!
|
||||
//! Per ADR-097 and the schema-layer spec. This module provides the
|
||||
//! foundational types and functions that every other module depends on:
|
||||
//! `TypeDef:*` kind detection, fixed byte-size lookups, natural alignment,
|
||||
//! `AlkType:*` kind detection, fixed byte-size lookups, natural alignment,
|
||||
//! schema annotation parsing (`encoding`, `align`, `maxLength`, `endian`),
|
||||
//! TUnion discriminator parsing, and `$ref` normalization from TypeBox
|
||||
//! bare-name refs to JSON Pointer refs.
|
||||
|
||||
use crate::error::TypedefError;
|
||||
use crate::error::AlkTypeError;
|
||||
use serde_json::Value;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
const TYPEDEF_PREFIX: &str = "TypeDef:";
|
||||
const ALKTYPE_PREFIX: &str = "AlkType:";
|
||||
|
||||
pub(crate) const U32_SIZE: usize = 4;
|
||||
pub(crate) const DISCRIMINATOR_PATH: &str = "__discriminator";
|
||||
|
||||
const BYTE_DISCRIMINATOR_TYPES: &[TypeDefKind] = &[
|
||||
TypeDefKind::Uint8,
|
||||
TypeDefKind::Uint16,
|
||||
TypeDefKind::Uint32,
|
||||
const BYTE_DISCRIMINATOR_TYPES: &[AlkTypeKind] = &[
|
||||
AlkTypeKind::Uint8,
|
||||
AlkTypeKind::Uint16,
|
||||
AlkTypeKind::Uint32,
|
||||
];
|
||||
|
||||
/// The 19 `TypeDef:*` kinds recognized by the engine.
|
||||
/// The 19 `AlkType:*` kinds recognized by the engine.
|
||||
///
|
||||
/// Each variant corresponds to a `TypeDef:<name>` JSON Schema keyword.
|
||||
/// Each variant corresponds to a `AlkType:<name>` JSON Schema keyword.
|
||||
/// The enum provides compile-time exhaustiveness checking and integer
|
||||
/// discriminant dispatch (jump table) instead of string comparison.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum TypeDefKind {
|
||||
pub enum AlkTypeKind {
|
||||
Int8,
|
||||
Int16,
|
||||
Int32,
|
||||
@@ -52,48 +52,48 @@ pub enum TypeDefKind {
|
||||
Timestamp,
|
||||
}
|
||||
|
||||
impl TypeDefKind {
|
||||
/// The JSON Schema keyword string, e.g. `"TypeDef:Int8"`.
|
||||
impl AlkTypeKind {
|
||||
/// The JSON Schema keyword string, e.g. `"AlkType:Int8"`.
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
TypeDefKind::Int8 => "TypeDef:Int8",
|
||||
TypeDefKind::Int16 => "TypeDef:Int16",
|
||||
TypeDefKind::Int32 => "TypeDef:Int32",
|
||||
TypeDefKind::Int64 => "TypeDef:Int64",
|
||||
TypeDefKind::Uint8 => "TypeDef:Uint8",
|
||||
TypeDefKind::Uint16 => "TypeDef:Uint16",
|
||||
TypeDefKind::Uint32 => "TypeDef:Uint32",
|
||||
TypeDefKind::Uint64 => "TypeDef:Uint64",
|
||||
TypeDefKind::Float32 => "TypeDef:Float32",
|
||||
TypeDefKind::Float64 => "TypeDef:Float64",
|
||||
TypeDefKind::Boolean => "TypeDef:Boolean",
|
||||
TypeDefKind::Enum => "TypeDef:Enum",
|
||||
TypeDefKind::String => "TypeDef:String",
|
||||
TypeDefKind::Bytes => "TypeDef:Bytes",
|
||||
TypeDefKind::Struct => "TypeDef:Struct",
|
||||
TypeDefKind::Union => "TypeDef:Union",
|
||||
TypeDefKind::Array => "TypeDef:Array",
|
||||
TypeDefKind::Record => "TypeDef:Record",
|
||||
TypeDefKind::Timestamp => "TypeDef:Timestamp",
|
||||
AlkTypeKind::Int8 => "AlkType:Int8",
|
||||
AlkTypeKind::Int16 => "AlkType:Int16",
|
||||
AlkTypeKind::Int32 => "AlkType:Int32",
|
||||
AlkTypeKind::Int64 => "AlkType:Int64",
|
||||
AlkTypeKind::Uint8 => "AlkType:Uint8",
|
||||
AlkTypeKind::Uint16 => "AlkType:Uint16",
|
||||
AlkTypeKind::Uint32 => "AlkType:Uint32",
|
||||
AlkTypeKind::Uint64 => "AlkType:Uint64",
|
||||
AlkTypeKind::Float32 => "AlkType:Float32",
|
||||
AlkTypeKind::Float64 => "AlkType:Float64",
|
||||
AlkTypeKind::Boolean => "AlkType:Boolean",
|
||||
AlkTypeKind::Enum => "AlkType:Enum",
|
||||
AlkTypeKind::String => "AlkType:String",
|
||||
AlkTypeKind::Bytes => "AlkType:Bytes",
|
||||
AlkTypeKind::Struct => "AlkType:Struct",
|
||||
AlkTypeKind::Union => "AlkType:Union",
|
||||
AlkTypeKind::Array => "AlkType:Array",
|
||||
AlkTypeKind::Record => "AlkType:Record",
|
||||
AlkTypeKind::Timestamp => "AlkType:Timestamp",
|
||||
}
|
||||
}
|
||||
|
||||
/// Fixed byte size, or `None` for variable-size / composite kinds.
|
||||
pub fn type_size(self) -> Option<usize> {
|
||||
match self {
|
||||
TypeDefKind::Float32 | TypeDefKind::Int32 | TypeDefKind::Uint32 | TypeDefKind::Enum => {
|
||||
AlkTypeKind::Float32 | AlkTypeKind::Int32 | AlkTypeKind::Uint32 | AlkTypeKind::Enum => {
|
||||
Some(4)
|
||||
}
|
||||
TypeDefKind::Float64 | TypeDefKind::Int64 | TypeDefKind::Uint64 => Some(8),
|
||||
TypeDefKind::Int8 | TypeDefKind::Uint8 | TypeDefKind::Boolean => Some(1),
|
||||
TypeDefKind::Int16 | TypeDefKind::Uint16 => Some(2),
|
||||
TypeDefKind::String
|
||||
| TypeDefKind::Bytes
|
||||
| TypeDefKind::Struct
|
||||
| TypeDefKind::Union
|
||||
| TypeDefKind::Array
|
||||
| TypeDefKind::Record
|
||||
| TypeDefKind::Timestamp => None,
|
||||
AlkTypeKind::Float64 | AlkTypeKind::Int64 | AlkTypeKind::Uint64 => Some(8),
|
||||
AlkTypeKind::Int8 | AlkTypeKind::Uint8 | AlkTypeKind::Boolean => Some(1),
|
||||
AlkTypeKind::Int16 | AlkTypeKind::Uint16 => Some(2),
|
||||
AlkTypeKind::String
|
||||
| AlkTypeKind::Bytes
|
||||
| AlkTypeKind::Struct
|
||||
| AlkTypeKind::Union
|
||||
| AlkTypeKind::Array
|
||||
| AlkTypeKind::Record
|
||||
| AlkTypeKind::Timestamp => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,18 +102,18 @@ impl TypeDefKind {
|
||||
/// composites.
|
||||
pub fn natural_alignment(self) -> usize {
|
||||
match self {
|
||||
TypeDefKind::Int8 | TypeDefKind::Uint8 | TypeDefKind::Boolean => 1,
|
||||
TypeDefKind::Int16 | TypeDefKind::Uint16 => 2,
|
||||
TypeDefKind::Int32
|
||||
| TypeDefKind::Uint32
|
||||
| TypeDefKind::Float32
|
||||
| TypeDefKind::Enum => 4,
|
||||
TypeDefKind::Float64 | TypeDefKind::Int64 | TypeDefKind::Uint64 => 8,
|
||||
TypeDefKind::String
|
||||
| TypeDefKind::Bytes
|
||||
| TypeDefKind::Record
|
||||
| TypeDefKind::Timestamp => 4,
|
||||
TypeDefKind::Struct | TypeDefKind::Union | TypeDefKind::Array => 1,
|
||||
AlkTypeKind::Int8 | AlkTypeKind::Uint8 | AlkTypeKind::Boolean => 1,
|
||||
AlkTypeKind::Int16 | AlkTypeKind::Uint16 => 2,
|
||||
AlkTypeKind::Int32
|
||||
| AlkTypeKind::Uint32
|
||||
| AlkTypeKind::Float32
|
||||
| AlkTypeKind::Enum => 4,
|
||||
AlkTypeKind::Float64 | AlkTypeKind::Int64 | AlkTypeKind::Uint64 => 8,
|
||||
AlkTypeKind::String
|
||||
| AlkTypeKind::Bytes
|
||||
| AlkTypeKind::Record
|
||||
| AlkTypeKind::Timestamp => 4,
|
||||
AlkTypeKind::Struct | AlkTypeKind::Union | AlkTypeKind::Array => 1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,18 +121,18 @@ impl TypeDefKind {
|
||||
pub fn is_fixed_size(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
TypeDefKind::Float32
|
||||
| TypeDefKind::Float64
|
||||
| TypeDefKind::Int8
|
||||
| TypeDefKind::Int16
|
||||
| TypeDefKind::Int32
|
||||
| TypeDefKind::Int64
|
||||
| TypeDefKind::Uint8
|
||||
| TypeDefKind::Uint16
|
||||
| TypeDefKind::Uint32
|
||||
| TypeDefKind::Uint64
|
||||
| TypeDefKind::Boolean
|
||||
| TypeDefKind::Enum
|
||||
AlkTypeKind::Float32
|
||||
| AlkTypeKind::Float64
|
||||
| AlkTypeKind::Int8
|
||||
| AlkTypeKind::Int16
|
||||
| AlkTypeKind::Int32
|
||||
| AlkTypeKind::Int64
|
||||
| AlkTypeKind::Uint8
|
||||
| AlkTypeKind::Uint16
|
||||
| AlkTypeKind::Uint32
|
||||
| AlkTypeKind::Uint64
|
||||
| AlkTypeKind::Boolean
|
||||
| AlkTypeKind::Enum
|
||||
)
|
||||
}
|
||||
|
||||
@@ -140,18 +140,18 @@ impl TypeDefKind {
|
||||
pub fn needs_endian(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
TypeDefKind::Int16
|
||||
| TypeDefKind::Int32
|
||||
| TypeDefKind::Int64
|
||||
| TypeDefKind::Uint16
|
||||
| TypeDefKind::Uint32
|
||||
| TypeDefKind::Uint64
|
||||
| TypeDefKind::Float32
|
||||
| TypeDefKind::Float64
|
||||
| TypeDefKind::Enum
|
||||
| TypeDefKind::String
|
||||
| TypeDefKind::Bytes
|
||||
| TypeDefKind::Timestamp
|
||||
AlkTypeKind::Int16
|
||||
| AlkTypeKind::Int32
|
||||
| AlkTypeKind::Int64
|
||||
| AlkTypeKind::Uint16
|
||||
| AlkTypeKind::Uint32
|
||||
| AlkTypeKind::Uint64
|
||||
| AlkTypeKind::Float32
|
||||
| AlkTypeKind::Float64
|
||||
| AlkTypeKind::Enum
|
||||
| AlkTypeKind::String
|
||||
| AlkTypeKind::Bytes
|
||||
| AlkTypeKind::Timestamp
|
||||
)
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ impl TypeDefKind {
|
||||
pub fn is_composite(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
TypeDefKind::Struct | TypeDefKind::Union | TypeDefKind::Array | TypeDefKind::Record
|
||||
AlkTypeKind::Struct | AlkTypeKind::Union | AlkTypeKind::Array | AlkTypeKind::Record
|
||||
)
|
||||
}
|
||||
|
||||
@@ -167,72 +167,72 @@ impl TypeDefKind {
|
||||
pub fn is_variable_length(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
TypeDefKind::String
|
||||
| TypeDefKind::Bytes
|
||||
| TypeDefKind::Timestamp
|
||||
| TypeDefKind::Record
|
||||
AlkTypeKind::String
|
||||
| AlkTypeKind::Bytes
|
||||
| AlkTypeKind::Timestamp
|
||||
| AlkTypeKind::Record
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TypeDefKind {
|
||||
impl fmt::Display for AlkTypeKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for TypeDefKind {
|
||||
type Err = TypedefError;
|
||||
impl FromStr for AlkTypeKind {
|
||||
type Err = AlkTypeError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"TypeDef:Int8" => Ok(TypeDefKind::Int8),
|
||||
"TypeDef:Int16" => Ok(TypeDefKind::Int16),
|
||||
"TypeDef:Int32" => Ok(TypeDefKind::Int32),
|
||||
"TypeDef:Int64" => Ok(TypeDefKind::Int64),
|
||||
"TypeDef:Uint8" => Ok(TypeDefKind::Uint8),
|
||||
"TypeDef:Uint16" => Ok(TypeDefKind::Uint16),
|
||||
"TypeDef:Uint32" => Ok(TypeDefKind::Uint32),
|
||||
"TypeDef:Uint64" => Ok(TypeDefKind::Uint64),
|
||||
"TypeDef:Float32" => Ok(TypeDefKind::Float32),
|
||||
"TypeDef:Float64" => Ok(TypeDefKind::Float64),
|
||||
"TypeDef:Boolean" => Ok(TypeDefKind::Boolean),
|
||||
"TypeDef:Enum" => Ok(TypeDefKind::Enum),
|
||||
"TypeDef:String" => Ok(TypeDefKind::String),
|
||||
"TypeDef:Bytes" => Ok(TypeDefKind::Bytes),
|
||||
"TypeDef:Struct" => Ok(TypeDefKind::Struct),
|
||||
"TypeDef:Union" => Ok(TypeDefKind::Union),
|
||||
"TypeDef:Array" => Ok(TypeDefKind::Array),
|
||||
"TypeDef:Record" => Ok(TypeDefKind::Record),
|
||||
"TypeDef:Timestamp" => Ok(TypeDefKind::Timestamp),
|
||||
other => Err(TypedefError::Schema(format!(
|
||||
"unknown TypeDef kind: {other}"
|
||||
"AlkType:Int8" => Ok(AlkTypeKind::Int8),
|
||||
"AlkType:Int16" => Ok(AlkTypeKind::Int16),
|
||||
"AlkType:Int32" => Ok(AlkTypeKind::Int32),
|
||||
"AlkType:Int64" => Ok(AlkTypeKind::Int64),
|
||||
"AlkType:Uint8" => Ok(AlkTypeKind::Uint8),
|
||||
"AlkType:Uint16" => Ok(AlkTypeKind::Uint16),
|
||||
"AlkType:Uint32" => Ok(AlkTypeKind::Uint32),
|
||||
"AlkType:Uint64" => Ok(AlkTypeKind::Uint64),
|
||||
"AlkType:Float32" => Ok(AlkTypeKind::Float32),
|
||||
"AlkType:Float64" => Ok(AlkTypeKind::Float64),
|
||||
"AlkType:Boolean" => Ok(AlkTypeKind::Boolean),
|
||||
"AlkType:Enum" => Ok(AlkTypeKind::Enum),
|
||||
"AlkType:String" => Ok(AlkTypeKind::String),
|
||||
"AlkType:Bytes" => Ok(AlkTypeKind::Bytes),
|
||||
"AlkType:Struct" => Ok(AlkTypeKind::Struct),
|
||||
"AlkType:Union" => Ok(AlkTypeKind::Union),
|
||||
"AlkType:Array" => Ok(AlkTypeKind::Array),
|
||||
"AlkType:Record" => Ok(AlkTypeKind::Record),
|
||||
"AlkType:Timestamp" => Ok(AlkTypeKind::Timestamp),
|
||||
other => Err(AlkTypeError::Schema(format!(
|
||||
"unknown AlkType kind: {other}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the `TypeDef:*` kind string if the schema node declares one.
|
||||
/// Returns `None` if the node has no `TypeDef:*` keyword.
|
||||
/// Returns the `AlkType:*` kind string if the schema node declares one.
|
||||
/// Returns `None` if the node has no `AlkType:*` keyword.
|
||||
///
|
||||
/// A TypeDef kind is recognized when the schema object has a key starting
|
||||
/// with `TypeDef:` whose value is `true`. (Object form with annotations
|
||||
/// A AlkType kind is recognized when the schema object has a key starting
|
||||
/// with `AlkType:` whose value is `true`. (Object form with annotations
|
||||
/// like `{ "encoding": "..." }` is handled by the annotation parsers, not
|
||||
/// here — `get_typedef_kind` only checks for the presence of the keyword.)
|
||||
pub fn get_typedef_kind(node: &Value) -> Option<&str> {
|
||||
/// here — `get_alktype_kind` only checks for the presence of the keyword.)
|
||||
pub fn get_alktype_kind(node: &Value) -> Option<&str> {
|
||||
let obj = node.as_object()?;
|
||||
for key in obj.keys() {
|
||||
if key.starts_with(TYPEDEF_PREFIX) && obj.get(key) == Some(&Value::Bool(true)) {
|
||||
if key.starts_with(ALKTYPE_PREFIX) && obj.get(key) == Some(&Value::Bool(true)) {
|
||||
return Some(key.as_str());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the `TypeDefKind` enum variant if the schema node declares one
|
||||
/// (boolean form only, like `get_typedef_kind`).
|
||||
pub fn get_typedef_kind_enum(node: &Value) -> Option<TypeDefKind> {
|
||||
get_typedef_kind(node).and_then(|s| s.parse().ok())
|
||||
/// Returns the `AlkTypeKind` enum variant if the schema node declares one
|
||||
/// (boolean form only, like `get_alktype_kind`).
|
||||
pub fn get_alktype_kind_enum(node: &Value) -> Option<AlkTypeKind> {
|
||||
get_alktype_kind(node).and_then(|s| s.parse().ok())
|
||||
}
|
||||
|
||||
/// Byte endianness for multi-byte integer and float fields.
|
||||
@@ -316,9 +316,9 @@ pub enum DiscriminatorKind {
|
||||
Byte {
|
||||
/// Byte position of the discriminator within the union's buffer.
|
||||
offset: usize,
|
||||
/// The `TypeDef:*` kind of the discriminator (typically
|
||||
/// `TypeDef:Uint8`).
|
||||
disc_type: TypeDefKind,
|
||||
/// The `AlkType:*` kind of the discriminator (typically
|
||||
/// `AlkType:Uint8`).
|
||||
disc_type: AlkTypeKind,
|
||||
},
|
||||
/// Field-name discriminator: a named field within the struct. Mapping keys
|
||||
/// are string values matching the discriminator field's value. The
|
||||
@@ -331,36 +331,36 @@ pub enum DiscriminatorKind {
|
||||
|
||||
/// Parse the `"discriminator"` annotation from a TUnion schema node.
|
||||
///
|
||||
/// Returns [`TypedefError::Schema`] for malformed discriminators (unknown
|
||||
/// Returns [`AlkTypeError::Schema`] for malformed discriminators (unknown
|
||||
/// `kind`, missing required `name`, or an unsupported discriminator `type`).
|
||||
pub fn parse_discriminator(node: &Value) -> Result<DiscriminatorKind, TypedefError> {
|
||||
pub fn parse_discriminator(node: &Value) -> Result<DiscriminatorKind, AlkTypeError> {
|
||||
let obj = node.as_object().ok_or_else(|| {
|
||||
TypedefError::Schema("discriminator requires a schema object".to_string())
|
||||
AlkTypeError::Schema("discriminator requires a schema object".to_string())
|
||||
})?;
|
||||
let disc = obj.get("discriminator").ok_or_else(|| {
|
||||
TypedefError::Schema("union is missing 'discriminator' annotation".to_string())
|
||||
AlkTypeError::Schema("union is missing 'discriminator' annotation".to_string())
|
||||
})?;
|
||||
let disc_obj = disc
|
||||
.as_object()
|
||||
.ok_or_else(|| TypedefError::Schema("'discriminator' must be an object".to_string()))?;
|
||||
.ok_or_else(|| AlkTypeError::Schema("'discriminator' must be an object".to_string()))?;
|
||||
let kind = disc_obj
|
||||
.get("kind")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| TypedefError::Schema("discriminator is missing 'kind' field".to_string()))?;
|
||||
.ok_or_else(|| AlkTypeError::Schema("discriminator is missing 'kind' field".to_string()))?;
|
||||
match kind {
|
||||
"byte" => {
|
||||
let offset = disc_obj.get("offset").and_then(Value::as_u64).unwrap_or(0) as usize;
|
||||
let disc_type_str = disc_obj
|
||||
.get("type")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("TypeDef:Uint8");
|
||||
let disc_type: TypeDefKind = disc_type_str.parse().map_err(|_| {
|
||||
TypedefError::Schema(format!(
|
||||
.unwrap_or("AlkType:Uint8");
|
||||
let disc_type: AlkTypeKind = disc_type_str.parse().map_err(|_| {
|
||||
AlkTypeError::Schema(format!(
|
||||
"discriminator 'type' must be one of {BYTE_DISCRIMINATOR_TYPES:?}, got {disc_type_str:?}"
|
||||
))
|
||||
})?;
|
||||
if !BYTE_DISCRIMINATOR_TYPES.contains(&disc_type) {
|
||||
return Err(TypedefError::Schema(format!(
|
||||
return Err(AlkTypeError::Schema(format!(
|
||||
"discriminator 'type' must be one of {BYTE_DISCRIMINATOR_TYPES:?}, got {disc_type:?}"
|
||||
)));
|
||||
}
|
||||
@@ -374,39 +374,39 @@ pub fn parse_discriminator(node: &Value) -> Result<DiscriminatorKind, TypedefErr
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| {
|
||||
TypedefError::Schema(
|
||||
AlkTypeError::Schema(
|
||||
"field discriminator is missing required 'name' field".to_string(),
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
Ok(DiscriminatorKind::Field { name })
|
||||
}
|
||||
other => Err(TypedefError::Schema(format!(
|
||||
other => Err(AlkTypeError::Schema(format!(
|
||||
"unknown discriminator 'kind': {other:?} (expected \"byte\" or \"field\")"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect a `TypeDef:*` kind from a schema node, accepting either the
|
||||
/// boolean form (`{ "TypeDef:String": true }`) or the object-annotation
|
||||
/// form (`{ "TypeDef:String": { "encoding": "..." } }`).
|
||||
/// Detect a `AlkType:*` kind from a schema node, accepting either the
|
||||
/// boolean form (`{ "AlkType:String": true }`) or the object-annotation
|
||||
/// form (`{ "AlkType:String": { "encoding": "..." } }`).
|
||||
///
|
||||
/// [`get_typedef_kind`] only recognizes the boolean form; layout computation
|
||||
/// [`get_alktype_kind`] only recognizes the boolean form; layout computation
|
||||
/// and engine dispatch also need to recognize the object form so that
|
||||
/// variable-length encoding annotations don't hide the kind.
|
||||
pub fn get_typedef_kind_loose(node: &Value) -> Option<&str> {
|
||||
pub fn get_alktype_kind_loose(node: &Value) -> Option<&str> {
|
||||
let obj = node.as_object()?;
|
||||
for key in obj.keys() {
|
||||
if key.starts_with(TYPEDEF_PREFIX) && obj.get(key).is_some_and(|v| !v.is_null()) {
|
||||
if key.starts_with(ALKTYPE_PREFIX) && obj.get(key).is_some_and(|v| !v.is_null()) {
|
||||
return Some(key.as_str());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Like [`get_typedef_kind_loose`] but returns the parsed [`TypeDefKind`] enum.
|
||||
pub fn get_typedef_kind_loose_enum(node: &Value) -> Option<TypeDefKind> {
|
||||
get_typedef_kind_loose(node).and_then(|s| s.parse().ok())
|
||||
/// Like [`get_alktype_kind_loose`] but returns the parsed [`AlkTypeKind`] enum.
|
||||
pub fn get_alktype_kind_loose_enum(node: &Value) -> Option<AlkTypeKind> {
|
||||
get_alktype_kind_loose(node).and_then(|s| s.parse().ok())
|
||||
}
|
||||
|
||||
/// Resolve a `$ref` against the root schema, or return the inline schema.
|
||||
@@ -473,49 +473,49 @@ mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn get_typedef_kind_detects_bool_keyword() {
|
||||
let schema = json!({"TypeDef:Uint32": true});
|
||||
assert_eq!(get_typedef_kind(&schema), Some("TypeDef:Uint32"));
|
||||
fn get_alktype_kind_detects_bool_keyword() {
|
||||
let schema = json!({"AlkType:Uint32": true});
|
||||
assert_eq!(get_alktype_kind(&schema), Some("AlkType:Uint32"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_typedef_kind_ignores_object_keyword() {
|
||||
let schema = json!({"TypeDef:String": {"encoding": "length-prefixed"}});
|
||||
assert_eq!(get_typedef_kind(&schema), None);
|
||||
fn get_alktype_kind_ignores_object_keyword() {
|
||||
let schema = json!({"AlkType:String": {"encoding": "length-prefixed"}});
|
||||
assert_eq!(get_alktype_kind(&schema), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_typedef_kind_none_for_plain_schema() {
|
||||
fn get_alktype_kind_none_for_plain_schema() {
|
||||
let schema = json!({"type": "object", "properties": {}});
|
||||
assert_eq!(get_typedef_kind(&schema), None);
|
||||
assert_eq!(get_alktype_kind(&schema), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_size_fixed_kinds() {
|
||||
assert_eq!(TypeDefKind::Float32.type_size(), Some(4));
|
||||
assert_eq!(TypeDefKind::Float64.type_size(), Some(8));
|
||||
assert_eq!(TypeDefKind::Int8.type_size(), Some(1));
|
||||
assert_eq!(TypeDefKind::Int16.type_size(), Some(2));
|
||||
assert_eq!(TypeDefKind::Int32.type_size(), Some(4));
|
||||
assert_eq!(TypeDefKind::Int64.type_size(), Some(8));
|
||||
assert_eq!(TypeDefKind::Uint8.type_size(), Some(1));
|
||||
assert_eq!(TypeDefKind::Uint16.type_size(), Some(2));
|
||||
assert_eq!(TypeDefKind::Uint32.type_size(), Some(4));
|
||||
assert_eq!(TypeDefKind::Uint64.type_size(), Some(8));
|
||||
assert_eq!(TypeDefKind::Boolean.type_size(), Some(1));
|
||||
assert_eq!(TypeDefKind::Enum.type_size(), Some(4));
|
||||
assert_eq!(AlkTypeKind::Float32.type_size(), Some(4));
|
||||
assert_eq!(AlkTypeKind::Float64.type_size(), Some(8));
|
||||
assert_eq!(AlkTypeKind::Int8.type_size(), Some(1));
|
||||
assert_eq!(AlkTypeKind::Int16.type_size(), Some(2));
|
||||
assert_eq!(AlkTypeKind::Int32.type_size(), Some(4));
|
||||
assert_eq!(AlkTypeKind::Int64.type_size(), Some(8));
|
||||
assert_eq!(AlkTypeKind::Uint8.type_size(), Some(1));
|
||||
assert_eq!(AlkTypeKind::Uint16.type_size(), Some(2));
|
||||
assert_eq!(AlkTypeKind::Uint32.type_size(), Some(4));
|
||||
assert_eq!(AlkTypeKind::Uint64.type_size(), Some(8));
|
||||
assert_eq!(AlkTypeKind::Boolean.type_size(), Some(1));
|
||||
assert_eq!(AlkTypeKind::Enum.type_size(), Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_size_variable_and_composite_kinds() {
|
||||
for kind in [
|
||||
TypeDefKind::String,
|
||||
TypeDefKind::Bytes,
|
||||
TypeDefKind::Struct,
|
||||
TypeDefKind::Union,
|
||||
TypeDefKind::Array,
|
||||
TypeDefKind::Record,
|
||||
TypeDefKind::Timestamp,
|
||||
AlkTypeKind::String,
|
||||
AlkTypeKind::Bytes,
|
||||
AlkTypeKind::Struct,
|
||||
AlkTypeKind::Union,
|
||||
AlkTypeKind::Array,
|
||||
AlkTypeKind::Record,
|
||||
AlkTypeKind::Timestamp,
|
||||
] {
|
||||
assert_eq!(kind.type_size(), None, "failed for {kind}");
|
||||
}
|
||||
@@ -523,60 +523,60 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn type_size_unknown_kind_returns_none() {
|
||||
assert!("TypeDef:Uint128".parse::<TypeDefKind>().is_err());
|
||||
assert!("TypeDef:Int128".parse::<TypeDefKind>().is_err());
|
||||
assert!("not-a-typedef".parse::<TypeDefKind>().is_err());
|
||||
assert!("AlkType:Uint128".parse::<AlkTypeKind>().is_err());
|
||||
assert!("AlkType:Int128".parse::<AlkTypeKind>().is_err());
|
||||
assert!("not-an-alktype".parse::<AlkTypeKind>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn natural_alignment_matches_spec() {
|
||||
assert_eq!(TypeDefKind::Int8.natural_alignment(), 1);
|
||||
assert_eq!(TypeDefKind::Uint8.natural_alignment(), 1);
|
||||
assert_eq!(TypeDefKind::Boolean.natural_alignment(), 1);
|
||||
assert_eq!(TypeDefKind::Int16.natural_alignment(), 2);
|
||||
assert_eq!(TypeDefKind::Uint16.natural_alignment(), 2);
|
||||
assert_eq!(TypeDefKind::Int32.natural_alignment(), 4);
|
||||
assert_eq!(TypeDefKind::Uint32.natural_alignment(), 4);
|
||||
assert_eq!(TypeDefKind::Float32.natural_alignment(), 4);
|
||||
assert_eq!(TypeDefKind::Enum.natural_alignment(), 4);
|
||||
assert_eq!(TypeDefKind::Float64.natural_alignment(), 8);
|
||||
assert_eq!(TypeDefKind::Int64.natural_alignment(), 8);
|
||||
assert_eq!(TypeDefKind::Uint64.natural_alignment(), 8);
|
||||
assert_eq!(TypeDefKind::String.natural_alignment(), 4);
|
||||
assert_eq!(TypeDefKind::Bytes.natural_alignment(), 4);
|
||||
assert_eq!(TypeDefKind::Record.natural_alignment(), 4);
|
||||
assert_eq!(TypeDefKind::Timestamp.natural_alignment(), 4);
|
||||
assert_eq!(TypeDefKind::Struct.natural_alignment(), 1);
|
||||
assert_eq!(TypeDefKind::Union.natural_alignment(), 1);
|
||||
assert_eq!(TypeDefKind::Array.natural_alignment(), 1);
|
||||
assert_eq!(AlkTypeKind::Int8.natural_alignment(), 1);
|
||||
assert_eq!(AlkTypeKind::Uint8.natural_alignment(), 1);
|
||||
assert_eq!(AlkTypeKind::Boolean.natural_alignment(), 1);
|
||||
assert_eq!(AlkTypeKind::Int16.natural_alignment(), 2);
|
||||
assert_eq!(AlkTypeKind::Uint16.natural_alignment(), 2);
|
||||
assert_eq!(AlkTypeKind::Int32.natural_alignment(), 4);
|
||||
assert_eq!(AlkTypeKind::Uint32.natural_alignment(), 4);
|
||||
assert_eq!(AlkTypeKind::Float32.natural_alignment(), 4);
|
||||
assert_eq!(AlkTypeKind::Enum.natural_alignment(), 4);
|
||||
assert_eq!(AlkTypeKind::Float64.natural_alignment(), 8);
|
||||
assert_eq!(AlkTypeKind::Int64.natural_alignment(), 8);
|
||||
assert_eq!(AlkTypeKind::Uint64.natural_alignment(), 8);
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_fixed_size_classifies_correctly() {
|
||||
for kind in [
|
||||
TypeDefKind::Float32,
|
||||
TypeDefKind::Float64,
|
||||
TypeDefKind::Int8,
|
||||
TypeDefKind::Int16,
|
||||
TypeDefKind::Int32,
|
||||
TypeDefKind::Int64,
|
||||
TypeDefKind::Uint8,
|
||||
TypeDefKind::Uint16,
|
||||
TypeDefKind::Uint32,
|
||||
TypeDefKind::Uint64,
|
||||
TypeDefKind::Boolean,
|
||||
TypeDefKind::Enum,
|
||||
AlkTypeKind::Float32,
|
||||
AlkTypeKind::Float64,
|
||||
AlkTypeKind::Int8,
|
||||
AlkTypeKind::Int16,
|
||||
AlkTypeKind::Int32,
|
||||
AlkTypeKind::Int64,
|
||||
AlkTypeKind::Uint8,
|
||||
AlkTypeKind::Uint16,
|
||||
AlkTypeKind::Uint32,
|
||||
AlkTypeKind::Uint64,
|
||||
AlkTypeKind::Boolean,
|
||||
AlkTypeKind::Enum,
|
||||
] {
|
||||
assert!(kind.is_fixed_size(), "expected fixed: {kind}");
|
||||
}
|
||||
for kind in [
|
||||
TypeDefKind::String,
|
||||
TypeDefKind::Bytes,
|
||||
TypeDefKind::Struct,
|
||||
TypeDefKind::Union,
|
||||
TypeDefKind::Array,
|
||||
TypeDefKind::Record,
|
||||
TypeDefKind::Timestamp,
|
||||
AlkTypeKind::String,
|
||||
AlkTypeKind::Bytes,
|
||||
AlkTypeKind::Struct,
|
||||
AlkTypeKind::Union,
|
||||
AlkTypeKind::Array,
|
||||
AlkTypeKind::Record,
|
||||
AlkTypeKind::Timestamp,
|
||||
] {
|
||||
assert!(!kind.is_fixed_size(), "expected variable: {kind}");
|
||||
}
|
||||
@@ -674,7 +674,7 @@ mod tests {
|
||||
disc,
|
||||
DiscriminatorKind::Byte {
|
||||
offset: 0,
|
||||
disc_type: TypeDefKind::Uint8,
|
||||
disc_type: AlkTypeKind::Uint8,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -682,14 +682,14 @@ mod tests {
|
||||
#[test]
|
||||
fn parse_discriminator_byte_explicit() {
|
||||
let schema = json!({
|
||||
"discriminator": {"kind": "byte", "offset": 4, "type": "TypeDef:Uint16"}
|
||||
"discriminator": {"kind": "byte", "offset": 4, "type": "AlkType:Uint16"}
|
||||
});
|
||||
let disc = parse_discriminator(&schema).expect("byte discriminator");
|
||||
assert_eq!(
|
||||
disc,
|
||||
DiscriminatorKind::Byte {
|
||||
offset: 4,
|
||||
disc_type: TypeDefKind::Uint16,
|
||||
disc_type: AlkTypeKind::Uint16,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -708,10 +708,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_discriminator_missing_discriminator_is_error() {
|
||||
let schema = json!({"TypeDef:Union": true});
|
||||
let schema = json!({"AlkType:Union": true});
|
||||
assert!(matches!(
|
||||
parse_discriminator(&schema),
|
||||
Err(TypedefError::Schema(_))
|
||||
Err(AlkTypeError::Schema(_))
|
||||
));
|
||||
}
|
||||
|
||||
@@ -720,7 +720,7 @@ mod tests {
|
||||
let schema = json!({"discriminator": {"kind": "field"}});
|
||||
assert!(matches!(
|
||||
parse_discriminator(&schema),
|
||||
Err(TypedefError::Schema(_))
|
||||
Err(AlkTypeError::Schema(_))
|
||||
));
|
||||
}
|
||||
|
||||
@@ -729,18 +729,18 @@ mod tests {
|
||||
let schema = json!({"discriminator": {"kind": "magic"}});
|
||||
assert!(matches!(
|
||||
parse_discriminator(&schema),
|
||||
Err(TypedefError::Schema(_))
|
||||
Err(AlkTypeError::Schema(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_discriminator_byte_invalid_type_is_error() {
|
||||
let schema = json!({
|
||||
"discriminator": {"kind": "byte", "type": "TypeDef:Float32"}
|
||||
"discriminator": {"kind": "byte", "type": "AlkType:Float32"}
|
||||
});
|
||||
assert!(matches!(
|
||||
parse_discriminator(&schema),
|
||||
Err(TypedefError::Schema(_))
|
||||
Err(AlkTypeError::Schema(_))
|
||||
));
|
||||
}
|
||||
|
||||
@@ -798,14 +798,14 @@ mod tests {
|
||||
fn normalize_refs_preserves_sibling_keys() {
|
||||
let mut schema = json!({
|
||||
"$ref": "Read",
|
||||
"typedef:annotation": "kept"
|
||||
"alktype:annotation": "kept"
|
||||
});
|
||||
normalize_refs(&mut schema);
|
||||
assert_eq!(
|
||||
schema,
|
||||
json!({
|
||||
"$ref": "#/$defs/Read",
|
||||
"typedef:annotation": "kept"
|
||||
"alktype:annotation": "kept"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
212
src/tunion.rs
212
src/tunion.rs
@@ -10,8 +10,8 @@
|
||||
//! endianness handling are uniform with the rest of the engine.
|
||||
|
||||
use crate::data_access::{read_enum, read_string, read_u16, read_u32, read_u8};
|
||||
use crate::error::TypedefError;
|
||||
use crate::schema::{get_typedef_kind, parse_discriminator, DiscriminatorKind, Endian, TypeDefKind, DISCRIMINATOR_PATH, U32_SIZE};
|
||||
use crate::error::AlkTypeError;
|
||||
use crate::schema::{get_alktype_kind, parse_discriminator, DiscriminatorKind, Endian, AlkTypeKind, DISCRIMINATOR_PATH, U32_SIZE};
|
||||
use serde_json::Value;
|
||||
|
||||
const STRING_PREFIX_SIZE: usize = 4;
|
||||
@@ -39,36 +39,36 @@ pub struct UnionDispatch {
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`TypedefError::Schema`] if the discriminator annotation is missing
|
||||
/// - [`AlkTypeError::Schema`] if the discriminator annotation is missing
|
||||
/// or malformed, or if the discriminator `type` is not one of
|
||||
/// `TypeDef:Uint8` / `TypeDef:Uint16` / `TypeDef:Uint32`.
|
||||
/// - [`TypedefError::Access`] if the buffer is too short to contain the
|
||||
/// `AlkType:Uint8` / `AlkType:Uint16` / `AlkType:Uint32`.
|
||||
/// - [`AlkTypeError::Access`] if the buffer is too short to contain the
|
||||
/// discriminator, or if the read value is not present in the union's
|
||||
/// `mapping`.
|
||||
pub fn read_byte_discriminator(
|
||||
buffer: &[u8],
|
||||
union_schema: &Value,
|
||||
endian: Endian,
|
||||
) -> Result<UnionDispatch, TypedefError> {
|
||||
) -> Result<UnionDispatch, AlkTypeError> {
|
||||
let disc = parse_discriminator(union_schema)?;
|
||||
let (offset, disc_type) = match disc {
|
||||
DiscriminatorKind::Byte { offset, disc_type } => (offset, disc_type),
|
||||
DiscriminatorKind::Field { .. } => {
|
||||
return Err(TypedefError::Schema(
|
||||
return Err(AlkTypeError::Schema(
|
||||
"read_byte_discriminator requires a byte-offset discriminator".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let (disc_value, discriminator_size) = match disc_type {
|
||||
TypeDefKind::Uint8 => (u32::from(read_u8(buffer, offset, DISCRIMINATOR_PATH)?), 1),
|
||||
TypeDefKind::Uint16 => (
|
||||
AlkTypeKind::Uint8 => (u32::from(read_u8(buffer, offset, DISCRIMINATOR_PATH)?), 1),
|
||||
AlkTypeKind::Uint16 => (
|
||||
u32::from(read_u16(buffer, offset, DISCRIMINATOR_PATH, endian)?),
|
||||
2,
|
||||
),
|
||||
TypeDefKind::Uint32 => (read_u32(buffer, offset, DISCRIMINATOR_PATH, endian)?, 4),
|
||||
AlkTypeKind::Uint32 => (read_u32(buffer, offset, DISCRIMINATOR_PATH, endian)?, 4),
|
||||
other => {
|
||||
return Err(TypedefError::Schema(format!(
|
||||
return Err(AlkTypeError::Schema(format!(
|
||||
"unsupported byte discriminator type: {other}"
|
||||
)));
|
||||
}
|
||||
@@ -80,7 +80,7 @@ pub fn read_byte_discriminator(
|
||||
let variant_offset =
|
||||
offset
|
||||
.checked_add(discriminator_size)
|
||||
.ok_or_else(|| TypedefError::Access {
|
||||
.ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: DISCRIMINATOR_PATH.to_string(),
|
||||
reason: format!(
|
||||
"offset {offset} + discriminator_size {discriminator_size} overflows usize"
|
||||
@@ -105,12 +105,12 @@ pub fn read_byte_discriminator(
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`TypedefError::Schema`] if the discriminator annotation is missing
|
||||
/// - [`AlkTypeError::Schema`] if the discriminator annotation is missing
|
||||
/// or malformed, the discriminator field is not declared in
|
||||
/// `properties`, the field has no `TypeDef:*` kind, or the field's
|
||||
/// kind is not one of `TypeDef:String` / `TypeDef:Uint8` /
|
||||
/// `TypeDef:Enum`.
|
||||
/// - [`TypedefError::Access`] if the buffer is too short to contain the
|
||||
/// `properties`, the field has no `AlkType:*` kind, or the field's
|
||||
/// kind is not one of `AlkType:String` / `AlkType:Uint8` /
|
||||
/// `AlkType:Enum`.
|
||||
/// - [`AlkTypeError::Access`] if the buffer is too short to contain the
|
||||
/// discriminator field, or if the read value is not present in the
|
||||
/// union's `mapping`.
|
||||
pub fn read_field_discriminator(
|
||||
@@ -118,12 +118,12 @@ pub fn read_field_discriminator(
|
||||
union_schema: &Value,
|
||||
disc_field_offset: usize,
|
||||
endian: Endian,
|
||||
) -> Result<UnionDispatch, TypedefError> {
|
||||
) -> Result<UnionDispatch, AlkTypeError> {
|
||||
let disc = parse_discriminator(union_schema)?;
|
||||
let name = match disc {
|
||||
DiscriminatorKind::Field { name } => name,
|
||||
DiscriminatorKind::Byte { .. } => {
|
||||
return Err(TypedefError::Schema(
|
||||
return Err(AlkTypeError::Schema(
|
||||
"read_field_discriminator requires a field-name discriminator".to_string(),
|
||||
));
|
||||
}
|
||||
@@ -134,26 +134,26 @@ pub fn read_field_discriminator(
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|props| props.get(&name))
|
||||
.ok_or_else(|| {
|
||||
TypedefError::Schema(format!(
|
||||
AlkTypeError::Schema(format!(
|
||||
"discriminator field '{name}' not found in union properties"
|
||||
))
|
||||
})?;
|
||||
|
||||
let kind = get_typedef_kind(field_schema)
|
||||
.and_then(|s| s.parse::<TypeDefKind>().ok())
|
||||
let kind = get_alktype_kind(field_schema)
|
||||
.and_then(|s| s.parse::<AlkTypeKind>().ok())
|
||||
.ok_or_else(|| {
|
||||
TypedefError::Schema(format!(
|
||||
"discriminator field '{name}' has no TypeDef:* kind"
|
||||
AlkTypeError::Schema(format!(
|
||||
"discriminator field '{name}' has no AlkType:* kind"
|
||||
))
|
||||
})?;
|
||||
|
||||
let (key, discriminator_field_size) = match kind {
|
||||
TypeDefKind::String => {
|
||||
AlkTypeKind::String => {
|
||||
let s = read_string(buffer, disc_field_offset, &name, endian)?;
|
||||
let size =
|
||||
STRING_PREFIX_SIZE
|
||||
.checked_add(s.len())
|
||||
.ok_or_else(|| TypedefError::Access {
|
||||
.ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: name.clone(),
|
||||
reason: format!(
|
||||
"string prefix {STRING_PREFIX_SIZE} + data length {} overflows usize",
|
||||
@@ -162,16 +162,16 @@ pub fn read_field_discriminator(
|
||||
})?;
|
||||
(s.to_string(), size)
|
||||
}
|
||||
TypeDefKind::Uint8 => {
|
||||
AlkTypeKind::Uint8 => {
|
||||
let v = read_u8(buffer, disc_field_offset, &name)?;
|
||||
(v.to_string(), 1)
|
||||
}
|
||||
TypeDefKind::Enum => {
|
||||
AlkTypeKind::Enum => {
|
||||
let v = read_enum(buffer, disc_field_offset, &name, endian)?;
|
||||
(v.to_string(), U32_SIZE)
|
||||
}
|
||||
other => {
|
||||
return Err(TypedefError::Schema(format!(
|
||||
return Err(AlkTypeError::Schema(format!(
|
||||
"unsupported discriminator field type: {other}"
|
||||
)));
|
||||
}
|
||||
@@ -181,7 +181,7 @@ pub fn read_field_discriminator(
|
||||
|
||||
let variant_offset = disc_field_offset
|
||||
.checked_add(discriminator_field_size)
|
||||
.ok_or_else(|| TypedefError::Access {
|
||||
.ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: name.clone(),
|
||||
reason: format!(
|
||||
"disc_field_offset {disc_field_offset} + discriminator_field_size {discriminator_field_size} overflows usize"
|
||||
@@ -201,24 +201,24 @@ pub fn read_field_discriminator(
|
||||
/// `$ref` pointers of the form `"#/$defs/<name>"` are resolved against
|
||||
/// the `union_schema`'s own `$defs` block (when the union schema is the
|
||||
/// schema root). For nested unions whose `$defs` live on an ancestor,
|
||||
/// the caller (typically `TypedefEngine::compile`) is expected to
|
||||
/// the caller (typically `AlkTypeEngine::compile`) is expected to
|
||||
/// resolve refs before reaching this function, or to inline the
|
||||
/// variant schemas into the mapping at load time.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`TypedefError::Schema`] if the union has no `mapping` object, the
|
||||
/// - [`AlkTypeError::Schema`] if the union has no `mapping` object, the
|
||||
/// `key` is not present, a `$ref` is malformed, or a `$ref` cannot be
|
||||
/// resolved against the union schema's own `$defs`.
|
||||
pub fn resolve_variant<'a>(union_schema: &'a Value, key: &str) -> Result<&'a Value, TypedefError> {
|
||||
pub fn resolve_variant<'a>(union_schema: &'a Value, key: &str) -> Result<&'a Value, AlkTypeError> {
|
||||
let mapping = union_schema
|
||||
.get("mapping")
|
||||
.and_then(Value::as_object)
|
||||
.ok_or_else(|| TypedefError::Schema("union is missing 'mapping' object".to_string()))?;
|
||||
.ok_or_else(|| AlkTypeError::Schema("union is missing 'mapping' object".to_string()))?;
|
||||
|
||||
let variant = mapping
|
||||
.get(key)
|
||||
.ok_or_else(|| TypedefError::Schema(format!("unknown mapping key: {key}")))?;
|
||||
.ok_or_else(|| AlkTypeError::Schema(format!("unknown mapping key: {key}")))?;
|
||||
|
||||
let ref_str = match variant.get("$ref").and_then(Value::as_str) {
|
||||
Some(r) => r,
|
||||
@@ -227,10 +227,10 @@ pub fn resolve_variant<'a>(union_schema: &'a Value, key: &str) -> Result<&'a Val
|
||||
|
||||
let pointer = ref_str
|
||||
.strip_prefix('#')
|
||||
.ok_or_else(|| TypedefError::Schema(format!("unsupported $ref form: {ref_str}")))?;
|
||||
.ok_or_else(|| AlkTypeError::Schema(format!("unsupported $ref form: {ref_str}")))?;
|
||||
|
||||
let resolved = resolve_json_pointer(union_schema, pointer).ok_or_else(|| {
|
||||
TypedefError::Schema(format!(
|
||||
AlkTypeError::Schema(format!(
|
||||
"cannot resolve $ref {ref_str} against union schema; ensure refs are inlined or the union schema contains $defs"
|
||||
))
|
||||
})?;
|
||||
@@ -239,27 +239,27 @@ pub fn resolve_variant<'a>(union_schema: &'a Value, key: &str) -> Result<&'a Val
|
||||
|
||||
/// Get the discriminator size in bytes for a byte-offset discriminator.
|
||||
///
|
||||
/// Returns 1 for `TypeDef:Uint8`, 2 for `TypeDef:Uint16`, and 4 for
|
||||
/// `TypeDef:Uint32`. Field-name discriminators have no fixed size and
|
||||
/// produce a [`TypedefError::Schema`].
|
||||
/// Returns 1 for `AlkType:Uint8`, 2 for `AlkType:Uint16`, and 4 for
|
||||
/// `AlkType:Uint32`. Field-name discriminators have no fixed size and
|
||||
/// produce a [`AlkTypeError::Schema`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`TypedefError::Schema`] if the discriminator annotation is
|
||||
/// - [`AlkTypeError::Schema`] if the discriminator annotation is
|
||||
/// missing/malformed, the discriminator `type` is unsupported, or the
|
||||
/// discriminator is a field-name discriminator.
|
||||
pub fn discriminator_size(union_schema: &Value) -> Result<usize, TypedefError> {
|
||||
pub fn discriminator_size(union_schema: &Value) -> Result<usize, AlkTypeError> {
|
||||
let disc = parse_discriminator(union_schema)?;
|
||||
match disc {
|
||||
DiscriminatorKind::Byte { disc_type, .. } => match disc_type {
|
||||
TypeDefKind::Uint8 => Ok(1),
|
||||
TypeDefKind::Uint16 => Ok(2),
|
||||
TypeDefKind::Uint32 => Ok(4),
|
||||
other => Err(TypedefError::Schema(format!(
|
||||
AlkTypeKind::Uint8 => Ok(1),
|
||||
AlkTypeKind::Uint16 => Ok(2),
|
||||
AlkTypeKind::Uint32 => Ok(4),
|
||||
other => Err(AlkTypeError::Schema(format!(
|
||||
"unsupported byte discriminator type: {other}"
|
||||
))),
|
||||
},
|
||||
DiscriminatorKind::Field { .. } => Err(TypedefError::Schema(
|
||||
DiscriminatorKind::Field { .. } => Err(AlkTypeError::Schema(
|
||||
"field-name discriminator has no fixed size".to_string(),
|
||||
)),
|
||||
}
|
||||
@@ -270,7 +270,7 @@ fn verify_mapping_key(
|
||||
key: &str,
|
||||
field_path: &str,
|
||||
raw_value: &str,
|
||||
) -> Result<(), TypedefError> {
|
||||
) -> Result<(), AlkTypeError> {
|
||||
let in_mapping = union_schema
|
||||
.get("mapping")
|
||||
.and_then(Value::as_object)
|
||||
@@ -279,7 +279,7 @@ fn verify_mapping_key(
|
||||
if in_mapping {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(TypedefError::Access {
|
||||
Err(AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("unknown discriminator value: {raw_value}"),
|
||||
})
|
||||
@@ -325,43 +325,43 @@ mod tests {
|
||||
|
||||
fn byte_union_schema(offset: usize, disc_type: &str) -> Value {
|
||||
json!({
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "byte", "offset": offset, "type": disc_type},
|
||||
"mapping": {
|
||||
"5": {"TypeDef:Struct": true, "properties": {"id": {"TypeDef:Uint32": true}}},
|
||||
"6": {"TypeDef:Struct": true, "properties": {"len": {"TypeDef:Uint16": true}}}
|
||||
"5": {"AlkType:Struct": true, "properties": {"id": {"AlkType:Uint32": true}}},
|
||||
"6": {"AlkType:Struct": true, "properties": {"len": {"AlkType:Uint16": true}}}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn field_union_schema(field_name: &str, field_kind: &str) -> Value {
|
||||
let field_schema = match field_kind {
|
||||
"TypeDef:Enum" => json!({
|
||||
"TypeDef:Enum": true,
|
||||
"AlkType:Enum" => json!({
|
||||
"AlkType:Enum": true,
|
||||
"enum": ["read", "write"]
|
||||
}),
|
||||
_ => json!({field_kind: true}),
|
||||
};
|
||||
let (key_a, key_b) = match field_kind {
|
||||
"TypeDef:String" => ("read", "write"),
|
||||
"AlkType:String" => ("read", "write"),
|
||||
_ => ("0", "1"),
|
||||
};
|
||||
json!({
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "field", "name": field_name},
|
||||
"properties": {
|
||||
field_name: field_schema
|
||||
},
|
||||
"mapping": {
|
||||
key_a: {"TypeDef:Struct": true, "properties": {"n": {"TypeDef:Uint32": true}}},
|
||||
key_b: {"TypeDef:Struct": true, "properties": {"m": {"TypeDef:Uint16": true}}}
|
||||
key_a: {"AlkType:Struct": true, "properties": {"n": {"AlkType:Uint32": true}}},
|
||||
key_b: {"AlkType:Struct": true, "properties": {"m": {"AlkType:Uint16": true}}}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_byte_discriminator_uint8_default_offset() {
|
||||
let schema = byte_union_schema(0, "TypeDef:Uint8");
|
||||
let schema = byte_union_schema(0, "AlkType:Uint8");
|
||||
let buf = [5u8, 0xAA, 0xBB, 0xCC];
|
||||
let d = read_byte_discriminator(&buf, &schema, LE).expect("read");
|
||||
assert_eq!(d.key, "5");
|
||||
@@ -371,7 +371,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn read_byte_discriminator_uint8_big_endian() {
|
||||
let schema = byte_union_schema(0, "TypeDef:Uint8");
|
||||
let schema = byte_union_schema(0, "AlkType:Uint8");
|
||||
let buf = [6u8];
|
||||
let d = read_byte_discriminator(&buf, &schema, BE).expect("read");
|
||||
assert_eq!(d.key, "6");
|
||||
@@ -380,7 +380,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn read_byte_discriminator_uint16_little_endian() {
|
||||
let schema = byte_union_schema(2, "TypeDef:Uint16");
|
||||
let schema = byte_union_schema(2, "AlkType:Uint16");
|
||||
let mut buf = vec![0u8; 4];
|
||||
buf[2..4].copy_from_slice(&5u16.to_le_bytes());
|
||||
let d = read_byte_discriminator(&buf, &schema, LE).expect("read");
|
||||
@@ -391,7 +391,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn read_byte_discriminator_uint16_big_endian() {
|
||||
let schema = byte_union_schema(0, "TypeDef:Uint16");
|
||||
let schema = byte_union_schema(0, "AlkType:Uint16");
|
||||
let buf = [0x00, 0x06, 0xAA, 0xBB];
|
||||
let d = read_byte_discriminator(&buf, &schema, BE).expect("read");
|
||||
assert_eq!(d.key, "6");
|
||||
@@ -400,7 +400,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn read_byte_discriminator_uint32_little_endian() {
|
||||
let schema = byte_union_schema(0, "TypeDef:Uint32");
|
||||
let schema = byte_union_schema(0, "AlkType:Uint32");
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[0..4].copy_from_slice(&5u32.to_le_bytes());
|
||||
let d = read_byte_discriminator(&buf, &schema, LE).expect("read");
|
||||
@@ -411,7 +411,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn read_byte_discriminator_uint32_big_endian() {
|
||||
let schema = byte_union_schema(0, "TypeDef:Uint32");
|
||||
let schema = byte_union_schema(0, "AlkType:Uint32");
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[0..4].copy_from_slice(&6u32.to_be_bytes());
|
||||
let d = read_byte_discriminator(&buf, &schema, BE).expect("read");
|
||||
@@ -421,11 +421,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn read_byte_discriminator_unknown_value_is_access_error() {
|
||||
let schema = byte_union_schema(0, "TypeDef:Uint8");
|
||||
let schema = byte_union_schema(0, "AlkType:Uint8");
|
||||
let buf = [99u8];
|
||||
let err = read_byte_discriminator(&buf, &schema, LE).unwrap_err();
|
||||
match err {
|
||||
TypedefError::Access { field_path, reason } => {
|
||||
AlkTypeError::Access { field_path, reason } => {
|
||||
assert_eq!(field_path, DISCRIMINATOR_PATH);
|
||||
assert!(reason.contains("99"), "reason: {reason}");
|
||||
}
|
||||
@@ -435,23 +435,23 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn read_byte_discriminator_buffer_too_short_is_access_error() {
|
||||
let schema = byte_union_schema(4, "TypeDef:Uint32");
|
||||
let schema = byte_union_schema(4, "AlkType:Uint32");
|
||||
let buf = [0u8; 2];
|
||||
let err = read_byte_discriminator(&buf, &schema, LE).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }));
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_byte_discriminator_field_kind_is_schema_error() {
|
||||
let schema = field_union_schema("type", "TypeDef:String");
|
||||
let schema = field_union_schema("type", "AlkType:String");
|
||||
let buf = [0u8; 16];
|
||||
let err = read_byte_discriminator(&buf, &schema, LE).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)));
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_field_discriminator_string() {
|
||||
let schema = field_union_schema("type", "TypeDef:String");
|
||||
let schema = field_union_schema("type", "AlkType:String");
|
||||
let mut buf = vec![0u8; 32];
|
||||
let value = "read";
|
||||
let len_bytes = (value.len() as u32).to_le_bytes();
|
||||
@@ -465,7 +465,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn read_field_discriminator_uint8() {
|
||||
let schema = field_union_schema("type", "TypeDef:Uint8");
|
||||
let schema = field_union_schema("type", "AlkType:Uint8");
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[0] = 0;
|
||||
let d = read_field_discriminator(&buf, &schema, 0, LE).expect("read");
|
||||
@@ -476,7 +476,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn read_field_discriminator_enum() {
|
||||
let schema = field_union_schema("type", "TypeDef:Enum");
|
||||
let schema = field_union_schema("type", "AlkType:Enum");
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[0..4].copy_from_slice(&0u32.to_le_bytes());
|
||||
let d = read_field_discriminator(&buf, &schema, 0, LE).expect("read");
|
||||
@@ -487,7 +487,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn read_field_discriminator_string_big_endian() {
|
||||
let schema = field_union_schema("type", "TypeDef:String");
|
||||
let schema = field_union_schema("type", "AlkType:String");
|
||||
let mut buf = vec![0u8; 32];
|
||||
let value = "write";
|
||||
let len_bytes = (value.len() as u32).to_be_bytes();
|
||||
@@ -500,12 +500,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn read_field_discriminator_unknown_value_is_access_error() {
|
||||
let schema = field_union_schema("type", "TypeDef:Uint8");
|
||||
let schema = field_union_schema("type", "AlkType:Uint8");
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[0] = 99;
|
||||
let err = read_field_discriminator(&buf, &schema, 0, LE).unwrap_err();
|
||||
match err {
|
||||
TypedefError::Access { field_path, reason } => {
|
||||
AlkTypeError::Access { field_path, reason } => {
|
||||
assert_eq!(field_path, "type");
|
||||
assert!(reason.contains("99"), "reason: {reason}");
|
||||
}
|
||||
@@ -516,51 +516,51 @@ mod tests {
|
||||
#[test]
|
||||
fn read_field_discriminator_field_not_found_is_schema_error() {
|
||||
let schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "field", "name": "missing"},
|
||||
"properties": {"other": {"TypeDef:Uint8": true}},
|
||||
"mapping": {"5": {"TypeDef:Struct": true}}
|
||||
"properties": {"other": {"AlkType:Uint8": true}},
|
||||
"mapping": {"5": {"AlkType:Struct": true}}
|
||||
});
|
||||
let buf = [0u8; 4];
|
||||
let err = read_field_discriminator(&buf, &schema, 0, LE).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)));
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_field_discriminator_no_typedef_kind_is_schema_error() {
|
||||
fn read_field_discriminator_no_alktype_kind_is_schema_error() {
|
||||
let schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "field", "name": "type"},
|
||||
"properties": {"type": {"type": "string"}},
|
||||
"mapping": {"read": {"TypeDef:Struct": true}}
|
||||
"mapping": {"read": {"AlkType:Struct": true}}
|
||||
});
|
||||
let buf = [0u8; 4];
|
||||
let err = read_field_discriminator(&buf, &schema, 0, LE).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)));
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_field_discriminator_unsupported_kind_is_schema_error() {
|
||||
let schema = field_union_schema("type", "TypeDef:Float32");
|
||||
let schema = field_union_schema("type", "AlkType:Float32");
|
||||
let buf = [0u8; 8];
|
||||
let err = read_field_discriminator(&buf, &schema, 0, LE).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)));
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_field_discriminator_byte_kind_is_schema_error() {
|
||||
let schema = byte_union_schema(0, "TypeDef:Uint8");
|
||||
let schema = byte_union_schema(0, "AlkType:Uint8");
|
||||
let buf = [5u8];
|
||||
let err = read_field_discriminator(&buf, &schema, 0, LE).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)));
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_variant_inline_schema() {
|
||||
let schema = byte_union_schema(0, "TypeDef:Uint8");
|
||||
let schema = byte_union_schema(0, "AlkType:Uint8");
|
||||
let variant = resolve_variant(&schema, "5").expect("resolve");
|
||||
assert_eq!(
|
||||
variant.get("TypeDef:Struct").and_then(Value::as_bool),
|
||||
variant.get("AlkType:Struct").and_then(Value::as_bool),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
@@ -568,78 +568,78 @@ mod tests {
|
||||
#[test]
|
||||
fn resolve_variant_ref_against_own_defs() {
|
||||
let schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "byte"},
|
||||
"mapping": {
|
||||
"5": {"$ref": "#/$defs/Read"}
|
||||
},
|
||||
"$defs": {
|
||||
"Read": {"TypeDef:Struct": true, "properties": {"id": {"TypeDef:Uint32": true}}}
|
||||
"Read": {"AlkType:Struct": true, "properties": {"id": {"AlkType:Uint32": true}}}
|
||||
}
|
||||
});
|
||||
let variant = resolve_variant(&schema, "5").expect("resolve");
|
||||
assert_eq!(
|
||||
variant.get("TypeDef:Struct").and_then(Value::as_bool),
|
||||
variant.get("AlkType:Struct").and_then(Value::as_bool),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_variant_unknown_key_is_schema_error() {
|
||||
let schema = byte_union_schema(0, "TypeDef:Uint8");
|
||||
let schema = byte_union_schema(0, "AlkType:Uint8");
|
||||
let err = resolve_variant(&schema, "999").unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)));
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_variant_missing_mapping_is_schema_error() {
|
||||
let schema = json!({"TypeDef:Union": true, "discriminator": {"kind": "byte"}});
|
||||
let schema = json!({"AlkType:Union": true, "discriminator": {"kind": "byte"}});
|
||||
let err = resolve_variant(&schema, "5").unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)));
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_variant_unresolvable_ref_is_schema_error() {
|
||||
let schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "byte"},
|
||||
"mapping": {
|
||||
"5": {"$ref": "#/$defs/Read"}
|
||||
}
|
||||
});
|
||||
let err = resolve_variant(&schema, "5").unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)));
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discriminator_size_uint8() {
|
||||
let schema = byte_union_schema(0, "TypeDef:Uint8");
|
||||
let schema = byte_union_schema(0, "AlkType:Uint8");
|
||||
assert_eq!(discriminator_size(&schema).unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discriminator_size_uint16() {
|
||||
let schema = byte_union_schema(0, "TypeDef:Uint16");
|
||||
let schema = byte_union_schema(0, "AlkType:Uint16");
|
||||
assert_eq!(discriminator_size(&schema).unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discriminator_size_uint32() {
|
||||
let schema = byte_union_schema(0, "TypeDef:Uint32");
|
||||
let schema = byte_union_schema(0, "AlkType:Uint32");
|
||||
assert_eq!(discriminator_size(&schema).unwrap(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discriminator_size_field_kind_is_schema_error() {
|
||||
let schema = field_union_schema("type", "TypeDef:String");
|
||||
let schema = field_union_schema("type", "AlkType:String");
|
||||
let err = discriminator_size(&schema).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)));
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discriminator_size_missing_discriminator_is_schema_error() {
|
||||
let schema = json!({"TypeDef:Union": true});
|
||||
let schema = json!({"AlkType:Union": true});
|
||||
let err = discriminator_size(&schema).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)));
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Custom keyword validators for all 17 `TypeDef:*` kinds, registered
|
||||
//! Custom keyword validators for all 17 `AlkType:*` kinds, registered
|
||||
//! via `jsonschema::options().with_keyword(...)`.
|
||||
//!
|
||||
//! Per ADR-098: the `jsonschema` crate handles all structural validation;
|
||||
@@ -10,11 +10,11 @@
|
||||
//! factories read parent context (e.g. `maxLength`) to pass into the
|
||||
//! validator struct.
|
||||
|
||||
use crate::error::TypedefError;
|
||||
use crate::error::AlkTypeError;
|
||||
use jsonschema::{Keyword, ValidationError};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
/// Build a jsonschema validator with all 17 `TypeDef:*` custom keywords
|
||||
/// Build a jsonschema validator with all 17 `AlkType:*` custom keywords
|
||||
/// registered.
|
||||
///
|
||||
/// The returned validator can validate JSON representations of data
|
||||
@@ -25,43 +25,43 @@ use serde_json::{Map, Value};
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`TypedefError::Schema`] if the schema is malformed or the
|
||||
/// Returns [`AlkTypeError::Schema`] if the schema is malformed or the
|
||||
/// underlying jsonschema validator cannot be built.
|
||||
pub fn build_validator(schema: &Value) -> Result<jsonschema::Validator, TypedefError> {
|
||||
pub fn build_validator(schema: &Value) -> Result<jsonschema::Validator, AlkTypeError> {
|
||||
jsonschema::options()
|
||||
.with_keyword("TypeDef:Float32", float32_factory)
|
||||
.with_keyword("TypeDef:Float64", float64_factory)
|
||||
.with_keyword("TypeDef:Int8", int8_factory)
|
||||
.with_keyword("TypeDef:Int16", int16_factory)
|
||||
.with_keyword("TypeDef:Int32", int32_factory)
|
||||
.with_keyword("TypeDef:Int64", int64_factory)
|
||||
.with_keyword("TypeDef:Uint8", uint8_factory)
|
||||
.with_keyword("TypeDef:Uint16", uint16_factory)
|
||||
.with_keyword("TypeDef:Uint32", uint32_factory)
|
||||
.with_keyword("TypeDef:Uint64", uint64_factory)
|
||||
.with_keyword("TypeDef:Boolean", boolean_factory)
|
||||
.with_keyword("TypeDef:String", string_factory)
|
||||
.with_keyword("TypeDef:Bytes", bytes_factory)
|
||||
.with_keyword("TypeDef:Enum", enum_factory)
|
||||
.with_keyword("TypeDef:Struct", struct_factory)
|
||||
.with_keyword("TypeDef:Union", union_factory)
|
||||
.with_keyword("TypeDef:Array", array_factory)
|
||||
.with_keyword("TypeDef:Record", record_factory)
|
||||
.with_keyword("TypeDef:Timestamp", timestamp_factory)
|
||||
.with_keyword("AlkType:Float32", float32_factory)
|
||||
.with_keyword("AlkType:Float64", float64_factory)
|
||||
.with_keyword("AlkType:Int8", int8_factory)
|
||||
.with_keyword("AlkType:Int16", int16_factory)
|
||||
.with_keyword("AlkType:Int32", int32_factory)
|
||||
.with_keyword("AlkType:Int64", int64_factory)
|
||||
.with_keyword("AlkType:Uint8", uint8_factory)
|
||||
.with_keyword("AlkType:Uint16", uint16_factory)
|
||||
.with_keyword("AlkType:Uint32", uint32_factory)
|
||||
.with_keyword("AlkType:Uint64", uint64_factory)
|
||||
.with_keyword("AlkType:Boolean", boolean_factory)
|
||||
.with_keyword("AlkType:String", string_factory)
|
||||
.with_keyword("AlkType:Bytes", bytes_factory)
|
||||
.with_keyword("AlkType:Enum", enum_factory)
|
||||
.with_keyword("AlkType:Struct", struct_factory)
|
||||
.with_keyword("AlkType:Union", union_factory)
|
||||
.with_keyword("AlkType:Array", array_factory)
|
||||
.with_keyword("AlkType:Record", record_factory)
|
||||
.with_keyword("AlkType:Timestamp", timestamp_factory)
|
||||
.build(schema)
|
||||
.map_err(|e| TypedefError::Schema(format!("validator build failed: {e}")))
|
||||
.map_err(|e| AlkTypeError::Schema(format!("validator build failed: {e}")))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Numeric validators (generated via macros)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
define_int_validator!(Int8Validator, int8_factory, "TypeDef:Int8", -128, 127);
|
||||
define_int_validator!(Int16Validator, int16_factory, "TypeDef:Int16", -32768, 32767);
|
||||
define_int_validator!(Int32Validator, int32_factory, "TypeDef:Int32", -2147483648, 2147483647);
|
||||
define_uint_validator!(Uint8Validator, uint8_factory, "TypeDef:Uint8", 255);
|
||||
define_uint_validator!(Uint16Validator, uint16_factory, "TypeDef:Uint16", 65535);
|
||||
define_uint_validator!(Uint32Validator, uint32_factory, "TypeDef:Uint32", 4294967295);
|
||||
define_int_validator!(Int8Validator, int8_factory, "AlkType:Int8", -128, 127);
|
||||
define_int_validator!(Int16Validator, int16_factory, "AlkType:Int16", -32768, 32767);
|
||||
define_int_validator!(Int32Validator, int32_factory, "AlkType:Int32", -2147483648, 2147483647);
|
||||
define_uint_validator!(Uint8Validator, uint8_factory, "AlkType:Uint8", 255);
|
||||
define_uint_validator!(Uint16Validator, uint16_factory, "AlkType:Uint16", 65535);
|
||||
define_uint_validator!(Uint32Validator, uint32_factory, "AlkType:Uint32", 4294967295);
|
||||
|
||||
// Int64/Uint64 use the full i64/u64 range, so the macro's `n <= $max` check
|
||||
// is always true (clippy: "comparison useless due to type limits"). Write
|
||||
@@ -88,7 +88,7 @@ fn int64_factory<'a>(
|
||||
if value.as_bool() == Some(true) {
|
||||
Ok(Box::new(Int64Validator))
|
||||
} else {
|
||||
Err(ValidationError::schema("TypeDef:Int64 must be set to true"))
|
||||
Err(ValidationError::schema("AlkType:Int64 must be set to true"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,19 +113,19 @@ fn uint64_factory<'a>(
|
||||
if value.as_bool() == Some(true) {
|
||||
Ok(Box::new(Uint64Validator))
|
||||
} else {
|
||||
Err(ValidationError::schema("TypeDef:Uint64 must be set to true"))
|
||||
Err(ValidationError::schema("AlkType:Uint64 must be set to true"))
|
||||
}
|
||||
}
|
||||
define_float_validator!(
|
||||
Float32Validator,
|
||||
float32_factory,
|
||||
"TypeDef:Float32",
|
||||
"AlkType:Float32",
|
||||
"expected a finite f32-compatible number"
|
||||
);
|
||||
define_float_validator!(
|
||||
Float64Validator,
|
||||
float64_factory,
|
||||
"TypeDef:Float64",
|
||||
"AlkType:Float64",
|
||||
"expected a finite f64 number"
|
||||
);
|
||||
|
||||
@@ -187,7 +187,7 @@ impl Keyword for BytesValidator {
|
||||
}
|
||||
}
|
||||
|
||||
/// `TypeDef:Enum` is a layout marker — the built-in `enum` keyword
|
||||
/// `AlkType:Enum` is a layout marker — the built-in `enum` keyword
|
||||
/// handles value-membership validation. The custom keyword exists solely
|
||||
/// for the layout engine to recognize the type as a fixed-size u32 index.
|
||||
struct EnumValidator;
|
||||
@@ -257,11 +257,11 @@ fn is_rfc3339_timestamp(s: &str) -> bool {
|
||||
// Composite validators (generated via macros)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
define_type_validator!(StructValidator, struct_factory, "TypeDef:Struct", is_object, "expected an object");
|
||||
define_type_validator!(UnionValidator, union_factory, "TypeDef:Union", is_object, "expected an object for union");
|
||||
define_type_validator!(ArrayValidator, array_factory, "TypeDef:Array", is_array, "expected an array");
|
||||
define_type_validator!(RecordValidator, record_factory, "TypeDef:Record", is_object, "expected an object for record");
|
||||
define_type_validator!(BooleanValidator, boolean_factory, "TypeDef:Boolean", is_boolean, "expected a boolean");
|
||||
define_type_validator!(StructValidator, struct_factory, "AlkType:Struct", is_object, "expected an object");
|
||||
define_type_validator!(UnionValidator, union_factory, "AlkType:Union", is_object, "expected an object for union");
|
||||
define_type_validator!(ArrayValidator, array_factory, "AlkType:Array", is_array, "expected an array");
|
||||
define_type_validator!(RecordValidator, record_factory, "AlkType:Record", is_object, "expected an object for record");
|
||||
define_type_validator!(BooleanValidator, boolean_factory, "AlkType:Boolean", is_boolean, "expected a boolean");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Factory closures for non-macro-generated validators
|
||||
@@ -274,7 +274,7 @@ fn string_factory<'a>(
|
||||
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
|
||||
if !value.is_boolean() && !value.is_object() {
|
||||
return Err(ValidationError::schema(
|
||||
"TypeDef:String must be set to true or an annotation object",
|
||||
"AlkType:String must be set to true or an annotation object",
|
||||
));
|
||||
}
|
||||
let max_length = parent
|
||||
@@ -291,7 +291,7 @@ fn bytes_factory<'a>(
|
||||
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
|
||||
if !value.is_boolean() && !value.is_object() {
|
||||
return Err(ValidationError::schema(
|
||||
"TypeDef:Bytes must be set to true or an annotation object",
|
||||
"AlkType:Bytes must be set to true or an annotation object",
|
||||
));
|
||||
}
|
||||
let max_length = parent
|
||||
@@ -309,7 +309,7 @@ fn enum_factory<'a>(
|
||||
if value.as_bool() == Some(true) {
|
||||
Ok(Box::new(EnumValidator))
|
||||
} else {
|
||||
Err(ValidationError::schema("TypeDef:Enum must be set to true"))
|
||||
Err(ValidationError::schema("AlkType:Enum must be set to true"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,7 +322,7 @@ fn timestamp_factory<'a>(
|
||||
Ok(Box::new(TimestampValidator))
|
||||
} else {
|
||||
Err(ValidationError::schema(
|
||||
"TypeDef:Timestamp must be set to true",
|
||||
"AlkType:Timestamp must be set to true",
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -339,13 +339,13 @@ mod tests {
|
||||
#[test]
|
||||
fn validates_valid_struct_instance() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "TypeDef:Uint32": true, "type": "integer" },
|
||||
"score": { "TypeDef:Float32": true, "type": "number" },
|
||||
"flag": { "TypeDef:Uint8": true, "type": "integer" },
|
||||
"count": { "TypeDef:Uint16": true, "type": "integer" }
|
||||
"id": { "AlkType:Uint32": true, "type": "integer" },
|
||||
"score": { "AlkType:Float32": true, "type": "number" },
|
||||
"flag": { "AlkType:Uint8": true, "type": "integer" },
|
||||
"count": { "AlkType:Uint16": true, "type": "integer" }
|
||||
},
|
||||
"required": ["id", "score", "flag", "count"]
|
||||
});
|
||||
@@ -362,9 +362,9 @@ mod tests {
|
||||
#[test]
|
||||
fn rejects_uint32_out_of_range() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": { "id": { "TypeDef:Uint32": true, "type": "integer" } },
|
||||
"properties": { "id": { "AlkType:Uint32": true, "type": "integer" } },
|
||||
"required": ["id"]
|
||||
});
|
||||
let validator = validator_for(&schema);
|
||||
@@ -375,9 +375,9 @@ mod tests {
|
||||
#[test]
|
||||
fn validates_int8_range() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": { "val": { "TypeDef:Int8": true, "type": "integer" } },
|
||||
"properties": { "val": { "AlkType:Int8": true, "type": "integer" } },
|
||||
"required": ["val"]
|
||||
});
|
||||
let validator = validator_for(&schema);
|
||||
@@ -391,11 +391,11 @@ mod tests {
|
||||
#[test]
|
||||
fn validates_int16_and_int32_ranges() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"i16": { "TypeDef:Int16": true, "type": "integer" },
|
||||
"i32": { "TypeDef:Int32": true, "type": "integer" }
|
||||
"i16": { "AlkType:Int16": true, "type": "integer" },
|
||||
"i32": { "AlkType:Int32": true, "type": "integer" }
|
||||
},
|
||||
"required": ["i16", "i32"]
|
||||
});
|
||||
@@ -409,11 +409,11 @@ mod tests {
|
||||
#[test]
|
||||
fn validates_uint16_and_uint32_ranges() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"u16": { "TypeDef:Uint16": true, "type": "integer" },
|
||||
"u32": { "TypeDef:Uint32": true, "type": "integer" }
|
||||
"u16": { "AlkType:Uint16": true, "type": "integer" },
|
||||
"u32": { "AlkType:Uint32": true, "type": "integer" }
|
||||
},
|
||||
"required": ["u16", "u32"]
|
||||
});
|
||||
@@ -426,9 +426,9 @@ mod tests {
|
||||
#[test]
|
||||
fn validates_int64_range() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": { "val": { "TypeDef:Int64": true, "type": "integer" } },
|
||||
"properties": { "val": { "AlkType:Int64": true, "type": "integer" } },
|
||||
"required": ["val"]
|
||||
});
|
||||
let validator = validator_for(&schema);
|
||||
@@ -441,9 +441,9 @@ mod tests {
|
||||
#[test]
|
||||
fn validates_uint64_range() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": { "val": { "TypeDef:Uint64": true, "type": "integer" } },
|
||||
"properties": { "val": { "AlkType:Uint64": true, "type": "integer" } },
|
||||
"required": ["val"]
|
||||
});
|
||||
let validator = validator_for(&schema);
|
||||
@@ -456,11 +456,11 @@ mod tests {
|
||||
#[test]
|
||||
fn validates_float_finiteness() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"f32": { "TypeDef:Float32": true, "type": "number" },
|
||||
"f64": { "TypeDef:Float64": true, "type": "number" }
|
||||
"f32": { "AlkType:Float32": true, "type": "number" },
|
||||
"f64": { "AlkType:Float64": true, "type": "number" }
|
||||
},
|
||||
"required": ["f32", "f64"]
|
||||
});
|
||||
@@ -473,9 +473,9 @@ mod tests {
|
||||
#[test]
|
||||
fn validates_boolean() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": { "active": { "TypeDef:Boolean": true, "type": "boolean" } },
|
||||
"properties": { "active": { "AlkType:Boolean": true, "type": "boolean" } },
|
||||
"required": ["active"]
|
||||
});
|
||||
let validator = validator_for(&schema);
|
||||
@@ -487,10 +487,10 @@ mod tests {
|
||||
#[test]
|
||||
fn validates_string_and_maxlength() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "TypeDef:String": true, "type": "string", "maxLength": 5 }
|
||||
"name": { "AlkType:String": true, "type": "string", "maxLength": 5 }
|
||||
},
|
||||
"required": ["name"]
|
||||
});
|
||||
@@ -504,10 +504,10 @@ mod tests {
|
||||
#[test]
|
||||
fn validates_bytes_and_maxlength() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"blob": { "TypeDef:Bytes": true, "type": "string", "maxLength": 4 }
|
||||
"blob": { "AlkType:Bytes": true, "type": "string", "maxLength": 4 }
|
||||
},
|
||||
"required": ["blob"]
|
||||
});
|
||||
@@ -520,11 +520,11 @@ mod tests {
|
||||
#[test]
|
||||
fn enum_validator_is_noop_and_builtin_enum_handles_membership() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"TypeDef:Enum": true,
|
||||
"AlkType:Enum": true,
|
||||
"type": "string",
|
||||
"enum": ["ok", "error", "pending"]
|
||||
}
|
||||
@@ -540,10 +540,10 @@ mod tests {
|
||||
#[test]
|
||||
fn validates_timestamp_rfc3339() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_at": { "TypeDef:Timestamp": true, "type": "string" }
|
||||
"created_at": { "AlkType:Timestamp": true, "type": "string" }
|
||||
},
|
||||
"required": ["created_at"]
|
||||
});
|
||||
@@ -557,13 +557,13 @@ mod tests {
|
||||
#[test]
|
||||
fn validates_array_type() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"TypeDef:Array": true,
|
||||
"AlkType:Array": true,
|
||||
"type": "array",
|
||||
"items": { "TypeDef:Uint8": true, "type": "integer" }
|
||||
"items": { "AlkType:Uint8": true, "type": "integer" }
|
||||
}
|
||||
},
|
||||
"required": ["items"]
|
||||
@@ -576,13 +576,13 @@ mod tests {
|
||||
#[test]
|
||||
fn validates_record_type() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"counts": {
|
||||
"TypeDef:Record": true,
|
||||
"AlkType:Record": true,
|
||||
"type": "object",
|
||||
"additionalProperties": { "TypeDef:Uint32": true, "type": "integer" }
|
||||
"additionalProperties": { "AlkType:Uint32": true, "type": "integer" }
|
||||
}
|
||||
},
|
||||
"required": ["counts"]
|
||||
@@ -595,11 +595,11 @@ mod tests {
|
||||
#[test]
|
||||
fn validates_union_type() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"packet": {
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "type": "string" }
|
||||
@@ -616,15 +616,15 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn build_validator_returns_schema_error_for_malformed_keyword() {
|
||||
let schema = json!({"TypeDef:Uint32": "not-a-bool"});
|
||||
let schema = json!({"AlkType:Uint32": "not-a-bool"});
|
||||
let err = build_validator(&schema).expect_err("should fail");
|
||||
assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_validator_maps_build_error_to_typedef_error() {
|
||||
fn build_validator_maps_build_error_to_alktype_error() {
|
||||
let schema = json!([1, 2, 3]);
|
||||
let err = build_validator(&schema).expect_err("schema must be an object");
|
||||
assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Integration tests for the `TypedefEngine` public API.
|
||||
//! Integration tests for the `AlkTypeEngine` public API.
|
||||
//!
|
||||
//! Exercises the engine across both layout modes, the convenience
|
||||
//! accessors, validation convenience methods, and the aligned-mode
|
||||
@@ -10,21 +10,21 @@ use serde_json::json;
|
||||
|
||||
fn mixed_fixed_struct_schema() -> serde_json::Value {
|
||||
json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"endian": "little",
|
||||
"properties": {
|
||||
"flag": { "TypeDef:Uint8": true },
|
||||
"id": { "TypeDef:Uint32": true },
|
||||
"score": { "TypeDef:Float32": true },
|
||||
"tag": { "TypeDef:String": true }
|
||||
"flag": { "AlkType:Uint8": true },
|
||||
"id": { "AlkType:Uint32": true },
|
||||
"score": { "AlkType:Float32": true },
|
||||
"tag": { "AlkType:String": true }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compile_aligned_builds_engine_with_offset_map() -> Result<(), TypedefError> {
|
||||
fn compile_aligned_builds_engine_with_offset_map() -> Result<(), AlkTypeError> {
|
||||
let mut schema = mixed_fixed_struct_schema();
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
assert_eq!(engine.mode(), LayoutMode::Aligned);
|
||||
assert!(engine.offset_map().is_some());
|
||||
assert!(engine.layout_builder().is_none());
|
||||
@@ -33,9 +33,9 @@ fn compile_aligned_builds_engine_with_offset_map() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compile_packed_builds_engine_with_builder_and_reader() -> Result<(), TypedefError> {
|
||||
fn compile_packed_builds_engine_with_builder_and_reader() -> Result<(), AlkTypeError> {
|
||||
let mut schema = mixed_fixed_struct_schema();
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed)?;
|
||||
assert_eq!(engine.mode(), LayoutMode::Packed);
|
||||
assert!(engine.offset_map().is_none());
|
||||
assert!(engine.layout_builder().is_some());
|
||||
@@ -44,20 +44,20 @@ fn compile_packed_builds_engine_with_builder_and_reader() -> Result<(), TypedefE
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compile_normalizes_bare_name_refs() -> Result<(), TypedefError> {
|
||||
fn compile_normalizes_bare_name_refs() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"child": { "$ref": "Child" }
|
||||
},
|
||||
"$defs": {
|
||||
"Child": {
|
||||
"TypeDef:Struct": true,
|
||||
"properties": { "x": { "TypeDef:Uint8": true } }
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "x": { "AlkType:Uint8": true } }
|
||||
}
|
||||
}
|
||||
});
|
||||
let _engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed)?;
|
||||
let _engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed)?;
|
||||
assert_eq!(
|
||||
schema["properties"]["child"]["$ref"],
|
||||
json!("#/$defs/Child")
|
||||
@@ -66,20 +66,20 @@ fn compile_normalizes_bare_name_refs() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compile_leaves_full_pointer_refs_unchanged() -> Result<(), TypedefError> {
|
||||
fn compile_leaves_full_pointer_refs_unchanged() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"child": { "$ref": "#/$defs/Child" }
|
||||
},
|
||||
"$defs": {
|
||||
"Child": {
|
||||
"TypeDef:Struct": true,
|
||||
"properties": { "x": { "TypeDef:Uint8": true } }
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "x": { "AlkType:Uint8": true } }
|
||||
}
|
||||
}
|
||||
});
|
||||
let _engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed)?;
|
||||
let _engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed)?;
|
||||
assert_eq!(
|
||||
schema["properties"]["child"]["$ref"],
|
||||
json!("#/$defs/Child")
|
||||
@@ -88,103 +88,103 @@ fn compile_leaves_full_pointer_refs_unchanged() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compile_returns_schema_error_when_no_typedef_kind() {
|
||||
fn compile_returns_schema_error_when_no_alktype_kind() {
|
||||
let mut schema = json!({ "type": "object", "properties": {} });
|
||||
let err = TypedefEngine::compile(&mut schema, LayoutMode::Aligned).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}");
|
||||
let err = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endian_parsed_from_schema_big() -> Result<(), TypedefError> {
|
||||
fn endian_parsed_from_schema_big() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"endian": "big",
|
||||
"properties": { "id": { "TypeDef:Uint32": true } }
|
||||
"properties": { "id": { "AlkType:Uint32": true } }
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed)?;
|
||||
assert_eq!(engine.endian(), Endian::Big);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endian_defaults_to_little() -> Result<(), TypedefError> {
|
||||
fn endian_defaults_to_little() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"properties": { "id": { "TypeDef:Uint32": true } }
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "id": { "AlkType:Uint32": true } }
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed)?;
|
||||
assert_eq!(engine.endian(), Endian::Little);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_json_accepts_valid_instance() -> Result<(), TypedefError> {
|
||||
fn validate_json_accepts_valid_instance() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "TypeDef:Uint32": true, "type": "integer" }
|
||||
"id": { "AlkType:Uint32": true, "type": "integer" }
|
||||
},
|
||||
"required": ["id"]
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
assert!(engine.validate_json(&json!({"id": 42})).is_ok());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_json_rejects_invalid_instance() -> Result<(), TypedefError> {
|
||||
fn validate_json_rejects_invalid_instance() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "TypeDef:Uint32": true, "type": "integer" }
|
||||
"id": { "AlkType:Uint32": true, "type": "integer" }
|
||||
},
|
||||
"required": ["id"]
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let err = engine.validate_json(&json!({"id": -1})).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Validation(_)), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_valid_json_returns_bool() -> Result<(), TypedefError> {
|
||||
fn is_valid_json_returns_bool() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "TypeDef:Uint32": true, "type": "integer" }
|
||||
"id": { "AlkType:Uint32": true, "type": "integer" }
|
||||
},
|
||||
"required": ["id"]
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
assert!(engine.is_valid_json(&json!({"id": 42})));
|
||||
assert!(!engine.is_valid_json(&json!({"id": -1})));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_write_aligned_round_trips_all_fixed_size_kinds() -> Result<(), TypedefError> {
|
||||
fn read_write_aligned_round_trips_all_fixed_size_kinds() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"endian": "little",
|
||||
"properties": {
|
||||
"i8": { "TypeDef:Int8": true },
|
||||
"u8": { "TypeDef:Uint8": true },
|
||||
"i16": { "TypeDef:Int16": true },
|
||||
"u16": { "TypeDef:Uint16": true },
|
||||
"i32": { "TypeDef:Int32": true },
|
||||
"u32": { "TypeDef:Uint32": true },
|
||||
"i64": { "TypeDef:Int64": true },
|
||||
"u64": { "TypeDef:Uint64": true },
|
||||
"f32": { "TypeDef:Float32": true },
|
||||
"f64": { "TypeDef:Float64": true },
|
||||
"b": { "TypeDef:Boolean": true },
|
||||
"e": { "TypeDef:Enum": true }
|
||||
"i8": { "AlkType:Int8": true },
|
||||
"u8": { "AlkType:Uint8": true },
|
||||
"i16": { "AlkType:Int16": true },
|
||||
"u16": { "AlkType:Uint16": true },
|
||||
"i32": { "AlkType:Int32": true },
|
||||
"u32": { "AlkType:Uint32": true },
|
||||
"i64": { "AlkType:Int64": true },
|
||||
"u64": { "AlkType:Uint64": true },
|
||||
"f32": { "AlkType:Float32": true },
|
||||
"f64": { "AlkType:Float64": true },
|
||||
"b": { "AlkType:Boolean": true },
|
||||
"e": { "AlkType:Enum": true }
|
||||
}
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let offset_map = engine.offset_map().expect("aligned mode has offset_map");
|
||||
let mut buffer = vec![0u8; offset_map.total_size()];
|
||||
|
||||
@@ -229,14 +229,14 @@ fn read_write_aligned_round_trips_all_fixed_size_kinds() -> Result<(), TypedefEr
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_write_aligned_round_trips_string() -> Result<(), TypedefError> {
|
||||
fn read_write_aligned_round_trips_string() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"name": { "TypeDef:String": true }
|
||||
"name": { "AlkType:String": true }
|
||||
}
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let offset_map = engine.offset_map().expect("aligned mode has offset_map");
|
||||
let mut buffer = vec![0u8; offset_map.total_size() + 64];
|
||||
engine.write_field(&mut buffer, "name", &FieldValue::String("hello world"))?;
|
||||
@@ -248,14 +248,14 @@ fn read_write_aligned_round_trips_string() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_write_aligned_round_trips_bytes() -> Result<(), TypedefError> {
|
||||
fn read_write_aligned_round_trips_bytes() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"blob": { "TypeDef:Bytes": true }
|
||||
"blob": { "AlkType:Bytes": true }
|
||||
}
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let offset_map = engine.offset_map().expect("aligned mode has offset_map");
|
||||
let payload = b"the quick brown fox".to_vec();
|
||||
let mut buffer = vec![0u8; offset_map.total_size() + payload.len()];
|
||||
@@ -268,109 +268,109 @@ fn read_write_aligned_round_trips_bytes() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_field_returns_access_error_in_packed_mode() -> Result<(), TypedefError> {
|
||||
fn read_field_returns_access_error_in_packed_mode() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"properties": { "id": { "TypeDef:Uint32": true } }
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "id": { "AlkType:Uint32": true } }
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed)?;
|
||||
let buffer = [0u8; 4];
|
||||
let err = engine.read_field(&buffer, "id").unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_field_returns_access_error_in_packed_mode() -> Result<(), TypedefError> {
|
||||
fn write_field_returns_access_error_in_packed_mode() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"properties": { "id": { "TypeDef:Uint32": true } }
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "id": { "AlkType:Uint32": true } }
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Packed)?;
|
||||
let mut buffer = [0u8; 4];
|
||||
let err = engine
|
||||
.write_field(&mut buffer, "id", &FieldValue::U32(1))
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_field_returns_offset_error_for_missing_path() -> Result<(), TypedefError> {
|
||||
fn read_field_returns_offset_error_for_missing_path() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"properties": { "id": { "TypeDef:Uint32": true } }
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "id": { "AlkType:Uint32": true } }
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let buffer = [0u8; 8];
|
||||
let err = engine.read_field(&buffer, "missing").unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Offset { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_field_returns_offset_error_for_missing_path() -> Result<(), TypedefError> {
|
||||
fn write_field_returns_offset_error_for_missing_path() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"properties": { "id": { "TypeDef:Uint32": true } }
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "id": { "AlkType:Uint32": true } }
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let mut buffer = [0u8; 8];
|
||||
let err = engine
|
||||
.write_field(&mut buffer, "missing", &FieldValue::U32(1))
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Offset { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_field_returns_access_error_for_composite_types() -> Result<(), TypedefError> {
|
||||
fn read_field_returns_access_error_for_composite_types() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"vals": {
|
||||
"TypeDef:Array": true,
|
||||
"items": { "TypeDef:Uint32": true }
|
||||
"AlkType:Array": true,
|
||||
"items": { "AlkType:Uint32": true }
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let buffer = [0u8; 8];
|
||||
let err = engine.read_field(&buffer, "vals").unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_field_returns_access_error_for_composite_value() -> Result<(), TypedefError> {
|
||||
fn write_field_returns_access_error_for_composite_value() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"properties": { "id": { "TypeDef:Uint32": true } }
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "id": { "AlkType:Uint32": true } }
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let mut buffer = [0u8; 8];
|
||||
let err = engine
|
||||
.write_field(&mut buffer, "id", &FieldValue::Struct { start: 0, end: 4 })
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_field_aligned_reads_nested_struct_byte_range() -> Result<(), TypedefError> {
|
||||
fn read_field_aligned_reads_nested_struct_byte_range() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"header": {
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"version": { "TypeDef:Uint8": true },
|
||||
"magic": { "TypeDef:Uint32": true }
|
||||
"version": { "AlkType:Uint8": true },
|
||||
"magic": { "AlkType:Uint32": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let offset_map = engine.offset_map().expect("aligned mode");
|
||||
let mut buffer = vec![0u8; offset_map.total_size()];
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Error path integration tests for `alktype`.
|
||||
//!
|
||||
//! Exercises the `TypedefError` variants across the crate:
|
||||
//! Exercises the `AlkTypeError` variants across the crate:
|
||||
//! `Access` (buffer too short, invalid UTF-8, invalid boolean byte,
|
||||
//! unknown discriminator value), `Schema` (missing TypeDef kind,
|
||||
//! unknown discriminator value), `Schema` (missing AlkType kind,
|
||||
//! malformed discriminator annotation), and `Offset` (missing
|
||||
//! variable-length field size in `LayoutBuilder::build`).
|
||||
|
||||
@@ -17,7 +17,7 @@ fn read_u32_buffer_too_short_returns_access_error() {
|
||||
let buffer = [0u8; 2];
|
||||
let err = data_access::read_u32(&buffer, 0, "header.id", Endian::Little).unwrap_err();
|
||||
match err {
|
||||
TypedefError::Access { field_path, reason } => {
|
||||
AlkTypeError::Access { field_path, reason } => {
|
||||
assert_eq!(field_path, "header.id");
|
||||
assert!(reason.contains("bounds"), "reason: {reason}");
|
||||
}
|
||||
@@ -29,49 +29,49 @@ fn read_u32_buffer_too_short_returns_access_error() {
|
||||
fn read_u16_buffer_too_short_returns_access_error() {
|
||||
let buffer = [0u8; 1];
|
||||
let err = data_access::read_u16(&buffer, 0, "tag", Endian::Little).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_u64_buffer_too_short_returns_access_error() {
|
||||
let buffer = [0u8; 4];
|
||||
let err = data_access::read_u64(&buffer, 0, "offset", Endian::Big).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_f32_buffer_too_short_returns_access_error() {
|
||||
let buffer = [0u8; 2];
|
||||
let err = data_access::read_f32(&buffer, 0, "score", Endian::Little).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_f64_buffer_too_short_returns_access_error() {
|
||||
let buffer = [0u8; 4];
|
||||
let err = data_access::read_f64(&buffer, 0, "score", Endian::Little).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_i32_buffer_too_short_returns_access_error() {
|
||||
let buffer = [0u8; 2];
|
||||
let err = data_access::read_i32(&buffer, 0, "id", Endian::Little).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_bool_buffer_too_short_returns_access_error() {
|
||||
let buffer: [u8; 0] = [];
|
||||
let err = data_access::read_bool(&buffer, 0, "flag").unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_string_buffer_too_short_on_prefix_returns_access_error() {
|
||||
let buffer = [0u8; 2];
|
||||
let err = data_access::read_string(&buffer, 0, "name", Endian::Little).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -79,7 +79,7 @@ fn read_string_buffer_too_short_on_data_returns_access_error() {
|
||||
let mut buffer = vec![0u8; 6];
|
||||
buffer[0..4].copy_from_slice(&100u32.to_le_bytes());
|
||||
let err = data_access::read_string(&buffer, 0, "name", Endian::Little).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -87,7 +87,7 @@ fn read_bytes_buffer_too_short_on_data_returns_access_error() {
|
||||
let mut buffer = vec![0u8; 5];
|
||||
buffer[0..4].copy_from_slice(&100u32.to_le_bytes());
|
||||
let err = data_access::read_bytes(&buffer, 0, "blob", Endian::Little).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -96,7 +96,7 @@ fn read_string_invalid_utf8_returns_access_error() {
|
||||
let invalid = [0xFFu8, 0xFE, 0xFD];
|
||||
let _ = data_access::write_bytes(&mut buffer, 0, &invalid, "name", Endian::Little);
|
||||
let err = data_access::read_string(&buffer, 0, "name", Endian::Little).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -104,7 +104,7 @@ fn read_bool_invalid_byte_returns_access_error() {
|
||||
let buffer = [0x02u8];
|
||||
let err = data_access::read_bool(&buffer, 0, "flag").unwrap_err();
|
||||
match err {
|
||||
TypedefError::Access { field_path, reason } => {
|
||||
AlkTypeError::Access { field_path, reason } => {
|
||||
assert_eq!(field_path, "flag");
|
||||
assert!(reason.contains("0x02"), "reason: {reason}");
|
||||
}
|
||||
@@ -113,14 +113,14 @@ fn read_bool_invalid_byte_returns_access_error() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_bool_zero_is_false() -> Result<(), TypedefError> {
|
||||
fn read_bool_zero_is_false() -> Result<(), AlkTypeError> {
|
||||
let buffer = [0x00u8];
|
||||
assert!(!data_access::read_bool(&buffer, 0, "flag")?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_bool_one_is_true() -> Result<(), TypedefError> {
|
||||
fn read_bool_one_is_true() -> Result<(), AlkTypeError> {
|
||||
let buffer = [0x01u8];
|
||||
assert!(data_access::read_bool(&buffer, 0, "flag")?);
|
||||
Ok(())
|
||||
@@ -130,14 +130,14 @@ fn read_bool_one_is_true() -> Result<(), TypedefError> {
|
||||
fn read_bool_three_is_access_error() {
|
||||
let buffer = [0x03u8];
|
||||
let err = data_access::read_bool(&buffer, 0, "flag").unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_u32_buffer_too_short_returns_access_error() {
|
||||
let mut buffer = [0u8; 2];
|
||||
let err = data_access::write_u32(&mut buffer, 0, 1, "id", Endian::Little).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -145,136 +145,136 @@ fn write_string_buffer_too_short_returns_access_error() {
|
||||
let mut buffer = vec![0u8; 4];
|
||||
let err =
|
||||
data_access::write_string(&mut buffer, 0, "hello", "name", Endian::Little).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compile_missing_typedef_kind_returns_schema_error() {
|
||||
fn compile_missing_alktype_kind_returns_schema_error() {
|
||||
let mut schema = json!({ "type": "object", "properties": {} });
|
||||
let err = TypedefEngine::compile(&mut schema, LayoutMode::Aligned).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}");
|
||||
let err = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_map_compute_missing_typedef_kind_returns_schema_error() {
|
||||
fn offset_map_compute_missing_alktype_kind_returns_schema_error() {
|
||||
let schema = json!({ "type": "object", "properties": {} });
|
||||
let err = OffsetMap::compute(&schema).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_map_compute_non_struct_top_level_returns_schema_error() {
|
||||
let schema = json!({ "TypeDef:Uint32": true });
|
||||
let schema = json!({ "AlkType:Uint32": true });
|
||||
let err = OffsetMap::compute(&schema).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_builder_new_missing_typedef_kind_returns_schema_error() {
|
||||
fn layout_builder_new_missing_alktype_kind_returns_schema_error() {
|
||||
let schema = json!({ "type": "object", "properties": {} });
|
||||
let err = LayoutBuilder::new(&schema).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_builder_new_non_struct_top_level_returns_schema_error() {
|
||||
let schema = json!({ "TypeDef:Uint32": true });
|
||||
let schema = json!({ "AlkType:Uint32": true });
|
||||
let err = LayoutBuilder::new(&schema).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_discriminator_missing_returns_schema_error() {
|
||||
let schema = json!({"TypeDef:Union": true});
|
||||
let schema = json!({"AlkType:Union": true});
|
||||
let err = parse_discriminator(&schema).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_discriminator_field_missing_name_returns_schema_error() {
|
||||
let schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "field"}
|
||||
});
|
||||
let err = parse_discriminator(&schema).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_discriminator_unknown_kind_returns_schema_error() {
|
||||
let schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "magic"}
|
||||
});
|
||||
let err = parse_discriminator(&schema).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_discriminator_byte_invalid_type_returns_schema_error() {
|
||||
let schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "TypeDef:Float32"}
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "AlkType:Float32"}
|
||||
});
|
||||
let err = parse_discriminator(&schema).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_byte_discriminator_unknown_value_returns_access_error() -> Result<(), TypedefError> {
|
||||
fn read_byte_discriminator_unknown_value_returns_access_error() -> Result<(), AlkTypeError> {
|
||||
let union_schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "TypeDef:Uint8"},
|
||||
"mapping": {"5": {"TypeDef:Struct": true, "properties": {"x": {"TypeDef:Uint8": true}}}}
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "AlkType:Uint8"},
|
||||
"mapping": {"5": {"AlkType:Struct": true, "properties": {"x": {"AlkType:Uint8": true}}}}
|
||||
});
|
||||
let buffer = [99u8, 0x00, 0x00];
|
||||
let err = tunion::read_byte_discriminator(&buffer, &union_schema, Endian::Little).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_byte_discriminator_buffer_too_short_returns_access_error() -> Result<(), TypedefError> {
|
||||
fn read_byte_discriminator_buffer_too_short_returns_access_error() -> Result<(), AlkTypeError> {
|
||||
let union_schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"discriminator": {"kind": "byte", "offset": 4, "type": "TypeDef:Uint32"},
|
||||
"mapping": {"5": {"TypeDef:Struct": true, "properties": {"x": {"TypeDef:Uint8": true}}}}
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "byte", "offset": 4, "type": "AlkType:Uint32"},
|
||||
"mapping": {"5": {"AlkType:Struct": true, "properties": {"x": {"AlkType:Uint8": true}}}}
|
||||
});
|
||||
let buffer = [0u8; 2];
|
||||
let err = tunion::read_byte_discriminator(&buffer, &union_schema, Endian::Little).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_field_discriminator_unknown_value_returns_access_error() -> Result<(), TypedefError> {
|
||||
fn read_field_discriminator_unknown_value_returns_access_error() -> Result<(), AlkTypeError> {
|
||||
let union_schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "field", "name": "type"},
|
||||
"properties": {"type": {"TypeDef:Uint8": true}},
|
||||
"mapping": {"0": {"TypeDef:Struct": true, "properties": {"x": {"TypeDef:Uint8": true}}}}
|
||||
"properties": {"type": {"AlkType:Uint8": true}},
|
||||
"mapping": {"0": {"AlkType:Struct": true, "properties": {"x": {"AlkType:Uint8": true}}}}
|
||||
});
|
||||
let mut buffer = vec![0u8; 8];
|
||||
buffer[0] = 99;
|
||||
let err =
|
||||
tunion::read_field_discriminator(&buffer, &union_schema, 0, Endian::Little).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_builder_missing_var_size_returns_offset_error() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"name": { "TypeDef:String": true }
|
||||
"name": { "AlkType:String": true }
|
||||
}
|
||||
});
|
||||
let builder = LayoutBuilder::new(&schema).expect("builder");
|
||||
let empty: HashMap<String, usize> = HashMap::new();
|
||||
let err = builder.build(&empty).unwrap_err();
|
||||
match err {
|
||||
TypedefError::Offset { field_path, reason } => {
|
||||
AlkTypeError::Offset { field_path, reason } => {
|
||||
assert_eq!(field_path, "name");
|
||||
assert!(
|
||||
reason.contains("missing variable-length field size"),
|
||||
@@ -288,54 +288,54 @@ fn layout_builder_missing_var_size_returns_offset_error() {
|
||||
#[test]
|
||||
fn layout_builder_missing_array_data_size_returns_offset_error() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"vals": {
|
||||
"TypeDef:Array": true,
|
||||
"items": { "TypeDef:Uint32": true }
|
||||
"AlkType:Array": true,
|
||||
"items": { "AlkType:Uint32": true }
|
||||
}
|
||||
}
|
||||
});
|
||||
let builder = LayoutBuilder::new(&schema).expect("builder");
|
||||
let empty: HashMap<String, usize> = HashMap::new();
|
||||
let err = builder.build(&empty).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Offset { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_builder_missing_discriminator_value_returns_offset_error() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"payload": {
|
||||
"TypeDef:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "TypeDef:Uint8"},
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "AlkType:Uint8"},
|
||||
"mapping": {"5": {"$ref": "#/$defs/Read"}}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"Read": {"TypeDef:Struct": true, "properties": {"x": {"TypeDef:Uint8": true}}}
|
||||
"Read": {"AlkType:Struct": true, "properties": {"x": {"AlkType:Uint8": true}}}
|
||||
}
|
||||
});
|
||||
let builder = LayoutBuilder::new(&schema).expect("builder");
|
||||
let empty: HashMap<String, usize> = HashMap::new();
|
||||
let err = builder.build(&empty).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Offset { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_builder_unknown_discriminator_value_returns_offset_error() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"payload": {
|
||||
"TypeDef:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "TypeDef:Uint8"},
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "AlkType:Uint8"},
|
||||
"mapping": {"5": {"$ref": "#/$defs/Read"}}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"Read": {"TypeDef:Struct": true, "properties": {"x": {"TypeDef:Uint8": true}}}
|
||||
"Read": {"AlkType:Struct": true, "properties": {"x": {"AlkType:Uint8": true}}}
|
||||
}
|
||||
});
|
||||
let builder = LayoutBuilder::new(&schema).expect("builder");
|
||||
@@ -343,7 +343,7 @@ fn layout_builder_unknown_discriminator_value_returns_offset_error() {
|
||||
vs.insert("payload.__discriminator".to_string(), 99);
|
||||
let err = builder.build(&vs).unwrap_err();
|
||||
match err {
|
||||
TypedefError::Offset { reason, .. } => {
|
||||
AlkTypeError::Offset { reason, .. } => {
|
||||
assert!(reason.contains("99"), "reason: {reason}");
|
||||
}
|
||||
other => panic!("expected Offset, got {other:?}"),
|
||||
@@ -353,34 +353,34 @@ fn layout_builder_unknown_discriminator_value_returns_offset_error() {
|
||||
#[test]
|
||||
fn sequential_reader_buffer_too_short_returns_access_error() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"id": { "TypeDef:Uint32": true }
|
||||
"id": { "AlkType:Uint32": true }
|
||||
}
|
||||
});
|
||||
let buffer = [0u8; 2];
|
||||
let mut reader = SequentialReader::new(&schema).unwrap();
|
||||
let err = reader.read_next(&buffer).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sequential_reader_unknown_field_returns_schema_error() {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"properties": { "a": { "TypeDef:Uint8": true } }
|
||||
"AlkType:Struct": true,
|
||||
"properties": { "a": { "AlkType:Uint8": true } }
|
||||
});
|
||||
let buffer = [0u8; 4];
|
||||
let mut reader = SequentialReader::new(&schema).unwrap();
|
||||
let err = reader.read_field(&buffer, "missing").unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sequential_reader_new_non_struct_returns_schema_error() {
|
||||
let schema = json!({ "TypeDef:Uint32": true });
|
||||
let schema = json!({ "AlkType:Uint32": true });
|
||||
let err = SequentialReader::new(&schema).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -391,7 +391,7 @@ fn read_string_indirect_data_region_too_short_returns_access_error() {
|
||||
let data_region = b"too short";
|
||||
let err = data_access::read_bytes_indirect(&index, 0, data_region, "blob", Endian::Little)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -400,7 +400,7 @@ fn read_bytes_indirect_index_too_short_returns_access_error() {
|
||||
let data_region = b"anything";
|
||||
let err = data_access::read_bytes_indirect(&buffer, 0, data_region, "blob", Endian::Little)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -411,5 +411,5 @@ fn read_string_indirect_invalid_utf8_returns_access_error() {
|
||||
let _ = data_access::write_u32(&mut index, 4, 3, "idx.len", Endian::Little);
|
||||
let err = data_access::read_string_indirect(&index, 0, data_region, "name", Endian::Little)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
}
|
||||
|
||||
@@ -19,14 +19,14 @@ fn var_sizes(pairs: &[(&str, usize)]) -> HashMap<String, usize> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_size_round_trip_via_offset_map() -> Result<(), TypedefError> {
|
||||
fn fixed_size_round_trip_via_offset_map() -> Result<(), AlkTypeError> {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"id": { "TypeDef:Uint32": true },
|
||||
"score": { "TypeDef:Float32": true },
|
||||
"flag": { "TypeDef:Uint8": true },
|
||||
"count": { "TypeDef:Uint16": true }
|
||||
"id": { "AlkType:Uint32": true },
|
||||
"score": { "AlkType:Float32": true },
|
||||
"flag": { "AlkType:Uint8": true },
|
||||
"count": { "AlkType:Uint16": true }
|
||||
}
|
||||
});
|
||||
let offset_map = OffsetMap::compute(&schema)?;
|
||||
@@ -62,17 +62,17 @@ fn fixed_size_round_trip_via_offset_map() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_size_round_trip_via_engine_aligned() -> Result<(), TypedefError> {
|
||||
fn fixed_size_round_trip_via_engine_aligned() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"endian": "little",
|
||||
"properties": {
|
||||
"id": { "TypeDef:Uint32": true },
|
||||
"score": { "TypeDef:Float32": true },
|
||||
"flag": { "TypeDef:Uint8": true }
|
||||
"id": { "AlkType:Uint32": true },
|
||||
"score": { "AlkType:Float32": true },
|
||||
"flag": { "AlkType:Uint8": true }
|
||||
}
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let offset_map = engine.offset_map().expect("aligned mode has offset_map");
|
||||
let mut buffer = vec![0u8; offset_map.total_size()];
|
||||
|
||||
@@ -91,7 +91,7 @@ fn fixed_size_round_trip_via_engine_aligned() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_round_trip_via_data_access() -> Result<(), TypedefError> {
|
||||
fn string_round_trip_via_data_access() -> Result<(), AlkTypeError> {
|
||||
let mut buffer = vec![0u8; 32];
|
||||
let written = data_access::write_string(&mut buffer, 0, "hello", "name", Endian::Little)?;
|
||||
assert_eq!(written, 4 + 5);
|
||||
@@ -105,14 +105,14 @@ fn string_round_trip_via_data_access() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_round_trip_via_engine_aligned() -> Result<(), TypedefError> {
|
||||
fn string_round_trip_via_engine_aligned() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"name": { "TypeDef:String": true }
|
||||
"name": { "AlkType:String": true }
|
||||
}
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let offset_map = engine.offset_map().expect("aligned mode has offset_map");
|
||||
let mut buffer = vec![0u8; offset_map.total_size() + 64];
|
||||
engine.write_field(&mut buffer, "name", &FieldValue::String("hello"))?;
|
||||
@@ -124,7 +124,7 @@ fn string_round_trip_via_engine_aligned() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bytes_round_trip_via_data_access() -> Result<(), TypedefError> {
|
||||
fn bytes_round_trip_via_data_access() -> Result<(), AlkTypeError> {
|
||||
let payload = [0xAA, 0xBB, 0xCC, 0xDD];
|
||||
let mut buffer = vec![0u8; 32];
|
||||
let written = data_access::write_bytes(&mut buffer, 0, &payload, "data", Endian::Little)?;
|
||||
@@ -139,18 +139,18 @@ fn bytes_round_trip_via_data_access() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_struct_round_trip_via_offset_map() -> Result<(), TypedefError> {
|
||||
fn nested_struct_round_trip_via_offset_map() -> Result<(), AlkTypeError> {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"header": {
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"version": { "TypeDef:Uint32": true },
|
||||
"magic": { "TypeDef:Uint32": true }
|
||||
"version": { "AlkType:Uint32": true },
|
||||
"magic": { "AlkType:Uint32": true }
|
||||
}
|
||||
},
|
||||
"payload": { "TypeDef:Bytes": true }
|
||||
"payload": { "AlkType:Bytes": true }
|
||||
}
|
||||
});
|
||||
let offset_map = OffsetMap::compute(&schema)?;
|
||||
@@ -208,21 +208,21 @@ fn nested_struct_round_trip_via_offset_map() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_struct_round_trip_via_engine_aligned() -> Result<(), TypedefError> {
|
||||
fn nested_struct_round_trip_via_engine_aligned() -> Result<(), AlkTypeError> {
|
||||
let mut schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"header": {
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"version": { "TypeDef:Uint8": true },
|
||||
"flags": { "TypeDef:Uint8": true }
|
||||
"version": { "AlkType:Uint8": true },
|
||||
"flags": { "AlkType:Uint8": true }
|
||||
}
|
||||
},
|
||||
"payload_len": { "TypeDef:Uint32": true }
|
||||
"payload_len": { "AlkType:Uint32": true }
|
||||
}
|
||||
});
|
||||
let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let engine = AlkTypeEngine::compile(&mut schema, LayoutMode::Aligned)?;
|
||||
let offset_map = engine.offset_map().expect("aligned mode");
|
||||
|
||||
assert_eq!(offset_map.get("header.version").unwrap().start, 0);
|
||||
@@ -250,13 +250,13 @@ fn nested_struct_round_trip_via_engine_aligned() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn big_endian_round_trip_via_offset_map() -> Result<(), TypedefError> {
|
||||
fn big_endian_round_trip_via_offset_map() -> Result<(), AlkTypeError> {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"endian": "big",
|
||||
"properties": {
|
||||
"id": { "TypeDef:Uint32": true },
|
||||
"offset": { "TypeDef:Float64": true }
|
||||
"id": { "AlkType:Uint32": true },
|
||||
"offset": { "AlkType:Float64": true }
|
||||
}
|
||||
});
|
||||
let offset_map = OffsetMap::compute(&schema)?;
|
||||
@@ -288,12 +288,12 @@ fn big_endian_round_trip_via_offset_map() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alignment_padding_round_trip_u8_then_u32() -> Result<(), TypedefError> {
|
||||
fn alignment_padding_round_trip_u8_then_u32() -> Result<(), AlkTypeError> {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"flag": { "TypeDef:Uint8": true },
|
||||
"id": { "TypeDef:Uint32": true }
|
||||
"flag": { "AlkType:Uint8": true },
|
||||
"id": { "AlkType:Uint32": true }
|
||||
}
|
||||
});
|
||||
let offset_map = OffsetMap::compute(&schema)?;
|
||||
@@ -333,14 +333,14 @@ fn alignment_padding_round_trip_u8_then_u32() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packed_layout_round_trip_via_layout_builder() -> Result<(), TypedefError> {
|
||||
fn packed_layout_round_trip_via_layout_builder() -> Result<(), AlkTypeError> {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"endian": "little",
|
||||
"properties": {
|
||||
"flag": { "TypeDef:Uint8": true },
|
||||
"id": { "TypeDef:Uint32": true },
|
||||
"payload": { "TypeDef:String": true }
|
||||
"flag": { "AlkType:Uint8": true },
|
||||
"id": { "AlkType:Uint32": true },
|
||||
"payload": { "AlkType:String": true }
|
||||
}
|
||||
});
|
||||
let builder = LayoutBuilder::new(&schema)?;
|
||||
@@ -390,14 +390,14 @@ fn packed_layout_round_trip_via_layout_builder() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sequential_reader_round_trip_packed_buffer() -> Result<(), TypedefError> {
|
||||
fn sequential_reader_round_trip_packed_buffer() -> Result<(), AlkTypeError> {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"endian": "little",
|
||||
"properties": {
|
||||
"id": { "TypeDef:Uint8": true },
|
||||
"name": { "TypeDef:String": true },
|
||||
"tail": { "TypeDef:Uint8": true }
|
||||
"id": { "AlkType:Uint8": true },
|
||||
"name": { "AlkType:String": true },
|
||||
"tail": { "AlkType:Uint8": true }
|
||||
}
|
||||
});
|
||||
let builder = LayoutBuilder::new(&schema)?;
|
||||
@@ -434,14 +434,14 @@ fn sequential_reader_round_trip_packed_buffer() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sequential_reader_read_field_walks_preceding_fields() -> Result<(), TypedefError> {
|
||||
fn sequential_reader_read_field_walks_preceding_fields() -> Result<(), AlkTypeError> {
|
||||
let schema = json!({
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"endian": "little",
|
||||
"properties": {
|
||||
"a": { "TypeDef:Uint8": true },
|
||||
"b": { "TypeDef:Uint32": true },
|
||||
"c": { "TypeDef:Uint8": true }
|
||||
"a": { "AlkType:Uint8": true },
|
||||
"b": { "AlkType:Uint32": true },
|
||||
"c": { "AlkType:Uint8": true }
|
||||
}
|
||||
});
|
||||
let mut buffer = vec![0u8; 16];
|
||||
@@ -461,13 +461,13 @@ fn sequential_reader_read_field_walks_preceding_fields() -> Result<(), TypedefEr
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tunion_byte_offset_discriminator_dispatch() -> Result<(), TypedefError> {
|
||||
fn tunion_byte_offset_discriminator_dispatch() -> Result<(), AlkTypeError> {
|
||||
let union_schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {
|
||||
"kind": "byte",
|
||||
"offset": 0,
|
||||
"type": "TypeDef:Uint8"
|
||||
"type": "AlkType:Uint8"
|
||||
},
|
||||
"mapping": {
|
||||
"5": { "$ref": "#/$defs/Read" },
|
||||
@@ -475,18 +475,18 @@ fn tunion_byte_offset_discriminator_dispatch() -> Result<(), TypedefError> {
|
||||
},
|
||||
"$defs": {
|
||||
"Read": {
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"handle": { "TypeDef:Uint32": true },
|
||||
"length": { "TypeDef:Uint32": true }
|
||||
"handle": { "AlkType:Uint32": true },
|
||||
"length": { "AlkType:Uint32": true }
|
||||
}
|
||||
},
|
||||
"Write": {
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"handle": { "TypeDef:Uint32": true },
|
||||
"length": { "TypeDef:Uint32": true },
|
||||
"data": { "TypeDef:Uint32": true }
|
||||
"handle": { "AlkType:Uint32": true },
|
||||
"length": { "AlkType:Uint32": true },
|
||||
"data": { "AlkType:Uint32": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -504,7 +504,7 @@ fn tunion_byte_offset_discriminator_dispatch() -> Result<(), TypedefError> {
|
||||
let variant = tunion::resolve_variant(&union_schema, &dispatch.key)?;
|
||||
assert_eq!(
|
||||
variant
|
||||
.get("TypeDef:Struct")
|
||||
.get("AlkType:Struct")
|
||||
.and_then(serde_json::Value::as_bool),
|
||||
Some(true)
|
||||
);
|
||||
@@ -512,20 +512,20 @@ fn tunion_byte_offset_discriminator_dispatch() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tunion_byte_offset_discriminator_size_lookup() -> Result<(), TypedefError> {
|
||||
fn tunion_byte_offset_discriminator_size_lookup() -> Result<(), AlkTypeError> {
|
||||
let u8_schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "TypeDef:Uint8"},
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "AlkType:Uint8"},
|
||||
"mapping": {}
|
||||
});
|
||||
let u16_schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "TypeDef:Uint16"},
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "AlkType:Uint16"},
|
||||
"mapping": {}
|
||||
});
|
||||
let u32_schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "TypeDef:Uint32"},
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "AlkType:Uint32"},
|
||||
"mapping": {}
|
||||
});
|
||||
assert_eq!(tunion::discriminator_size(&u8_schema)?, 1);
|
||||
|
||||
@@ -9,16 +9,16 @@
|
||||
|
||||
use alktype::data_access;
|
||||
use alktype::tunion;
|
||||
use alktype::{Endian, TypedefError};
|
||||
use alktype::{Endian, AlkTypeError};
|
||||
use serde_json::json;
|
||||
|
||||
fn sftp_like_byte_union() -> serde_json::Value {
|
||||
json!({
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {
|
||||
"kind": "byte",
|
||||
"offset": 0,
|
||||
"type": "TypeDef:Uint8"
|
||||
"type": "AlkType:Uint8"
|
||||
},
|
||||
"mapping": {
|
||||
"5": { "$ref": "#/$defs/Read" },
|
||||
@@ -26,18 +26,18 @@ fn sftp_like_byte_union() -> serde_json::Value {
|
||||
},
|
||||
"$defs": {
|
||||
"Read": {
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"handle": { "TypeDef:Uint32": true },
|
||||
"length": { "TypeDef:Uint32": true }
|
||||
"handle": { "AlkType:Uint32": true },
|
||||
"length": { "AlkType:Uint32": true }
|
||||
}
|
||||
},
|
||||
"Write": {
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"handle": { "TypeDef:Uint32": true },
|
||||
"length": { "TypeDef:Uint32": true },
|
||||
"data": { "TypeDef:Uint32": true }
|
||||
"handle": { "AlkType:Uint32": true },
|
||||
"length": { "AlkType:Uint32": true },
|
||||
"data": { "AlkType:Uint32": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ fn sftp_like_byte_union() -> serde_json::Value {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_byte_discriminator_uint8_dispatches_to_read() -> Result<(), TypedefError> {
|
||||
fn read_byte_discriminator_uint8_dispatches_to_read() -> Result<(), AlkTypeError> {
|
||||
let union_schema = sftp_like_byte_union();
|
||||
let mut buffer = vec![0u8; 16];
|
||||
buffer[0] = 5;
|
||||
@@ -59,7 +59,7 @@ fn read_byte_discriminator_uint8_dispatches_to_read() -> Result<(), TypedefError
|
||||
let variant = tunion::resolve_variant(&union_schema, &dispatch.key)?;
|
||||
assert_eq!(
|
||||
variant
|
||||
.get("TypeDef:Struct")
|
||||
.get("AlkType:Struct")
|
||||
.and_then(serde_json::Value::as_bool),
|
||||
Some(true)
|
||||
);
|
||||
@@ -67,7 +67,7 @@ fn read_byte_discriminator_uint8_dispatches_to_read() -> Result<(), TypedefError
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_byte_discriminator_uint8_dispatches_to_write() -> Result<(), TypedefError> {
|
||||
fn read_byte_discriminator_uint8_dispatches_to_write() -> Result<(), AlkTypeError> {
|
||||
let union_schema = sftp_like_byte_union();
|
||||
let mut buffer = vec![0u8; 16];
|
||||
buffer[0] = 6;
|
||||
@@ -88,16 +88,16 @@ fn read_byte_discriminator_uint8_dispatches_to_write() -> Result<(), TypedefErro
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_byte_discriminator_uint16_little_endian() -> Result<(), TypedefError> {
|
||||
fn read_byte_discriminator_uint16_little_endian() -> Result<(), AlkTypeError> {
|
||||
let schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {
|
||||
"kind": "byte",
|
||||
"offset": 2,
|
||||
"type": "TypeDef:Uint16"
|
||||
"type": "AlkType:Uint16"
|
||||
},
|
||||
"mapping": {
|
||||
"5": {"TypeDef:Struct": true, "properties": {"id": {"TypeDef:Uint32": true}}}
|
||||
"5": {"AlkType:Struct": true, "properties": {"id": {"AlkType:Uint32": true}}}
|
||||
}
|
||||
});
|
||||
let mut buffer = vec![0u8; 16];
|
||||
@@ -110,16 +110,16 @@ fn read_byte_discriminator_uint16_little_endian() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_byte_discriminator_uint32_big_endian() -> Result<(), TypedefError> {
|
||||
fn read_byte_discriminator_uint32_big_endian() -> Result<(), AlkTypeError> {
|
||||
let schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {
|
||||
"kind": "byte",
|
||||
"offset": 0,
|
||||
"type": "TypeDef:Uint32"
|
||||
"type": "AlkType:Uint32"
|
||||
},
|
||||
"mapping": {
|
||||
"101": {"TypeDef:Struct": true, "properties": {"id": {"TypeDef:Uint32": true}}}
|
||||
"101": {"AlkType:Struct": true, "properties": {"id": {"AlkType:Uint32": true}}}
|
||||
}
|
||||
});
|
||||
let mut buffer = vec![0u8; 16];
|
||||
@@ -132,21 +132,21 @@ fn read_byte_discriminator_uint32_big_endian() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_byte_discriminator_unknown_value_returns_access_error() -> Result<(), TypedefError> {
|
||||
fn read_byte_discriminator_unknown_value_returns_access_error() -> Result<(), AlkTypeError> {
|
||||
let union_schema = sftp_like_byte_union();
|
||||
let buffer = [99u8, 0x00, 0x00, 0x00];
|
||||
let err = tunion::read_byte_discriminator(&buffer, &union_schema, Endian::Big).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_field_discriminator_string_dispatches_to_read() -> Result<(), TypedefError> {
|
||||
fn read_field_discriminator_string_dispatches_to_read() -> Result<(), AlkTypeError> {
|
||||
let union_schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "field", "name": "type"},
|
||||
"properties": {
|
||||
"type": { "TypeDef:String": true }
|
||||
"type": { "AlkType:String": true }
|
||||
},
|
||||
"mapping": {
|
||||
"read": {"$ref": "#/$defs/Read"},
|
||||
@@ -154,17 +154,17 @@ fn read_field_discriminator_string_dispatches_to_read() -> Result<(), TypedefErr
|
||||
},
|
||||
"$defs": {
|
||||
"Read": {
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"handle": { "TypeDef:Uint32": true },
|
||||
"length": { "TypeDef:Uint32": true }
|
||||
"handle": { "AlkType:Uint32": true },
|
||||
"length": { "AlkType:Uint32": true }
|
||||
}
|
||||
},
|
||||
"Write": {
|
||||
"TypeDef:Struct": true,
|
||||
"AlkType:Struct": true,
|
||||
"properties": {
|
||||
"handle": { "TypeDef:Uint32": true },
|
||||
"data": { "TypeDef:Bytes": true }
|
||||
"handle": { "AlkType:Uint32": true },
|
||||
"data": { "AlkType:Bytes": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,7 +180,7 @@ fn read_field_discriminator_string_dispatches_to_read() -> Result<(), TypedefErr
|
||||
let variant = tunion::resolve_variant(&union_schema, &dispatch.key)?;
|
||||
assert_eq!(
|
||||
variant
|
||||
.get("TypeDef:Struct")
|
||||
.get("AlkType:Struct")
|
||||
.and_then(serde_json::Value::as_bool),
|
||||
Some(true)
|
||||
);
|
||||
@@ -188,12 +188,12 @@ fn read_field_discriminator_string_dispatches_to_read() -> Result<(), TypedefErr
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_field_discriminator_string_dispatches_to_write() -> Result<(), TypedefError> {
|
||||
fn read_field_discriminator_string_dispatches_to_write() -> Result<(), AlkTypeError> {
|
||||
let union_schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "field", "name": "type"},
|
||||
"properties": {
|
||||
"type": { "TypeDef:String": true }
|
||||
"type": { "AlkType:String": true }
|
||||
},
|
||||
"mapping": {
|
||||
"read": {"$ref": "#/$defs/Read"},
|
||||
@@ -201,12 +201,12 @@ fn read_field_discriminator_string_dispatches_to_write() -> Result<(), TypedefEr
|
||||
},
|
||||
"$defs": {
|
||||
"Read": {
|
||||
"TypeDef:Struct": true,
|
||||
"properties": {"x": {"TypeDef:Uint8": true}}
|
||||
"AlkType:Struct": true,
|
||||
"properties": {"x": {"AlkType:Uint8": true}}
|
||||
},
|
||||
"Write": {
|
||||
"TypeDef:Struct": true,
|
||||
"properties": {"y": {"TypeDef:Uint16": true}}
|
||||
"AlkType:Struct": true,
|
||||
"properties": {"y": {"AlkType:Uint16": true}}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -228,16 +228,16 @@ fn read_field_discriminator_string_dispatches_to_write() -> Result<(), TypedefEr
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_field_discriminator_uint8_field() -> Result<(), TypedefError> {
|
||||
fn read_field_discriminator_uint8_field() -> Result<(), AlkTypeError> {
|
||||
let union_schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "field", "name": "tag"},
|
||||
"properties": {
|
||||
"tag": { "TypeDef:Uint8": true }
|
||||
"tag": { "AlkType:Uint8": true }
|
||||
},
|
||||
"mapping": {
|
||||
"0": {"TypeDef:Struct": true, "properties": {"a": {"TypeDef:Uint32": true}}},
|
||||
"1": {"TypeDef:Struct": true, "properties": {"b": {"TypeDef:Uint16": true}}}
|
||||
"0": {"AlkType:Struct": true, "properties": {"a": {"AlkType:Uint32": true}}},
|
||||
"1": {"AlkType:Struct": true, "properties": {"b": {"AlkType:Uint16": true}}}
|
||||
}
|
||||
});
|
||||
let mut buffer = vec![0u8; 8];
|
||||
@@ -254,40 +254,40 @@ fn read_field_discriminator_uint8_field() -> Result<(), TypedefError> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_field_discriminator_unknown_value_returns_access_error() -> Result<(), TypedefError> {
|
||||
fn read_field_discriminator_unknown_value_returns_access_error() -> Result<(), AlkTypeError> {
|
||||
let union_schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "field", "name": "tag"},
|
||||
"properties": {
|
||||
"tag": { "TypeDef:Uint8": true }
|
||||
"tag": { "AlkType:Uint8": true }
|
||||
},
|
||||
"mapping": {
|
||||
"0": {"TypeDef:Struct": true, "properties": {"a": {"TypeDef:Uint32": true}}}
|
||||
"0": {"AlkType:Struct": true, "properties": {"a": {"AlkType:Uint32": true}}}
|
||||
}
|
||||
});
|
||||
let mut buffer = vec![0u8; 8];
|
||||
buffer[0] = 99;
|
||||
let err =
|
||||
tunion::read_field_discriminator(&buffer, &union_schema, 0, Endian::Little).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discriminator_size_returns_correct_values() -> Result<(), TypedefError> {
|
||||
fn discriminator_size_returns_correct_values() -> Result<(), AlkTypeError> {
|
||||
let u8_schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "TypeDef:Uint8"},
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "AlkType:Uint8"},
|
||||
"mapping": {}
|
||||
});
|
||||
let u16_schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "TypeDef:Uint16"},
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "AlkType:Uint16"},
|
||||
"mapping": {}
|
||||
});
|
||||
let u32_schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "TypeDef:Uint32"},
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "byte", "type": "AlkType:Uint32"},
|
||||
"mapping": {}
|
||||
});
|
||||
assert_eq!(tunion::discriminator_size(&u8_schema)?, 1);
|
||||
@@ -299,25 +299,25 @@ fn discriminator_size_returns_correct_values() -> Result<(), TypedefError> {
|
||||
#[test]
|
||||
fn discriminator_size_field_kind_returns_schema_error() {
|
||||
let schema = json!({
|
||||
"TypeDef:Union": true,
|
||||
"AlkType:Union": true,
|
||||
"discriminator": {"kind": "field", "name": "type"},
|
||||
"properties": {"type": {"TypeDef:Uint8": true}},
|
||||
"properties": {"type": {"AlkType:Uint8": true}},
|
||||
"mapping": {}
|
||||
});
|
||||
let err = tunion::discriminator_size(&schema).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_variant_returns_schema_error_for_unknown_key() {
|
||||
let union_schema = sftp_like_byte_union();
|
||||
let err = tunion::resolve_variant(&union_schema, "999").unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_discriminator_missing_returns_schema_error() {
|
||||
let schema = json!({"TypeDef:Union": true});
|
||||
let schema = json!({"AlkType:Union": true});
|
||||
let err = alktype::parse_discriminator(&schema).unwrap_err();
|
||||
assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}");
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user