Rule Cascade
LearnTesting

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

the rcas command
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
the shape of a bundle
{ "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.

bundles.ruleset.yaml
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 }
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "quantity": 11
  }
}

Result, from the engine

Decisiondeny2 findings, server channel

  • LRN-BND-001errorblockingYou can order at most 10 items./quantity
  • LRN-BND-002errorblockingEvery order needs a reference./reference
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

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
bundles.ruleset.yaml
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: [] }
request.json
{
  "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
Course overview

On this page