Files
skills/docs/foundation/flow/rules/src/serializer/serializer.md
2026-05-29 08:20:56 +01:00

5.2 KiB

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

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

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.