Files
suaveplan/packages/foundation/bitpacker/bitpacker-strategies/src/strategies/conditional/conditional.ts
2026-05-27 23:47:53 +01:00

153 lines
4.1 KiB
TypeScript

import type {
BitPackerSchema,
PackingContext,
PackingStrategy,
} from "@suaveplan/bitpacker";
type ConditionalOp = "==" | "!=" | "<" | "<=" | ">" | ">=" | "in" | "has";
interface IfShape {
field: string;
op: ConditionalOp;
value: unknown;
}
interface ConditionalSchemaShape {
_if?: IfShape;
_when?: IfShape;
_then: BitPackerSchema;
_else?: BitPackerSchema;
}
const NO_BRANCH = Symbol("NO_BRANCH");
const VALID_OPS: readonly ConditionalOp[] = [
"==",
"!=",
"<",
"<=",
">",
">=",
"in",
"has",
];
function getPredicate(obj: {
_if?: unknown;
_when?: unknown;
}): IfShape | undefined {
const raw = obj._if ?? obj._when;
if (!raw || typeof raw !== "object") return undefined;
const predicate = raw as {
field?: unknown;
op?: unknown;
value?: unknown;
};
if (typeof predicate.field !== "string" || predicate.field.length === 0) {
return undefined;
}
if (
typeof predicate.op !== "string" ||
!(VALID_OPS as readonly string[]).includes(predicate.op)
) {
return undefined;
}
if (!("value" in predicate)) return undefined;
return predicate as IfShape;
}
export function isConditionalSchema(
schema: unknown
): schema is ConditionalSchemaShape {
if (
typeof schema !== "object" ||
schema === null ||
Array.isArray(schema)
) {
return false;
}
const obj = schema as {
_if?: unknown;
_when?: unknown;
_then?: unknown;
};
const predicate = getPredicate(obj);
if (predicate === undefined) return false;
return obj._then !== undefined;
}
export function evaluatePredicate(
ctx: unknown,
field: string,
op: ConditionalOp,
value: unknown
): boolean {
const fieldValue =
typeof ctx === "object" && ctx !== null
? (ctx as Record<string, unknown>)[field]
: undefined;
switch (op) {
case "==":
return fieldValue === value;
case "!=":
return fieldValue !== value;
case "<":
return (fieldValue as number) < (value as number);
case "<=":
return (fieldValue as number) <= (value as number);
case ">":
return (fieldValue as number) > (value as number);
case ">=":
return (fieldValue as number) >= (value as number);
case "in":
return Array.isArray(value) ? value.includes(fieldValue) : false;
case "has":
return Array.isArray(fieldValue)
? (fieldValue as unknown[]).includes(value)
: false;
}
}
export class ConditionalPackingStrategy implements PackingStrategy {
canHandle(schema: BitPackerSchema): boolean {
return isConditionalSchema(schema);
}
pack(
data: unknown,
schema: BitPackerSchema,
context: PackingContext
): void {
const cfg = schema as unknown as ConditionalSchemaShape;
const branch = this.resolveBranch(cfg, context, data);
if (branch === NO_BRANCH) return;
context.packValue(data, branch as BitPackerSchema);
}
unpack(schema: BitPackerSchema, context: PackingContext): unknown {
const cfg = schema as unknown as ConditionalSchemaShape;
const branch = this.resolveBranch(cfg, context, context.parentData);
if (branch === NO_BRANCH) return undefined;
return context.unpackValue(branch as BitPackerSchema);
}
private resolveBranch(
cfg: ConditionalSchemaShape,
context: PackingContext,
data: unknown
): BitPackerSchema | typeof NO_BRANCH {
const predicate = cfg._if ?? cfg._when;
if (predicate === undefined) return NO_BRANCH;
const scope = context.parentData ?? data;
const matched = evaluatePredicate(
scope,
predicate.field,
predicate.op,
predicate.value
);
if (matched) return cfg._then;
if ("_else" in cfg) return cfg._else as BitPackerSchema;
return NO_BRANCH;
}
}