first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
# Operators module — `@suaveplan/rules/operators`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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 `OperatorDefinition`s 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
```ts
import { defaultOperatorRegistry } from "@suaveplan/rules/operators";
const op = defaultOperatorRegistry.mustGet("greaterThan");
await op.evaluate(21, 18); // true
```
## Usage: Advanced Example
```ts
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.freeze`d 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.