Rule Cascade
LearnRule basics

Rules for every item of a list

forEach runs a validation rule once per element of an array, with the element bound to item.

forEach names an array in the entity. The rule runs once for every element, in order, with the element bound to item. The target field is relative to the element, and each finding points at the element that failed: /items/1/qty is the quantity of the second line.

Syntax

forEach
- id: order.item.qty
  kind: validation
  target: { entity: Order, field: /qty }   # relative to each element
  forEach: /items                          # the array
  assert: { op: gte, args: [{ var: item.qty }, 1] }

Example

The second line of the order has a quantity of zero.

for-each.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.for-each, version: 1.0.0, title: "For each item" }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
  - id: order.item.qty
    kind: validation
    target: { entity: Order, field: /qty }
    operations: [create]
    forEach: /items
    assert: { op: gte, args: [{ var: item.qty }, 1] }
    severity: error
    finding: { code: LRN-EACH-001, message: item.qtyTooLow, args: { sku: { var: item.sku } } }
messages:
  en:
    item.qtyTooLow: "Order at least one of {sku}."
tests:
  - name: the second line has no quantity
    entity: Order
    operation: create
    given:
      data:
        items:
          - { sku: A-1, qty: 2, price: 5 }
          - { sku: B-2, qty: 0, price: 7 }
    expect:
      decision: deny
      findings:
        - { rule: order.item.qty, fields: [/items/1/qty], message: Order at least one of B-2. }
  - name: every line has a quantity
    entity: Order
    operation: create
    given:
      data:
        items:
          - { sku: A-1, qty: 2, price: 5 }
    expect: { decision: allow, findings: [] }
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "items": [
      {
        "sku": "A-1",
        "qty": 2,
        "price": 5
      },
      {
        "sku": "B-2",
        "qty": 0,
        "price": 7
      }
    ]
  }
}

Result, from the engine

Decisiondeny1 finding, server channel

  • LRN-EACH-001errorblockingOrder at least one of B-2./items/1/qty
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

Common mistakes

  • Writing the full path in the target (/items/qty). The target is relative to the element: /qty.
  • Using data.items.qty in the assert. Inside the rule, the element is item.
  • An absent or null array counts as empty: the rule runs zero times.

Exercise

Add a rule: every line needs a price above zero. Use the code LRN-EACH-002 and the message "Give {sku} a price above zero."

Hint

Copy the rule. Change the target field to /price and the assert to item.price greater than 0.

Show answer
for-each.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.for-each, version: 1.0.0, title: "For each item" }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
  - id: order.item.qty
    kind: validation
    target: { entity: Order, field: /qty }
    operations: [create]
    forEach: /items
    assert: { op: gte, args: [{ var: item.qty }, 1] }
    severity: error
    finding: { code: LRN-EACH-001, message: item.qtyTooLow, args: { sku: { var: item.sku } } }
  - id: order.item.price
    kind: validation
    target: { entity: Order, field: /price }
    operations: [create]
    forEach: /items
    assert: { op: gt, args: [{ var: item.price }, 0] }
    severity: error
    finding: { code: LRN-EACH-002, message: item.priceMissing, args: { sku: { var: item.sku } } }
messages:
  en:
    item.qtyTooLow: "Order at least one of {sku}."
    item.priceMissing: "Give {sku} a price above zero."
tests:
  - name: the first line is free
    entity: Order
    operation: create
    given:
      data:
        items:
          - { sku: A-1, qty: 2, price: 0 }
          - { sku: B-2, qty: 1, price: 7 }
    expect:
      decision: deny
      findings:
        - { rule: order.item.price, fields: [/items/0/price], message: Give A-1 a price above zero. }
  - name: every line has a quantity and a price
    entity: Order
    operation: create
    given:
      data:
        items:
          - { sku: A-1, qty: 2, price: 5 }
    expect: { decision: allow, findings: [] }
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "items": [
      {
        "sku": "A-1",
        "qty": 2,
        "price": 0
      },
      {
        "sku": "B-2",
        "qty": 1,
        "price": 7
      }
    ]
  }
}

Result, from the engine

Decisiondeny1 finding, server channel

  • LRN-EACH-002errorblockingGive A-1 a price above zero./items/0/price
Course overview

On this page