Rule Cascade
LearnEffects

Computed values

Compute rules fill in or recompute fields before validation runs.

A compute rule assigns values to fields. Each assignment has a field, a value expression and a mode: default writes only when the field is empty, always overwrites it. Compute rules run first, so validation sees the computed values. Each assignment comes back as a value effect.

Syntax

a compute rule
- id: <rule id>
  kind: compute
  target: { entity: Order }
  operations: [create]
  assign:
    - { field: /shipping, mode: default, value: 4 }
    - { field: /total, mode: always, value: <expression> }
a golden test can expect effects
expect:
  decision: allow
  findings: []
  effects:
    - { type: value, field: /total, value: 29 }

Example

Express delivery sets shipping to 10, and the total is recomputed as quantity × price + shipping.

computed-values.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.computed, version: 1.0.0, title: "Computed values" }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
  - id: order.compute.totals
    kind: compute
    target: { entity: Order }
    operations: [create]
    assign:
      - field: /shipping
        mode: default
        value: { op: if, args: [{ op: eq, args: [{ var: data.express }, true] }, 10, 4] }
      - field: /total
        mode: always
        value: { op: add, args: [{ op: mul, args: [{ var: data.quantity }, { var: data.price }] }, { var: data.shipping }] }
  - id: order.total.max
    kind: validation
    target: { entity: Order, field: /total }
    operations: [create]
    assert: { op: lte, args: [{ var: data.total }, 1000] }
    severity: error
    finding: { code: LRN-CMP-001, message: order.totalMax }
messages:
  en:
    order.totalMax: "An order may not cost more than 1000."
tests:
  - name: express shipping is added to the total
    entity: Order
    operation: create
    given:
      data: { quantity: 2, price: 9.5, express: true }
    expect:
      decision: allow
      findings: []
      effects:
        - { type: value, field: /shipping, value: 10 }
        - { type: value, field: /total, value: 29 }
  - name: a shipping price the caller gave is kept, the total is always recomputed
    entity: Order
    operation: create
    given:
      data: { quantity: 2, price: 9.5, shipping: 0, total: 1 }
    expect:
      decision: allow
      findings: []
      effects:
        - { type: value, field: /total, value: 19 }
  - name: validation sees the computed total
    entity: Order
    operation: create
    given:
      data: { quantity: 200, price: 9.5 }
    expect:
      decision: deny
      findings:
        - { rule: order.total.max }
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "quantity": 2,
    "price": 9.5,
    "express": true
  }
}

Result, from the engine

Decisionallow0 findings, server channel

  • computed value /shipping = 10
  • computed value /total = 29
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

Common mistakes

  • Using always for a field the user may set. default keeps what the caller sent.
  • Two rules that assign different values to one field. That is a conflict, an evaluation error (see Field states).

Exercise

Add a discount that defaults to 0, and subtract it from the total. Test that a discount of 5 gives a total of 24.

Hint

Add an assignment for /discount with mode: default and value 0 before /total, then subtract data.discount in the total.

Show answer
computed-values.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.computed, version: 1.0.0, title: "Computed values" }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
  - id: order.compute.totals
    kind: compute
    target: { entity: Order }
    operations: [create]
    assign:
      - field: /shipping
        mode: default
        value: { op: if, args: [{ op: eq, args: [{ var: data.express }, true] }, 10, 4] }
      - field: /discount
        mode: default
        value: 0
      - field: /total
        mode: always
        value: { op: sub, args: [{ op: add, args: [{ op: mul, args: [{ var: data.quantity }, { var: data.price }] }, { var: data.shipping }] }, { var: data.discount }] }
  - id: order.total.max
    kind: validation
    target: { entity: Order, field: /total }
    operations: [create]
    assert: { op: lte, args: [{ var: data.total }, 1000] }
    severity: error
    finding: { code: LRN-CMP-001, message: order.totalMax }
messages:
  en:
    order.totalMax: "An order may not cost more than 1000."
tests:
  - name: express shipping is added to the total
    entity: Order
    operation: create
    given:
      data: { quantity: 2, price: 9.5, express: true }
    expect:
      decision: allow
      findings: []
      effects:
        - { type: value, field: /shipping, value: 10 }
        - { type: value, field: /discount, value: 0 }
        - { type: value, field: /total, value: 29 }
  - name: a shipping price the caller gave is kept, the total is always recomputed
    entity: Order
    operation: create
    given:
      data: { quantity: 2, price: 9.5, shipping: 0, total: 1 }
    expect:
      decision: allow
      findings: []
      effects:
        - { type: value, field: /total, value: 19 }
  - name: validation sees the computed total
    entity: Order
    operation: create
    given:
      data: { quantity: 200, price: 9.5 }
    expect:
      decision: deny
      findings:
        - { rule: order.total.max }
  - name: a discount lowers the total
    entity: Order
    operation: create
    given:
      data: { quantity: 2, price: 9.5, express: true, discount: 5 }
    expect:
      decision: allow
      findings: []
      effects:
        - { type: value, field: /total, value: 24 }
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "quantity": 2,
    "price": 9.5,
    "express": true
  }
}

Result, from the engine

Decisionallow0 findings, server channel

  • computed value /shipping = 10
  • computed value /discount = 0
  • computed value /total = 29
Course overview

On this page