Initial Commit
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user