Skip to content

Instantly share code, notes, and snippets.

@meoyawn
Created November 11, 2024 07:48
Show Gist options
  • Save meoyawn/f7dd5ed6561391ab596230be851b4d71 to your computer and use it in GitHub Desktop.
Save meoyawn/f7dd5ed6561391ab596230be851b4d71 to your computer and use it in GitHub Desktop.
minimal implementation of jsonlogic.com just in case something isn't working
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