Created
November 11, 2024 07:48
-
-
Save meoyawn/f7dd5ed6561391ab596230be851b4d71 to your computer and use it in GitHub Desktop.
minimal implementation of jsonlogic.com just in case something isn't working
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Comparable = number | string | Date | boolean | bigint | |
type CompareOp = "<" | ">" | "<=" | ">=" | "===" | "!==" | |
const compareFns: Readonly< | |
Record<CompareOp, <T extends Comparable>(a: T, b: T) => boolean> | |
> = { | |
"<": (a, b) => a < b, | |
"<=": (a, b) => a <= b, | |
">": (a, b) => a > b, | |
">=": (a, b) => a >= b, | |
"===": (a, b) => a === b, | |
"!==": (a, b) => a !== b, | |
} | |
const compareOp = | |
(op: CompareOp) => | |
<T extends Comparable>(...args: T[]): boolean => { | |
if (args.length < 2) { | |
throw new Error(`Illegal "${op}": ${JSON.stringify(args)}`) | |
} | |
const fn = compareFns[op] | |
for (let i = 0; i < args.length - 1; i++) { | |
if (!fn(args[i], args[i + 1])) { | |
return false | |
} | |
} | |
return true | |
} | |
const operators = { | |
"==": compareOp("==="), | |
"!=": compareOp("!=="), | |
"<": compareOp("<"), | |
"<=": compareOp("<="), | |
">": compareOp(">"), | |
">=": compareOp("<="), | |
"!": (a: boolean): boolean => !a, | |
max: (...args: number[]): number => Math.max(...args), | |
min: (...args: number[]): number => Math.min(...args), | |
and: (...args: boolean[]): boolean => args.every(Boolean), | |
or: (...args: boolean[]): boolean => args.some(Boolean), | |
in: (small: unknown, big: unknown): boolean => { | |
if (typeof big === "string") { | |
if (typeof small !== "string") { | |
throw new Error(`Illegal "in": ${JSON.stringify([small, big])}`) | |
} | |
return big.includes(small) | |
} | |
if (Array.isArray(big)) { | |
return big.includes(small) | |
} | |
return false | |
}, | |
if: (...pairs: unknown[]): unknown => { | |
if (pairs.length < 3) { | |
throw new Error(`Illegal "if": ${JSON.stringify(pairs)}`) | |
} | |
for (let i = 0; i < pairs.length - 1; i += 2) { | |
if (pairs[i]) { | |
return pairs[i + 1] | |
} | |
} | |
if (pairs.length % 2 === 0) { | |
throw new Error(`No else clause: ${JSON.stringify(pairs)}`) | |
} | |
return pairs[pairs.length - 1] | |
}, | |
} as const satisfies Record<string, (...x: never[]) => unknown> | |
type Operator = keyof typeof operators | |
const isOperator = (op: string): op is Operator => op in operators | |
type Indexable = Record<PropertyKey, unknown> | undefined | null | |
const dataOps = { | |
var: (data: unknown, path: unknown, def?: unknown): unknown => { | |
let ret = data as Indexable | |
for (const k of String(path).split(".")) { | |
if (k) { | |
ret = ret?.[k] as Indexable | |
} | |
} | |
if (ret === null || ret === undefined) { | |
if (def !== undefined) { | |
return def | |
} | |
} | |
return ret | |
}, | |
} as const | |
type DataOperator = keyof typeof dataOps | |
const isDataOp = (op: string): op is DataOperator => op in dataOps | |
type Expr = { | |
[K in Operator]?: unknown[] | |
} & { | |
[K in DataOperator]?: PropertyKey | |
} | |
export function evaluate(expr: unknown, data?: unknown): unknown { | |
if (expr === null || typeof expr !== "object" || Array.isArray(expr)) { | |
return expr | |
} | |
const entries = Object.entries(expr) | |
if (entries.length !== 1) { | |
throw new Error(`Must be a single expression: ${JSON.stringify(expr)}`) | |
} | |
const [op, args] = Object.entries(expr as Expr)[0] | |
if (isDataOp(op)) return dataOps[op](data, args) | |
if (!isOperator(op)) throw new Error(`Unknown operator: "${op}"`) | |
const argz = Array.isArray(args) ? args : [args] | |
const evaluatedArgs = argz.map(arg => evaluate(arg, data)) | |
return operators[op](...evaluatedArgs) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment