Rule Cascade
LearnLevels and inheritance

Custom operators

An operator your application supplies, declared in the ruleset.

A custom operator is a function the host application registers in every engine that evaluates the ruleset. Its name starts with x-, and the ruleset declares it under operators. Arguments are plain JSON values. An operator that is not registered, or that fails, makes the rule fail closed with RULE-EVALUATION-ERROR. Prefer a function when the core operators can express it.

Syntax

declare and use a custom operator
operators:
  x-luhn: { description: True when the text has a valid Luhn check digit., args: 1 }

# in a rule
assert: { op: x-luhn, args: [{ var: data.cardNumber }] }

Example

The playground registers the operators of the conformance suite: x-luhn, x-test-reverse and x-test-sum. Here x-luhn catches a mistyped card number.

custom-operators.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.custom-operators, version: 1.0.0, title: "Custom operators" }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
operators:
  x-luhn: { description: True when the text is a number with a valid Luhn check digit., args: 1 }
rules:
  - id: order.card.luhn
    kind: validation
    target: { entity: Order, field: /cardNumber }
    operations: [create]
    when: { op: exists, args: [{ var: data.cardNumber }] }
    assert: { op: x-luhn, args: [{ var: data.cardNumber }] }
    severity: error
    finding: { code: LRN-OPS-001, message: order.cardInvalid }
messages:
  en:
    order.cardInvalid: "This card number is not valid."
tests:
  - name: a mistyped card number
    entity: Order
    operation: create
    given:
      data: { cardNumber: "4111111111111112" }
    expect:
      decision: deny
      findings:
        - { rule: order.card.luhn }
  - name: a valid card number
    entity: Order
    operation: create
    given:
      data: { cardNumber: "4111111111111111" }
    expect: { decision: allow, findings: [] }
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "cardNumber": "4111111111111112"
  }
}

Result, from the engine

Decisiondeny1 finding, server channel

  • LRN-OPS-001errorblockingThis card number is not valid./cardNumber
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

Common mistakes

  • Using an x- operator without declaring it: OPERATOR_UNDECLARED. The declaration lets every host check at start-up that it registered all the operators the ruleset needs.
  • Writing different code in each language. A custom operator must give the same answer in every runtime, which is why a function is usually the better choice.
custom-operators-undeclared.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.custom-operators, version: 1.0.0, title: "Custom operators" }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
  - id: order.card.luhn
    kind: validation
    target: { entity: Order, field: /cardNumber }
    operations: [create]
    when: { op: exists, args: [{ var: data.cardNumber }] }
    assert: { op: x-luhn, args: [{ var: data.cardNumber }] }
    severity: error
    finding: { code: LRN-OPS-001, message: order.cardInvalid }
messages:
  en:
    order.cardInvalid: "This card number is not valid."
tests:
  - name: a mistyped card number
    entity: Order
    operation: create
    given:
      data: { cardNumber: "4111111111111112" }
    expect:
      decision: deny
      findings:
        - { rule: order.card.luhn }
  - name: a valid card number
    entity: Order
    operation: create
    given:
      data: { cardNumber: "4111111111111111" }
    expect: { decision: allow, findings: [] }
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "cardNumber": "4111111111111112"
  }
}

Result, from the engine

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

  • OPERATOR_UNDECLARED rule: custom operator x-luhn is not declared (line 15)
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

Exercise

Use x-test-reverse (it reverses a text) to require that a reference, when present, reads the same backwards. Test it with abc (denied) and abba (allowed).

Hint

Declare x-test-reverse under operators, then assert eq(x-test-reverse(data.reference), data.reference).

Show answer
custom-operators.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.custom-operators, version: 1.0.0, title: "Custom operators" }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
operators:
  x-luhn: { description: True when the text is a number with a valid Luhn check digit., args: 1 }
  x-test-reverse: { description: The text with its characters in reverse order., args: 1 }
rules:
  - id: order.card.luhn
    kind: validation
    target: { entity: Order, field: /cardNumber }
    operations: [create]
    when: { op: exists, args: [{ var: data.cardNumber }] }
    assert: { op: x-luhn, args: [{ var: data.cardNumber }] }
    severity: error
    finding: { code: LRN-OPS-001, message: order.cardInvalid }
  - id: order.reference.palindrome
    kind: validation
    target: { entity: Order, field: /reference }
    operations: [create]
    when: { op: exists, args: [{ var: data.reference }] }
    assert: { op: eq, args: [{ op: x-test-reverse, args: [{ var: data.reference }] }, { var: data.reference }] }
    severity: error
    finding: { code: LRN-OPS-002, message: order.referencePalindrome }
messages:
  en:
    order.cardInvalid: "This card number is not valid."
    order.referencePalindrome: "The reference must read the same backwards."
tests:
  - name: a mistyped card number
    entity: Order
    operation: create
    given:
      data: { cardNumber: "4111111111111112" }
    expect:
      decision: deny
      findings:
        - { rule: order.card.luhn }
  - name: a valid card number
    entity: Order
    operation: create
    given:
      data: { cardNumber: "4111111111111111" }
    expect: { decision: allow, findings: [] }
  - name: a reference that is not a palindrome
    entity: Order
    operation: create
    given:
      data: { cardNumber: "4111111111111111", reference: "abc" }
    expect:
      decision: deny
      findings:
        - { rule: order.reference.palindrome }
  - name: a palindrome reference
    entity: Order
    operation: create
    given:
      data: { cardNumber: "4111111111111111", reference: "abba" }
    expect: { decision: allow, findings: [] }
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "cardNumber": "4111111111111112"
  }
}

Result, from the engine

Decisiondeny1 finding, server channel

  • LRN-OPS-001errorblockingThis card number is not valid./cardNumber
Course overview

On this page