Vertep

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.