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,66 @@
# 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 `switch` over `ComparisonOperator`; `default` is typed `never` so 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` / `notMatches` accept either a string source or a `RegExp` instance; the source is recompiled to a `RegExp` per call.
## Usage: Basic Example
```ts
import { compareValues } from "@suaveplan/rules/engine/operators-internal";
compareValues(21, "greaterThanOrEqual", 18); // true
compareValues([1, 2, 3], "containsAll", [1, 2]); // true
```
## Usage: Advanced Example
```ts
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` / `strictEquals` use `===`; `notEquals` / `strictNotEquals` use `!==`. Both pairs are equivalent — the strict variants are kept so consumers using a JSON DSL can be explicit.
- **numeric**: `greaterThan` / `greaterThanOrEqual` / `lessThan` / `lessThanOrEqual` cast both sides to `number`. `between` / `notBetween` accept `[min, max]` tuples and are inclusive on both bounds.
- **collection**: `in` / `notIn` test membership of `actual` in `expected` (an array). `contains` / `notContains` work on either arrays or strings; non-array, non-string `actual` returns `false` for `contains` and `true` for `notContains`. `containsAll` / `containsAny` / `overlaps` require both sides to be arrays.
- **existence**: `exists` is `actual !== undefined`; `notExists` is the inverse. `isNull` / `isNotNull` are strict `=== null` checks. `isEmpty` / `isNotEmpty` cover string / array / object / null / undefined.
- **type**: `isType` is `typeof actual === expected`; `isNotType` is the inverse. `hasProperty` is `Object.hasOwn(actual, expected)` over objects (false otherwise).
- **length**: `lengthEquals` / `lengthGreaterThan` / `lengthLessThan` operate on string / array / object (object uses `Object.keys` count). Other types yield `NaN` which makes the comparison fall through to `false`.
- **string**: `startsWith` / `endsWith` require `typeof actual === "string"`. `matches` accepts a string or `RegExp`; on non-string `actual`, `matches` is `false` and `notMatches` is `true`.
### Throws
- `RulesError(RULES.OPERATOR_NOT_FOUND)` when `operator` is outside the documented union.
## Implementation Notes
- The dispatcher does NOT memoise the per-call `RegExp` construction in `matches` / `notMatches`. For rules that fire frequently with the same pattern, callers can pre-compile and pass the `RegExp` instance instead of a string — `compareValues` accepts both.
- The `default` branch uses `const unknownOperator: never = operator;` to leverage the compiler's exhaustiveness checking; the runtime `throw` is a defensive belt-and-braces against `as never` casts at the boundary.
- `countable(value)` is the shared length helper; non-countable inputs return `NaN` so every length comparison naturally yields `false` (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.evaluate` contract which accepts a `Promise<boolean>` return.