Bundles, manifests and checksums
Compile once, ship one sealed JSON file, and prove every engine evaluates the same rules.
A runtime does not need your YAML. The compiler turns a ruleset and its parents into a bundle:
one JSON file with a server manifest (every rule) and a client manifest (only the client
and both rules, with the params, functions and messages they need). Both carry the checksum.
The checksum is sha256: plus the SHA-256 of the canonical JSON of the resolved ruleset: keys
sorted, no whitespace, plain numbers. Two engines that load the same documents compute the same
checksum. Log it with every decision, and you always know which rules made it.
ruleCascade is the version of the specification, ruleCascadeBundle the version of the bundle
format, and metadata.version your ruleset's own semantic version.
Syntax
rcas check orders.ruleset.yaml # lint, load, run the golden tests
rcas compile orders.ruleset.yaml -o orders.bundle.json # seal it into a bundle
rcas manifest orders.bundle.json --channel client # the manifest a browser receives
rcas evaluate --bundle orders.bundle.json request.json # evaluate one request
rcas engine # JSON Lines engine protocol on stdin/stdout{ "ruleCascadeBundle": "1.0.0", "id": "learn.bundles", "version": "1.0.0", "checksum": "sha256:...",
"manifests": { "server": { }, "client": { } } }The engine command speaks one JSON request per line in and one response per line out. Programs in
any language use it with the commands version, load, manifest, evaluate, expression and
compile.
Example
order.reference.server is a server rule, so it is in the server manifest only. The request
below is evaluated on the server.
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.bundles, version: 1.0.0, title: Bundles }
scope:
- { level: organization, id: learn }
entities:
Order:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
- id: order.quantity.max
kind: validation
target: { entity: Order, field: /quantity }
operations: [create]
assert: { op: lte, args: [{ var: data.quantity }, 10] }
severity: error
finding: { code: LRN-BND-001, message: order.tooMany }
- id: order.reference.server
kind: validation
target: { entity: Order, field: /reference }
operations: [create]
enforcement: server
assert: { op: exists, args: [{ var: data.reference }] }
severity: error
finding: { code: LRN-BND-002, message: order.noReference }
messages:
en:
order.tooMany: "You can order at most 10 items."
order.noReference: "Every order needs a reference."
tests:
- name: the server manifest has both rules
entity: Order
operation: create
given:
data: { quantity: 11 }
expect:
decision: deny
findings:
- { rule: order.quantity.max }
- { rule: order.reference.server }
- name: the client manifest has only the quantity rule
entity: Order
operation: create
channel: client
given:
data: { quantity: 11 }
expect:
decision: deny
findings:
- { rule: order.quantity.max }{
"entity": "Order",
"operation": "create",
"data": {
"quantity": 11
}
}Result, from the engine
Decisiondeny2 findings, server channel
LRN-BND-001errorblockingYou can order at most 10 items./quantityLRN-BND-002errorblockingEvery order needs a reference./reference
Open it in the playground and press Check: the playground shows the ruleset's checksum. Change any rule, press Check again, and the checksum changes. Switch the channel to client and evaluate: only the quantity rule is in the client manifest.
Common mistakes
- Compiling on every request. Compile in CI, store the bundle, and load it at start-up.
- Editing a bundle by hand. A runtime trusts a bundle: the compiler already ran every check. Change the YAML and compile again.
- Reusing a version. Give every change a new
metadata.version, and keep old bundles immutable, so a rollback is a file you already have.
Exercise
Release version 1.1.0 of this ruleset with a new rule: express delivery (express: true) is only
for France (country: "FR"). Write golden tests for an express order to DE (denied) and to FR
(allowed).
Hint
Raise metadata.version to 1.1.0, add a validation rule with a when guard, and add its message.
Show answer
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.bundles, version: 1.1.0, title: Bundles }
scope:
- { level: organization, id: learn }
entities:
Order:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
- id: order.quantity.max
kind: validation
target: { entity: Order, field: /quantity }
operations: [create]
assert: { op: lte, args: [{ var: data.quantity }, 10] }
severity: error
finding: { code: LRN-BND-001, message: order.tooMany }
- id: order.reference.server
kind: validation
target: { entity: Order, field: /reference }
operations: [create]
enforcement: server
assert: { op: exists, args: [{ var: data.reference }] }
severity: error
finding: { code: LRN-BND-002, message: order.noReference }
- id: order.express.country
kind: validation
target: { entity: Order, field: /express }
operations: [create]
when: { op: eq, args: [{ var: data.express }, true] }
assert: { op: eq, args: [{ var: data.country }, "FR"] }
severity: error
finding: { code: LRN-BND-003, message: order.expressOnlyFr }
messages:
en:
order.tooMany: "You can order at most 10 items."
order.noReference: "Every order needs a reference."
order.expressOnlyFr: "Express delivery is only available in France."
tests:
- name: express outside France is denied
entity: Order
operation: create
given:
data: { quantity: 2, reference: "A-1", express: true, country: "DE" }
expect:
decision: deny
findings:
- { rule: order.express.country, fields: [/express] }
- name: express in France is allowed
entity: Order
operation: create
given:
data: { quantity: 2, reference: "A-1", express: true, country: "FR" }
expect: { decision: allow, findings: [] }{
"entity": "Order",
"operation": "create",
"data": {
"quantity": 2,
"reference": "A-1",
"express": true,
"country": "DE"
}
}Result, from the engine
Decisiondeny1 finding, server channel
LRN-BND-003errorblockingExpress delivery is only available in France./express