json-logic-engine
Version:
Construct complex rules with JSON & process them.
1,449 lines (1,306 loc) • 104 kB
JavaScript
// @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