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:
2026-08-02 07:05:53 +00:00
parent 1cfb3638d1
commit 5e268e8f47
31 changed files with 1851 additions and 1851 deletions

View File

@@ -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]

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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`)

View File

@@ -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

View File

@@ -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.

View File

@@ -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) —

View File

@@ -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).

View File

@@ -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>;

View File

@@ -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).

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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]

View File

@@ -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)
);
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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)
}
};

View File

@@ -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]

View File

@@ -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

View File

@@ -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(_)));
}
}

View File

@@ -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:?}");
}
}

View File

@@ -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()];

View File

@@ -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:?}");
}

View File

@@ -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);

View File

@@ -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:?}");
}