Rule Cascade
LearnRule basics

Targets

Say which entity a rule is about, and which field or fields its findings point at.

A rule's target says what the rule is about. entity names the data, and field or fields names the JSON Pointers a finding points at. A form uses those pointers to show the message next to the right input. The compiler checks every pointer against the entity's schema.

Syntax

target
target: { entity: Customer }                              # the whole entity
target: { entity: Customer, field: /email }               # one field
target: { entity: Customer, fields: [/email, /backupEmail] }   # several fields

Use field or fields, never both.

Example

The second rule targets two fields, so its finding points at both.

targets.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.targets, version: 1.0.0, title: Targets }
scope:
  - { level: organization, id: learn }
entities:
  Customer:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Customer" }
rules:
  - id: customer.email.required
    kind: validation
    target: { entity: Customer, field: /email }
    operations: [create]
    assert: { op: exists, args: [{ var: data.email }] }
    severity: error
    finding: { code: LRN-TGT-001, message: customer.emailMissing }
  - id: customer.backup-email.different
    kind: validation
    target: { entity: Customer, fields: [/email, /backupEmail] }
    operations: [create]
    when: { op: exists, args: [{ var: data.backupEmail }] }
    assert: { op: ne, args: [{ var: data.email }, { var: data.backupEmail }] }
    severity: error
    finding: { code: LRN-TGT-002, message: customer.sameEmails }
messages:
  en:
    customer.emailMissing: "Enter an e-mail address."
    customer.sameEmails: "The backup e-mail must differ from the main e-mail."
tests:
  - name: the same address twice is denied on both fields
    entity: Customer
    operation: create
    given:
      data: { email: ana@example.com, backupEmail: ana@example.com }
    expect:
      decision: deny
      findings:
        - { rule: customer.backup-email.different, fields: [/email, /backupEmail] }
  - name: two different addresses are allowed
    entity: Customer
    operation: create
    given:
      data: { email: ana@example.com, backupEmail: ana@work.example }
    expect: { decision: allow, findings: [] }
request.json
{
  "entity": "Customer",
  "operation": "create",
  "data": {
    "email": "ana@example.com",
    "backupEmail": "ana@example.com"
  }
}

Result, from the engine

Decisiondeny1 finding, server channel

  • LRN-TGT-002errorblockingThe backup e-mail must differ from the main e-mail./email, /backupEmail
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

Common mistakes

  • A typo in a pointer. The compiler checks every path against the schema and refuses the ruleset with PATH_UNKNOWN:
targets-fix.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.targets-fix, version: 1.0.0, title: "Targets, fix the path" }
scope:
  - { level: organization, id: learn }
entities:
  Customer:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Customer" }
rules:
  - id: customer.email.required
    kind: validation
    target: { entity: Customer, field: /email }
    operations: [create]
    assert: { op: exists, args: [{ var: data.email }] }
    severity: error
    finding: { code: LRN-TGT-001, message: customer.emailMissing }
  - id: customer.backup-email.different
    kind: validation
    target: { entity: Customer, fields: [/email, /backupEmial] }
    operations: [create]
    when: { op: exists, args: [{ var: data.backupEmail }] }
    assert: { op: ne, args: [{ var: data.email }, { var: data.backupEmail }] }
    severity: error
    finding: { code: LRN-TGT-002, message: customer.sameEmails }
messages:
  en:
    customer.emailMissing: "Enter an e-mail address."
    customer.sameEmails: "The backup e-mail must differ from the main e-mail."
tests:
  - name: the same address twice is denied on both fields
    entity: Customer
    operation: create
    given:
      data: { email: ana@example.com, backupEmail: ana@example.com }
    expect:
      decision: deny
      findings:
        - { rule: customer.backup-email.different, fields: [/email, /backupEmail] }
  - name: two different addresses are allowed
    entity: Customer
    operation: create
    given:
      data: { email: ana@example.com, backupEmail: ana@work.example }
    expect: { decision: allow, findings: [] }
request.json
{
  "entity": "Customer",
  "operation": "create",
  "data": {
    "email": "ana@example.com",
    "backupEmail": "ana@example.com"
  }
}

Result, from the engine

does not loadThe engine refuses the ruleset before it evaluates anything.

  • PATH_UNKNOWN target /backupEmial is not in the Customer schema (line 19)
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.
  • Naming an entity that entities does not declare. That is ENTITY_UNKNOWN.
  • Writing a field as email or data.email. A target field is a JSON Pointer: /email.

Exercise

This ruleset does not load. Fix it so that it loads and its golden tests pass.

Hint

Read the problem: it names the path the schema does not have. Compare it with the field names in the schema.

Show answer
targets-fix.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.targets-fix, version: 1.0.0, title: "Targets, fix the path" }
scope:
  - { level: organization, id: learn }
entities:
  Customer:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Customer" }
rules:
  - id: customer.email.required
    kind: validation
    target: { entity: Customer, field: /email }
    operations: [create]
    assert: { op: exists, args: [{ var: data.email }] }
    severity: error
    finding: { code: LRN-TGT-001, message: customer.emailMissing }
  - id: customer.backup-email.different
    kind: validation
    target: { entity: Customer, fields: [/email, /backupEmail] }
    operations: [create]
    when: { op: exists, args: [{ var: data.backupEmail }] }
    assert: { op: ne, args: [{ var: data.email }, { var: data.backupEmail }] }
    severity: error
    finding: { code: LRN-TGT-002, message: customer.sameEmails }
messages:
  en:
    customer.emailMissing: "Enter an e-mail address."
    customer.sameEmails: "The backup e-mail must differ from the main e-mail."
tests:
  - name: the same address twice is denied on both fields
    entity: Customer
    operation: create
    given:
      data: { email: ana@example.com, backupEmail: ana@example.com }
    expect:
      decision: deny
      findings:
        - { rule: customer.backup-email.different, fields: [/email, /backupEmail] }
  - name: two different addresses are allowed
    entity: Customer
    operation: create
    given:
      data: { email: ana@example.com, backupEmail: ana@work.example }
    expect: { decision: allow, findings: [] }
request.json
{
  "entity": "Customer",
  "operation": "create",
  "data": {
    "email": "ana@example.com",
    "backupEmail": "ana@example.com"
  }
}

Result, from the engine

Decisiondeny1 finding, server channel

  • LRN-TGT-002errorblockingThe backup e-mail must differ from the main e-mail./email, /backupEmail
Course overview

On this page