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
switchoverComparisonOperator;defaultis typedneverso 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/notMatchesaccept either a string source or aRegExpinstance; the source is recompiled to aRegExpper 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/strictEqualsuse===;notEquals/strictNotEqualsuse!==. Both pairs are equivalent — the strict variants are kept so consumers using a JSON DSL can be explicit. - numeric:
greaterThan/greaterThanOrEqual/lessThan/lessThanOrEqualcast both sides tonumber.between/notBetweenaccept[min, max]tuples and are inclusive on both bounds. - collection:
in/notIntest membership ofactualinexpected(an array).contains/notContainswork on either arrays or strings; non-array, non-stringactualreturnsfalseforcontainsandtruefornotContains.containsAll/containsAny/overlapsrequire both sides to be arrays. - existence:
existsisactual !== undefined;notExistsis the inverse.isNull/isNotNullare strict=== nullchecks.isEmpty/isNotEmptycover string / array / object / null / undefined. - type:
isTypeistypeof actual === expected;isNotTypeis the inverse.hasPropertyisObject.hasOwn(actual, expected)over objects (false otherwise). - length:
lengthEquals/lengthGreaterThan/lengthLessThanoperate on string / array / object (object usesObject.keyscount). Other types yieldNaNwhich makes the comparison fall through tofalse. - string:
startsWith/endsWithrequiretypeof actual === "string".matchesaccepts a string orRegExp; on non-stringactual,matchesisfalseandnotMatchesistrue.
Throws
RulesError(RULES.OPERATOR_NOT_FOUND)whenoperatoris outside the documented union.
Implementation Notes
- The dispatcher does NOT memoise the per-call
RegExpconstruction inmatches/notMatches. For rules that fire frequently with the same pattern, callers can pre-compile and pass theRegExpinstance instead of a string —compareValuesaccepts both. - The
defaultbranch usesconst unknownOperator: never = operator;to leverage the compiler's exhaustiveness checking; the runtimethrowis a defensive belt-and-braces againstas nevercasts at the boundary. countable(value)is the shared length helper; non-countable inputs returnNaNso every length comparison naturally yieldsfalse(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.evaluatecontract which accepts aPromise<boolean>return.