Rule Cascade
LearnValues and types

Semantic types

Declare a type such as Email once, bind fields to it, and write one rule that checks every bound field.

A semantic type gives a meaning to fields, such as Email or Money. Declare it under types, then bind fields to it in the entity's fieldTypes. A validation rule whose target is { type: Email } runs once for every bound field, in every entity. Inside the rule, value is the field's value and field is its pointer.

Syntax

types and fieldTypes
types:
  Email: { base: string, description: An e-mail address. }   # base and description are documentation
entities:
  Customer:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Customer" }
    fieldTypes: { /email: Email, /backupEmail: Email }
rules:
  - id: email.has-at
    kind: validation
    target: { type: Email }          # no entity, no field
    assert: { op: contains, args: [{ var: value }, "@"] }

Example

One rule checks both e-mail fields. The backup address has no @, so the finding points at /backupEmail, and the message names the field through { var: field }.

semantic-types.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.semantic-types, version: 1.0.0, title: "Semantic types" }
scope:
  - { level: organization, id: learn }
types:
  Email:
    base: string
    description: An e-mail address.
entities:
  Customer:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Customer" }
    fieldTypes:
      /email: Email
      /backupEmail: Email
rules:
  - id: email.has-at
    kind: validation
    target: { type: Email }
    operations: [create, update]
    when: { op: exists, args: [{ var: value }] }
    assert: { op: contains, args: [{ var: value }, "@"] }
    severity: error
    finding: { code: LRN-TYP-001, message: email.invalid, args: { field: { var: field } } }
messages:
  en:
    email.invalid: "{field} is not an e-mail address."
tests:
  - name: the backup e-mail has no @
    entity: Customer
    operation: create
    given:
      data: { email: ana@example.com, backupEmail: ana.example.com }
    expect:
      decision: deny
      findings:
        - { rule: email.has-at, fields: [/backupEmail], message: /backupEmail is not an e-mail address. }
  - name: both addresses are fine
    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-TYP-001errorblocking/backupEmail is not an e-mail address./backupEmail
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

Common mistakes

  • A typo in the type name. The compiler refuses a target or binding to an undeclared type with TYPE_UNKNOWN:
semantic-types-fix.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.semantic-types-fix, version: 1.0.0, title: "Semantic types, fix the type" }
scope:
  - { level: organization, id: learn }
types:
  Email:
    base: string
    description: An e-mail address.
entities:
  Customer:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Customer" }
    fieldTypes:
      /email: Email
      /backupEmail: Email
rules:
  - id: email.has-at
    kind: validation
    target: { type: Emial }
    operations: [create, update]
    when: { op: exists, args: [{ var: value }] }
    assert: { op: contains, args: [{ var: value }, "@"] }
    severity: error
    finding: { code: LRN-TYP-001, message: email.invalid, args: { field: { var: field } } }
messages:
  en:
    email.invalid: "{field} is not an e-mail address."
tests:
  - name: the backup e-mail has no @
    entity: Customer
    operation: create
    given:
      data: { email: ana@example.com, backupEmail: ana.example.com }
    expect:
      decision: deny
      findings:
        - { rule: email.has-at, fields: [/backupEmail], message: /backupEmail is not an e-mail address. }
  - name: both addresses are fine
    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.

  • TYPE_UNKNOWN undeclared type Emial (line 19)
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.
  • Rebinding an inherited field. A child ruleset may add bindings but not change one its parent made (FIELD_TYPE_REBOUND).
  • Giving a type rule a field or a forEach. A type rule gets its fields from the bindings, and only validation rules may target a type.

Exercise

Tickets have a /reporter e-mail too. Make the same rule check it, without writing a new rule. Test a ticket whose reporter is ana.

Hint

Declare the Ticket entity with fieldTypes: { /reporter: Email }. The rule itself does not change.

Show answer
semantic-types.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.semantic-types, version: 1.0.0, title: "Semantic types" }
scope:
  - { level: organization, id: learn }
types:
  Email:
    base: string
    description: An e-mail address.
entities:
  Customer:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Customer" }
    fieldTypes:
      /email: Email
      /backupEmail: Email
  Ticket:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Ticket" }
    fieldTypes:
      /reporter: Email
rules:
  - id: email.has-at
    kind: validation
    target: { type: Email }
    operations: [create, update]
    when: { op: exists, args: [{ var: value }] }
    assert: { op: contains, args: [{ var: value }, "@"] }
    severity: error
    finding: { code: LRN-TYP-001, message: email.invalid, args: { field: { var: field } } }
messages:
  en:
    email.invalid: "{field} is not an e-mail address."
tests:
  - name: the same rule checks the ticket reporter
    entity: Ticket
    operation: create
    given:
      data: { title: Printer on fire, reporter: ana }
    expect:
      decision: deny
      findings:
        - { rule: email.has-at, fields: [/reporter], message: /reporter is not an e-mail address. }
  - name: the backup e-mail has no @
    entity: Customer
    operation: create
    given:
      data: { email: ana@example.com, backupEmail: ana.example.com }
    expect:
      decision: deny
      findings:
        - { rule: email.has-at, fields: [/backupEmail], message: /backupEmail is not an e-mail address. }
  - name: both addresses are fine
    entity: Customer
    operation: create
    given:
      data: { email: ana@example.com, backupEmail: ana@work.example }
    expect: { decision: allow, findings: [] }
request.json
{
  "entity": "Ticket",
  "operation": "create",
  "data": {
    "title": "Printer on fire",
    "reporter": "ana"
  }
}

Result, from the engine

Decisiondeny1 finding, server channel

  • LRN-TYP-001errorblocking/reporter is not an e-mail address./reporter
Course overview

On this page