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
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.
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: [] }{
"entity": "Order",
"operation": "create",
"data": {
"cardNumber": "4111111111111112"
}
}Result, from the engine
Decisiondeny1 finding, server channel
LRN-OPS-001errorblockingThis card number is not valid./cardNumber
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.
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: [] }{
"entity": "Order",
"operation": "create",
"data": {
"cardNumber": "4111111111111112"
}
}Result, from the engine
does not loadThe engine refuses the ruleset before it evaluates anything.
OPERATOR_UNDECLAREDrule: custom operator x-luhn is not declared (line 15)
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
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: [] }{
"entity": "Order",
"operation": "create",
"data": {
"cardNumber": "4111111111111112"
}
}Result, from the engine
Decisiondeny1 finding, server channel
LRN-OPS-001errorblockingThis card number is not valid./cardNumber