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