Rule Cascade
LearnLevels and inheritance

Extends

Inherit a parent ruleset's rules, params and messages.

extends names one parent ruleset and the versions you accept. The child starts from the parent's rules, params, entities, types, functions and messages, then adds its own. The parent's rules run first, and each finding's source says which ruleset the rule came from.

Syntax

extends
extends:
  - ruleset: learn.base
    version: "^1.0.0"         # 1.0.0 or later, same major; "1.2.0" means exactly 1.2.0
    checksum: sha256:...      # optional: pin the exact parent content

Example

The base ruleset (version 1.2.0) requires at least one item. The shop extends it and requires a country. One request gets a finding from each.

extends.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.shop, version: 1.0.0, title: "Shop rules" }
scope:
  - { level: organization, id: learn }
  - { level: project, id: shop }
extends:
  - { ruleset: learn.base, version: "^1.0.0" }
rules:
  - id: shop.country.required
    kind: validation
    target: { entity: Order, field: /country }
    operations: [create]
    assert: { op: exists, args: [{ var: data.country }] }
    severity: error
    finding: { code: LRN-EXT-001, message: shop.countryRequired }
messages:
  en:
    shop.countryRequired: "Choose the country to deliver to."
tests:
  - name: inherited and own rules run together
    entity: Order
    operation: create
    given:
      data: { quantity: 0 }
    expect:
      decision: deny
      findings:
        - { rule: base.quantity.min }
        - { rule: shop.country.required }
  - name: a complete order is allowed
    entity: Order
    operation: create
    given:
      data: { quantity: 2, country: "PT" }
    expect: { decision: allow, findings: [] }
base.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.base, version: 1.2.0, title: "Base rules" }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
  - id: base.quantity.min
    kind: validation
    target: { entity: Order, field: /quantity }
    operations: [create]
    assert: { op: gte, args: [{ var: data.quantity }, 1] }
    severity: error
    finding: { code: LRN-BAS-001, message: base.quantityMin }
messages:
  en:
    base.quantityMin: "Order at least one item."
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "quantity": 0
  }
}

Result, from the engine

Decisiondeny2 findings, server channel

  • LRN-BAS-001errorblockingOrder at least one item./quantity
  • LRN-EXT-001errorblockingChoose the country to deliver to./country
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

Common mistakes

Loading checks the parent before anything else, in this order:

  • The parent is not in the registry: EXTENDS_NOT_FOUND.
  • The parent's version is outside the range: EXTENDS_VERSION_MISMATCH. Below, the shop asks for ^2.0.0, but the base is 1.2.0.
  • The parent extends the child, directly or not: EXTENDS_CYCLE.
  • A checksum pin differs from the parent's checksum: EXTENDS_CHECKSUM_MISMATCH.
  • The child declares a rule id the parent already has: RULE_DUPLICATE. Change an inherited rule with an override instead (see Override a rule).
extends-version.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.shop, version: 1.0.0, title: "Shop rules" }
scope:
  - { level: organization, id: learn }
  - { level: project, id: shop }
extends:
  - { ruleset: learn.base, version: "^2.0.0" }
rules:
  - id: shop.country.required
    kind: validation
    target: { entity: Order, field: /country }
    operations: [create]
    assert: { op: exists, args: [{ var: data.country }] }
    severity: error
    finding: { code: LRN-EXT-001, message: shop.countryRequired }
messages:
  en:
    shop.countryRequired: "Choose the country to deliver to."
tests:
  - name: inherited and own rules run together
    entity: Order
    operation: create
    given:
      data: { quantity: 0 }
    expect:
      decision: deny
      findings:
        - { rule: base.quantity.min }
        - { rule: shop.country.required }
  - name: a complete order is allowed
    entity: Order
    operation: create
    given:
      data: { quantity: 2, country: "PT" }
    expect: { decision: allow, findings: [] }
base.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.base, version: 1.2.0, title: "Base rules" }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
  - id: base.quantity.min
    kind: validation
    target: { entity: Order, field: /quantity }
    operations: [create]
    assert: { op: gte, args: [{ var: data.quantity }, 1] }
    severity: error
    finding: { code: LRN-BAS-001, message: base.quantityMin }
messages:
  en:
    base.quantityMin: "Order at least one item."
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "quantity": 0
  }
}

Result, from the engine

does not loadThe engine refuses the ruleset before it evaluates anything.

  • EXTENDS_VERSION_MISMATCH learn.shop: parent learn.base is 1.2.0, need ^2.0.0
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

Exercise

Pin the parent to exactly version 1.2.0. Then add a child rule that requires an e-mail address, with a test that shows its finding.

Hint

Write the version 1.2.0 without the caret, and add a validation rule with its own code and message.

Show answer
extends.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.shop, version: 1.0.0, title: "Shop rules" }
scope:
  - { level: organization, id: learn }
  - { level: project, id: shop }
extends:
  - { ruleset: learn.base, version: "1.2.0" }
rules:
  - id: shop.country.required
    kind: validation
    target: { entity: Order, field: /country }
    operations: [create]
    assert: { op: exists, args: [{ var: data.country }] }
    severity: error
    finding: { code: LRN-EXT-001, message: shop.countryRequired }
  - id: shop.email.required
    kind: validation
    target: { entity: Order, field: /email }
    operations: [create]
    assert: { op: exists, args: [{ var: data.email }] }
    severity: error
    finding: { code: LRN-EXT-002, message: shop.emailRequired }
messages:
  en:
    shop.countryRequired: "Choose the country to deliver to."
    shop.emailRequired: "Give an e-mail address."
tests:
  - name: inherited and own rules run together
    entity: Order
    operation: create
    given:
      data: { quantity: 0, email: "ana@example.com" }
    expect:
      decision: deny
      findings:
        - { rule: base.quantity.min }
        - { rule: shop.country.required }
  - name: a complete order is allowed
    entity: Order
    operation: create
    given:
      data: { quantity: 2, country: "PT", email: "ana@example.com" }
    expect: { decision: allow, findings: [] }
  - name: the child's own rule
    entity: Order
    operation: create
    given:
      data: { quantity: 2, country: "PT" }
    expect:
      decision: deny
      findings:
        - { rule: shop.email.required }
base.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.base, version: 1.2.0, title: "Base rules" }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
  - id: base.quantity.min
    kind: validation
    target: { entity: Order, field: /quantity }
    operations: [create]
    assert: { op: gte, args: [{ var: data.quantity }, 1] }
    severity: error
    finding: { code: LRN-BAS-001, message: base.quantityMin }
messages:
  en:
    base.quantityMin: "Order at least one item."
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "quantity": 0,
    "email": "ana@example.com"
  }
}

Result, from the engine

Decisiondeny2 findings, server channel

  • LRN-BAS-001errorblockingOrder at least one item./quantity
  • LRN-EXT-001errorblockingChoose the country to deliver to./country
Course overview

On this page