UNPKG

json-logic-engine

Version:

Construct complex rules with JSON & process them.

1,449 lines (1,306 loc) 104 kB
// @ts-check // Note: Each of these iterators executes synchronously, and will not "run in parallel" // I am supporting filter, reduce, some, every, map async function filter (arr, iter) { const result = [] let index = 0 for (const item of arr) { if (await iter(item, index++, arr)) result.push(item) } return result } async function some (arr, iter) { let index = 0 for (const item of arr) { if (await iter(item, index++, arr)) return true } return false } async function every (arr, iter) { let index = 0 for (const item of arr) { if (!(await iter(item, index++, arr))) return false } return true } async function map (arr, iter) { const result = [] let index = 0 for (const item of arr) { result.push(await iter(item, index++, arr)) } return result } async function reduce (arr, iter, defaultValue) { if (arr.length === 0) { if (typeof defaultValue !== 'undefined') { return defaultValue } throw new Error('Array has no elements.') } const start = typeof defaultValue === 'undefined' ? 1 : 0 let data = start ? arr[0] : defaultValue for (let i = start; i < arr.length; i++) { data = await iter(data, arr[i]) } return data } const asyncIterators = { filter, some, every, map, reduce } // @ts-check const Sync = Symbol.for('json_logic_sync') const Compiled = Symbol.for('json_logic_compiled') const OriginalImpl = Symbol.for('json_logic_original') const Unfound = Symbol.for('json_logic_unfound') /** * Checks if an item is synchronous. * This allows us to optimize the logic a bit * further so that we don't need to await everything. * * @param {*} item * @returns {Boolean} */ function isSync (item) { if (typeof item === 'function') return item[Sync] === true if (Array.isArray(item)) return item.every(isSync) if (item && item.asyncMethod && !item.method) return false return true } const Constants = { Sync, OriginalImpl, isSync } // @ts-check function declareSync (obj, sync = true) { obj[Sync] = sync return obj } /** * Coerces a value into an array. * This is used for unary value operations. */ function coerceArray (value) { return Array.isArray(value) ? value : [value] } const counts = new WeakMap() /** * Counts the number of arguments a function has; paying attention to the function's signature * to avoid edge cases. * This is used to allow for compiler optimizations. * @param {(...args: any[]) => any} fn * @returns {number} */ function countArguments (fn) { if (!fn || typeof fn !== 'function' || !fn.length) return 0 if (!counts.has(fn)) counts.set(fn, _countArguments(fn)) return counts.get(fn) } /** * Counts the number of arguments a function has; paying attention to the function's signature. * This is the internal implementation that does not use a WeakMap. * @param {(...args: any[]) => any} fn * @returns {number} */ function _countArguments (fn) { if (!fn || typeof fn !== 'function' || !fn.length) return 0 let fnStr = fn.toString() if (fnStr[0] !== '(' && fnStr[0] !== 'f') return 0 fnStr = fnStr.substring(fnStr.indexOf('('), fnStr.indexOf('{')).replace(/=>/g, '') // regex to check for "..." or "=" const regex = /\.{3}|=/ if (regex.test(fnStr)) return 0 return fn.length } /** * Used to precoerce a data value to a number, for the purposes of coalescing. * @param {any} item */ function precoerceNumber (item) { if (Number.isNaN(item)) throw NaN if (!item) return item if (item && typeof item === 'object') throw NaN return item } /** * Used to assert in compiled templates that a value is an array of at least a certain size. * @param {*} arr * @param {number} size */ function assertSize (arr, size) { // eslint-disable-next-line no-throw-literal if (!Array.isArray(arr) || arr.length < size) throw { type: 'Invalid Arguments' } return arr } /** * Used to assert in compiled templates that when a numeric comparison is made, both values are numbers. * @param {*} item * @param {*} prev * @returns {number} */ function compareCheck (item, prev, strict) { if (strict || (typeof item === 'string' && typeof prev === 'string')) return item if (Number.isNaN(+precoerceNumber(item)) && prev !== null) throw NaN if (Number.isNaN(+precoerceNumber(prev))) throw NaN // The following two checks allow us to handle null == 0 and 0 == null; it's honestly // kind of gross that JavaScript works this way out of the box. Like, why is 0 <= null true, // but null == false. It's just weird. if (prev === null && !item) return null if (item === null && !prev) return 0 return item } // @ts-check /** * Provides a simple way to compile logic into a function that can be run. * @param {string[]} strings * @param {...any} items * @returns {{ [Compiled]: string }} */ function compileTemplate (strings, ...items) { let res = '' const buildState = this for (let i = 0; i < strings.length; i++) { res += strings[i] if (i < items.length) { if (typeof items[i] === 'function') { this.methods.push(items[i]) if (!isSync(items[i])) buildState.asyncDetected = true res += (isSync(items[i]) ? '' : ' await ') + 'methods[' + (buildState.methods.length - 1) + ']' } else if (items[i] && typeof items[i][Compiled] !== 'undefined') res += items[i][Compiled] else res += buildString(items[i], buildState) } } return { [Compiled]: res } } /** * @typedef BuildState * Used to keep track of the compilation. * @property {*} [engine] * @property {Object} [notTraversed] * @property {Object} [methods] * @property {Object} [state] * @property {Array} [processing] * @property {*} [async] * @property {Array} [above] * @property {Boolean} [asyncDetected] * @property {*} [values] * @property {Boolean} [avoidInlineAsync] * @property {string} [extraArguments] * @property {(strings: string[], ...items: any[]) => { compiled: string }} [compile] A function that can be used to compile a template. */ /** * Checks if the value passed in is a primitive JS object / value. * @param {*} x * @returns */ function isPrimitive (x, preserveObject) { if (typeof x === 'number' && (x === Infinity || x === -Infinity || Number.isNaN(x))) return false return ( x === null || x === undefined || ['Number', 'String', 'Boolean'].includes(x.constructor.name) || (!preserveObject && x.constructor.name === 'Object') ) } /** * Checks if the method & its inputs are deterministic. * @param {*} method * @param {*} engine * @param {BuildState} buildState * @returns */ function isDeterministic$1 (method, engine, buildState) { if (Array.isArray(method)) { return method.every((i) => isDeterministic$1(i, engine, buildState)) } if (method && typeof method === 'object') { const func = Object.keys(method)[0] const lower = method[func] if (engine.isData(method, func)) return true if (func === undefined) return true // eslint-disable-next-line no-throw-literal if (!engine.methods[func]) throw { type: 'Unknown Operator', key: func } if (engine.methods[func].lazy) { return typeof engine.methods[func].deterministic === 'function' ? engine.methods[func].deterministic(lower, buildState) : engine.methods[func].deterministic } return typeof engine.methods[func].deterministic === 'function' ? engine.methods[func].deterministic(lower, buildState) : engine.methods[func].deterministic && isDeterministic$1(lower, engine, buildState) } return true } /** * Checks if the method & its inputs are synchronous. * @param {*} method * @param {*} engine */ function isDeepSync (method, engine) { if (!engine.async) return true if (Array.isArray(method)) return method.every(i => isDeepSync(i, engine)) if (method && typeof method === 'object') { const keys = Object.keys(method) if (keys.length === 0) return true const func = keys[0] const lower = method[func] if (!isSync(engine.methods[func])) return false if (engine.methods[func].lazy) { if (typeof engine.methods[func][Sync] === 'function' && engine.methods[func][Sync](method, { engine })) return true return false } return isDeepSync(lower, engine) } return true } /** * Builds the string for the function that will be evaluated. * @param {*} method * @param {BuildState} buildState * @returns */ function buildString (method, buildState = {}) { const { notTraversed = [], async, processing = [], values = [], engine } = buildState function pushValue (value, preserveObject = false) { if (isPrimitive(value, preserveObject)) return JSON.stringify(value) values.push(value) return `values[${values.length - 1}]` } if (Array.isArray(method)) { let res = '' for (let i = 0; i < method.length; i++) { if (i > 0) res += ',' res += buildString(method[i], buildState) } return '[' + res + ']' } let asyncDetected = false function makeAsync (result) { buildState.asyncDetected = buildState.asyncDetected || asyncDetected if (async && asyncDetected) return `await ${result}` return result } if (method && typeof method === 'object') { const keys = Object.keys(method) const func = keys[0] if (!func) return pushValue(method) if (!engine.methods[func] || keys.length > 1) { // Check if this is supposed to be "data" rather than a function. if (engine.isData(method, func)) return pushValue(method, true) // eslint-disable-next-line no-throw-literal throw { type: 'Unknown Operator', key: func } } if ( !buildState.engine.disableInline && engine.methods[func] && isDeterministic$1(method, engine, buildState) ) { if (isDeepSync(method, engine)) { return pushValue((engine.fallback || engine).run(method), true) } else if (!buildState.avoidInlineAsync) { processing.push(engine.run(method).then((i) => pushValue(i))) return `__%%%${processing.length - 1}%%%__` } else { buildState.asyncDetected = true return `(await ${pushValue(engine.run(method))})` } } let lower = method[func] if ((!lower || typeof lower !== 'object') && (!engine.methods[func].lazy)) lower = [lower] if (engine.methods[func] && engine.methods[func].compile) { let str = engine.methods[func].compile(lower, buildState) if (str[Compiled]) str = str[Compiled] if ((str || '').startsWith('await')) buildState.asyncDetected = true if (str !== false) return str } let coerce = engine.methods[func].optimizeUnary ? '' : 'coerceArray' if (!coerce && Array.isArray(lower) && lower.length === 1 && !Array.isArray(lower[0])) lower = lower[0] else if (coerce && Array.isArray(lower)) coerce = '' const argumentsDict = [', context', ', context, above', ', context, above, engine'] if (typeof engine.methods[func] === 'function') { asyncDetected = !isSync(engine.methods[func]) const argumentsNeeded = argumentsDict[countArguments(engine.methods[func]) - 1] || argumentsDict[2] return makeAsync(`engine.methods["${func}"](${coerce}(` + buildString(lower, buildState) + ')' + argumentsNeeded + ')') } else { asyncDetected = Boolean(async && engine.methods[func] && engine.methods[func].asyncMethod) const argCount = countArguments(asyncDetected ? engine.methods[func].asyncMethod : engine.methods[func].method) let argumentsNeeded = argumentsDict[argCount - 1] || argumentsDict[2] if (asyncDetected && typeof engine.methods[func][Sync] === 'function' && engine.methods[func][Sync](lower, { engine })) { asyncDetected = false argumentsNeeded = argumentsNeeded.replace('engine', 'engine.fallback') } if (engine.methods[func] && !engine.methods[func].lazy) { return makeAsync(`engine.methods["${func}"]${asyncDetected ? '.asyncMethod' : '.method'}(${coerce}(` + buildString(lower, buildState) + ')' + argumentsNeeded + ')') } else { notTraversed.push(lower) return makeAsync(`engine.methods["${func}"]${asyncDetected ? '.asyncMethod' : '.method'}(` + `notTraversed[${notTraversed.length - 1}]` + argumentsNeeded + ')') } } } return pushValue(method) } /** * Synchronously compiles the logic to a function that can run the logic more optimally. * @param {*} method * @param {BuildState} [buildState] * @returns */ function build (method, buildState = {}) { Object.assign( buildState, Object.assign( { notTraversed: [], methods: [], state: {}, processing: [], async: buildState.engine.async, asyncDetected: false, values: [], compile: compileTemplate }, buildState ) ) const str = buildString(method, buildState) return processBuiltString(method, str, buildState) } /** * Asynchronously compiles the logic to a function that can run the logic more optimally. Also supports async logic methods. * @param {*} method * @param {BuildState} [buildState] * @returns */ async function buildAsync (method, buildState = {}) { Object.assign( buildState, Object.assign( { notTraversed: [], methods: [], state: {}, processing: [], async: buildState.engine.async, asyncDetected: false, values: [], compile: compileTemplate }, buildState ) ) const str = buildString(method, buildState) buildState.processing = await Promise.all(buildState.processing || []) return processBuiltString(method, str, buildState) } /** * Takes the string that's been generated and does some post-processing on it to be evaluated. * @param {*} method * @param {*} str * @param {BuildState} buildState * @returns */ function processBuiltString (method, str, buildState) { const { engine, methods, notTraversed, processing = [], values } = buildState const above = [] processing.forEach((item, x) => { str = str.replace(`__%%%${x}%%%__`, item) }) const final = `(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, precoerceNumber, assertSize, compareCheck) => ${buildState.asyncDetected ? 'async' : ''} (context ${buildState.extraArguments ? ',' + buildState.extraArguments : ''}) => { ${str.includes('prev') ? 'let prev;' : ''} const result = ${str}; return result }` // console.log(str) // console.log(final) // eslint-disable-next-line no-eval return Object.assign( (typeof globalThis !== 'undefined' ? globalThis : global).eval(final)(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, precoerceNumber, assertSize, compareCheck), { [Sync]: !buildState.asyncDetected, deterministic: !str.includes('('), aboveDetected: typeof str === 'string' && str.includes(', above') }) } const Compiler = { build, buildAsync, buildString } // @ts-check /** * Checks if optional chaining is supported for the compiler * @returns {Boolean} */ const getIsOptionalChainingSupported = () => { try { // eslint-disable-next-line no-unused-vars const test = {} // eslint-disable-next-line no-eval const isUndefined = (typeof globalThis !== 'undefined' ? globalThis : global).eval('(test) => test?.foo?.bar')(test) return isUndefined === undefined } catch (err) { return false } } const chainingSupported = getIsOptionalChainingSupported() const parsedPaths = new Map() /** * Splits a path string into an array of parts; lightly memoized. * It will reset the entire cache after 2048 paths, this could be improved * by implementing an LRU cache or something, but I'm trying to keep * this library fairly dep free, and the code not too cumbersome. * * Memoizing the splitPath function can be seen as cheating, but I think it's likely * that a lot of the same paths will be used for logic, so it's a good optimization. * * @param {string} str * @returns {string[]} */ function splitPathMemoized (str) { if (parsedPaths.has(str)) return parsedPaths.get(str) if (parsedPaths.size > 2048) parsedPaths.clear() const parts = splitPath(str) parsedPaths.set(str, parts) return parts } /** * Splits a path string into an array of parts. * * @example splitPath('a.b.c') // ['a', 'b', 'c'] * @example splitPath('a\\.b.c') // ['a.b', 'c'] * @example splitPath('a\\\\.b.c') // ['a\\', 'b', 'c'] * @example splitPath('a\\\\\\.b.c') // ['a\\.b', 'c'] * @example splitPath('hello') // ['hello'] * @example splitPath('hello\\') // ['hello\\'] * @example splitPath('hello\\\\') // ['hello\\'] * * @param {string} str * @param {string} separator * @returns {string[]} */ function splitPath (str, separator = '.', escape = '\\', up = '/') { const parts = [] let current = '' for (let i = 0; i < str.length; i++) { const char = str[i] if (char === escape) { if (str[i + 1] === separator || str[i + 1] === up) { current += str[i + 1] i++ } else if (str[i + 1] === escape) { current += escape i++ // The following else might be something tweaked in a spec. } else current += escape } else if (char === separator) { parts.push(current) current = '' } else current += char } // The if prevents me from pushing more sections than characters // This is so that "." will [''] and not ['',''] // But .h will be ['','.h'] // .. becomes ['',''], ..h becomes ['', '', 'h'] if (parts.length !== str.length) parts.push(current) return parts } /** @type {Record<'get' | 'missing' | 'missing_some' | 'var', { method: (...args) => any }>} **/ const legacyMethods = { get: { [Sync]: true, method: ([data, key, defaultValue], context, above, engine) => { const notFound = defaultValue === undefined ? null : defaultValue const subProps = splitPathMemoized(String(key)) for (let i = 0; i < subProps.length; i++) { if (data === null || data === undefined) return notFound // Descending into context data = data[subProps[i]] if (data === undefined) return notFound } if (engine.allowFunctions || typeof data[key] !== 'function') return data return null }, deterministic: true, compile: (data, buildState) => { let defaultValue = null let key = data let obj = null if (Array.isArray(data) && data.length <= 3) { obj = data[0] key = data[1] defaultValue = typeof data[2] === 'undefined' ? null : data[2] // Bail out if the key is dynamic; dynamic keys are not really optimized by this block. if (key && typeof key === 'object') return false key = key.toString() const pieces = splitPathMemoized(key) if (!chainingSupported) { return `(((a,b) => (typeof a === 'undefined' || a === null) ? b : a)(${pieces.reduce( (text, i) => `(${text}||0)[${JSON.stringify(i)}]`, `(${buildString(obj, buildState)}||0)` )}, ${buildString(defaultValue, buildState)}))` } return `((${buildString(obj, buildState)})${pieces .map((i) => `?.[${buildString(i, buildState)}]`) .join('')} ?? ${buildString(defaultValue, buildState)})` } return false } }, var: { [OriginalImpl]: true, [Sync]: true, method: (key, context, above, engine) => { let b if (Array.isArray(key)) { b = key[1] key = key[0] } let iter = 0 while (typeof key === 'string' && key.startsWith('../') && iter < above.length) { context = above[iter++] key = key.substring(3) // A performance optimization that allows you to pass the previous above array without spreading it as the last argument if (iter === above.length && Array.isArray(context)) { iter = 0 above = context context = above[iter++] } } const notFound = b === undefined ? null : b if (typeof key === 'undefined' || key === '' || key === null) { if (engine.allowFunctions || typeof context !== 'function') return context return null } const subProps = splitPathMemoized(String(key)) for (let i = 0; i < subProps.length; i++) { if (context === null || context === undefined) return notFound // Descending into context context = context[subProps[i]] if (context === undefined) return notFound } if (engine.allowFunctions || typeof context !== 'function') return context return null }, deterministic: (data, buildState) => buildState.insideIterator && !String(data).includes('../../'), optimizeUnary: true, compile: (data, buildState) => { let key = data let defaultValue = null if ( !key || typeof data === 'string' || typeof data === 'number' || (Array.isArray(data) && data.length <= 2) ) { if (Array.isArray(data)) { key = data[0] defaultValue = typeof data[1] === 'undefined' ? null : data[1] } if (key === '../index' && buildState.iteratorCompile) return 'index' // this counts the number of var accesses to determine if they're all just using this override. // this allows for a small optimization :) if (typeof key === 'undefined' || key === null || key === '') return 'context' if (typeof key !== 'string' && typeof key !== 'number') return false key = key.toString() if (key.includes('../')) return false const pieces = splitPathMemoized(key) // support older versions of node if (!chainingSupported) { const res = `((((a,b) => (typeof a === 'undefined' || a === null) ? b : a)(${pieces.reduce( (text, i) => `(${text}||0)[${JSON.stringify(i)}]`, '(context||0)' )}, ${buildString(defaultValue, buildState)})))` if (buildState.engine.allowFunctions) return res return `(typeof (prev = ${res}) === 'function' ? null : prev)` } const res = `(context${pieces .map((i) => `?.[${JSON.stringify(i)}]`) .join('')} ?? ${buildString(defaultValue, buildState)})` if (buildState.engine.allowFunctions) return res return `(typeof (prev = ${res}) === 'function' ? null : prev)` } return false } }, missing: { [Sync]: true, optimizeUnary: false, method: (checked, context) => { if (!checked.length) return [] // Check every item in checked const missing = [] for (let i = 0; i < checked.length; i++) { // check context for the key, exiting early if any is null const path = splitPathMemoized(String(checked[i])) let data = context let found = true for (let j = 0; j < path.length; j++) { if (!data) { found = false break } data = data[path[j]] if (data === undefined) { found = false break } } if (!found) missing.push(checked[i]) } return missing }, compile: (data, buildState) => { if (!Array.isArray(data)) return false if (data.length === 0) return buildState.compile`[]` if (data.length === 1 && typeof data[0] === 'string' && !data[0].includes('.')) return buildState.compile`(context || 0)[${data[0]}] === undefined ? [${data[0]}] : []` if (data.length === 2 && typeof data[0] === 'string' && typeof data[1] === 'string' && !data[0].includes('.') && !data[1].includes('.')) return buildState.compile`(context || 0)[${data[0]}] === undefined ? (context || 0)[${data[1]}] === undefined ? [${data[0]}, ${data[1]}] : [${data[0]}] : (context || 0)[${data[1]}] === undefined ? [${data[1]}] : []` return false }, deterministic: (data, buildState) => { if (Array.isArray(data) && data.length === 0) return true return false } }, missing_some: { [Sync]: true, optimizeUnary: false, method: ([needCount, options], context) => { const missing = legacyMethods.missing.method(options, context) if (options.length - missing.length >= needCount) return [] return missing }, compile: ([needCount, options], buildState) => { if (!Array.isArray(options)) return false let compilation = legacyMethods.missing.compile(options, buildState) if (!compilation) compilation = buildState.compile`engine.methods.missing.method(${{ [Compiled]: JSON.stringify(options) }}, context)` return buildState.compile`${options.length} - (prev = ${compilation}).length < ${needCount} ? prev : []` }, deterministic: false } } const legacyMethods$1 = { ...legacyMethods } // @ts-check const INVALID_ARGUMENTS = { type: 'Invalid Arguments' } function isDeterministic (method, engine, buildState) { if (Array.isArray(method)) { return method.every((i) => isDeterministic(i, engine, buildState)) } if (method && typeof method === 'object') { const func = Object.keys(method)[0] const lower = method[func] if (engine.isData(method, func) || func === undefined) return true // eslint-disable-next-line no-throw-literal if (!engine.methods[func]) throw { type: 'Unknown Operator', key: func } if (engine.methods[func].lazy) { return typeof engine.methods[func].deterministic === 'function' ? engine.methods[func].deterministic(lower, buildState) : engine.methods[func].deterministic } return typeof engine.methods[func].deterministic === 'function' ? engine.methods[func].deterministic(lower, buildState) : engine.methods[func].deterministic && isDeterministic(lower, engine, buildState) } return true } function isSyncDeep (method, engine, buildState) { if (Array.isArray(method)) { return method.every((i) => isSyncDeep(i, engine, buildState)) } if (method && typeof method === 'object') { const func = Object.keys(method)[0] const lower = method[func] if (engine.isData(method, func) || func === undefined) return true // eslint-disable-next-line no-throw-literal if (!engine.methods[func]) throw { type: 'Unknown Operator', key: func } if (engine.methods[func].lazy) return typeof engine.methods[func][Sync] === 'function' ? engine.methods[func][Sync](lower, buildState) : engine.methods[func][Sync] return typeof engine.methods[func][Sync] === 'function' ? engine.methods[func][Sync](lower, buildState) : engine.methods[func][Sync] && isSyncDeep(lower, engine, buildState) } return true } /** * Runs the logic with the given data. */ function runOptimizedOrFallback (logic, engine, data, above) { if (!logic) return logic if (typeof logic !== 'object') return logic if (!engine.disableInterpretedOptimization && engine.optimizedMap.has(logic)) { const optimized = engine.optimizedMap.get(logic) if (typeof optimized === 'function') return optimized(data, above) return optimized } return engine.run(logic, data, { above }) } const oldAll = createArrayIterativeMethod('every', true) const defaultMethods = { '+': (data) => { if (!data) return 0 if (typeof data === 'string') return precoerceNumber(+data) if (typeof data === 'number') return precoerceNumber(+data) if (typeof data === 'boolean') return precoerceNumber(+data) if (typeof data === 'object' && !Array.isArray(data)) throw NaN let res = 0 for (let i = 0; i < data.length; i++) { if (data[i] && typeof data[i] === 'object') throw NaN res += +data[i] } if (Number.isNaN(res)) throw NaN return res }, '*': (data) => { if (data.length === 0) return 1 let res = 1 for (let i = 0; i < data.length; i++) { if (data[i] && typeof data[i] === 'object') throw NaN res *= +data[i] } if (Number.isNaN(res)) throw NaN return res }, '/': (data) => { if (data[0] && typeof data[0] === 'object') throw NaN if (data.length === 0) throw INVALID_ARGUMENTS if (data.length === 1) { if (!+data[0] || (data[0] && typeof data[0] === 'object')) throw NaN return 1 / +data[0] } let res = +data[0] for (let i = 1; i < data.length; i++) { if ((data[i] && typeof data[i] === 'object') || !data[i]) throw NaN res /= +data[i] } if (Number.isNaN(res) || res === Infinity) throw NaN return res }, '-': (data) => { if (!data) return 0 if (typeof data === 'string') return precoerceNumber(-data) if (typeof data === 'number') return precoerceNumber(-data) if (typeof data === 'boolean') return precoerceNumber(-data) if (typeof data === 'object' && !Array.isArray(data)) throw NaN if (data[0] && typeof data[0] === 'object') throw NaN if (data.length === 0) throw INVALID_ARGUMENTS if (data.length === 1) return -data[0] let res = data[0] for (let i = 1; i < data.length; i++) { if (data[i] && typeof data[i] === 'object') throw NaN res -= +data[i] } if (Number.isNaN(res)) throw NaN return res }, '%': (data) => { if (data[0] && typeof data[0] === 'object') throw NaN if (data.length < 2) throw INVALID_ARGUMENTS let res = +data[0] for (let i = 1; i < data.length; i++) { if (data[i] && typeof data[i] === 'object') throw NaN res %= +data[i] } if (Number.isNaN(res)) throw NaN return res }, throw: (type) => { if (Array.isArray(type)) type = type[0] if (typeof type === 'object') throw type // eslint-disable-next-line no-throw-literal throw { type } }, max: (data) => { if (!data.length || typeof data[0] !== 'number') throw INVALID_ARGUMENTS let max = data[0] for (let i = 1; i < data.length; i++) { if (typeof data[i] !== 'number') throw INVALID_ARGUMENTS if (data[i] > max) max = data[i] } return max }, min: (data) => { if (!data.length || typeof data[0] !== 'number') throw INVALID_ARGUMENTS let min = data[0] for (let i = 1; i < data.length; i++) { if (typeof data[i] !== 'number') throw INVALID_ARGUMENTS if (data[i] < min) min = data[i] } return min }, in: ([item, array]) => (array || []).includes(item), preserve: { lazy: true, method: declareSync((i) => i, true), [Sync]: () => true }, if: { [OriginalImpl]: true, method: (input, context, above, engine) => { if (!Array.isArray(input)) throw INVALID_ARGUMENTS if (input.length === 1) return runOptimizedOrFallback(input[0], engine, context, above) if (input.length < 2) return null input = [...input] if (input.length % 2 !== 1) input.push(null) // fallback to the default if the condition is false const onFalse = input.pop() // while there are still conditions while (input.length) { const check = input.shift() const onTrue = input.shift() const test = runOptimizedOrFallback(check, engine, context, above) // if the condition is true, run the true branch if (engine.truthy(test)) return runOptimizedOrFallback(onTrue, engine, context, above) } return runOptimizedOrFallback(onFalse, engine, context, above) }, [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), deterministic: (data, buildState) => { return isDeterministic(data, buildState.engine, buildState) }, asyncMethod: async (input, context, above, engine) => { if (!Array.isArray(input)) throw INVALID_ARGUMENTS // check the bounds if (input.length === 1) return engine.run(input[0], context, { above }) if (input.length < 2) return null input = [...input] if (input.length % 2 !== 1) input.push(null) // fallback to the default if the condition is false const onFalse = input.pop() // while there are still conditions while (input.length) { const check = input.shift() const onTrue = input.shift() const test = await engine.run(check, context, { above }) // if the condition is true, run the true branch if (engine.truthy(test)) return engine.run(onTrue, context, { above }) } return engine.run(onFalse, context, { above }) }, lazy: true }, '<': createComparator('<', (a, b) => a < b), '<=': createComparator('<=', (a, b) => a <= b), '>': createComparator('>', (a, b) => a > b), '>=': createComparator('>=', (a, b) => a >= b), // eslint-disable-next-line eqeqeq '==': createComparator('==', (a, b) => a == b), '===': createComparator('===', (a, b) => a === b), // eslint-disable-next-line eqeqeq '!=': createComparator('!=', (a, b) => a != b), '!==': createComparator('!==', (a, b) => a !== b), or: { [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), method: (arr, context, above, engine) => { if (!Array.isArray(arr)) throw INVALID_ARGUMENTS if (!arr.length) return null let item for (let i = 0; i < arr.length; i++) { item = runOptimizedOrFallback(arr[i], engine, context, above) if (engine.truthy(item)) return item } return item }, asyncMethod: async (arr, _1, _2, engine) => { if (!Array.isArray(arr)) throw INVALID_ARGUMENTS if (!arr.length) return null let item for (let i = 0; i < arr.length; i++) { item = await engine.run(arr[i], _1, { above: _2 }) if (engine.truthy(item)) return item } return item }, deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState), compile: (data, buildState) => { let res = buildState.compile`` if (Array.isArray(data)) { if (!data.length) return buildState.compile`null` for (let i = 0; i < data.length; i++) res = buildState.compile`${res} engine.truthy(prev = ${data[i]}) ? prev : ` res = buildState.compile`${res} prev` return res } return false }, lazy: true }, '??': { [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), method: (arr, context, above, engine) => { if (!Array.isArray(arr)) throw INVALID_ARGUMENTS let item for (let i = 0; i < arr.length; i++) { item = runOptimizedOrFallback(arr[i], engine, context, above) if (item !== null && item !== undefined) return item } if (item === undefined) return null return item }, asyncMethod: async (arr, _1, _2, engine) => { if (!Array.isArray(arr)) throw INVALID_ARGUMENTS let item for (let i = 0; i < arr.length; i++) { item = await engine.run(arr[i], _1, { above: _2 }) if (item !== null && item !== undefined) return item } if (item === undefined) return null return item }, deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState), compile: (data, buildState) => { if (!chainingSupported) return false if (Array.isArray(data) && data.length) { return `(${data.map((i, x) => { const built = buildString(i, buildState) if (Array.isArray(i) || !i || typeof i !== 'object' || x === data.length - 1) return built return '(' + built + ')' }).join(' ?? ')})` } return `(${buildString(data, buildState)}).reduce((a,b) => (a) ?? b, null)` }, lazy: true }, try: { [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), method: (arr, context, above, engine) => { if (!Array.isArray(arr)) arr = [arr] let item let lastError for (let i = 0; i < arr.length; i++) { try { // Todo: make this message thing more robust. if (lastError) item = runOptimizedOrFallback(arr[i], engine, { type: lastError.type || lastError.error || lastError.message || lastError.constructor.name }, [null, context, above]) else item = runOptimizedOrFallback(arr[i], engine, context, above) return item } catch (e) { if (Number.isNaN(e)) lastError = { message: 'NaN' } else lastError = e } } throw lastError }, asyncMethod: async (arr, _1, _2, engine) => { if (!Array.isArray(arr)) arr = [arr] let item let lastError for (let i = 0; i < arr.length; i++) { try { // Todo: make this message thing more robust. if (lastError) item = await engine.run(arr[i], { type: lastError.type || lastError.error || lastError.message || lastError.constructor.name }, { above: [null, _1, _2] }) else item = await engine.run(arr[i], _1, { above: _2 }) return item } catch (e) { if (Number.isNaN(e)) lastError = { message: 'NaN' } else lastError = e } } throw lastError }, deterministic: (data, buildState) => { return isDeterministic(data[0], buildState.engine, { ...buildState, insideTry: true }) && isDeterministic(data, buildState.engine, { ...buildState, insideIterator: true, insideTry: true }) }, lazy: true, compile: (data, buildState) => { if (!Array.isArray(data) || !data.length) return false let res try { res = buildState.compile`((context, above) => { try { return ${data[0]} } catch(err) { above = [null, context, above]; context = { type: err.type || err.message || err.toString() }; ` } catch (err) { // eslint-disable-next-line no-ex-assign if (Number.isNaN(err)) err = { type: 'NaN' } res = { [Compiled]: `((context, above) => { { above = [null, context, above]; context = ${JSON.stringify(err)}; ` } } if (data.length > 1) { for (let i = 1; i < data.length; i++) { try { if (i === data.length - 1) res = buildState.compile`${res} try { return ${data[i]} } catch(err) { throw err; } ` else res = buildState.compile`${res} try { return ${data[i]} } catch(err) { context = { type: err.type || err.message || err.toString() }; } ` } catch (err) { // eslint-disable-next-line no-ex-assign if (Number.isNaN(err)) err = { type: 'NaN' } if (i === data.length - 1) res = buildState.compile`${res} throw ${{ [Compiled]: JSON.stringify(err) }} ` else res = buildState.compile`${res} ${{ [Compiled]: `context = ${JSON.stringify(err)};` }}` } } } else { if (res[Compiled].includes('err')) res = buildState.compile`${res} throw err;` else res = buildState.compile`${res} throw context;` } res = buildState.compile`${res} } })(context, above)` if (res[Compiled].includes('await')) res[Compiled] = res[Compiled].replace('((context', '(async (context') return res } }, and: { [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), method: (arr, context, above, engine) => { if (!Array.isArray(arr)) throw INVALID_ARGUMENTS if (!arr.length) return null let item for (let i = 0; i < arr.length; i++) { item = runOptimizedOrFallback(arr[i], engine, context, above) if (!engine.truthy(item)) return item } return item }, asyncMethod: async (arr, _1, _2, engine) => { if (!Array.isArray(arr)) throw INVALID_ARGUMENTS if (!arr.length) return null let item for (let i = 0; i < arr.length; i++) { item = await engine.run(arr[i], _1, { above: _2 }) if (!engine.truthy(item)) return item } return item }, lazy: true, deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState), compile: (data, buildState) => { let res = buildState.compile`` if (Array.isArray(data)) { if (!data.length) return buildState.compile`null` for (let i = 0; i < data.length; i++) res = buildState.compile`${res} !engine.truthy(prev = ${data[i]}) ? prev : ` res = buildState.compile`${res} prev` return res } return false } }, substr: ([string, from, end]) => { if (end < 0) { const result = string.substr(from) return result.substr(0, result.length + end) } return string.substr(from, end) }, length: { method: (data, context, above, engine) => { if (!data) throw INVALID_ARGUMENTS const parsed = runOptimizedOrFallback(data, engine, context, above) const i = Array.isArray(data) ? parsed[0] : parsed if (typeof i === 'string' || Array.isArray(i)) return i.length if (i && typeof i === 'object') return Object.keys(i).length throw INVALID_ARGUMENTS }, asyncMethod: async (data, context, above, engine) => { if (!data) throw INVALID_ARGUMENTS const parsed = await runOptimizedOrFallback(data, engine, context, above) const i = Array.isArray(data) ? parsed[0] : parsed if (typeof i === 'string' || Array.isArray(i)) return i.length if (i && typeof i === 'object') return Object.keys(i).length throw INVALID_ARGUMENTS }, deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState), lazy: true }, exists: { method: (key, context, above, engine) => { const result = defaultMethods.val.method(key, context, above, engine, Unfound) return result !== Unfound }, deterministic: false }, val: { [OriginalImpl]: true, [Sync]: true, method: (args, context, above, engine, /** @type {null | Symbol} */ unFound = null) => { if (Array.isArray(args) && args.length === 1 && !Array.isArray(args[0])) args = args[0] // A unary optimization if (!Array.isArray(args)) { if (unFound && !(context && args in context)) return unFound if (context === null || context === undefined) return null const result = context[args] if (typeof result === 'undefined') return null return result } let result = context let start = 0 // This block handles scope traversal if (Array.isArray(args[0]) && args[0].length === 1) { start++ const climb = +Math.abs(args[0][0]) let pos = 0 for (let i = 0; i < climb; i++) { result = above[pos++] if (i === above.length - 1 && Array.isArray(result)) { above = result result = result[0] pos = 1 } } } // This block handles traversing the path for (let i = start; i < args.length; i++) { if (unFound && !(result && args[i] in result)) return unFound if (result === null || result === undefined) return null result = result[args[i]] } if (typeof result === 'undefined') return unFound if (typeof result === 'function' && !engine.allowFunctions) return unFound return result }, optimizeUnary: true, deterministic: (data, buildState) => { if (buildState.insideIterator) { if (Array.isArray(data) && Array.isArray(data[0]) && Math.abs(data[0][0]) >= 2) return false return true } return false }, compile: (data, buildState) => { function wrapNull (data) { let res if (!chainingSupported) res = buildState.compile`(((a) => a === null || a === undefined ? null : a)(${data}))` else res = buildState.compile`(${data} ?? null)` if (!buildState.engine.allowFunctions) res = buildState.compile`(typeof (prev = ${res}) === 'function' ? null : prev)` return res } if (typeof data === 'object' && !Array.isArray(data)) { // If the input for this function can be inlined, we will do so right here. if (isSyncDeep(data, buildState.engine, buildState) && isDeterministic(data, buildState.engine, buildState) && !buildState.engine.disableInline) data = (buildState.engine.fallback || buildState.engine).run(data, buildState.context, { above: buildState.above }) else return false } if (Array.isArray(data) && Array.isArray(data[0])) { // A very, very specific optimization. if (buildState.iteratorCompile && Math.abs(data[0][0] || 0) === 1 && data[1] === 'index') return buildState.compile`index` return false } if (Array.isArray(data) && data.length === 1) data = data[0] if (data === null) return wrapNull(buildState.compile`context`) if (!Array.isArray(data)) { if (chainingSupported) return wrapNull(buildState.compile`context?.[${data}]`) return wrapNull(buildState.compile`(context || 0)[${data}]`) } if (Array.isArray(data)) { let res = buildState.compile`context` for (let i = 0; i < data.length; i++) { if (data[i] === null) continue if (chainingSupported) res = buildState.compile`${res}?.[${data[i]}]` else res = buildState.compile`(${res}|| 0)[${data[i]}]` } return wrapNull(buildState.compile`(${res})`) } return false } }, map: createArrayIterativeMethod('map'), some: { ...createArrayIterativeMethod('some', true), method: (input, context, above, engine) => { if (!Array.isArray(input)) throw INVALID_ARGUMENTS let [selector, mapper] = input selector = runOptimizedOrFallback(selector, engine, context, above) || [] for (let i = 0; i < selector.length; i++) { if (engine.truthy(runOptimizedOrFallback(mapper, engine, selector[i], [selector, context, above]))) return true } return false } }, all: { [Sync]: oldAll[Sync], method: (args, context, above, engine) => { if (!Array.isArray(args)) throw INVALID_ARGUMENTS const selector = runOptimizedOrFallback(args[0], engine, context, above) || [] if (Array.isArray(selector) && selector.length === 0) return false const mapper = args[1] for (let i = 0; i < selector.length; i++) { if (!engine.truthy(runOptimizedOrFallback(mapper, engine, selector[i], [selector, context, above]))) return false } return true }, asyncMethod: async (args, context, above, engine) => { if (Array.isArray(args)) { const first = await engine.run(args[0], context, above) if (Array.isArray(first) && first.length === 0) return false } return oldAll.asyncMethod(args, context, above, engine) }, compile: (data, buildState) => { if (!Array.isArray(data)) return false return buildState.compile`Array.isArray(prev = ${data[0]}) && prev.length === 0 ? false : ${oldAll.compile([{ [Compiled]: 'prev' }, data[1]], buildState)}` }, deterministic: oldAll.deterministic, lazy: oldAll.lazy }, none: { [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), lazy: true, method: (val, context, above, engine) => !defaultMethods.some.method(val, context, above, engine), asyncMethod: async (val, context, above, engine) => !(await defaultMethods.some.asyncMethod(val, context, above, engine)), compile: (data, buildState) => { const result = defaultMethods.some.compile(data, buildState) return result ? buildState.compile`!(${result})` : false } }, merge: (args) => { if (!Array.isArray(args)) return [args] const result = [] for (let i = 0; i < args.length; i++) { if (Array.isArray(args[i])) { for (let j = 0; j < args[i].length; j++) { result.push(args[i][j]) } } else result.push(args[i]) } return result }, filter: createArrayIterativeMethod('filter', true), reduce: { deterministic: (data, buildState) => { return ( isDeterministic(data[0], buildState.engine, buildState) && isDeterministic(data[1], buildState.engine, { ...buildState, insideIterator: true }) ) }, compile: (data, buildState) => { if (!Array.isArray(data)) throw INVALID_ARGUMENTS const { async } = buildState let [selector, mapper, defaultValue] = data selector = buildString(selector, buildState) if (typeof defaultValue !== 'undefined') { defaultValue = buildString(defaultValue, buildState) } const mapState = { ...buildState, extraArguments: 'above', avoidInlineAsync: true } mapper = build(mapper, mapState) const aboveArray = mapper.aboveDetected ? '[null, context, above]' : 'null' buildState.methods.push(mapper) if (async) { if (!isSync(mapper) || selector.includes('await')) { buildState.asyncDetected = true if (typeof defaultValue !== 'undefined') { return `await asyncIterators.reduce(${selector} || [], (a,b) => methods[${ buildState.methods.length - 1 }]({ accumulator: a, current: b }, ${aboveArray}), ${defaultValue})` } return `await asyncIterators.reduce(${selector} || [], (a,b) => methods[${ buildState.methods.length - 1 }]({ accumulator: a, current: b }, ${aboveArray}))` } } if (typeof defaultValue !== 'undefined') { return `(${selector} || []).reduce((a,b) => methods[${ buildState.methods.length - 1 }]({ accumulator: a, current: b }, ${aboveArray}), ${defaultValue})` } return `(${selector} || []).reduce((a,b) => methods[${ buildState.methods.length - 1 }]({ accumulator: a, current: b }, ${aboveArray}))` }, method: (input, context, above, engine) => { if (!Array.isArray(input)) throw INVALID_ARGUMENT