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:
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 }.
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: [] }{
"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
Common mistakes
- A typo in the type name. The compiler refuses a target or binding to an undeclared type with
TYPE_UNKNOWN:
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: [] }{
"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_UNKNOWNundeclared type Emial (line 19)
- 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
fieldor aforEach. 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
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: [] }{
"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