UNPKG

json-logic-engine

Version:

Construct complex rules with JSON & process them.

243 lines (207 loc) 12.1 kB
// This is the synchronous version of the optimizer; which the Async one should be based on. import { isDeterministic } from './compiler.js' import { OriginalImpl } from './constants.js' import { coerceArray } from './utilities/coerceArray.js' import { precoerceNumber } from './utilities/downgrade.js' import { splitPathMemoized } from './utilities/splitPath.js' /** * Turns an expression like { '+': [1, 2] } into a function that can be called with data. * @param {*} logic * @param {*} engine * @param {string} methodName * @param {any[]} above * @returns A method that can be called to execute the logic. */ function getMethod (logic, engine, methodName, above) { const method = engine.methods[methodName] const called = method.method ? method.method : method if (method.lazy) { const args = logic[methodName] return (data, abv) => called(args, data, abv || above, engine) } let args = logic[methodName] if ((!args || typeof args !== 'object') && !method.optimizeUnary) args = [args] if (Array.isArray(args) && args.length === 1 && method.optimizeUnary && !Array.isArray(args[0])) args = args[0] if (Array.isArray(args)) { const optimizedArgs = args.map(l => optimize(l, engine, above)) if (optimizedArgs.every(l => typeof l !== 'function')) return (data, abv) => called(optimizedArgs, data, abv || above, engine) if (optimizedArgs.length === 1) { const first = optimizedArgs[0] return (data, abv) => called([first(data, abv)], data, abv || above, engine) } if (optimizedArgs.length === 2) { const [first, second] = optimizedArgs if (typeof first === 'function' && typeof second === 'function') return (data, abv) => called([first(data, abv), second(data, abv)], data, abv || above, engine) if (typeof first === 'function') return (data, abv) => called([first(data, abv), second], data, abv || above, engine) return (data, abv) => called([first, second(data, abv)], data, abv || above, engine) } return (data, abv) => { const evaluatedArgs = optimizedArgs.map(l => typeof l === 'function' ? l(data, abv) : l) return called(evaluatedArgs, data, abv || above, engine) } } else { const optimizedArgs = optimize(args, engine, above) if (method.optimizeUnary) { const singleLayer = (data) => !data || typeof data[optimizedArgs] === 'undefined' || (typeof data[optimizedArgs] === 'function' && !engine.allowFunctions) ? null : data[optimizedArgs] if (typeof optimizedArgs === 'function') return (data, abv) => called(optimizedArgs(data, abv), data, abv || above, engine) if ((methodName === 'var' || methodName === 'val') && engine.methods[methodName][OriginalImpl]) { if (!optimizedArgs && methodName !== 'val') return (data) => !data || typeof data === 'undefined' || (typeof data === 'function' && !engine.allowFunctions) ? null : data if (methodName === 'val' || typeof optimizedArgs === 'number' || (!optimizedArgs.includes('.') && !optimizedArgs.includes('\\'))) return singleLayer if (methodName === 'var' && !optimizedArgs.startsWith('../')) { const path = splitPathMemoized(String(optimizedArgs)) let prev if (path.length === 2) { const [first, second] = path return (data) => (typeof (prev = (data && data[first] && data[first][second])) !== 'function' || engine.allowFunctions) && (typeof prev !== 'undefined') ? prev : null } if (path.length === 3) { const [first, second, third] = path return (data) => (typeof (prev = (data && data[first] && data[first][second] && data[first][second][third])) !== 'function' || engine.allowFunctions) && (typeof prev !== 'undefined') ? prev : null } } } return (data, abv) => called(optimizedArgs, data, abv || above, engine) } if (typeof optimizedArgs === 'function') return (data, abv) => called(coerceArray(optimizedArgs(data, abv)), data, abv || above, engine) return (data, abv) => called(coerceArray(optimizedArgs), data, abv || above, engine) } } const comparisons = { '<': (a, b) => a < b, '<=': (a, b) => a <= b, '>': (a, b) => a > b, '>=': (a, b) => a >= b, // eslint-disable-next-line eqeqeq '==': (a, b) => a == b, '===': (a, b) => a === b, // eslint-disable-next-line eqeqeq '!=': (a, b) => a != b, '!==': (a, b) => a !== b } /** * Macro-type replacements to lift inefficient logic into more efficient forms. */ function checkIdioms (logic, engine, above) { // Hyper-Optimizations for val calls. if (logic.val && engine.methods.val[OriginalImpl] && Array.isArray(logic.val) && logic.val.length <= 3 && logic.val.every(i => typeof i !== 'object')) { let prev if (logic.val.length === 1) { const first = logic.val[0] return (data) => (typeof (prev = (data && data[first])) !== 'function' || engine.allowFunctions) && (typeof prev !== 'undefined') ? prev : null } if (logic.val.length === 2) { const [first, second] = logic.val return (data) => (typeof (prev = (data && data[first] && data[first][second])) !== 'function' || engine.allowFunctions) && (typeof prev !== 'undefined') ? prev : null } if (logic.val.length === 3) { const [first, second, third] = logic.val return (data) => (typeof (prev = (data && data[first] && data[first][second] && data[first][second][third])) !== 'function' || engine.allowFunctions) && (typeof prev !== 'undefined') ? prev : null } } if ((logic.if || logic['?:']) && engine.methods.if[OriginalImpl] && Array.isArray(logic.if || logic['?:']) && (logic.if || logic['?:']).length === 3) { const [condition, truthy, falsy] = logic.if || logic['?:'] const C = optimize(condition, engine, above) const T = optimize(truthy, engine, above) const F = optimize(falsy, engine, above) if (typeof C === 'function' && typeof T === 'function' && typeof F === 'function') return (data, abv) => engine.truthy(C(data, abv)) ? T(data, abv) : F(data, abv) if (typeof C === 'function' && typeof T === 'function') return (data, abv) => engine.truthy(C(data, abv)) ? T(data, abv) : F if (typeof C === 'function' && typeof F === 'function') return (data, abv) => engine.truthy(C(data, abv)) ? T : F(data, abv) if (typeof C === 'function') return (data, abv) => engine.truthy(C(data, abv)) ? T : F // Otherwise, C is not a function, and we can just return the result of the evaluation. return engine.truthy(C) ? T : F } if (logic.filter && engine.methods.filter[OriginalImpl] && Array.isArray(logic.filter) && logic.filter.length === 2) { const [collection, filter] = logic.filter const filterF = optimize(filter, engine, above) if (typeof filterF !== 'function') return engine.truthy(filterF) ? optimize(collection, engine, above) : [] } // Hyper-Optimizations for Comparison Operators. for (const comparison in comparisons) { if (logic[comparison] && Array.isArray(logic[comparison]) && engine.methods[comparison][OriginalImpl]) { const _comparisonFunc = comparisons[comparison] const comparisonFunc = comparison.length === 3 ? _comparisonFunc : function comparisonFunc (a, b) { if (typeof a === 'string' && typeof b === 'string') return _comparisonFunc(a, b) if (Number.isNaN(+precoerceNumber(a))) throw NaN if (Number.isNaN(+precoerceNumber(b)) && a !== null) throw NaN return _comparisonFunc(+a, +b) } if (logic[comparison].length === 2) { const [a, b] = logic[comparison] const A = optimize(a, engine, above) const B = optimize(b, engine, above) if (typeof A === 'function' && typeof B === 'function') return (data, abv) => comparisonFunc(A(data, abv), B(data, abv)) if (typeof A === 'function') return (data, abv) => comparisonFunc(A(data, abv), B) if (typeof B === 'function') return (data, abv) => comparisonFunc(A, B(data, abv)) return comparisonFunc(A, B) } if (logic[comparison].length === 3) { const [a, b, c] = logic[comparison] const A = optimize(a, engine, above) const B = optimize(b, engine, above) const C = optimize(c, engine, above) let prev if (typeof A === 'function' && typeof B === 'function' && typeof C === 'function') return (data, abv) => comparisonFunc(A(data, abv), (prev = B(data, abv))) && comparisonFunc(prev, C(data, abv)) if (typeof A === 'function' && typeof B === 'function') return (data, abv) => comparisonFunc(A(data, abv), (prev = B(data, abv))) && comparisonFunc(prev, C) if (typeof A === 'function' && typeof C === 'function') return (data, abv) => comparisonFunc(A(data, abv), B) && comparisonFunc(B, C(data, abv)) if (typeof B === 'function' && typeof C === 'function') return (data, abv) => comparisonFunc(A, (prev = B(data, abv))) && comparisonFunc(prev, C(data, abv)) if (typeof A === 'function') return (data, abv) => comparisonFunc(A(data, abv), B) && comparisonFunc(B, C) if (typeof B === 'function') return (data, abv) => comparisonFunc(A, (prev = B(data, abv))) && comparisonFunc(prev, C) if (typeof C === 'function') return (data, abv) => comparisonFunc(A, B) && comparisonFunc(B, C(data, abv)) return comparisonFunc(A, B) && comparisonFunc(B, C) } } } if (logic.reduce && Array.isArray(logic.reduce)) { let [root, mapper, defaultValue] = logic.reduce if (mapper['+'] && mapper['+'].length === 2 && (mapper['+'][0] || 0).var && (mapper['+'][1] || 0).var) { const accumulatorFound = mapper['+'][0].var === 'accumulator' || mapper['+'][1].var === 'accumulator' const currentFound = mapper['+'][0].var === 'current' || mapper['+'][1].var === 'current' defaultValue = defaultValue || 0 if (accumulatorFound && currentFound) return optimize({ '+': [{ '+': root }, defaultValue] }, engine, above) } if (mapper['*'] && mapper['*'].length === 2 && (mapper['*'][0] || 0).var && (mapper['*'][1] || 0).var) { const accumulatorFound = mapper['*'][0].var === 'accumulator' || mapper['*'][1].var === 'accumulator' const currentFound = mapper['*'][0].var === 'current' || mapper['*'][1].var === 'current' defaultValue = typeof defaultValue === 'undefined' ? 1 : defaultValue if (accumulatorFound && currentFound) return optimize({ '*': [{ '*': root }, defaultValue] }, engine, above) } } } /** * Processes the logic for the engine once so that it doesn't need to be traversed again. * @param {*} logic * @param {*} engine * @param {any[]} above * @returns A function that optimizes the logic for the engine in advance. */ export function optimize (logic, engine, above = []) { if (Array.isArray(logic)) { const arr = logic.map(l => optimize(l, engine, above)) if (arr.every(l => typeof l !== 'function')) return arr return (data, abv) => arr.map(l => typeof l === 'function' ? l(data, abv) : l) }; if (logic && typeof logic === 'object') { const idiomEnhancement = checkIdioms(logic, engine, above) if (typeof idiomEnhancement !== 'undefined') return idiomEnhancement const keys = Object.keys(logic) const methodName = keys[0] if (keys.length === 0) return logic const isData = engine.isData(logic, methodName) if (isData) return () => logic // eslint-disable-next-line no-throw-literal if (keys.length > 1) throw { type: 'Unknown Operator' } // If we have a deterministic function, we can just return the result of the evaluation, // basically inlining the operation. const deterministic = !engine.disableInline && isDeterministic(logic, engine, { engine }) if (methodName in engine.methods) { const result = getMethod(logic, engine, methodName, above) if (deterministic) return result() return result } // eslint-disable-next-line no-throw-literal throw { type: 'Unknown Operator', key: methodName } } return logic }