json-logic-engine
Version:
Construct complex rules with JSON & process them.
1,102 lines (1,014 loc) • 43 kB
JavaScript
// @ts-check
import asyncIterators from './async_iterators.js'
import { Sync, isSync, Unfound, OriginalImpl, Compiled } from './constants.js'
import declareSync from './utilities/declareSync.js'
import { build, buildString } from './compiler.js'
import chainingSupported from './utilities/chainingSupported.js'
import legacyMethods from './legacy.js'
import { precoerceNumber } from './utilities/downgrade.js'
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_ARGUMENTS
let [selector, mapper, defaultValue] = input
defaultValue = runOptimizedOrFallback(defaultValue, engine, context, above)
selector = runOptimizedOrFallback(selector, engine, context, above) || []
let func = (accumulator, current) => engine.run(mapper, { accumulator, current }, { above: [selector, context, above] })
if (engine.optimizedMap.has(mapper) && typeof engine.optimizedMap.get(mapper) === 'function') {
const optimized = engine.optimizedMap.get(mapper)
func = (accumulator, current) => optimized({ accumulator, current }, [selector, context, above])
}
if (typeof defaultValue === 'undefined') return selector.reduce(func)
return selector.reduce(func, defaultValue)
},
[Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
asyncMethod: async (input, context, above, engine) => {
if (!Array.isArray(input)) throw INVALID_ARGUMENTS
let [selector, mapper, defaultValue] = input
defaultValue = await engine.run(defaultValue, context, {
above
})
selector =
(await engine.run(selector, context, {
above
})) || []
return asyncIterators.reduce(
selector,
(accumulator, current) => {
return engine.run(
mapper,
{
accumulator,
current
},
{
above: [selector, context, above]
}
)
},
defaultValue
)
},
lazy: true
},
'!': (value, _1, _2, engine) => Array.isArray(value) ? !engine.truthy(value[0]) : !engine.truthy(value),
'!!': (value, _1, _2, engine) => Boolean(Array.isArray(value) ? engine.truthy(value[0]) : engine.truthy(value)),
cat: {
[OriginalImpl]: true,
[Sync]: true,
method: (arr) => {
if (typeof arr === 'string') return arr
if (!Array.isArray(arr)) return arr.toString()
let res = ''
for (let i = 0; i < arr.length; i++) {
if (arr[i] === null || arr[i] === undefined) continue
res += arr[i]
}
return res
},
deterministic: true,
optimizeUnary: true,
compile: (data, buildState) => {
if (typeof data === 'string') return JSON.stringify(data)
if (typeof data === 'number') return '"' + JSON.stringify(data) + '"'
if (!Array.isArray(data)) return false
let res = buildState.compile`''`
for (let i = 0; i < data.length; i++) res = buildState.compile`${res} + ${data[i]}`
return buildState.compile`(${res})`
}
},
keys: ([obj]) => typeof obj === 'object' ? Object.keys(obj) : [],
pipe: {
lazy: true,
[Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
method: (args, context, above, engine) => {
if (!Array.isArray(args)) throw new Error('Data for pipe must be an array')
let answer = engine.run(args[0], context, { above: [args, context, above] })
for (let i = 1; i < args.length; i++) answer = engine.run(args[i], answer, { above: [args, context, above] })
return answer
},
asyncMethod: async (args, context, above, engine) => {
if (!Array.isArray(args)) throw new Error('Data for pipe must be an array')
let answer = await engine.run(args[0], context, { above: [args, context, above] })
for (let i = 1; i < args.length; i++) answer = await engine.run(args[i], answer, { above: [args, context, above] })
return answer
},
compile: (args, buildState) => {
let res = buildState.compile`${args[0]}`
for (let i = 1; i < args.length; i++) res = buildState.compile`${build(args[i], { ...buildState, extraArguments: 'above' })}(${res}, [null, context, above])`
return res
},
deterministic: (data, buildState) => {
if (!Array.isArray(data)) return false
data = [...data]
const first = data.shift()
return isDeterministic(first, buildState.engine, buildState) && isDeterministic(data, buildState.engine, { ...buildState, insideIterator: true })
}
},
eachKey: {
lazy: true,
[Sync]: (data, buildState) => isSyncDeep(Object.values(data[Object.keys(data)[0]]), buildState.engine, buildState),
method: (object, context, above, engine) => {
const result = Object.keys(object).reduce((accumulator, key) => {
const item = object[key]
Object.defineProperty(accumulator, key, {
enumerable: true,
value: engine.run(item, context, { above })
})
return accumulator
}, {})
return result
},
deterministic: (data, buildState) => {
if (data && typeof data === 'object') {
return Object.values(data).every((i) => {
return isDeterministic(i, buildState.engine, buildState)
})
}
throw INVALID_ARGUMENTS
},
compile: (data, buildState) => {
// what's nice about this is that I don't have to worry about whether it's async or not, the lower entries take care of that ;)
// however, this is not engineered support yields, I will have to make a note of that & possibly support it at a later point.
if (data && typeof data === 'object') {
const result = `({ ${Object.keys(data)
.reduce((accumulator, key) => {
accumulator.push(
// @ts-ignore Never[] is not accurate
`${JSON.stringify(key)}: ${buildString(data[key], buildState)}`
)
return accumulator
}, [])
.join(',')} })`
return result
}
throw INVALID_ARGUMENTS
},
asyncMethod: async (object, context, above, engine) => {
const result = await asyncIterators.reduce(
Object.keys(object),
async (accumulator, key) => {
const item = object[key]
Object.defineProperty(accumulator, key, {
enumerable: true,
value: await engine.run(item, context, { above })
})
return accumulator
},
{}
)
return result
}
}
}
function createComparator (name, func) {
const opStr = { [Compiled]: name }
const strict = name.length === 3
return {
method: (args, context, above, engine) => {
if (!Array.isArray(args) || args.length <= 1) throw INVALID_ARGUMENTS
if (args.length === 2) {
const a = runOptimizedOrFallback(args[0], engine, context, above)
const b = runOptimizedOrFallback(args[1], engine, context, above)
if (strict || (typeof a === 'string' && typeof b === 'string')) return func(a, b)
if (Number.isNaN(+precoerceNumber(a))) throw NaN
if (Number.isNaN(+precoerceNumber(b)) && a !== null) throw NaN
return func(+a, +b)
}
let prev = runOptimizedOrFallback(args[0], engine, context, above)
for (let i = 1; i < args.length; i++) {
const current = runOptimizedOrFallback(args[i], engine, context, above)
if (strict || (typeof current === 'string' && typeof prev === 'string')) if (!func(prev, current)) return false
if (Number.isNaN(+precoerceNumber(current)) && prev !== null) throw NaN
if (i === 1 && Number.isNaN(+precoerceNumber(prev))) throw NaN
if (!func(+prev, +current)) return false
prev = current
}
return true
},
asyncMethod: async (args, context, above, engine) => {
if (!Array.isArray(args) || args.length <= 1) throw INVALID_ARGUMENTS
if (args.length === 2) {
const a = await runOptimizedOrFallback(args[0], engine, context, above)
const b = await runOptimizedOrFallback(args[1], engine, context, above)
if (strict || (typeof a === 'string' && typeof b === 'string')) return func(a, b)
if (Number.isNaN(+precoerceNumber(a))) throw NaN
if (Number.isNaN(+precoerceNumber(b)) && a !== null) throw NaN
return func(+a, +b)
}
let prev = await runOptimizedOrFallback(args[0], engine, context, above)
for (let i = 1; i < args.length; i++) {
const current = await runOptimizedOrFallback(args[i], engine, context, above)
if (strict || (typeof current === 'string' && typeof prev === 'string')) if (!func(prev, current)) return false
if (Number.isNaN(+precoerceNumber(current)) && prev !== null) throw NaN
if (i === 1 && Number.isNaN(+precoerceNumber(prev))) throw NaN
if (!func(+prev, +current)) return false
prev = current
}
return true
},
compile: (data, buildState) => {
if (!Array.isArray(data)) return false
if (data.length < 2) return false
if (data.length === 2) return buildState.compile`((prev = ${data[0]}) ${opStr} compareCheck(${data[1]}, prev, ${strict}))`
let res = buildState.compile`((prev = ${data[0]}) ${opStr} (prev = compareCheck(${data[1]}, prev, ${strict})))`
for (let i = 2; i < data.length; i++) res = buildState.compile`(${res} && prev ${opStr} (prev = compareCheck(${data[i]}, prev, ${strict})))`
return res
},
[OriginalImpl]: true,
[Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState),
lazy: true
}
}
function createArrayIterativeMethod (name, useTruthy = false) {
return {
deterministic: (data, buildState) => {
return (
isDeterministic(data[0], buildState.engine, buildState) &&
isDeterministic(data[1], buildState.engine, {
...buildState,
insideIterator: true
})
)
},
[OriginalImpl]: true,
[Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
method: (input, context, above, engine) => {
if (!Array.isArray(input)) throw INVALID_ARGUMENTS
let [selector, mapper] = input
selector = runOptimizedOrFallback(selector, engine, context, above) || []
return selector[name]((i, index) => {
if (!mapper || typeof mapper !== 'object') return useTruthy ? engine.truthy(mapper) : mapper
const result = runOptimizedOrFallback(mapper, engine, i, [{ iterator: selector, index }, context, above])
return useTruthy ? engine.truthy(result) : result
})
},
asyncMethod: async (input, context, above, engine) => {
if (!Array.isArray(input)) throw INVALID_ARGUMENTS
let [selector, mapper] = input
selector = (await engine.run(selector, context, { above })) || []
return asyncIterators[name](selector, async (i, index) => {
if (!mapper || typeof mapper !== 'object') return useTruthy ? engine.truthy(mapper) : mapper
const result = await engine.run(mapper, i, {
above: [{ iterator: selector, index }, context, above]
})
return useTruthy ? engine.truthy(result) : result
})
},
compile: (data, buildState) => {
if (!Array.isArray(data)) throw INVALID_ARGUMENTS
const { async } = buildState
const [selector, mapper] = data
const mapState = {
...buildState,
avoidInlineAsync: true,
iteratorCompile: true,
extraArguments: 'index, above'
}
const method = build(mapper, mapState)
const aboveArray = method.aboveDetected ? buildState.compile`[{ iterator: z, index: x }, context, above]` : buildState.compile`null`
const useTruthyMethod = useTruthy ? buildState.compile`engine.truthy` : buildState.compile``
if (async) {
if (!isSync(method)) {
buildState.asyncDetected = true
return buildState.compile`await asyncIterators[${name}](${selector} || [], async (i, x, z) => ${useTruthyMethod}(${method}(i, x, ${aboveArray})))`
}
}
return buildState.compile`(${selector} || [])[${name}]((i, x, z) => ${useTruthyMethod}(${method}(i, x, ${aboveArray})))`
},
lazy: true
}
}
defaultMethods.every = defaultMethods.all
defaultMethods['?:'] = defaultMethods.if
// declare all of the functions here synchronous
Object.keys(defaultMethods).forEach((item) => {
if (typeof defaultMethods[item] === 'function') {
defaultMethods[item][Sync] = true
}
defaultMethods[item].deterministic =
typeof defaultMethods[item].deterministic === 'undefined'
? true
: defaultMethods[item].deterministic
})
// @ts-ignore Allow custom attribute
defaultMethods.if.compile = function (data, buildState) {
if (!Array.isArray(data)) return false
if (data.length < 3) return false
data = [...data]
if (data.length % 2 !== 1) data.push(null)
const onFalse = data.pop()
let res = buildState.compile``
while (data.length) {
const condition = data.shift()
const onTrue = data.shift()
res = buildState.compile`${res} engine.truthy(${condition}) ? ${onTrue} : `
}
return buildState.compile`(${res} ${onFalse})`
}
/**
* Transforms the operands of the arithmetic operation to numbers.
*/
function numberCoercion (i, buildState) {
if (Array.isArray(i)) return precoerceNumber(NaN)
if (typeof i === 'number' || typeof i === 'boolean') return '+' + buildString(i, buildState)
if (typeof i === 'string') return '+' + precoerceNumber(+i)
// check if it's already a number once built
const f = buildString(i, buildState)
// regex match
if (/^-?\d+(\.\d*)?$/.test(f)) return '+' + f
// if it starts with " it's a string
if (f.startsWith('"')) return '+' + precoerceNumber(+JSON.parse(f))
if (f === 'true') return '1'
if (f === 'false') return '0'
if (f === 'null') return '0'
if (f.startsWith('[') || f.startsWith('{')) return precoerceNumber(NaN)
return `(+precoerceNumber(${f}))`
}
// @ts-ignore Allow custom attribute
defaultMethods['+'].compile = function (data, buildState) {
if (Array.isArray(data)) {
if (data.length === 0) return '(+0)'
return `precoerceNumber(${data.map(i => numberCoercion(i, buildState)).join(' + ')})`
}
if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') return `precoerceNumber(+${buildString(data, buildState)})`
return buildState.compile`(Array.isArray(prev = ${data}) ? prev.reduce((a,b) => (+a)+(+precoerceNumber(b)), 0) : +precoerceNumber(prev))`
}
// @ts-ignore Allow custom attribute
defaultMethods['%'].compile = function (data, buildState) {
if (Array.isArray(data)) {
if (data.length < 2) throw INVALID_ARGUMENTS
return `precoerceNumber(${data.map(i => numberCoercion(i, buildState)).join(' % ')})`
}
return `assertSize(${buildString(data, buildState)}, 2).reduce((a,b) => (+precoerceNumber(a))%(+precoerceNumber(b)))`
}
// @ts-ignore Allow custom attribute
defaultMethods.in.compile = function (data, buildState) {
if (!Array.isArray(data)) return false
return buildState.compile`(${data[1]} || []).includes(${data[0]})`
}
// @ts-ignore Allow custom attribute
defaultMethods['-'].compile = function (data, buildState) {
if (Array.isArray(data)) {
if (data.length === 0) throw INVALID_ARGUMENTS
return `${data.length === 1 ? '-' : ''}precoerceNumber(${data.map(i => numberCoercion(i, buildState)).join(' - ')})`
}
if (typeof data === 'string' || typeof data === 'number') return `(-${buildString(data, buildState)})`
return buildState.compile`(Array.isArray(prev = ${data}) ? prev.length === 1 ? -precoerceNumber(prev[0]) : assertSize(prev, 1).reduce((a,b) => (+precoerceNumber(a))-(+precoerceNumber(b))) : -precoerceNumber(prev))`
}
// @ts-ignore Allow custom attribute
defaultMethods['/'].compile = function (data, buildState) {
if (Array.isArray(data)) {
if (data.length === 0) throw INVALID_ARGUMENTS
if (data.length === 1) data = [1, data[0]]
return `precoerceNumber(${data.map((i, x) => {
let res = numberCoercion(i, buildState)
if (x && res === '+0') precoerceNumber(NaN)
if (x) res = `precoerceNumber(${res} || NaN)`
return res
}).join(' / ')})`
}
return `assertSize(prev = ${buildString(data, buildState)}, 1) && prev.length === 1 ? 1 / precoerceNumber(prev[0] || NaN) : prev.reduce((a,b) => (+precoerceNumber(a))/(+precoerceNumber(b || NaN)))`
}
// @ts-ignore Allow custom attribute
defaultMethods['*'].compile = function (data, buildState) {
if (Array.isArray(data)) {
if (data.length === 0) return '1'
return `precoerceNumber(${data.map(i => numberCoercion(i, buildState)).join(' * ')})`
}
return `(${buildString(data, buildState)}).reduce((a,b) => (+precoerceNumber(a))*(+precoerceNumber(b)), 1)`
}
// @ts-ignore Allow custom attribute
defaultMethods['!'].compile = function (
data,
buildState
) {
if (Array.isArray(data)) return buildState.compile`(!engine.truthy(${data[0]}))`
return buildState.compile`(!engine.truthy(${data}))`
}
defaultMethods.not = defaultMethods['!']
// @ts-ignore Allow custom attribute
defaultMethods['!!'].compile = function (data, buildState) {
if (Array.isArray(data)) return buildState.compile`(!!engine.truthy(${data[0]}))`
return buildState.compile`(!!engine.truthy(${data}))`
}
defaultMethods.none.deterministic = defaultMethods.some.deterministic
// @ts-ignore
defaultMethods.throw.deterministic = (data, buildState) => {
return buildState.insideTry && isDeterministic(data, buildState.engine, buildState)
}
// @ts-ignore Allowing a optimizeUnary attribute that can be used for performance optimizations
defaultMethods['+'].optimizeUnary = defaultMethods['-'].optimizeUnary = defaultMethods['!'].optimizeUnary = defaultMethods['!!'].optimizeUnary = defaultMethods.cat.optimizeUnary = defaultMethods.throw.optimizeUnary = true
export default {
...defaultMethods,
...legacyMethods
}