Rule Cascade
LearnRule basics

The four rule kinds

validation, state, compute and action rules — what each one produces.

Every rule has an id, a kind, a target and the operations it applies to. The kind decides what the rule produces. A validation rule produces a finding, a state rule produces field states, a compute rule produces values and an action rule produces commands. title and description are for people; the engine does not read them.

Syntax

one rule of each kind
- { id: <id>, kind: compute,    ..., assign:   [ { field, value } ] }
- { id: <id>, kind: state,      ..., when: <expr>, effects: [ { field, set } ] }
- { id: <id>, kind: validation, ..., assert: <expr>, severity, finding }
- { id: <id>, kind: action,     ..., enforcement: server, commands: [ { name, type, idempotencyKey } ] }
- { id: <id>, kind: x-<name>,   ... }   # an extension: conforming engines ignore it
KindBodyProduces
computeassignComputed values, written before validation runs
statewhen, effectsField states (visible, enabled, required, read-only) while when is true
validationassert, severity, findingA finding when assert is false
actioncommands, enforcement: serverCommands, only after an allowed server evaluation
x-*anythingNothing. Reserved for extensions

Example

One rule of each kind. The order costs 400 three times, so the computed total is too high.

rule-kinds.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.rule-kinds, version: 1.0.0, title: The four rule kinds }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
  - id: order.total.compute
    kind: compute
    title: Work out the total
    description: The total is the price times the quantity.
    target: { entity: Order, field: /total }
    operations: [create]
    assign:
      - { field: /total, value: { op: mul, args: [{ var: data.price }, { var: data.quantity }] }, mode: always }
  - id: order.gift-message.show
    kind: state
    title: Show the gift message for gift-wrapped orders
    target: { entity: Order, field: /giftMessage }
    operations: [create]
    when: { op: eq, args: [{ var: data.giftWrap }, true] }
    effects:
      - { field: /giftMessage, set: { visible: true } }
  - id: order.total.max
    kind: validation
    title: At most 1000 per order
    target: { entity: Order, field: /total }
    operations: [create]
    assert: { op: lte, args: [{ var: data.total }, 1000] }
    severity: error
    finding: { code: LRN-KND-001, message: order.totalTooHigh }
  - id: order.placed.announce
    kind: action
    title: Announce the new order
    target: { entity: Order }
    operations: [create]
    enforcement: server
    commands:
      - name: order.placed
        type: event
        idempotencyKey: [{ var: data.id }]
  - id: order.audit.note
    kind: x-audit
    target: { entity: Order }
    operations: [create]
    x-note: An extension kind. Engines ignore it.
messages:
  en:
    order.totalTooHigh: "An order may cost at most 1000."
tests:
  - name: a gift order of 1200 is denied and announces nothing
    entity: Order
    operation: create
    given:
      data: { id: o-1, price: 400, quantity: 3, giftWrap: true }
    expect:
      decision: deny
      findings:
        - { rule: order.total.max, fields: [/total] }
      effects:
        - { type: value, field: /total, value: 1200 }
        - { type: state, field: /giftMessage, set: { visible: true } }
      commands: []
  - name: an order of 800 is allowed and announced
    entity: Order
    operation: create
    given:
      data: { id: o-2, price: 400, quantity: 2 }
    expect:
      decision: allow
      findings: []
      effects:
        - { type: value, field: /total, value: 800 }
      commands: [order.placed]
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "id": "o-1",
    "price": 400,
    "quantity": 3,
    "giftWrap": true
  }
}

Result, from the engine

Decisiondeny1 finding, server channel

  • LRN-KND-001errorblockingAn order may cost at most 1000./total
  • computed value /total = 1200
  • field state /giftMessage = {"visible":true}
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

Common mistakes

  • Expecting commands from a denied request. Action rules run only when the decision is allow.
  • Leaving out enforcement: server on an action rule. The compiler refuses it: commands never run in a browser.
  • Reusing a rule id. Every id is unique in a ruleset and everything it inherits.

Exercise

Gift-wrapped orders show the gift message field. Make the field required as well, and update the golden test to expect it.

Hint

A state effect's set can hold more than one property: visible, enabled, required, readOnly.

Show answer
rule-kinds.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.rule-kinds, version: 1.0.0, title: The four rule kinds }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
  - id: order.total.compute
    kind: compute
    title: Work out the total
    description: The total is the price times the quantity.
    target: { entity: Order, field: /total }
    operations: [create]
    assign:
      - { field: /total, value: { op: mul, args: [{ var: data.price }, { var: data.quantity }] }, mode: always }
  - id: order.gift-message.show
    kind: state
    title: Show the gift message for gift-wrapped orders
    target: { entity: Order, field: /giftMessage }
    operations: [create]
    when: { op: eq, args: [{ var: data.giftWrap }, true] }
    effects:
      - { field: /giftMessage, set: { visible: true, required: true } }
  - id: order.total.max
    kind: validation
    title: At most 1000 per order
    target: { entity: Order, field: /total }
    operations: [create]
    assert: { op: lte, args: [{ var: data.total }, 1000] }
    severity: error
    finding: { code: LRN-KND-001, message: order.totalTooHigh }
  - id: order.placed.announce
    kind: action
    title: Announce the new order
    target: { entity: Order }
    operations: [create]
    enforcement: server
    commands:
      - name: order.placed
        type: event
        idempotencyKey: [{ var: data.id }]
  - id: order.audit.note
    kind: x-audit
    target: { entity: Order }
    operations: [create]
    x-note: An extension kind. Engines ignore it.
messages:
  en:
    order.totalTooHigh: "An order may cost at most 1000."
tests:
  - name: a gift order of 1200 is denied and announces nothing
    entity: Order
    operation: create
    given:
      data: { id: o-1, price: 400, quantity: 3, giftWrap: true }
    expect:
      decision: deny
      findings:
        - { rule: order.total.max, fields: [/total] }
      effects:
        - { type: value, field: /total, value: 1200 }
        - { type: state, field: /giftMessage, set: { visible: true, required: true } }
      commands: []
  - name: an order of 800 is allowed and announced
    entity: Order
    operation: create
    given:
      data: { id: o-2, price: 400, quantity: 2 }
    expect:
      decision: allow
      findings: []
      effects:
        - { type: value, field: /total, value: 800 }
      commands: [order.placed]
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "id": "o-1",
    "price": 400,
    "quantity": 3,
    "giftWrap": true
  }
}

Result, from the engine

Decisiondeny1 finding, server channel

  • LRN-KND-001errorblockingAn order may cost at most 1000./total
  • computed value /total = 1200
  • field state /giftMessage = {"visible":true,"required":true}
Course overview

On this page