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