# Serializer — `@suaveplan/rules/serializer` _(no submodules)_ ## 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; ``` ## 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` — keyed by `CustomRule.name`. `options.computeResolvers: Record` — 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.