json-logic-engine
Version:
Construct complex rules with JSON & process them.
198 lines (180 loc) • 7.54 kB
JavaScript
import { buildString } from './compiler.js'
import { splitPathMemoized } from './utilities/splitPath.js'
import chainingSupported from './utilities/chainingSupported.js'
import { Sync, OriginalImpl, Compiled } from './constants.js'
/** @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
}
}
export default { ...legacyMethods }