Draft builder API (ADR-009) and generalized validation validate_bytes (ADR-010) for v0.1.0
- ADR-009: fluent Rust builder producing serde_json::Value, covers AlkType kinds + standard JSON Schema; resolves OQ-003 (alkcall is the unblocking consumer) - ADR-010: AlkTypeEngine::validate_bytes(&[u8]) as the single-call binary-buffer validation entry point; materialize Value from bytes, then validate; two methods on one struct, not a trait - builder.md: full builder API spec (Schema, Definitions, Discriminator) with four usage examples (channels chunk header, call input schema, SFTP Packet union, OperationSpec error schemas) - validation.md: new validate_bytes subsection + entry-point comparison table; AlkTypeEngine impl block updated; design decisions table updated - overview.md: builder.md added to component pointers; 'Not a schema builder' scope boundary retired; ADR-009/010 added to decisions table; OQ-003 marked resolved; Consumers table adds alkcall as first consumer - open-questions.md + questions/003: OQ-003 moved from deferred(scope) to resolved (ADR-009) - README.md: builder doc + ADR-009/010 added; OQ-003 marked resolved; two new Key Design Principles (9, 10) for v0.1.0 additions Doc-only change; 346 tests pass, clippy clean.
This commit is contained in:
1 parent
57d8ed25ba
commit
1a8a44ed0e
8 files changed
+1250
-39
No files matched your search
@@ -0,0 +1,631 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-08-11
|
||||
---
|
||||
|
||||
# alktype — Builder API
|
||||
|
||||
The builder layer: a fluent Rust API for constructing alktype JSON
|
||||
Schemas (both `AlkType:*`-bearing binary-layout schemas and plain
|
||||
JSON-Schema-only operation payload schemas) at runtime, producing
|
||||
`serde_json::Value`. Decided in [ADR-009](decisions/009-builder-api.md);
|
||||
resolves [OQ-003](questions/003-builder-api-for-schema-construction.md).
|
||||
|
||||
## What
|
||||
|
||||
The `builder` module provides a single `Schema` builder type and a
|
||||
`Definitions` helper for named `$defs`. The builder's `.build()` method
|
||||
returns a `serde_json::Value` — the same form alktype already consumes
|
||||
via `AlkTypeEngine::compile` (for `AlkType:*` schemas) and the same form
|
||||
`OperationSpec.input_schema` / `output_schema` / `error_schemas` hold
|
||||
(for plain JSON Schema, no `AlkType:*` kinds).
|
||||
|
||||
The builder covers:
|
||||
|
||||
- All 19 `AlkType:*` kinds (binary-layout schemas) — see
|
||||
[schema-layer.md](schema-layer.md) for the kinds.
|
||||
- All standard JSON Schema keywords needed for operation payload
|
||||
schemas: `type`, `properties`, `required`, `items`, `enum`, `format`,
|
||||
`additionalProperties`, `minimum`, `maximum`, `minItems`,
|
||||
`maxItems`, `minLength`, `maxLength`, `$ref`, `$defs`.
|
||||
- All ADR-003 schema annotations: `endian`, `align`, `encoding`
|
||||
(length-prefixed / offset-indirect), `maxLength`-as-reservation, and
|
||||
TUnion `discriminator` (byte-offset and field-name).
|
||||
|
||||
## Why
|
||||
|
||||
alkcall (the merged `alknet-call` + `alknet-channels` extraction) is
|
||||
alktype's first consumer and needs to build schemas at runtime from
|
||||
Rust code, for two roles:
|
||||
|
||||
1. **Binary layout schemas** (channels' 8-byte chunk header, future
|
||||
binary call frames) — `AlkType:*` schemas, fed to
|
||||
`AlkTypeEngine::compile` (packed mode, big-endian).
|
||||
2. **JSON payload schemas** (call's `OperationSpec.input_schema` /
|
||||
`output_schema` / `error_schemas`) — plain JSON Schema, no
|
||||
`AlkType:*` kinds, validated via the standard `jsonschema` validator.
|
||||
|
||||
A single builder serving both roles means alkcall imports one module
|
||||
for schema construction. See [ADR-009](decisions/009-builder-api.md)
|
||||
for the decision rationale (why `Value` not a typed `Schema` enum, why
|
||||
both AlkType and standard JSON Schema in one builder).
|
||||
|
||||
## Architecture
|
||||
|
||||
### Output type: `serde_json::Value`
|
||||
|
||||
The builder returns `serde_json::Value`. There is no typed `Schema`
|
||||
enum. This matches ADR-001's "schemas are JSON" principle and avoids
|
||||
duplicating the JSON form that `AlkTypeEngine::compile`,
|
||||
`jsonschema::Validator`, and `OperationSpec` all consume.
|
||||
|
||||
### Module placement
|
||||
|
||||
`src/builder.rs`, re-exported from the crate root. The builder is a
|
||||
peer of `schema.rs` (which parses schemas) and `engine.rs` (which
|
||||
compiles them). The builder constructs; it does not parse or compile.
|
||||
|
||||
```rust
|
||||
// src/lib.rs (additions)
|
||||
pub mod builder;
|
||||
pub use builder::{Schema, Definitions};
|
||||
```
|
||||
|
||||
### Field order is load-bearing
|
||||
|
||||
`serde_json` with `preserve_order` is already a dependency (ADR-001).
|
||||
The builder's `Value` output uses `serde_json::Map` (which preserves
|
||||
insertion order under `preserve_order`), so field declaration order in
|
||||
the builder is the field order in the binary layout. This is critical
|
||||
for packed mode (ADR-002) where field order determines offsets.
|
||||
|
||||
## Public API
|
||||
|
||||
### `Schema` builder
|
||||
|
||||
`Schema` is the single entry point. Constructors for each AlkType kind
|
||||
and each standard JSON Schema type; setters for annotations and
|
||||
constraints; `.build()` produces `Value`.
|
||||
|
||||
#### AlkType kind constructors
|
||||
|
||||
One constructor per `AlkTypeKind` variant (see [schema-layer.md](schema-layer.md)
|
||||
§"The 19 AlkType Kinds"):
|
||||
|
||||
```rust
|
||||
impl Schema {
|
||||
// Fixed-size integer kinds
|
||||
pub fn int8() -> Self;
|
||||
pub fn int16() -> Self;
|
||||
pub fn int32() -> Self;
|
||||
pub fn int64() -> Self;
|
||||
pub fn uint8() -> Self;
|
||||
pub fn uint16() -> Self;
|
||||
pub fn uint32() -> Self;
|
||||
pub fn uint64() -> Self;
|
||||
// Float kinds
|
||||
pub fn float32() -> Self;
|
||||
pub fn float64() -> Self;
|
||||
// Other fixed-size
|
||||
pub fn boolean() -> Self;
|
||||
pub fn enum_of(values: &[&str]) -> Self; // u32 index; values in declaration order
|
||||
// Variable-length kinds
|
||||
pub fn string() -> Self;
|
||||
pub fn bytes() -> Self;
|
||||
pub fn timestamp() -> Self;
|
||||
// Composite kinds
|
||||
pub fn struct_() -> Self; // fields added via .field()
|
||||
pub fn union_(disc: Discriminator) -> Self; // variants via .mapping()
|
||||
pub fn array_of(element: Schema) -> Self;
|
||||
pub fn record_of(value: Schema) -> Self;
|
||||
}
|
||||
```
|
||||
|
||||
Each constructor sets the corresponding `"AlkType:<Kind>": true` key.
|
||||
For example, `Schema::uint32()` produces `{"AlkType:Uint32": true}`.
|
||||
|
||||
**`enum_of`** sets both `"AlkType:Enum": true` and the standard
|
||||
`"enum"` keyword with the provided values (declaration order is the
|
||||
index order — see [schema-layer.md](schema-layer.md) §"TEnum binary
|
||||
representation"):
|
||||
|
||||
```rust
|
||||
Schema::enum_of(&["read", "write", "execute"])
|
||||
// -> { "AlkType:Enum": true, "enum": ["read", "write", "execute"] }
|
||||
```
|
||||
|
||||
**`array_of`** and **`record_of`** take the element/value schema as a
|
||||
nested `Schema`:
|
||||
|
||||
```rust
|
||||
Schema::array_of(Schema::uint32())
|
||||
// -> { "AlkType:Array": true, "items": { "AlkType:Uint32": true } }
|
||||
|
||||
Schema::record_of(Schema::float32())
|
||||
// -> { "AlkType:Record": true, "values": { "AlkType:Float32": true } }
|
||||
```
|
||||
|
||||
#### Standard JSON Schema type constructors
|
||||
|
||||
For plain JSON Schema (no `AlkType:*` kinds) — call's
|
||||
`input_schema` / `output_schema` / `error_schemas`:
|
||||
|
||||
```rust
|
||||
impl Schema {
|
||||
pub fn object() -> Self; // type: "object" — fields via .field()
|
||||
pub fn array() -> Self; // type: "array" — items via .items()
|
||||
pub fn string_() -> Self; // type: "string"
|
||||
pub fn integer() -> Self; // type: "integer"
|
||||
pub fn number() -> Self; // type: "number"
|
||||
pub fn boolean_()-> Self; // type: "boolean"
|
||||
pub fn null() -> Self; // type: "null"
|
||||
pub fn any() -> Self; // {} (no type constraint)
|
||||
}
|
||||
```
|
||||
|
||||
The `_` suffix disambiguates standard JSON Schema types from AlkType
|
||||
kinds (`string` is the AlkType kind; `string_` is the standard JSON
|
||||
Schema type — the AlkType kind constructor sets `"AlkType:String":
|
||||
true`, the standard constructor sets `"type": "string"`). This is
|
||||
deliberate: the two are distinct schema forms and the builder makes
|
||||
the distinction visible at the call site.
|
||||
|
||||
#### Annotation setters
|
||||
|
||||
Annotation setters mirror ADR-003. Each setter is named after the
|
||||
annotation it produces; calling the setter sets the corresponding JSON
|
||||
key. Setters return `Self` for chaining.
|
||||
|
||||
```rust
|
||||
impl Schema {
|
||||
/// Schema-level endianness (ADR-003 §1). Default little.
|
||||
pub fn endian(mut self, endian: Endian) -> Self;
|
||||
|
||||
/// Struct or field alignment (ADR-003 §2). Struct-level sets the
|
||||
/// default; field-level overrides.
|
||||
pub fn align(mut self, align: usize) -> Self;
|
||||
|
||||
/// Variable-length encoding strategy (ADR-003 §3).
|
||||
/// Default length-prefixed.
|
||||
pub fn encoding(mut self, encoding: VariableEncoding) -> Self;
|
||||
|
||||
/// maxLength — standard JSON Schema keyword. In aligned mode with
|
||||
/// a variable-length type, reserves this many bytes (strategy 2).
|
||||
/// In packed mode, validation constraint only.
|
||||
pub fn max_length(mut self, max: usize) -> Self;
|
||||
}
|
||||
```
|
||||
|
||||
`Endian` and `VariableEncoding` are re-exported from `schema.rs` (no
|
||||
new types — the builder uses the existing enums). The setters produce
|
||||
the exact JSON shapes from ADR-003:
|
||||
|
||||
```rust
|
||||
Schema::struct_()
|
||||
.endian(Endian::Big)
|
||||
.field("channel_id", Schema::uint32())
|
||||
.field("length", Schema::uint32())
|
||||
.build()
|
||||
// -> {
|
||||
// "AlkType:Struct": true,
|
||||
// "endian": "big",
|
||||
// "properties": {
|
||||
// "channel_id": { "AlkType:Uint32": true },
|
||||
// "length": { "AlkType:Uint32": true }
|
||||
// }
|
||||
// }
|
||||
```
|
||||
|
||||
#### Composite builders
|
||||
|
||||
`struct_()`, `union_()`, `array_of()`, `record_of()` are the
|
||||
composite constructors. `struct_()` and `union_()` need additional
|
||||
setters to populate their children:
|
||||
|
||||
```rust
|
||||
impl Schema {
|
||||
/// Add a field to a struct (or object). Field order is load-bearing
|
||||
/// for binary layouts (packed mode field order = byte order).
|
||||
/// The field's schema is built from the passed `Schema`.
|
||||
pub fn field(mut self, name: &str, field: Schema) -> Self;
|
||||
|
||||
/// Mark fields as required (standard JSON Schema `required` keyword).
|
||||
/// Can be called multiple times; required names accumulate.
|
||||
/// Field names must have been added via `.field()`.
|
||||
pub fn required(mut self, names: &[&str]) -> Self;
|
||||
|
||||
/// Set the items schema for a standard `array` type.
|
||||
pub fn items(mut self, item: Schema) -> Self;
|
||||
|
||||
/// Set the additionalProperties schema for a standard `object` type.
|
||||
pub fn additional_properties(mut self, props: Schema) -> Self;
|
||||
|
||||
/// Add a variant to a union. `disc_value` is the stringified
|
||||
/// discriminator value (mapping key). The variant schema is built
|
||||
/// from the passed `Schema`.
|
||||
pub fn mapping(mut self, disc_value: &str, variant: Schema) -> Self;
|
||||
}
|
||||
```
|
||||
|
||||
**`field`** sets `properties[name] = field.build()`. Repeated calls
|
||||
append. Field order in the built `Value` is the call order (because
|
||||
`serde_json::Map` preserves insertion order under `preserve_order`).
|
||||
|
||||
**`required`** sets the standard JSON Schema `"required"` array. The
|
||||
builder does not check that the named fields exist (that's a
|
||||
compile-time check at `AlkTypeEngine::compile`, per ADR-004's
|
||||
load-time validation strategy — see [ADR-009](decisions/009-builder-api.md)
|
||||
§"What the builder is not"). Calling `required` multiple times
|
||||
accumulates names:
|
||||
|
||||
```rust
|
||||
Schema::object()
|
||||
.field("path", Schema::string_())
|
||||
.field("offset", Schema::integer().minimum(0))
|
||||
.field("length", Schema::integer().minimum(0))
|
||||
.required(["path"])
|
||||
.required(["offset", "length"])
|
||||
// -> {
|
||||
// "type": "object",
|
||||
// "properties": { "path": {...}, "offset": {...}, "length": {...} },
|
||||
// "required": ["path", "offset", "length"]
|
||||
// }
|
||||
```
|
||||
|
||||
#### Constraint setters (standard JSON Schema)
|
||||
|
||||
For operation payload schemas (call's `input_schema` etc.):
|
||||
|
||||
```rust
|
||||
impl Schema {
|
||||
/// `minimum` (inclusive lower bound for numbers/integers).
|
||||
pub fn minimum(mut self, min: f64) -> Self;
|
||||
|
||||
/// `maximum` (inclusive upper bound for numbers/integers).
|
||||
pub fn maximum(mut self, max: f64) -> Self;
|
||||
|
||||
/// `minLength` (minimum string length).
|
||||
pub fn min_length(mut self, min: usize) -> Self;
|
||||
|
||||
/// `minItems` (minimum array length).
|
||||
pub fn min_items(mut self, min: usize) -> Self;
|
||||
|
||||
/// `maxItems` (maximum array length).
|
||||
pub fn max_items(mut self, max: usize) -> Self;
|
||||
|
||||
/// `format` (e.g. "date-time", "uri", "email").
|
||||
pub fn format(mut self, fmt: &str) -> Self;
|
||||
|
||||
/// `title` (human-readable description).
|
||||
pub fn title(mut self, t: &str) -> Self;
|
||||
|
||||
/// `description` (human-readable description).
|
||||
pub fn description(mut self, d: &str) -> Self;
|
||||
}
|
||||
```
|
||||
|
||||
These set the corresponding standard JSON Schema keywords. They apply
|
||||
to both AlkType-kind schemas and standard JSON Schema type schemas
|
||||
(e.g., `Schema::string().max_length(4096)` sets `maxLength`, which
|
||||
serves as both a validation constraint and, in aligned mode, a
|
||||
fixed-size reservation — ADR-003 §3).
|
||||
|
||||
#### `.build()`
|
||||
|
||||
```rust
|
||||
impl Schema {
|
||||
/// Produce the final `serde_json::Value`.
|
||||
pub fn build(self) -> Value;
|
||||
}
|
||||
```
|
||||
|
||||
Consumes the builder and returns the assembled `Value`. The builder is
|
||||
not `Clone` (to discourage partial builds); each `Schema` is consumed
|
||||
once. To reuse a sub-schema, build it once and pass the `Value` to a
|
||||
`Schema::from_value` constructor (below).
|
||||
|
||||
#### `Schema::from_value` — adopt an existing Value
|
||||
|
||||
```rust
|
||||
impl Schema {
|
||||
/// Adopt an existing JSON Schema `Value` as a `Schema`, for
|
||||
/// composition with builder-constructed schemas. Does not validate
|
||||
/// the schema; just wraps it.
|
||||
pub fn from_value(value: Value) -> Self;
|
||||
}
|
||||
```
|
||||
|
||||
For consumers that have some schemas as JSON (e.g., loaded from a
|
||||
TypeBox-produced file) and others built via the fluent API, and want
|
||||
to compose them. `from_value` wraps the `Value` so it can be passed to
|
||||
`.field()` / `.items()` / `.mapping()` like any other `Schema`.
|
||||
|
||||
### `Discriminator` for `union_()`
|
||||
|
||||
`union_()` takes a `Discriminator` describing the union's dispatch
|
||||
mechanism. This mirrors `schema.rs::DiscriminatorKind` but with a
|
||||
builder-friendly shape (the kind enum is re-exported from `schema.rs`,
|
||||
not duplicated):
|
||||
|
||||
```rust
|
||||
pub enum Discriminator {
|
||||
/// Byte-offset discriminator (ADR-003 §4 Kind A).
|
||||
/// `offset` is the byte position; `disc_type` is the AlkType kind
|
||||
/// of the discriminator (Uint8/Uint16/Uint32).
|
||||
Byte {
|
||||
offset: usize,
|
||||
disc_type: AlkTypeKind, // restricted to Uint8/Uint16/Uint32
|
||||
},
|
||||
/// Field-name discriminator (ADR-003 §4 Kind B).
|
||||
/// `name` is the field holding the discriminator value.
|
||||
Field {
|
||||
name: &str, // or String — see "Open question" below
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Byte-offset example** (channels-style protocol dispatch):
|
||||
|
||||
```rust
|
||||
let packet = Schema::union_(Discriminator::Byte {
|
||||
offset: 0,
|
||||
disc_type: AlkTypeKind::Uint8,
|
||||
})
|
||||
.mapping("5", Schema::ref_def("Read"))
|
||||
.mapping("6", Schema::ref_def("Write"))
|
||||
.mapping("101", Schema::ref_def("Status"))
|
||||
.build();
|
||||
// -> {
|
||||
// "AlkType:Union": true,
|
||||
// "discriminator": { "kind": "byte", "offset": 0, "type": "AlkType:Uint8" },
|
||||
// "mapping": { "5": {"$ref":"#/$defs/Read"}, "6": {...}, "101": {...} }
|
||||
// }
|
||||
```
|
||||
|
||||
**Field-name example** (typedef.ts pattern):
|
||||
|
||||
```rust
|
||||
let event = Schema::union_(Discriminator::Field { name: "type" })
|
||||
.mapping("read", Schema::ref_def("Read"))
|
||||
.mapping("write", Schema::ref_def("Write"))
|
||||
.build();
|
||||
// -> {
|
||||
// "AlkType:Union": true,
|
||||
// "discriminator": { "kind": "field", "name": "type" },
|
||||
// "mapping": { "read": {...}, "write": {...} }
|
||||
// }
|
||||
```
|
||||
|
||||
### `Definitions` — named `$defs` for cross-reference
|
||||
|
||||
`Definitions` is a helper for building named `$defs` that schemas can
|
||||
`$ref` by name. This is the ergonomics win for alkcall's
|
||||
`OperationSpec`, where input/output/error schemas reference shared
|
||||
definitions (e.g., `FileNotFound`, `RateLimited`).
|
||||
|
||||
```rust
|
||||
pub struct Definitions { /* ... */ }
|
||||
|
||||
impl Definitions {
|
||||
pub fn new() -> Self;
|
||||
|
||||
/// Define a named schema. Returns a `Schema` that produces
|
||||
/// `{"$ref": "#/$defs/<name>"}` — the JSON Pointer form that
|
||||
/// `jsonschema` and `AlkTypeEngine::compile` expect (after
|
||||
/// `normalize_refs`, which the engine runs at compile time).
|
||||
pub fn define(&mut self, name: &str, schema: Schema) -> Schema;
|
||||
|
||||
/// Like `define`, but the schema is an existing `Value` (adopted
|
||||
/// via `Schema::from_value`).
|
||||
pub fn define_value(&mut self, name: &str, value: Value) -> Schema;
|
||||
|
||||
/// Produce the `{"$defs": { ... }}` object to merge into a
|
||||
/// top-level schema. Call once at the end.
|
||||
pub fn build(self) -> Value;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```rust
|
||||
let mut defs = Definitions::new();
|
||||
|
||||
let file_not_found = defs.define("FileNotFound",
|
||||
Schema::object()
|
||||
.field("path", Schema::string_())
|
||||
.field("errno", Schema::integer())
|
||||
.required(["path", "errno"])
|
||||
);
|
||||
|
||||
let rate_limited = defs.define("RateLimited",
|
||||
Schema::object()
|
||||
.field("retry_after_ms", Schema::integer().minimum(0))
|
||||
.required(["retry_after_ms"])
|
||||
);
|
||||
|
||||
let read_file_error = Schema::object()
|
||||
.field("code", Schema::string_())
|
||||
.field("details", Schema::any()) // one of the defined errors
|
||||
.required(["code"])
|
||||
.build();
|
||||
|
||||
// Merge $defs into the top-level schema that references them
|
||||
let mut top = Schema::object()
|
||||
.field("error", read_file_error)
|
||||
.build();
|
||||
top.as_object_mut().unwrap().insert("$defs".to_string(), defs.build());
|
||||
```
|
||||
|
||||
`define` returns a `Schema` (the `$ref` to the definition), so it can
|
||||
be passed directly to `.field()` / `.mapping()` / `.items()` without a
|
||||
separate `ref_def` call:
|
||||
|
||||
```rust
|
||||
let op = Schema::object()
|
||||
.field("error", defs.define("FileNotFound", /* ... */))
|
||||
.build();
|
||||
```
|
||||
|
||||
#### `Schema::ref_def` — reference a definition by name
|
||||
|
||||
```rust
|
||||
impl Schema {
|
||||
/// Produce a `{"$ref": "#/$defs/<name>"}` schema. The definition
|
||||
/// must exist in the `$defs` of the top-level schema at compile
|
||||
/// time. The builder does not check this.
|
||||
pub fn ref_def(name: &str) -> Self;
|
||||
}
|
||||
```
|
||||
|
||||
For cases where the `Definitions::define` return value isn't handy
|
||||
(e.g., referencing a definition defined elsewhere). Produces the same
|
||||
`{"$ref": "#/$defs/<name>"}` form.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Example 1: channels' 8-byte chunk header (binary layout)
|
||||
|
||||
```rust
|
||||
use alktype::{Schema, Endian};
|
||||
|
||||
let chunk_header = Schema::struct_()
|
||||
.endian(Endian::Big)
|
||||
.field("channel_id", Schema::uint32())
|
||||
.field("length", Schema::uint32())
|
||||
.build();
|
||||
|
||||
// -> {
|
||||
// "AlkType:Struct": true,
|
||||
// "endian": "big",
|
||||
// "properties": {
|
||||
// "channel_id": { "AlkType:Uint32": true },
|
||||
// "length": { "AlkType:Uint32": true }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Feed to AlkTypeEngine::compile(&mut chunk_header, LayoutMode::Packed)
|
||||
// then validate incoming frames via engine.validate_bytes(&frame).
|
||||
```
|
||||
|
||||
### Example 2: call's `OperationSpec` input schema (JSON payload)
|
||||
|
||||
```rust
|
||||
use alktype::Schema;
|
||||
|
||||
let read_file_input = Schema::object()
|
||||
.field("path", Schema::string_().max_length(4096))
|
||||
.field("offset", Schema::integer().minimum(0))
|
||||
.field("length", Schema::integer().minimum(0))
|
||||
.required(["path"])
|
||||
.build();
|
||||
|
||||
// -> {
|
||||
// "type": "object",
|
||||
// "properties": {
|
||||
// "path": { "type": "string", "maxLength": 4096 },
|
||||
// "offset": { "type": "integer", "minimum": 0 },
|
||||
// "length": { "type": "integer", "minimum": 0 }
|
||||
// },
|
||||
// "required": ["path"]
|
||||
// }
|
||||
//
|
||||
// Stored in OperationSpec.input_schema; validated via the standard
|
||||
// jsonschema validator (validate_json for parsed payloads, or via
|
||||
// serde_json::from_slice then validate_json for wire frames).
|
||||
```
|
||||
|
||||
### Example 3: SFTP `Packet` union (binary layout, byte discriminator)
|
||||
|
||||
```rust
|
||||
use alktype::{Schema, Discriminator, AlkTypeKind};
|
||||
|
||||
let mut defs = Definitions::new();
|
||||
defs.define("Init", Schema::struct_().field("version", Schema::uint32()));
|
||||
defs.define("Open", Schema::struct_().field("path", Schema::string()).field("flags", Schema::uint32()));
|
||||
defs.define("Read", Schema::struct_().field("handle", Schema::bytes()).field("offset", Schema::uint64()).field("len", Schema::uint32()));
|
||||
defs.define("Write", Schema::struct_().field("handle", Schema::bytes()).field("offset", Schema::uint64()).field("data", Schema::bytes()));
|
||||
defs.define("Status", Schema::struct_().field("code", Schema::uint32()).field("message", Schema::string()));
|
||||
|
||||
let packet = Schema::union_(Discriminator::Byte {
|
||||
offset: 0,
|
||||
disc_type: AlkTypeKind::Uint8,
|
||||
})
|
||||
.mapping("1", Schema::ref_def("Init"))
|
||||
.mapping("3", Schema::ref_def("Open"))
|
||||
.mapping("5", Schema::ref_def("Read"))
|
||||
.mapping("6", Schema::ref_def("Write"))
|
||||
.mapping("101", Schema::ref_def("Status"))
|
||||
.build();
|
||||
```
|
||||
|
||||
### Example 4: OperationSpec error schemas (named `$defs`)
|
||||
|
||||
```rust
|
||||
use alktype::{Definitions, Schema};
|
||||
|
||||
let mut defs = Definitions::new();
|
||||
|
||||
let file_not_found = defs.define("FileNotFound",
|
||||
Schema::object()
|
||||
.field("path", Schema::string_())
|
||||
.field("errno", Schema::integer())
|
||||
.required(["path", "errno"])
|
||||
);
|
||||
|
||||
let rate_limited = defs.define("RateLimited",
|
||||
Schema::object()
|
||||
.field("retry_after_ms", Schema::integer().minimum(0))
|
||||
.required(["retry_after_ms"])
|
||||
);
|
||||
|
||||
// An operation's error schemas reference these definitions
|
||||
let op_errors = vec![
|
||||
ErrorDefinition {
|
||||
code: "FILE_NOT_FOUND".to_string(),
|
||||
description: "File not found".to_string(),
|
||||
schema: Schema::ref_def("FileNotFound").build(),
|
||||
http_status: Some(404),
|
||||
},
|
||||
ErrorDefinition {
|
||||
code: "RATE_LIMITED".to_string(),
|
||||
description: "Rate limited".to_string(),
|
||||
schema: Schema::ref_def("RateLimited").build(),
|
||||
http_status: Some(429),
|
||||
},
|
||||
];
|
||||
// $defs is built once and stored alongside the OperationSpec
|
||||
```
|
||||
|
||||
## Design Decisions
|
||||
|
||||
| Decision | ADR | Summary |
|
||||
|----------|-----|---------|
|
||||
| Builder API for schema construction | [ADR-009](decisions/009-builder-api.md) | Fluent Rust API producing `serde_json::Value`; covers AlkType kinds + standard JSON Schema; resolves OQ-003 |
|
||||
| Schema annotations | [ADR-003](decisions/003-schema-annotations.md) | The annotation shapes the builder's setters produce |
|
||||
| Load-time validation strategy | [ADR-004](decisions/004-error-handling-validation-strategy.md) | The builder does not pre-validate; compile-time is the validation point |
|
||||
|
||||
## Open Questions
|
||||
|
||||
- **OQ-004** (open): `Discriminator::Field` name type — `&str` or
|
||||
`String`? The builder consumes `Self` on each setter, so the
|
||||
discriminator's `name` needs to outlive the `Schema` it's embedded
|
||||
in. `&str` borrows; `String` owns. Resolve during implementation
|
||||
(likely `String` for ownership simplicity, since the builder owns
|
||||
its content until `.build()`).
|
||||
|
||||
## References
|
||||
|
||||
- [ADR-009](decisions/009-builder-api.md) — the decision this spec implements
|
||||
- [ADR-001](decisions/001-alktype-purpose-scope-jsonschema-engine.md) —
|
||||
scope boundaries this module extends; "schemas are JSON" principle
|
||||
- [ADR-003](decisions/003-schema-annotations.md) — the annotation
|
||||
shapes the builder's setters produce
|
||||
- [schema-layer.md](schema-layer.md) — the 19 AlkType kinds the
|
||||
builder's constructors produce
|
||||
- [validation.md](validation.md) — the validation layer that consumes
|
||||
builder output (via `AlkTypeEngine::compile`)
|
||||
- `@alkdev/alknet: docs/architecture/crates/call/operation-registry.md`
|
||||
— `OperationSpec` (the alkcall consumer for the JSON-payload role)
|
||||
- `@alkdev/alknet: docs/architecture/crates/channels/channels-wire.md`
|
||||
— the 8-byte chunk header (the alkcall consumer for the
|
||||
binary-layout role)
|
||||
Reference in new issue
Block a user