How a ruleset works
The parts of a ruleset, and the three things that happen to it — compile, publish, evaluate.
A ruleset is one YAML or JSON document of kind: RuleSet. It says who owns it, which data its
rules talk about, the rules themselves, the messages users read and the tests that prove it works.
A compiler checks the document and turns it into a bundle. An engine evaluates a request against
that bundle and returns a decision. Evaluation is a pure function: the same request always gives the
same answer, in every language.
Syntax
ruleCascade: 1.0.0 # envelope: the version of the specification
kind: RuleSet
metadata: { id, version, title, owner, status }
scope: [ { level, id } ] # hierarchy: where the ruleset sits
entities: # vocabulary: the data the rules check
<Entity>:
description: <text>
schema: { $ref: "<schema file>#/<pointer>" }
rules: [ ... ] # the logic
messages: { <locale>: { <key>: <template> } } # presentation
bindings: # which API operations the rules guard
openapi: [ { document, entity, operations: { <operation>: <operationId> } } ]
tests: [ ... ] # golden tests| Section | What it holds |
|---|---|
ruleCascade, kind, metadata | The envelope: specification version, document kind, the ruleset's id, version, title, owner and status. |
scope | Where the ruleset sits: a list of {level, id}, least specific first. |
entities | The data rules may mention. Each entity points at a JSON Schema, usually inside an OpenAPI file, with schema.$ref. description is for people. |
rules | The logic. Each rule has an id, a kind, a target and operations. |
messages | Message templates, per locale. |
bindings.openapi | Which OpenAPI operation each rule operation guards, for tools that wire rules into an API. |
tests | Golden tests every engine must pass. |
Three things happen to a ruleset, often in three different programs:
- Compile. The compiler validates the document, checks every path against the entity schema, computes a checksum and writes a bundle.
- Publish. You hand the bundle, or one manifest of it, to whoever evaluates.
- Evaluate. An engine takes the bundle and a request and returns a decision, findings, effects and commands.
Example
This ruleset has one of every main section. The request has no e-mail address.
# Envelope: the spec version, the kind of document and who owns it.
ruleCascade: 1.0.0
kind: RuleSet
metadata:
id: learn.how-it-works
version: 1.0.0
title: Orders
owner: shop-team
status: active
# Hierarchy: where the ruleset sits in the enterprise.
scope:
- { level: organization, id: learn }
# Vocabulary: the data the rules may talk about.
entities:
Order:
description: An order placed in the web shop.
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
# Rules: the logic.
rules:
- id: order.email.required
kind: validation
target: { entity: Order, field: /email }
operations: [create]
assert: { op: exists, args: [{ var: data.email }] }
severity: error
finding: { code: LRN-HOW-001, message: order.emailMissing }
# Presentation: what users read.
messages:
en:
order.emailMissing: "Enter an e-mail address for the receipt."
# Bindings: which API operations the rules guard.
bindings:
openapi:
- document: ./learn.openapi.yaml
entity: Order
operations: { create: createOrder }
# Conformance: golden tests every engine must pass.
tests:
- name: an order without an e-mail is denied
entity: Order
operation: create
given:
data: { quantity: 2 }
expect:
decision: deny
findings:
- { rule: order.email.required, fields: [/email] }
- name: an order with an e-mail is allowed
entity: Order
operation: create
given:
data: { quantity: 2, email: ana@example.com }
expect: { decision: allow, findings: [] }The entity schema, learn.openapi.yaml (the same for every lesson)
openapi: 3.1.0
info: { title: Learn Rule Cascade, version: 1.0.0 }
paths: {}
components:
schemas:
Order:
type: object
properties:
id: { type: string }
quantity: { type: integer }
price: { type: number }
total: { type: number }
discount: { type: number }
shipping: { type: number }
coupon: { type: string }
email: { type: string }
country: { type: string }
status: { type: string }
express: { type: boolean }
giftWrap: { type: boolean }
giftMessage: { type: string }
notes: { type: string }
reference: { type: string }
orderDate: { type: string }
deliveryDate: { type: string }
paymentMethod: { type: string }
cardNumber: { type: string }
currency: { type: string }
weight: { type: number }
priority: { type: integer }
approved: { type: boolean }
cancelReason: { type: string }
promoCodes: { type: array, items: { type: string } }
scores: { type: array, items: { type: number } }
tags: { type: array, items: { type: string } }
items:
type: array
items:
type: object
properties:
sku: { type: string }
qty: { type: integer }
price: { type: number }
Customer:
type: object
properties:
id: { type: string }
name: { type: string }
nickname: { type: string }
email: { type: string }
backupEmail: { type: string }
phone: { type: string }
country: { type: string }
dateOfBirth: { type: string }
vatId: { type: string }
company: { type: string }
accountType: { type: string }
newsletter: { type: boolean }
tier: { type: string }
age: { type: integer }
website: { type: string }
postcode: { type: string }
username: { type: string }
iban: { type: string }
bio: { type: string }
signupDate: { type: string }
lastLogin: { type: string }
creditLimit: { type: number }
balance: { type: number }
verified: { type: boolean }
roles: { type: array, items: { type: string } }
address:
type: object
properties:
street: { type: string }
city: { type: string }
postcode: { type: string }
country: { type: string }
contacts:
type: array
items:
type: object
properties:
name: { type: string }
email: { type: string }
phone: { type: string }
Ticket:
type: object
properties:
id: { type: string }
title: { type: string }
description: { type: string }
status: { type: string }
priority: { type: string }
assignee: { type: string }
reporter: { type: string }
dueDate: { type: string }
createdAt: { type: string }
closedAt: { type: string }
resolution: { type: string }
estimate: { type: number }
labels: { type: array, items: { type: string } }{
"entity": "Order",
"operation": "create",
"data": {
"quantity": 2
}
}Result, from the engine
Decisiondeny1 finding, server channel
LRN-HOW-001errorblockingEnter an e-mail address for the receipt./email
Common mistakes
- Writing the schema inline.
schemaholds only a$refto a schema document. - Forgetting the message. Every finding's message key must exist in the default locale.
- Expecting the engine to read files or the clock while it evaluates. It never does: pass facts in the request.
Exercise
Add a second rule: an order must have a /country. Give it the code LRN-HOW-002 and the message
"Choose the country to deliver to." Test it with an order that has an e-mail but no country.
Hint
Copy the e-mail rule, change its id, field, code and message key, and add the message under messages.en.
Show answer
ruleCascade: 1.0.0
kind: RuleSet
metadata:
id: learn.how-it-works
version: 1.0.0
title: Orders
owner: shop-team
status: active
scope:
- { level: organization, id: learn }
entities:
Order:
description: An order placed in the web shop.
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
- id: order.email.required
kind: validation
target: { entity: Order, field: /email }
operations: [create]
assert: { op: exists, args: [{ var: data.email }] }
severity: error
finding: { code: LRN-HOW-001, message: order.emailMissing }
- id: order.country.required
kind: validation
target: { entity: Order, field: /country }
operations: [create]
assert: { op: exists, args: [{ var: data.country }] }
severity: error
finding: { code: LRN-HOW-002, message: order.countryMissing }
messages:
en:
order.emailMissing: "Enter an e-mail address for the receipt."
order.countryMissing: "Choose the country to deliver to."
bindings:
openapi:
- document: ./learn.openapi.yaml
entity: Order
operations: { create: createOrder }
tests:
- name: an order without a country is denied
entity: Order
operation: create
given:
data: { quantity: 2, email: ana@example.com }
expect:
decision: deny
findings:
- { rule: order.country.required, fields: [/country] }
- name: a complete order is allowed
entity: Order
operation: create
given:
data: { quantity: 2, email: ana@example.com, country: ES }
expect: { decision: allow, findings: [] }{
"entity": "Order",
"operation": "create",
"data": {
"quantity": 2,
"email": "ana@example.com"
}
}Result, from the engine
Decisiondeny1 finding, server channel
LRN-HOW-002errorblockingChoose the country to deliver to./country