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,98 @@
# Serializer — `@suaveplan/rules/serializer`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
JSON-based serializer for the `Rule` discriminated union and `Ruleset`. Rules round-trip through compact or pretty JSON, with a documented placeholder strategy for function-valued fields (`CustomRule.validate`, `ComputedRule.compute`). On deserialization, callers supply name-keyed resolvers (`customResolvers`, `computeResolvers`) that the serializer wires back into the reconstructed tree — missing resolvers raise `RULES.DESERIALIZE_FAILED` so callers can detect partial serialisation up front.
The module also exposes structural utilities — `cloneRule` (deep copy via round-trip), `hashRule` (commutative-order-insensitive FNV-1a hash), `areRulesEqual` (hash-based equality), and `validateSerializedRule` (boundary Zod validator).
## Features
- `serializeRule` / `serializeRuleToObject` / `serializeRuleset` / `serializeRulesetToObject` — JSON / object forms.
- `deserializeRule` / `deserializeRuleFromObject` / `deserializeRuleset` / `deserializeRulesetFromObject` — accept JSON or object input.
- `FUNCTION_PLACEHOLDER` constant exposed as the literal marker substituted for `validate` / `compute`.
- `cloneRule` produces a structurally-equal but distinct rule tree.
- `hashRule` produces a stable hash that is invariant under AND/OR/group child reordering.
- `areRulesEqual` is the hash-based equality.
- `validateSerializedRule` Zod-validates a `SerializedRule` shape; raises `RULES.SERIALIZE_FAILED` on rejection.
- OTel histogram `rules.serializer.duration_ms{op}` emitted for both `serialize` and `deserialize`.
## Usage: Basic Example
```ts
import { condition } from "@suaveplan/rules/builder";
import { deserializeRule, serializeRule } from "@suaveplan/rules/serializer";
const rule = condition("age", "greaterThanOrEqual", 18);
const json = serializeRule(rule);
const back = deserializeRule(json);
```
## Usage: Advanced Example
```ts
import { rule } from "@suaveplan/rules/builder";
import {
deserializeRuleset,
serializeRuleset,
} from "@suaveplan/rules/serializer";
const policy = rule()
.field("age").greaterThanOrEqual(18)
.and()
.custom("kyc-cleared", async (data: { id: string }) => verifyKyc(data.id))
.buildRuleset("rs-onboarding", "Onboarding");
const json = serializeRuleset(policy, { pretty: true });
const back = deserializeRuleset(json, {
customResolvers: {
"kyc-cleared": async (data: unknown) =>
verifyKyc((data as { id: string }).id),
},
});
declare function verifyKyc(id: string): Promise<boolean>;
```
## API Reference
### Serialization
- `serializeRuleToObject(rule)` → `SerializedRule`
- `serializeRule(rule, options?)` → `string` (pretty option indents)
- `serializeRulesetToObject(ruleset)` → `SerializedRuleset`
- `serializeRuleset(ruleset, options?)` → `string`
### Deserialization
- `deserializeRuleFromObject(input, options?)` → `Rule`
- `deserializeRule(json, options?)` → `Rule`
- `deserializeRulesetFromObject(input, options?)` → `Ruleset`
- `deserializeRuleset(json, options?)` → `Ruleset`
`options.customResolvers: Record<string, validator>` — keyed by `CustomRule.name`. `options.computeResolvers: Record<string, computeFn>` — keyed by `ComputedRule.field`.
### Structural utilities
- `cloneRule(rule)` → `Rule` — deep copy via round-trip.
- `hashRule(rule)` → `string` — FNV-1a (8 hex chars). Commutative-order-insensitive for AND / OR / group children.
- `areRulesEqual(a, b)` → `boolean` — hash equality.
- `validateSerializedRule(input)` → `SerializedRule` — Zod validator; raises `RULES.SERIALIZE_FAILED` on bad input.
### Errors
- `RULES.SERIALIZE_FAILED` — unknown rule type at serialize time, or `validateSerializedRule` schema rejection.
- `RULES.DESERIALIZE_FAILED` — bad JSON, schema rejection, missing resolver, or malformed ruleset.
## Implementation Notes
- Function-valued fields use the literal `"__suaveplan_rules_function__"` placeholder. Callers MUST supply a name-keyed resolver to reconstruct them — there is no implicit fallback, by design: silently dropping a custom validator would change rule semantics in production.
- `hashRule` canonicalises the rule tree by sorting children of `and`/`or`/`group` operators by their JSON representation before serialising. This means `and(a, b)` and `and(b, a)` hash to the same value, but order matters for `not` (which has exactly one child) and for sequence-sensitive future operators.
- The Zod schema for `SerializedRule` is a `z.discriminatedUnion` on `type`. The `value` field on conditions is `z.unknown()` because legitimate rule values include arbitrary JSON.
- The serializer is intentionally narrow on metadata: `RuleMetadata` is passed through with `passthrough()` so callers that extend it (e.g. with `tenant: string` field) survive round-trip without recompiling the schema.
- FNV-1a is sufficient for a structural hash (8 hex chars, ~32 bits, low collision rate for the rule sizes we expect). Cryptographic-strength hashing is not the goal; a stable, fast equality check is.
- The OTel histogram is recorded once per top-level entrypoint (`serializeRuleToObject` / `deserializeRuleFromObject`) rather than per-recursive-call to keep cardinality manageable.