Rule Cascade
LearnOperators

List operators

list, in, all, some, none, sum, map and filter build and inspect lists.

List operators build lists and look inside them. The collection operators (all, some, none, sum, map, filter) evaluate their second argument once per element, with the element bound to item. A missing list counts as empty.

Syntax

list operators
{ op: in, args: [<value>, { op: list, args: ["DE", "FR"] }] }
{ op: all, args: [{ var: data.items }, <predicate using item>] }
{ op: sum, args: [{ var: data.items }, { var: item.qty }] }
OperatorArgumentsResult
listany numberA list of the arguments
inany, listBoolean: the value equals one element
all, some, nonelist, predicateBoolean. Every element is evaluated: no short cut
sumlist, expressionSum of the expression over the elements; 0 for an empty list
maplist, expressionA list of the expression's value for each element
filterlist, predicateThe elements for which the predicate is true

Example

The request ships to the US and contains a sale item.

list.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.list, version: 1.0.0, title: List operators }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
  - id: order.country.supported
    kind: validation
    target: { entity: Order, field: /country }
    operations: [create]
    assert: { op: in, args: [{ var: data.country }, { op: list, args: ["DE", "FR", "ES"] }] }
    severity: error
    finding: { code: LST-001, message: country.supported }
  - id: order.items.quantity
    kind: validation
    target: { entity: Order, field: /items }
    operations: [create]
    assert: { op: all, args: [{ var: data.items }, { op: gt, args: [{ var: item.qty }, 0] }] }
    severity: error
    finding: { code: LST-002, message: items.quantity }
  - id: order.gift-wrap.needs-gift-item
    kind: validation
    target: { entity: Order, field: /giftWrap }
    operations: [create]
    when: { op: eq, args: [{ var: data.giftWrap }, true] }
    assert: { op: some, args: [{ var: data.items }, { op: eq, args: [{ var: item.sku }, "GIFT"] }] }
    severity: error
    finding: { code: LST-003, message: giftWrap.needsItem }
  - id: order.tags.not-blocked
    kind: validation
    target: { entity: Order, field: /tags }
    operations: [create]
    assert: { op: none, args: [{ var: data.tags }, { op: eq, args: [{ var: item }, "blocked"] }] }
    severity: error
    finding: { code: LST-004, message: tags.blocked }
  - id: order.quantity.sum
    kind: validation
    target: { entity: Order, field: /quantity }
    operations: [create]
    assert: { op: eq, args: [{ var: data.quantity }, { op: sum, args: [{ var: data.items }, { var: item.qty }] }] }
    severity: error
    finding: { code: LST-005, message: quantity.sum }
  - id: order.items.no-sale
    kind: validation
    target: { entity: Order, field: /items }
    operations: [create]
    assert:
      op: not
      args:
        - { op: in, args: ["SALE", { op: map, args: [{ var: data.items }, { var: item.sku }] }] }
    severity: error
    finding: { code: LST-006, message: items.noSale }
  - id: order.items.expensive
    kind: validation
    target: { entity: Order, field: /items }
    operations: [create]
    assert:
      op: lte
      args:
        - { op: len, args: [{ op: filter, args: [{ var: data.items }, { op: gt, args: [{ var: item.price }, 100] }] }] }
        - 2
    severity: error
    finding: { code: LST-007, message: items.expensive }
messages:
  en:
    country.supported: "We deliver to DE, FR and ES."
    items.quantity: "Every item needs a quantity above 0."
    giftWrap.needsItem: "Gift wrap needs a GIFT item in the order."
    tags.blocked: "This order is blocked."
    quantity.sum: "The quantity must be the sum of the item quantities."
    items.noSale: "Sale items cannot be ordered online."
    items.expensive: "An order may have at most two items above 100."
tests:
  - name: a delivery to the US with a sale item
    entity: Order
    operation: create
    given:
      data:
        country: "US"
        quantity: 3
        giftWrap: false
        tags: ["new"]
        items:
          - { sku: "A-1", qty: 2, price: 10 }
          - { sku: "SALE", qty: 1, price: 5 }
    expect:
      decision: deny
      findings:
        - { rule: order.country.supported }
        - { rule: order.items.no-sale }
  - name: a wrapped gift to Germany
    entity: Order
    operation: create
    given:
      data:
        country: "DE"
        quantity: 2
        giftWrap: true
        tags: []
        items:
          - { sku: "A-1", qty: 1, price: 120 }
          - { sku: "GIFT", qty: 1, price: 3 }
    expect: { decision: allow, findings: [] }
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "country": "US",
    "quantity": 3,
    "giftWrap": false,
    "tags": [
      "new"
    ],
    "items": [
      {
        "sku": "A-1",
        "qty": 2,
        "price": 10
      },
      {
        "sku": "SALE",
        "qty": 1,
        "price": 5
      }
    ]
  }
}

Result, from the engine

Decisiondeny2 findings, server channel

  • LST-001errorblockingWe deliver to DE, FR and ES./country
  • LST-006errorblockingSale items cannot be ordered online./items
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

Common mistakes

  • A YAML list is not an expression. Write { op: list, args: ["DE", "FR"] }, not ["DE", "FR"].
  • Inside a collection operator, item is the current element. For a list of strings, item itself is the string: { var: item }.
  • all of an empty list is true. Add a len check if the list must not be empty.

Exercise

Every item must cost at least 1, and the items together (qty times price) may be worth at most 500. Write two rules.

Hint

all with gte checks each price. sum with mul of item.qty and item.price gives the value of the items.

Show answer
list.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.list, version: 1.0.0, title: List operators }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
  - id: order.items.price-min
    kind: validation
    target: { entity: Order, field: /items }
    operations: [create]
    assert: { op: all, args: [{ var: data.items }, { op: gte, args: [{ var: item.price }, 1] }] }
    severity: error
    finding: { code: LST-001, message: items.priceMin }
  - id: order.items.value-max
    kind: validation
    target: { entity: Order, field: /items }
    operations: [create]
    assert:
      op: lte
      args:
        - { op: sum, args: [{ var: data.items }, { op: mul, args: [{ var: item.qty }, { var: item.price }] }] }
        - 500
    severity: error
    finding: { code: LST-002, message: items.valueMax }
messages:
  en:
    items.priceMin: "Every item costs at least 1."
    items.valueMax: "The items may be worth at most 500 together."
tests:
  - name: items worth 600 are denied
    entity: Order
    operation: create
    given:
      data:
        items:
          - { sku: "A-1", qty: 3, price: 200 }
    expect:
      decision: deny
      findings:
        - { rule: order.items.value-max }
  - name: a free item is denied
    entity: Order
    operation: create
    given:
      data:
        items:
          - { sku: "A-1", qty: 1, price: 0 }
    expect:
      decision: deny
      findings:
        - { rule: order.items.price-min }
  - name: items worth 500 are allowed
    entity: Order
    operation: create
    given:
      data:
        items:
          - { sku: "A-1", qty: 2, price: 200 }
          - { sku: "B-2", qty: 1, price: 100 }
    expect: { decision: allow, findings: [] }
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "items": [
      {
        "sku": "A-1",
        "qty": 3,
        "price": 200
      }
    ]
  }
}

Result, from the engine

Decisiondeny1 finding, server channel

  • LST-002errorblockingThe items may be worth at most 500 together./items
Course overview

On this page