153 lines
4.1 KiB
TypeScript
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;
|
|
}
|
|
}
|