first commit
This commit is contained in:
84
docs/foundation/flow/rules/src/operators/operators.md
Normal file
84
docs/foundation/flow/rules/src/operators/operators.md
Normal 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.
|
||||
Reference in New Issue
Block a user