Typed order
An order chart in YAML on the CEL datamodel, with a types block: records, a union, optional and list. The vt command checks every guard and assignment against the types and projects them to JSON Schema, SHACL, CEL and TypeScript.
Files: examples/typed-order/order.yaml, examples/typed-order/order-ill-typed.yaml, examples/typed-order/vt/. All commands run from the repository root.
The chart
# A typed order chart in YAML on the CEL datamodel.
#
# `types` names record and union types, `events` gives every event its payload type and
# `context` gives every data item a type and an initial value. Guards and assignments are CEL
# expressions: `ctx` is the context, `ev.data` the event payload. `vt typecheck` checks every
# expression against these declarations before the chart ever runs.
#
# Illustrates the type-system capability (named types, typed events and context, Command Line
# Type Tools), yaml-format and cel-datamodel.
vertep: 1
name: order
datamodel: cel
types:
Item: { sku: string, qty: int, price: decimal }
Pay: { amount: decimal, method: { oneOf: { Card: { last4: string }, Invoice: null } } }
Ship: { carrier: string, tracking: "optional<string>" }
events:
add: Item
checkout: null
pay: Pay
approve: null
ship: Ship
cancel: null
context:
items: { type: "list<Item>", init: "[]" }
total: { type: decimal, init: "0" }
limit: { type: decimal, init: "500" }
initial: shopping
states:
shopping:
on: { cancel: cancelled }
initial: cart
states:
cart:
on:
add:
- cond: "ev.data.qty > 0"
actions:
- assign: { location: ctx.items, expr: "ctx.items + [ev.data]" }
- assign: { location: ctx.total, expr: "ctx.total + ev.data.price * decimal(ev.data.qty)" }
checkout:
- { cond: "size(ctx.items) > 0", target: payment }
payment:
on:
pay:
- { cond: "ev.data.amount < ctx.total", target: payment }
- { cond: "ctx.total > ctx.limit", target: review }
- { target: fulfilment }
review:
on: { approve: fulfilment }
fulfilment:
on: { ship: shipped }
shipped: { type: final }
cancelled: { type: final }
Type-check it
vt typecheck examples/typed-order/order.yaml
examples/typed-order/order.yaml: ok: 3 named types, 6 typed events, 3 context fields
order-ill-typed.yaml assigns the item's sku, a string, to ctx.total, a decimal. The error names the file, line and column, the path in the document, the state and the transition:
vt typecheck examples/typed-order/order-ill-typed.yaml # exit code 1
examples/typed-order/order-ill-typed.yaml:44:19: states.shopping.states.cart.on.add[0].actions[1]: state "cart", transition 0 (on add), action 1: the value assigned to `ctx.total` has type string, expected decimal
Project the types
One type language, several projections. The CEL type environment is what the checker uses for expressions:
vt schema --format cel examples/typed-order/order.yaml
ctx.items: list(Item)
ctx.total: decimal
ctx.limit: decimal
ev.data[add]: Item
ev.data[checkout]: null_type
ev.data[pay]: Pay
ev.data[approve]: null_type
ev.data[ship]: Ship
ev.data[cancel]: null_type
vt schema --format jsonschema examples/typed-order/order.yaml
{
"context": {
"$defs": {
"Item": {
"additionalProperties": false,
"properties": {
"price": {
"anyOf": [
{
"type": "number"
},
{
"pattern": "^-?[0-9]+(\\.[0-9]+)?$",
"type": "string"
}
]
},
"qty": {
"exclusiveMaximum": 9223372036854775808,
"minimum": -9223372036854775808,
"type": "integer"
},
"sku": {
"type": "string"
}
},
"required": [
"sku",
"qty",
"price"
],
"title": "Item",
"type": "object"
}
},
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"items": {
"items": {
Generate TypeScript
vt gen ts writes a module for @vertep/web: the event union, the context interface, the state names and a typed createActor.
vt gen ts examples/typed-order/order.yaml
export type Item = { sku: string; qty: number; price: number | string };
export type Pay = { amount: number | string; method: { tag: "Card"; value: { last4: string } } | { tag: "Invoice" } };
export type Ship = { carrier: string; tracking?: string | null };
export type OrderEvent =
| { type: "add" } & Item
| { type: "checkout" }
| { type: "pay" } & Pay
| { type: "approve" }
| { type: "ship" } & Ship
| { type: "cancel" };
export interface OrderContext {
items: Item[];
total: number | string;
limit: number | string;
}
export type OrderStateId = "shopping" | "cart" | "payment" | "review" | "fulfilment" | "shipped" | "cancelled";
Every host loads a chart that declares types with its typed CEL datamodel: the actor runtime, the agent host and the browser worker refuse a chart with CEL type errors, and an event whose payload does not match its declared type is replaced by error.execution before the chart sees it.
- CEL-level errors, such as an operator applied to the wrong types, are reported only after the structural checks pass, and without a line and column.
vt schema --format rustis listed in--helpbut exits with code 2: Rust types come from thestatechart!macro.