Rule Cascade
LearnLevels and inheritance

Params and override policies

Typed constants that a child ruleset may change only as far as the parent allows.

A param is a typed constant a rule reads as params.<name>. The parent declares it with a type and a default. A child changes it under overrides.params, as far as the param's overridePolicy allows: locked (never), tighten-only (only in its tightenDirection, lower or higher) or open (any value).

Syntax

declare a param (parent)
params:
  maxQuantity:
    type: integer            # string, number, integer, boolean, stringList, numberList
    default: 10
    description: Most items in one order.
    overridePolicy: tighten-only
    tightenDirection: lower
change it (child)
overrides:
  params:
    maxQuantity: 5

Example

store.ruleset.yaml declares four params, one of each policy. The kiosk lowers maxQuantity to 5 and turns gift wrapping off. A request for 7 items is denied, and the message shows the kiosk's limit.

params.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.kiosk, version: 1.0.0, title: "Kiosk rules" }
scope:
  - { level: organization, id: learn }
  - { level: project, id: kiosk }
extends:
  - { ruleset: learn.store, version: "^1.0.0" }
overrides:
  params:
    maxQuantity: 5
    giftWrapAllowed: false
rules: []
tests:
  - name: the kiosk allows only five items
    entity: Order
    operation: create
    given:
      data: { quantity: 7, total: 30, currency: "EUR" }
    expect:
      decision: deny
      findings:
        - { rule: store.quantity.max, message: "You can order at most 5 items." }
  - name: the kiosk does not gift wrap
    entity: Order
    operation: create
    given:
      data: { quantity: 2, total: 30, currency: "EUR", giftWrap: true }
    expect:
      decision: deny
      findings:
        - { rule: store.gift-wrap }
  - name: five items are fine
    entity: Order
    operation: create
    given:
      data: { quantity: 5, total: 30, currency: "EUR" }
    expect: { decision: allow, findings: [] }
store.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.store, version: 1.0.0, title: "Store rules" }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
params:
  maxQuantity:
    type: integer
    default: 10
    description: Most items in one order.
    overridePolicy: tighten-only
    tightenDirection: lower
  minTotal:
    type: number
    default: 5
    description: Smallest order total.
    overridePolicy: tighten-only
    tightenDirection: higher
  currency:
    type: string
    default: EUR
    overridePolicy: locked
  giftWrapAllowed:
    type: boolean
    default: true
    overridePolicy: open
rules:
  - id: store.quantity.max
    kind: validation
    target: { entity: Order, field: /quantity }
    operations: [create]
    assert: { op: lte, args: [{ var: data.quantity }, { var: params.maxQuantity }] }
    severity: error
    finding: { code: LRN-PAR-001, message: store.quantityMax, args: { max: { var: params.maxQuantity } } }
  - id: store.total.min
    kind: validation
    target: { entity: Order, field: /total }
    operations: [create]
    assert: { op: gte, args: [{ var: data.total }, { var: params.minTotal }] }
    severity: error
    finding: { code: LRN-PAR-002, message: store.totalMin, args: { min: { var: params.minTotal } } }
  - id: store.currency
    kind: validation
    target: { entity: Order, field: /currency }
    operations: [create]
    assert: { op: eq, args: [{ var: data.currency }, { var: params.currency }] }
    severity: error
    finding: { code: LRN-PAR-003, message: store.currency, args: { currency: { var: params.currency } } }
  - id: store.gift-wrap
    kind: validation
    target: { entity: Order, field: /giftWrap }
    operations: [create]
    assert: { op: or, args: [{ op: ne, args: [{ var: data.giftWrap }, true] }, { var: params.giftWrapAllowed }] }
    severity: error
    finding: { code: LRN-PAR-004, message: store.giftWrap }
messages:
  en:
    store.quantityMax: "You can order at most {max} items."
    store.totalMin: "The smallest order is {min}."
    store.currency: "This store sells in {currency}."
    store.giftWrap: "Gift wrapping is not available here."
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "quantity": 7,
    "total": 30,
    "currency": "EUR"
  }
}

Result, from the engine

Decisiondeny1 finding, server channel

  • LRN-PAR-001errorblockingYou can order at most 5 items./quantity
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

Common mistakes

  • Moving a tighten-only param the wrong way. The kiosk below raises maxQuantity to 20, so it does not load: PARAM_LOOSENED.
  • Changing a locked param (PARAM_LOCKED), overriding a param nobody declared (PARAM_UNKNOWN), declaring an inherited param again (PARAM_REDEFINED), a value of the wrong type (PARAM_TYPE_MISMATCH), or reading params.x that is not declared (PARAM_UNDECLARED).
params-loosened.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.kiosk, version: 1.0.0, title: "Kiosk rules" }
scope:
  - { level: organization, id: learn }
  - { level: project, id: kiosk }
extends:
  - { ruleset: learn.store, version: "^1.0.0" }
overrides:
  params:
    maxQuantity: 20
rules: []
tests:
  - name: the kiosk allows only twenty items
    entity: Order
    operation: create
    given:
      data: { quantity: 25, total: 30, currency: "EUR" }
    expect:
      decision: deny
      findings:
        - { rule: store.quantity.max, message: "You can order at most 20 items." }
  - name: twenty items are fine
    entity: Order
    operation: create
    given:
      data: { quantity: 20, total: 30, currency: "EUR" }
    expect: { decision: allow, findings: [] }
store.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.store, version: 1.0.0, title: "Store rules" }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
params:
  maxQuantity:
    type: integer
    default: 10
    description: Most items in one order.
    overridePolicy: tighten-only
    tightenDirection: lower
  minTotal:
    type: number
    default: 5
    description: Smallest order total.
    overridePolicy: tighten-only
    tightenDirection: higher
  currency:
    type: string
    default: EUR
    overridePolicy: locked
  giftWrapAllowed:
    type: boolean
    default: true
    overridePolicy: open
rules:
  - id: store.quantity.max
    kind: validation
    target: { entity: Order, field: /quantity }
    operations: [create]
    assert: { op: lte, args: [{ var: data.quantity }, { var: params.maxQuantity }] }
    severity: error
    finding: { code: LRN-PAR-001, message: store.quantityMax, args: { max: { var: params.maxQuantity } } }
  - id: store.total.min
    kind: validation
    target: { entity: Order, field: /total }
    operations: [create]
    assert: { op: gte, args: [{ var: data.total }, { var: params.minTotal }] }
    severity: error
    finding: { code: LRN-PAR-002, message: store.totalMin, args: { min: { var: params.minTotal } } }
  - id: store.currency
    kind: validation
    target: { entity: Order, field: /currency }
    operations: [create]
    assert: { op: eq, args: [{ var: data.currency }, { var: params.currency }] }
    severity: error
    finding: { code: LRN-PAR-003, message: store.currency, args: { currency: { var: params.currency } } }
  - id: store.gift-wrap
    kind: validation
    target: { entity: Order, field: /giftWrap }
    operations: [create]
    assert: { op: or, args: [{ op: ne, args: [{ var: data.giftWrap }, true] }, { var: params.giftWrapAllowed }] }
    severity: error
    finding: { code: LRN-PAR-004, message: store.giftWrap }
messages:
  en:
    store.quantityMax: "You can order at most {max} items."
    store.totalMin: "The smallest order is {min}."
    store.currency: "This store sells in {currency}."
    store.giftWrap: "Gift wrapping is not available here."
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "quantity": 25,
    "total": 30,
    "currency": "EUR"
  }
}

Result, from the engine

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

  • PARAM_LOOSENED learn.kiosk: param maxQuantity may only move lower (line 11)
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

Exercise

The kiosk does not take orders below 20. Override minTotal and add a test where a total of 15 is denied with the kiosk's minimum in the message.

Hint

minTotal is tighten-only with tightenDirection: higher, so a bigger value is allowed. Add minTotal: 20 under overrides.params.

Show answer
params.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.kiosk, version: 1.0.0, title: "Kiosk rules" }
scope:
  - { level: organization, id: learn }
  - { level: project, id: kiosk }
extends:
  - { ruleset: learn.store, version: "^1.0.0" }
overrides:
  params:
    maxQuantity: 5
    giftWrapAllowed: false
    minTotal: 20
rules: []
tests:
  - name: the kiosk allows only five items
    entity: Order
    operation: create
    given:
      data: { quantity: 7, total: 25, currency: "EUR" }
    expect:
      decision: deny
      findings:
        - { rule: store.quantity.max, message: "You can order at most 5 items." }
  - name: the kiosk does not gift wrap
    entity: Order
    operation: create
    given:
      data: { quantity: 2, total: 25, currency: "EUR", giftWrap: true }
    expect:
      decision: deny
      findings:
        - { rule: store.gift-wrap }
  - name: five items are fine
    entity: Order
    operation: create
    given:
      data: { quantity: 5, total: 25, currency: "EUR" }
    expect: { decision: allow, findings: [] }
  - name: the kiosk minimum is 20
    entity: Order
    operation: create
    given:
      data: { quantity: 1, total: 15, currency: "EUR" }
    expect:
      decision: deny
      findings:
        - { rule: store.total.min, message: "The smallest order is 20." }
store.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.store, version: 1.0.0, title: "Store rules" }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
params:
  maxQuantity:
    type: integer
    default: 10
    description: Most items in one order.
    overridePolicy: tighten-only
    tightenDirection: lower
  minTotal:
    type: number
    default: 5
    description: Smallest order total.
    overridePolicy: tighten-only
    tightenDirection: higher
  currency:
    type: string
    default: EUR
    overridePolicy: locked
  giftWrapAllowed:
    type: boolean
    default: true
    overridePolicy: open
rules:
  - id: store.quantity.max
    kind: validation
    target: { entity: Order, field: /quantity }
    operations: [create]
    assert: { op: lte, args: [{ var: data.quantity }, { var: params.maxQuantity }] }
    severity: error
    finding: { code: LRN-PAR-001, message: store.quantityMax, args: { max: { var: params.maxQuantity } } }
  - id: store.total.min
    kind: validation
    target: { entity: Order, field: /total }
    operations: [create]
    assert: { op: gte, args: [{ var: data.total }, { var: params.minTotal }] }
    severity: error
    finding: { code: LRN-PAR-002, message: store.totalMin, args: { min: { var: params.minTotal } } }
  - id: store.currency
    kind: validation
    target: { entity: Order, field: /currency }
    operations: [create]
    assert: { op: eq, args: [{ var: data.currency }, { var: params.currency }] }
    severity: error
    finding: { code: LRN-PAR-003, message: store.currency, args: { currency: { var: params.currency } } }
  - id: store.gift-wrap
    kind: validation
    target: { entity: Order, field: /giftWrap }
    operations: [create]
    assert: { op: or, args: [{ op: ne, args: [{ var: data.giftWrap }, true] }, { var: params.giftWrapAllowed }] }
    severity: error
    finding: { code: LRN-PAR-004, message: store.giftWrap }
messages:
  en:
    store.quantityMax: "You can order at most {max} items."
    store.totalMin: "The smallest order is {min}."
    store.currency: "This store sells in {currency}."
    store.giftWrap: "Gift wrapping is not available here."
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "quantity": 7,
    "total": 25,
    "currency": "EUR"
  }
}

Result, from the engine

Decisiondeny1 finding, server channel

  • LRN-PAR-001errorblockingYou can order at most 5 items./quantity
Course overview

On this page