Files
skills/docs/foundation/flow/rules/src/engine/operators-internal.md
2026-05-29 08:20:56 +01:00

4.9 KiB

Internal operator dispatcher — @suaveplan/rules/engine/operators-internal

Purpose

compareValues(actual, operator, expected) is the built-in comparison dispatcher used by RuleEngine until the dedicated operators module ships. It maps every member of the 33-entry ComparisonOperator union to a deterministic boolean over (actual, expected). Unknown operators raise RulesError(RULES.OPERATOR_NOT_FOUND); the engine intercepts that error to increment the rules.operator.unknown_total{name} counter and converts the failure into a passed: false evaluation result.

This file is private to the engine (not re-exported from the package root). The public operators module — when it lands — will register the same 30+ operators against a real OperatorRegistry and the engine will dispatch through that registry instead. Both paths satisfy the same OperatorDefinition.evaluate contract, so call sites do not change.

Features

  • Single-function module — no class, no state, no module-level side effects.
  • Exhaustive switch over ComparisonOperator; default is typed never so adding a new operator name to the union is a compile error here.
  • Handles null / undefined / non-string / non-array inputs gracefully — every operator has a documented behaviour on the wrong shape rather than throwing.
  • matches / notMatches accept either a string source or a RegExp instance; the source is recompiled to a RegExp per call.

Usage: Basic Example

import { compareValues } from "@suaveplan/rules/engine/operators-internal";

compareValues(21, "greaterThanOrEqual", 18); // true
compareValues([1, 2, 3], "containsAll", [1, 2]); // true

Usage: Advanced Example

import { compareValues } from "@suaveplan/rules/engine/operators-internal";
import { RulesError, isRulesError } from "@suaveplan/rules/errors";

function safeCompare(a: unknown, op: string, b: unknown): boolean {
    try {
        return compareValues(a, op as never, b);
    } catch (err) {
        if (isRulesError(err) && err.code === "RULES.OPERATOR_NOT_FOUND") {
            return false; // unknown operator behaves as "no match"
        }
        throw err;
    }
}

API Reference

compareValues(actual: unknown, operator: ComparisonOperator, expected: unknown): boolean

Returns the boolean outcome of applying operator to (actual, expected). Documented per-operator semantics:

  • equality: equals / strictEquals use ===; notEquals / strictNotEquals use !==. Both pairs are equivalent — the strict variants are kept so consumers using a JSON DSL can be explicit.
  • numeric: greaterThan / greaterThanOrEqual / lessThan / lessThanOrEqual cast both sides to number. between / notBetween accept [min, max] tuples and are inclusive on both bounds.
  • collection: in / notIn test membership of actual in expected (an array). contains / notContains work on either arrays or strings; non-array, non-string actual returns false for contains and true for notContains. containsAll / containsAny / overlaps require both sides to be arrays.
  • existence: exists is actual !== undefined; notExists is the inverse. isNull / isNotNull are strict === null checks. isEmpty / isNotEmpty cover string / array / object / null / undefined.
  • type: isType is typeof actual === expected; isNotType is the inverse. hasProperty is Object.hasOwn(actual, expected) over objects (false otherwise).
  • length: lengthEquals / lengthGreaterThan / lengthLessThan operate on string / array / object (object uses Object.keys count). Other types yield NaN which makes the comparison fall through to false.
  • string: startsWith / endsWith require typeof actual === "string". matches accepts a string or RegExp; on non-string actual, matches is false and notMatches is true.

Throws

  • RulesError(RULES.OPERATOR_NOT_FOUND) when operator is outside the documented union.

Implementation Notes

  • The dispatcher does NOT memoise the per-call RegExp construction in matches / notMatches. For rules that fire frequently with the same pattern, callers can pre-compile and pass the RegExp instance instead of a string — compareValues accepts both.
  • The default branch uses const unknownOperator: never = operator; to leverage the compiler's exhaustiveness checking; the runtime throw is a defensive belt-and-braces against as never casts at the boundary.
  • countable(value) is the shared length helper; non-countable inputs return NaN so every length comparison naturally yields false (NaN comparisons are always false).
  • The function is intentionally synchronous. Async operators are out of scope for the built-in dispatcher — plugins that register async operators on the engine go through the OperatorDefinition.evaluate contract which accepts a Promise<boolean> return.