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_PLACEHOLDERconstant exposed as the literal marker substituted forvalidate/compute.cloneRuleproduces a structurally-equal but distinct rule tree.hashRuleproduces a stable hash that is invariant under AND/OR/group child reordering.areRulesEqualis the hash-based equality.validateSerializedRuleZod-validates aSerializedRuleshape; raisesRULES.SERIALIZE_FAILEDon rejection.- OTel histogram
rules.serializer.duration_ms{op}emitted for bothserializeanddeserialize.
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)→SerializedRuleserializeRule(rule, options?)→string(pretty option indents)serializeRulesetToObject(ruleset)→SerializedRulesetserializeRuleset(ruleset, options?)→string
Deserialization
deserializeRuleFromObject(input, options?)→RuledeserializeRule(json, options?)→RuledeserializeRulesetFromObject(input, options?)→RulesetdeserializeRuleset(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; raisesRULES.SERIALIZE_FAILEDon bad input.
Errors
RULES.SERIALIZE_FAILED— unknown rule type at serialize time, orvalidateSerializedRuleschema 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. hashRulecanonicalises the rule tree by sorting children ofand/or/groupoperators by their JSON representation before serialising. This meansand(a, b)andand(b, a)hash to the same value, but order matters fornot(which has exactly one child) and for sequence-sensitive future operators.- The Zod schema for
SerializedRuleis az.discriminatedUnionontype. Thevaluefield on conditions isz.unknown()because legitimate rule values include arbitrary JSON. - The serializer is intentionally narrow on metadata:
RuleMetadatais passed through withpassthrough()so callers that extend it (e.g. withtenant: stringfield) 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.