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

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 (raises RULES.OPERATOR_NOT_FOUND), has, names, unregister. Supports per-operator aliases.
  • 30 working comparison operators wrapping the engine's internal compareValues dispatcher 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 direct evaluate call 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 the OperatorDefinition.evaluate(actual, expected, context?) shape). They are kept for enumeration completeness and would be removed if a future change unifies the dispatch model.
  • BUILTIN_OPERATORS is Object.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 OperatorDefinition reference. 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.