5.1 KiB
Operators module — @suaveplan/rules/operators
(no submodules)
Purpose
Exposes the canonical OperatorRegistry class plus 33 built-in operator definitions covering equality, numeric comparison, collection membership, existence, type, length, and string-shape checks. The engine can dispatch comparison through this registry (or a caller-supplied alternative) instead of its internal compareValues table — both paths satisfy the same OperatorDefinition.evaluate contract, so call sites do not change.
The four logical operators (and, or, not, xor) are surfaced as placeholder OperatorDefinitions for callers who enumerate the universe of operators (e.g. building a UI). Their evaluate throws — the engine handles logical operators through its evaluateLogical path, not the registry.
Features
OperatorRegistry—register,get,mustGet(raisesRULES.OPERATOR_NOT_FOUND),has,names,unregister. Supports per-operatoraliases.- 30 working comparison operators wrapping the engine's internal
compareValuesdispatcher so behaviour is identical between the registry path and the engine's built-in fast path. - 4 logical placeholder operators (
and/or/not/xor) that throw on direct invocation — they exist for enumeration and discoverability. BUILTIN_OPERATORS— readonly tuple of every built-in.createDefaultOperatorRegistry()— fresh registry instance pre-populated with the built-ins. Recommended path for tests and callers that want isolation.defaultOperatorRegistry— process-global pre-populated singleton for one-shot convenience.
Usage: Basic Example
import { defaultOperatorRegistry } from "@suaveplan/rules/operators";
const op = defaultOperatorRegistry.mustGet("greaterThan");
await op.evaluate(21, 18); // true
Usage: Advanced Example
import {
OperatorRegistry,
createDefaultOperatorRegistry,
} from "@suaveplan/rules/operators";
import type { OperatorDefinition } from "@suaveplan/rules";
const isOddOperator: OperatorDefinition<number, void> = {
name: "isOdd",
description: "actual % 2 !== 0",
evaluate: (actual) => Number(actual) % 2 !== 0,
};
const registry: OperatorRegistry = createDefaultOperatorRegistry();
registry.register("isOdd", isOddOperator);
// register an alias too:
registry.register("notDivisibleByTwo", { ...isOddOperator, aliases: ["odd"] });
registry.has("odd"); // true (alias)
registry.unregister("isOdd"); // true
API Reference
class OperatorRegistry implements OperatorRegistryLike
| Method | Returns | Notes |
|---|---|---|
register(name, definition) |
void |
Empty name → RULES.OPERATOR_INVALID. Definition aliases are registered as additional names. Re-registration replaces the prior entry. |
get(name) |
OperatorDefinition | undefined |
Lenient lookup. |
mustGet(name) |
OperatorDefinition |
Strict lookup; raises RulesError(RULES.OPERATOR_NOT_FOUND) when absent. |
has(name) |
boolean |
Includes aliases. |
names() |
readonly string[] |
Snapshot of all keys (includes aliases). |
unregister(name) |
boolean |
true when removed, false when absent. |
Built-in definitions
Every name in the ComparisonOperator union has a matching <name>Operator export — for example equalsOperator, greaterThanOperator, betweenOperator, containsOperator, startsWithOperator, matchesOperator, lengthEqualsOperator. Each definition delegates to compareValues(actual, name, expected) so registry-path and engine-internal-path behaviour are bit-identical.
createDefaultOperatorRegistry(): OperatorRegistry
Returns a fresh registry pre-populated with BUILTIN_OPERATORS. Prefer this over defaultOperatorRegistry when you need isolation between tests, requests, or tenants.
defaultOperatorRegistry: OperatorRegistry
Process-global pre-populated registry. Mutable — callers who register custom operators against it will see those changes from every other consumer of the same singleton. Use with care; prefer createDefaultOperatorRegistry() when in doubt.
Implementation Notes
- The four logical placeholder operators (
andOperator,orOperator,notOperator,xorOperator) throw on directevaluatecall because logical operators in this package have always been engine-managed (they need access to the child rule list, depth tracker, and abort signal — none of which fit theOperatorDefinition.evaluate(actual, expected, context?)shape). They are kept for enumeration completeness and would be removed if a future change unifies the dispatch model. BUILTIN_OPERATORSisObject.freezed to make its shape part of the public contract — consumers can iterate it but cannot mutate.- The registry's alias handling is deliberately simple: aliases register additional keys pointing at the same
OperatorDefinitionreference.unregister(alias)removes only the alias, not the primary name. - This module does NOT register itself against the engine at import time. Engine consumers wire it explicitly via
engine.use(plugin)when they want registry-based dispatch.