Rule Cascade
LearnChannels and enforcement

Triggers and views

Run the right rules at the right moment and in the right place of a form.

A form evaluates rules many times: when the page opens, when a value changes, when a field loses focus, and on submit. A rule's triggers list the moments it runs on the client. Its target can also name a place in the user interface: a page, screen, section and component.

A request can carry a trigger and a view. On the client channel, only rules whose triggers contain the trigger run. On both channels, a rule that names a place runs only when the view names the same place, or does not name that level. The finding reports the place as its location.

Syntax

a rule with triggers and a place
- id: <rule id>
  kind: validation
  target: { entity: Order, page: checkout, section: contact, component: <id>, field: /email }
  triggers: [blur, submit]      # load | change | blur | submit; default [submit]
  # ...
a request from the contact section, as the field loses focus
{ "entity": "Order", "operation": "create", "trigger": "blur",
  "view": { "page": "checkout", "section": "contact" }, "data": { } }

Example

The user leaves the e-mail field. Only the contact section is checked, so the 50 items in the basket are not reported yet.

triggers-and-views.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.triggers, version: 1.0.0, title: Triggers and views }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
  - id: order.email.present
    kind: validation
    target: { entity: Order, page: checkout, section: contact, field: /email }
    operations: [create]
    triggers: [blur, submit]
    assert: { op: exists, args: [{ var: data.email }] }
    severity: error
    finding: { code: LRN-TRG-001, message: order.emailMissing }
  - id: order.quantity.max
    kind: validation
    target: { entity: Order, page: checkout, section: cart, component: basket, field: /quantity }
    operations: [create]
    triggers: [change, submit]
    assert: { op: lte, args: [{ var: data.quantity }, 10] }
    severity: error
    finding: { code: LRN-TRG-002, message: order.tooMany }
  - id: order.status.closed
    kind: validation
    target: { entity: Order, page: checkout, screen: summary, field: /status }
    operations: [create]
    triggers: [load, submit]
    assert: { op: ne, args: [{ var: data.status }, "closed"] }
    severity: warning
    finding: { code: LRN-TRG-003, message: order.closed }
messages:
  en:
    order.emailMissing: "Enter an e-mail address."
    order.tooMany: "You can order at most 10 items."
    order.closed: "This shop is closed today. You can still place the order."
tests:
  - name: leaving the e-mail field checks only the contact section
    entity: Order
    operation: create
    channel: client
    given:
      data: { quantity: 50, status: "closed" }
      trigger: blur
      view: { page: checkout, section: contact }
    expect:
      decision: deny
      findings:
        - { rule: order.email.present, location: { page: checkout, section: contact } }
  - name: changing the quantity checks only the basket
    entity: Order
    operation: create
    channel: client
    given:
      data: { quantity: 50, status: "closed" }
      trigger: change
      view: { page: checkout, section: cart }
    expect:
      decision: deny
      findings:
        - { rule: order.quantity.max, location: { page: checkout, section: cart, component: basket } }
  - name: opening the page shows the closed warning
    entity: Order
    operation: create
    channel: client
    given:
      data: { quantity: 1, status: "closed" }
      trigger: load
    expect:
      decision: allow
      findings:
        - { rule: order.status.closed, severity: warning }
  - name: the server ignores triggers and runs every rule
    entity: Order
    operation: create
    given:
      data: { quantity: 50, status: "closed" }
      trigger: blur
    expect:
      decision: deny
      findings:
        - { rule: order.email.present }
        - { rule: order.quantity.max }
        - { rule: order.status.closed }
request.json (client channel)
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "quantity": 50,
    "status": "closed"
  },
  "trigger": "blur",
  "view": {
    "page": "checkout",
    "section": "contact"
  }
}

Result, from the engine

Decisiondeny1 finding, client channel

  • LRN-TRG-001errorblockingEnter an e-mail address./email
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

Open it in the playground and try the other golden tests: change in the cart section, and load when the page opens. The server ignores trigger and always runs every rule that fits the view.

Common mistakes

  • Forgetting submit. A rule with triggers: [blur] does not run on submit. Keep submit in the list unless you really mean it.
  • Expecting triggers to filter on the server. Triggers are a client concept. The server always evaluates at the operation itself.
  • Free-form place names. Page, screen, section and component ids are lower-case kebab-case, like checkout or order-summary. They are logical names, not framework class names.

Exercise

Check the quantity as soon as the user leaves the quantity field, not only when it changes. Prove it with a client test: trigger blur in the cart section with 50 items.

Hint

Add blur to the triggers of order.quantity.max.

Show answer
triggers-and-views.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.triggers, version: 1.0.0, title: Triggers and views }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
  - id: order.email.present
    kind: validation
    target: { entity: Order, page: checkout, section: contact, field: /email }
    operations: [create]
    triggers: [blur, submit]
    assert: { op: exists, args: [{ var: data.email }] }
    severity: error
    finding: { code: LRN-TRG-001, message: order.emailMissing }
  - id: order.quantity.max
    kind: validation
    target: { entity: Order, page: checkout, section: cart, component: basket, field: /quantity }
    operations: [create]
    triggers: [change, blur, submit]
    assert: { op: lte, args: [{ var: data.quantity }, 10] }
    severity: error
    finding: { code: LRN-TRG-002, message: order.tooMany }
  - id: order.status.closed
    kind: validation
    target: { entity: Order, page: checkout, screen: summary, field: /status }
    operations: [create]
    triggers: [load, submit]
    assert: { op: ne, args: [{ var: data.status }, "closed"] }
    severity: warning
    finding: { code: LRN-TRG-003, message: order.closed }
messages:
  en:
    order.emailMissing: "Enter an e-mail address."
    order.tooMany: "You can order at most 10 items."
    order.closed: "This shop is closed today. You can still place the order."
tests:
  - name: leaving the quantity field now checks it
    entity: Order
    operation: create
    channel: client
    given:
      data: { quantity: 50 }
      trigger: blur
      view: { page: checkout, section: cart }
    expect:
      decision: deny
      findings:
        - { rule: order.quantity.max, location: { page: checkout, section: cart, component: basket } }
  - name: leaving the e-mail field still skips the basket
    entity: Order
    operation: create
    channel: client
    given:
      data: { quantity: 50 }
      trigger: blur
      view: { page: checkout, section: contact }
    expect:
      decision: deny
      findings:
        - { rule: order.email.present }
request.json (client channel)
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "quantity": 50
  },
  "trigger": "blur",
  "view": {
    "page": "checkout",
    "section": "cart"
  }
}

Result, from the engine

Decisiondeny1 finding, client channel

  • LRN-TRG-002errorblockingYou can order at most 10 items./quantity
Course overview

On this page