diff --git a/asyncLogic.js b/asyncLogic.js index 96d6208..fb1d95e 100644 --- a/asyncLogic.js +++ b/asyncLogic.js @@ -3,7 +3,7 @@ import defaultMethods from './defaultMethods.js' import LogicEngine from './logic.js' -import { isSync } from './constants.js' +import { isSync, OriginalImpl } from './constants.js' import declareSync from './utilities/declareSync.js' import { buildAsync } from './compiler.js' import omitUndefined from './utilities/omitUndefined.js' @@ -75,6 +75,13 @@ class AsyncLogicEngine { if (this.isData(logic, func)) return logic if (!this.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`) + // A small but useful micro-optimization for some of the most common functions. + // Later on, I could define something to shut this off if var / val are redefined. + if ((func === 'var' || func === 'val') && this.methods[func][OriginalImpl]) { + const input = (!data || typeof data !== 'object') ? data : this.fallback.run(data, context, { above }) + return this.methods[func].method(input, context, above, this) + } + if (typeof this.methods[func] === 'function') { const input = (!data || typeof data !== 'object') ? [data] : await this.run(data, context, { above }) const result = await this.methods[func](coerceArray(input), context, above, this) @@ -210,5 +217,5 @@ class AsyncLogicEngine { return logic } } -Object.assign(AsyncLogicEngine.prototype.truthy, { IDENTITY: true }) +Object.assign(AsyncLogicEngine.prototype.truthy, { [OriginalImpl]: true }) export default AsyncLogicEngine diff --git a/async_optimizer.js b/async_optimizer.js index 284f7ce..7c27584 100644 --- a/async_optimizer.js +++ b/async_optimizer.js @@ -28,7 +28,7 @@ function getMethod (logic, engine, methodName, above) { } let args = logic[methodName] - if (!args || typeof args !== 'object') args = [args] + if ((!args || typeof args !== 'object') && !method.optimizeUnary) args = [args] if (Array.isArray(args)) { const optimizedArgs = args.map(l => optimize(l, engine, above)) @@ -50,12 +50,12 @@ function getMethod (logic, engine, methodName, above) { if (isSync(optimizedArgs) && (method.method || method[Sync])) { const called = method.method ? method.method : method - return declareSync((data, abv) => called(coerceArray(typeof optimizedArgs === 'function' ? optimizedArgs(data, abv) : optimizedArgs, method.optimizeUnary), data, abv || above, engine), true) + if (method.optimizeUnary) return declareSync((data, abv) => called(typeof optimizedArgs === 'function' ? optimizedArgs(data, abv) : optimizedArgs, data, abv || above, engine.fallback), true) + return declareSync((data, abv) => called(coerceArray(typeof optimizedArgs === 'function' ? optimizedArgs(data, abv) : optimizedArgs), data, abv || above, engine), true) } - return async (data, abv) => { - return called(coerceArray(typeof optimizedArgs === 'function' ? await optimizedArgs(data, abv) : optimizedArgs, method.optimizeUnary), data, abv || above, engine) - } + if (method.optimizeUnary) return async (data, abv) => called(typeof optimizedArgs === 'function' ? await optimizedArgs(data, abv) : optimizedArgs, data, abv || above, engine) + return async (data, abv) => called(coerceArray(typeof optimizedArgs === 'function' ? await optimizedArgs(data, abv) : optimizedArgs), data, abv || above, engine) } } diff --git a/compatibility.js b/compatibility.js index 0c27844..6bc75de 100644 --- a/compatibility.js +++ b/compatibility.js @@ -1,7 +1,9 @@ +import { Sync } from './constants.js' import defaultMethods from './defaultMethods.js' const oldAll = defaultMethods.all const all = { + [Sync]: oldAll[Sync], method: (args, context, above, engine) => { if (Array.isArray(args)) { const first = engine.run(args[0], context, above) diff --git a/compiler.js b/compiler.js index eb573f6..128eac9 100644 --- a/compiler.js +++ b/compiler.js @@ -205,7 +205,7 @@ function buildString (method, buildState = {}) { } let coerce = engine.methods[func].optimizeUnary ? '' : 'coerceArray' - if (!coerce && Array.isArray(lower) && lower.length === 1) lower = lower[0] + 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'] diff --git a/constants.js b/constants.js index a4e2e7b..8b59eb2 100644 --- a/constants.js +++ b/constants.js @@ -3,7 +3,8 @@ export const Sync = Symbol.for('json_logic_sync') export const Compiled = Symbol.for('json_logic_compiled') -export const EfficientTop = Symbol.for('json_logic_efficientTop') +export const OriginalImpl = Symbol.for('json_logic_original') +export const Unfound = Symbol.for('json_logic_unfound') /** * Checks if an item is synchronous. @@ -22,6 +23,6 @@ export function isSync (item) { export default { Sync, - EfficientTop, + OriginalImpl, isSync } diff --git a/defaultMethods.js b/defaultMethods.js index c3d5458..7b5565c 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -2,12 +2,12 @@ 'use strict' import asyncIterators from './async_iterators.js' -import { Sync, isSync } from './constants.js' +import { Sync, isSync, Unfound, OriginalImpl } from './constants.js' import declareSync from './utilities/declareSync.js' import { build, buildString } from './compiler.js' import chainingSupported from './utilities/chainingSupported.js' import InvalidControlInput from './errors/InvalidControlInput.js' -import { splitPathMemoized } from './utilities/splitPath.js' +import legacyMethods from './legacy.js' function isDeterministic (method, engine, buildState) { if (Array.isArray(method)) { @@ -211,7 +211,46 @@ const defaultMethods = { xor: ([a, b]) => a ^ b, // Why "executeInLoop"? Because if it needs to execute to get an array, I do not want to execute the arguments, // Both for performance and safety reasons. + '??': { + [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), + method: (arr, _1, _2, engine) => { + // See "executeInLoop" above + const executeInLoop = Array.isArray(arr) + if (!executeInLoop) arr = engine.run(arr, _1, { above: _2 }) + + let item + for (let i = 0; i < arr.length; i++) { + item = executeInLoop ? engine.run(arr[i], _1, { above: _2 }) : arr[i] + if (item !== null && item !== undefined) return item + } + + if (item === undefined) return null + return item + }, + asyncMethod: async (arr, _1, _2, engine) => { + // See "executeInLoop" above + const executeInLoop = Array.isArray(arr) + if (!executeInLoop) arr = await engine.run(arr, _1, { above: _2 }) + + let item + for (let i = 0; i < arr.length; i++) { + item = executeInLoop ? await engine.run(arr[i], _1, { above: _2 }) : arr[i] + 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) => buildString(i, buildState)).join(' ?? ')})` + return `(${buildString(data, buildState)}).reduce((a,b) => a ?? b, null)` + }, + traverse: false + }, or: { + [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), method: (arr, _1, _2, engine) => { // See "executeInLoop" above const executeInLoop = Array.isArray(arr) @@ -240,16 +279,14 @@ const defaultMethods = { }, deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState), compile: (data, buildState) => { - if (!buildState.engine.truthy.IDENTITY) return false - if (Array.isArray(data)) { - return `(${data.map((i) => buildString(i, buildState)).join(' || ')})` - } else { - return `(${buildString(data, buildState)}).reduce((a,b) => a||b, false)` - } + if (!buildState.engine.truthy[OriginalImpl]) return false + if (Array.isArray(data) && data.length) return `(${data.map((i) => buildString(i, buildState)).join(' || ')})` + return `(${buildString(data, buildState)}).reduce((a,b) => a||b, false)` }, traverse: false }, and: { + [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), method: (arr, _1, _2, engine) => { // See "executeInLoop" above const executeInLoop = Array.isArray(arr) @@ -277,12 +314,9 @@ const defaultMethods = { traverse: false, deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState), compile: (data, buildState) => { - if (!buildState.engine.truthy.IDENTITY) return false - if (Array.isArray(data)) { - return `(${data.map((i) => buildString(i, buildState)).join(' && ')})` - } else { - return `(${buildString(data, buildState)}).reduce((a,b) => a&&b, true)` - } + if (!buildState.engine.truthy[OriginalImpl]) return false + if (Array.isArray(data) && data.length) return `(${data.map((i) => buildString(i, buildState)).join(' && ')})` + return `(${buildString(data, buildState)}).reduce((a,b) => a&&b, true)` } }, substr: ([string, from, end]) => { @@ -297,88 +331,103 @@ const defaultMethods = { if (i && typeof i === 'object') return Object.keys(i).length return 0 }, - get: { - 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 + exists: { + method: (key, context, above, engine) => { + const result = defaultMethods.val.method(key, context, above, engine, Unfound) + return result !== Unfound + }, + traverse: true, + 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 + } } } - if (engine.allowFunctions || typeof data[key] !== 'function') { - return data + // 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]] } - } - }, - var: (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++] + 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) { + if (!chainingSupported) return buildState.compile`(methods.preventFunctions(((a) => a === null || a === undefined ? null : a)(${data})))` + return buildState.compile`(methods.preventFunctions(${data} ?? null))` } - } - const notFound = b === undefined ? null : b - if (typeof key === 'undefined' || key === '' || key === null) { - if (engine.allowFunctions || typeof context !== 'function') { - return context + if (!buildState.engine.allowFunctions) buildState.methods.preventFunctions = a => typeof a === 'function' ? null : a + else buildState.methods.preventFunctions = a => a + + 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 } - return null - } - const subProps = splitPathMemoized(String(key)) - for (let i = 0; i < subProps.length; i++) { - if (context === null || context === undefined) { - return notFound + 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 } - // Descending into context - context = context[subProps[i]] - if (context === undefined) { - return notFound + 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 (engine.allowFunctions || typeof context !== 'function') { - return context - } - return null - }, - missing: (checked, context, above, engine) => { - return (Array.isArray(checked) ? checked : [checked]).filter((key) => { - return defaultMethods.var(key, context, above, engine) === null - }) - }, - missing_some: ([needCount, options], context, above, engine) => { - const missing = defaultMethods.missing(options, context, above, engine) - if (options.length - missing.length >= needCount) { - return [] - } else { - return missing + 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), all: createArrayIterativeMethod('every', true), none: { + [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), traverse: false, // todo: add async build & build method: (val, context, above, engine) => { @@ -399,7 +448,7 @@ const defaultMethods = { }, merge: (arrays) => (Array.isArray(arrays) ? [].concat(...arrays) : [arrays]), every: createArrayIterativeMethod('every'), - filter: createArrayIterativeMethod('filter'), + filter: createArrayIterativeMethod('filter', true), reduce: { deterministic: (data, buildState) => { return ( @@ -429,7 +478,7 @@ const defaultMethods = { buildState.methods.push(mapper) if (async) { if (!isSync(mapper) || selector.includes('await')) { - buildState.detectAsync = true + buildState.asyncDetected = true if (typeof defaultValue !== 'undefined') { return `await asyncIterators.reduce(${selector} || [], (a,b) => methods[${ buildState.methods.length - 1 @@ -508,11 +557,30 @@ const defaultMethods = { }, '!': (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: (arr) => { - if (typeof arr === 'string') return arr - let res = '' - for (let i = 0; i < arr.length; i++) res += arr[i] - return res + 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, + traverse: 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: { @@ -633,8 +701,8 @@ function createArrayIterativeMethod (name, useTruthy = false) { (await engine.run(selector, context, { above })) || [] - return asyncIterators[name](selector, (i, index) => { - const result = engine.run(mapper, i, { + return asyncIterators[name](selector, async (i, index) => { + const result = await engine.run(mapper, i, { above: [{ iterator: selector, index }, context, above] }) return useTruthy ? engine.truthy(result) : result @@ -654,15 +722,16 @@ function createArrayIterativeMethod (name, useTruthy = false) { 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 (!isSyncDeep(mapper, buildState.engine, buildState)) { - buildState.detectAsync = true - return buildState.compile`await asyncIterators[${name}](${selector} || [], async (i, x, z) => ${method}(i, x, ${aboveArray}))` + 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) => ${method}(i, x, ${aboveArray}))` + return buildState.compile`(${selector} || [])[${name}]((i, x, z) => ${useTruthyMethod}(${method}(i, x, ${aboveArray})))` }, traverse: false } @@ -678,16 +747,7 @@ Object.keys(defaultMethods).forEach((item) => { ? true : defaultMethods[item].deterministic }) -// @ts-ignore Allow custom attribute -defaultMethods.var.deterministic = (data, buildState) => { - return buildState.insideIterator && !String(data).includes('../../') -} -Object.assign(defaultMethods.missing, { - deterministic: false -}) -Object.assign(defaultMethods.missing_some, { - deterministic: false -}) + // @ts-ignore Allow custom attribute defaultMethods['<'].compile = function (data, buildState) { if (!Array.isArray(data)) return false @@ -871,84 +931,11 @@ defaultMethods['!!'].compile = function (data, buildState) { return `(!!engine.truthy(${data}))` } defaultMethods.none.deterministic = defaultMethods.some.deterministic -defaultMethods.get.compile = function (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) => { - return `(${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 -} -// @ts-ignore Allow custom attribute -defaultMethods.var.compile = function (data, buildState) { - let key = data - let defaultValue = null - buildState.varTop = buildState.varTop || new Set() - 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) - const [top] = pieces - buildState.varTop.add(top) - - if (!buildState.engine.allowFunctions) buildState.methods.preventFunctions = a => typeof a === 'function' ? null : a - else buildState.methods.preventFunctions = a => a - - // support older versions of node - if (!chainingSupported) { - return `(methods.preventFunctions(((a,b) => (typeof a === 'undefined' || a === null) ? b : a)(${pieces.reduce( - (text, i) => `(${text}||0)[${JSON.stringify(i)}]`, - '(context||0)' - )}, ${buildString(defaultValue, buildState)})))` - } - return `(methods.preventFunctions(context${pieces - .map((i) => `?.[${JSON.stringify(i)}]`) - .join('')} ?? ${buildString(defaultValue, buildState)}))` - } - return false -} // @ts-ignore Allowing a optimizeUnary attribute that can be used for performance optimizations -defaultMethods['+'].optimizeUnary = defaultMethods['-'].optimizeUnary = defaultMethods.var.optimizeUnary = defaultMethods['!'].optimizeUnary = defaultMethods['!!'].optimizeUnary = defaultMethods.cat.optimizeUnary = true +defaultMethods['+'].optimizeUnary = defaultMethods['-'].optimizeUnary = defaultMethods['!'].optimizeUnary = defaultMethods['!!'].optimizeUnary = defaultMethods.cat.optimizeUnary = true export default { - ...defaultMethods + ...defaultMethods, + ...legacyMethods } diff --git a/legacy.js b/legacy.js new file mode 100644 index 0000000..98ea589 --- /dev/null +++ b/legacy.js @@ -0,0 +1,163 @@ +'use strict' +import { buildString } from './compiler.js' +import { splitPathMemoized } from './utilities/splitPath.js' +import chainingSupported from './utilities/chainingSupported.js' +import { Sync, OriginalImpl } 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) + + if (!buildState.engine.allowFunctions) buildState.methods.preventFunctions = a => typeof a === 'function' ? null : a + else buildState.methods.preventFunctions = a => a + + // support older versions of node + if (!chainingSupported) { + return `(methods.preventFunctions(((a,b) => (typeof a === 'undefined' || a === null) ? b : a)(${pieces.reduce( + (text, i) => `(${text}||0)[${JSON.stringify(i)}]`, + '(context||0)' + )}, ${buildString(defaultValue, buildState)})))` + } + return `(methods.preventFunctions(context${pieces + .map((i) => `?.[${JSON.stringify(i)}]`) + .join('')} ?? ${buildString(defaultValue, buildState)}))` + } + return false + } + }, + missing: { + [Sync]: true, + optimizeUnary: false, + method: (checked, context, above, engine) => { + return (Array.isArray(checked) ? checked : [checked]).filter((key) => { + return legacyMethods.var.method(key, context, above, engine) === null + }) + }, + deterministic: false + }, + missing_some: { + [Sync]: true, + optimizeUnary: false, + method: ([needCount, options], context, above, engine) => { + const missing = legacyMethods.missing.method(options, context, above, engine) + if (options.length - missing.length >= needCount) { + return [] + } else { + return missing + } + }, + deterministic: false + } +} + + +export default {...legacyMethods} diff --git a/logic.js b/logic.js index 45e8a0e..3348d22 100644 --- a/logic.js +++ b/logic.js @@ -9,6 +9,7 @@ import omitUndefined from './utilities/omitUndefined.js' import { optimize } from './optimizer.js' import { applyPatches } from './compatibility.js' import { coerceArray } from './utilities/coerceArray.js' +import { OriginalImpl } from './constants.js' /** * An engine capable of running synchronous JSON Logic. @@ -71,6 +72,13 @@ class LogicEngine { if (!this.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`) + // A small but useful micro-optimization for some of the most common functions. + // Later on, I could define something to shut this off if var / val are redefined. + if ((func === 'var' || func === 'val') && this.methods[func][OriginalImpl]) { + const input = (!data || typeof data !== 'object') ? data : this.run(data, context, { above }) + return this.methods[func].method(input, context, above, this, null) + } + if (typeof this.methods[func] === 'function') { const input = (!data || typeof data !== 'object') ? [data] : coerceArray(this.run(data, context, { above })) return this.methods[func](input, context, above, this) @@ -174,5 +182,5 @@ class LogicEngine { return logic } } -Object.assign(LogicEngine.prototype.truthy, { IDENTITY: true }) +Object.assign(LogicEngine.prototype.truthy, { [OriginalImpl]: true }) export default LogicEngine diff --git a/optimizer.js b/optimizer.js index dc90e2f..767124f 100644 --- a/optimizer.js +++ b/optimizer.js @@ -20,7 +20,7 @@ function getMethod (logic, engine, methodName, above) { } let args = logic[methodName] - if (!args || typeof args !== 'object') args = [args] + if ((!args || typeof args !== 'object') && !method.optimizeUnary) args = [args] if (Array.isArray(args)) { const optimizedArgs = args.map(l => optimize(l, engine, above)) @@ -30,9 +30,8 @@ function getMethod (logic, engine, methodName, above) { } } else { const optimizedArgs = optimize(args, engine, above) - return (data, abv) => { - return called(coerceArray(typeof optimizedArgs === 'function' ? optimizedArgs(data, abv) : optimizedArgs, method.optimizeUnary), data, abv || above, engine) - } + if (method.optimizeUnary) return (data, abv) => called(typeof optimizedArgs === 'function' ? optimizedArgs(data, abv) : optimizedArgs, data, abv || above, engine) + return (data, abv) => called(coerceArray(typeof optimizedArgs === 'function' ? optimizedArgs(data, abv) : optimizedArgs), data, abv || above, engine) } } diff --git a/perf.js b/perf.js index 3d2678a..8197e23 100644 --- a/perf.js +++ b/perf.js @@ -4,13 +4,13 @@ async function test () { const logic = { if: [ { - '>': [{ var: 'x' }, { '+': [11, 5, { '+': [1, { var: 'y' }, 1] }, 2] }] + '>': [{ val: 'x' }, { '+': [11, 5, { '+': [1, { val: 'y' }, 1] }, 2] }] }, { - '*': [{ var: 'x' }, { '*': { map: [[2, 5, 5], { var: '' }] } }] + '*': [{ val: 'x' }, { '*': { map: [[2, 5, 5], { val: [] }] } }] }, { - '/': [{ var: 'x' }, { '-': { map: [[100, 50, 30, 10], { var: '' }] } }] + '/': [{ val: 'x' }, { '-': { map: [[100, 50, 30, 10], { val: [] }] } }] } ] } diff --git a/perf2.js b/perf2.js index 0d57deb..d19a5e9 100644 --- a/perf2.js +++ b/perf2.js @@ -8,13 +8,13 @@ async function main () { const logic = { if: [ { - '>': [{ var: 'x' }, { '+': [11, 5, { '+': [1, { var: 'y' }, 1] }, 2] }] + '>': [{ val: 'x' }, { '+': [11, 5, { '+': [1, { val: 'y' }, 1] }, 2] }] }, { - '*': [{ var: 'x' }, { '*': { map: [[2, 5, 5], { var: '' }] } }] + '*': [{ val: 'x' }, { '*': { map: [[2, 5, 5], { val: [] }] } }] }, { - '/': [{ var: 'x' }, { '-': { map: [[100, 50, 30, 10], { var: '' }] } }] + '/': [{ val: 'x' }, { '-': { map: [[100, 50, 30, 10], { val: [] }] } }] } ] } diff --git a/perf3.js b/perf3.js index 8ffb5d4..6dacee3 100644 --- a/perf3.js +++ b/perf3.js @@ -1,6 +1,6 @@ import { LogicEngine } from './index.js' const x = new LogicEngine() -const f = x.build({ '<': [{ var: '' }, 10] }) +const f = x.build({ '<': [{ val: [] }, 10] }) console.time('Logic Built') for (let i = 0; i < 1e6; i++) { f(i % 20) diff --git a/perf4.js b/perf4.js index e610c4c..5332e6f 100644 --- a/perf4.js +++ b/perf4.js @@ -4,7 +4,7 @@ import { LogicEngine } from './index.js' const optimized = new LogicEngine() const unoptimized = new LogicEngine(undefined, { disableInterpretedOptimization: true }) -const dynamicRule = { '+': [1, 2, 3, { var: 'a' }] } +const dynamicRule = { '+': [1, 2, 3, { val: 'a' }] } const staticRule = { '+': [1, 2, 3, 4] } console.time('Optimized, Same Object, Dynamic') @@ -35,7 +35,7 @@ console.log('----') console.time('Optimized, Different Object, Dynamic') for (let i = 0; i < 1e6; i++) { - optimized.run({ '+': [1, 2, 3, { var: 'a' }] }, { a: i }) + optimized.run({ '+': [1, 2, 3, { val: 'a' }] }, { a: i }) } console.timeEnd('Optimized, Different Object, Dynamic') @@ -49,7 +49,7 @@ console.timeEnd('Optimized, Different Object, Static') console.time('Unoptimized, Different Object, Dynamic') for (let i = 0; i < 1e6; i++) { - unoptimized.run({ '+': [1, 2, 3, { var: 'a' }] }, { a: i }) + unoptimized.run({ '+': [1, 2, 3, { val: 'a' }] }, { a: i }) } console.timeEnd('Unoptimized, Different Object, Dynamic') diff --git a/suites/additional.json b/suites/additional.json index 2360b52..1f6cf6d 100644 --- a/suites/additional.json +++ b/suites/additional.json @@ -4,7 +4,7 @@ [ 1, { - "var": "x" + "val": "x" }, 3 ], @@ -21,11 +21,11 @@ { "if": [ { - "var": "x" + "val": "x" }, [ { - "var": "y" + "val": "y" } ], 99 @@ -43,20 +43,20 @@ { "reduce": [ { - "var": "integers" + "val": "integers" }, { "+": [ { - "var": "current" + "val": "current" }, { - "var": "accumulator" + "val": "accumulator" } ] }, { - "var": "start_with" + "val": "start_with" } ] }, @@ -70,5 +70,11 @@ "start_with": 59 }, 69 - ] + ], + { + "description": "Simple Inlineable Val Chained", + "rule": { "val": { "cat": ["te", "st"] } }, + "data": { "test": 1 }, + "result": 1 + } ] \ No newline at end of file diff --git a/suites/chained.json b/suites/chained.json index 0cd6dae..b0c95dd 100644 --- a/suites/chained.json +++ b/suites/chained.json @@ -14,7 +14,7 @@ }, { "description": "Max with Logic Chaining", - "rule": { "max": { "var": "data" } }, + "rule": { "max": { "val": "data" } }, "data": { "data": [1, 2, 3] }, "result": 3 }, @@ -26,7 +26,7 @@ }, { "description": "Cat with Logic Chaining (Simple)", - "rule": { "cat": { "var": "text" } }, + "rule": { "cat": { "val": "text" } }, "data": { "text": ["Hello ", "World", "!"] }, "result": "Hello World!" }, @@ -35,10 +35,10 @@ "max": { "map": [{ "filter": [ - { "var": "people" }, - { "===": [{ "var": "department" }, "Engineering"] } + { "val": "people" }, + { "===": [{ "val": "department" }, "Engineering"] } ]}, - { "var": "salary" } + { "val": "salary" } ] } }, diff --git a/suites/coalesce.json b/suites/coalesce.json new file mode 100644 index 0000000..36121ca --- /dev/null +++ b/suites/coalesce.json @@ -0,0 +1,93 @@ +[ + "Test Specification for ??", + { + "description": "Coalesces a string alone", + "rule": { "??": ["hello"] }, + "data": null, + "result": "hello" + }, + { + "description": "Coalesces a number alone", + "rule": { "??": [1] }, + "data": null, + "result": 1 + }, + { + "description": "Coalesces a boolean alone", + "rule": { "??": [true] }, + "data": null, + "result": true + }, + { + "description": "Coalesces an object from context alone", + "rule": { "??": [{ "val": "person" }]}, + "data": { "person": { "name": "John" } }, + "result": { "name": "John" } + }, + { + "description": "Empty behavior", + "rule": { "??": [] }, + "data": null, + "result": null + }, + { + "description": "Coalesces a string with nulls before", + "rule": { "??": [null, "hello"] }, + "data": null, + "result": "hello" + }, + { + "description": "Coalesces a string with multiple nulls before", + "rule": { "??": [null, null, null, "hello"] }, + "data": null, + "result": "hello" + }, + { + "description": "Coalesces a string with nulls after", + "rule": { "??": ["hello", null] }, + "data": null, + "result": "hello" + }, + { + "description": "Coalesces a string with nulls both before and after", + "rule": { "??": [null, "hello", null] }, + "data": null, + "result": "hello" + }, + { + "description": "Coalesces a number with nulls both before and after", + "rule": { "??": [null, 1, null] }, + "data": null, + "result": 1 + }, + { + "description": "Uses the first non-null value", + "rule": { "??": [null, 1, "hello"] }, + "data": null, + "result": 1 + }, + { + "description": "Uses the first non-null value, even if it is false", + "rule": { "??": [null, false, "hello"] }, + "data": null, + "result": false + }, + { + "description": "Uses the first non-null value from context", + "rule": { "??": [{ "val": ["person", "name"] }, { "val": "name" }, "Unknown Name"] }, + "data": { "person": { "name": "John" }, "name": "Jane" }, + "result": "John" + }, + { + "description": "Uses the first non-null value from context (with person undefined)", + "rule": { "??": [{ "val": ["person", "name"] }, { "val": "name" }, "Unknown Name"] }, + "data": { "name": "Jane" }, + "result": "Jane" + }, + { + "description": "Uses the first non-null value from context (without any context)", + "rule": { "??": [{ "val": ["person", "name"] }, { "val": "name" }, "Unknown Name"] }, + "data": {}, + "result": "Unknown Name" + } +] \ No newline at end of file diff --git a/suites/exists.json b/suites/exists.json new file mode 100644 index 0000000..af03dfd --- /dev/null +++ b/suites/exists.json @@ -0,0 +1,51 @@ +[ + "Test Specification for exists", + { + "description": "Checks if a normal key exists", + "rule": { "exists": "hello" }, + "data": { "hello" : 1 }, + "result": true + }, + { + "description": "Checks if a normal key exists (array)", + "rule": { "exists": ["hello"] }, + "data": { "hello" : 1 }, + "result": true + }, + { + "description": "Checks if a normal key exists (false)", + "rule": { "exists": "hello" }, + "data": { "world" : 1 }, + "result": false + }, + { + "description": "Checks if an empty key exists (true)", + "rule": { "exists": [""] }, + "data": { "" : 1 }, + "result": true + }, + { + "description": "Checks if an empty key exists (false)", + "rule": { "exists": [""] }, + "data": { "hello" : 1 }, + "result": false + }, + { + "description": "Checks if a nested key exists", + "rule": { "exists": ["hello", "world"] }, + "data": { "hello" : { "world": false } }, + "result": true + }, + { + "description": "Checks if a nested key exists (false)", + "rule": { "exists": ["hello", "world"] }, + "data": { "hello" : { "x": false } }, + "result": false + }, + { + "description": "Checks if a null value exists", + "rule": { "exists": "hello" }, + "data": { "hello" : null }, + "result": true + } +] \ No newline at end of file diff --git a/suites/scopes.json b/suites/scopes.json index 1a56d89..06e713c 100644 --- a/suites/scopes.json +++ b/suites/scopes.json @@ -4,8 +4,8 @@ "description": "Map can add each number to index", "rule": { "map": [ - { "var": "numbers" }, - { "+": [{ "var": "../index" }, { "var": "" }]} + { "val": "numbers" }, + { "+": [{ "val": [[1], "index"] }, { "val": [] }]} ] }, "data": { "numbers": [1,2,3] }, @@ -15,8 +15,8 @@ "description": "Map can add each number to value from context", "rule": { "map": [ - { "var": "numbers" }, - { "+": [{ "var": "../../value" }, { "var": "" }]} + { "val": "numbers" }, + { "+": [{ "val": [[2], "value"] }, { "val": [] }]} ] }, "data": { "numbers": [1,2,3], "value": 10 }, @@ -26,8 +26,8 @@ "description": "Filter can use parent context to filter", "rule": { "filter": [ - { "var": "people" }, - { "===": [{ "var": "department" }, { "var": "../../department" }] } + { "val": "people" }, + { "===": [{ "val": "department" }, { "val": [[2], "department"] }] } ] }, "data": { @@ -57,8 +57,8 @@ "description": "Access an escaped key from above", "rule": { "map": [ - { "var": "arr" }, - { "+": [{ "var": "../../\\.\\./" }, { "var": "../../..\\/" }]} + { "val": "arr" }, + { "+": [{ "val": [[2], "", "", "/"] }, { "val": [[2], "../"] }]} ] }, "data": { "arr": [1,2,3], "../": 10, "": { "": { "/": 7 }} }, diff --git a/suites/val-compat.json b/suites/val-compat.json new file mode 100644 index 0000000..b3be786 --- /dev/null +++ b/suites/val-compat.json @@ -0,0 +1,667 @@ +[ + "If the consequents are logic, they get evaluated", + { + "description": "[1,{\"var\":\"x\"},3]", + "rule": [ 1, {"val": "x"}, 3 ], + "data": {"x": 2}, + "result": [1, 2, 3] + }, + { + "description": "{\"if\":[{\"var\":\"x\"},[{\"var\":\"y\"}],99]}", + "rule": { + "if": [ + {"val": "x"}, + [ {"val": "y"} ], + 99 + ] + }, + "data": {"x": true, "y": 42}, + "result": [42] + }, + { + "description": "{\"var\":[\"a\"]}", + "rule": { "val": ["a"] }, + "data": {"a": 1}, + "result": 1 + }, + { + "description": "{\"var\":[\"b\"]}", + "rule": { "val": ["b"] }, + "data": {"a": 1}, + "result": null + }, + { + "description": "{\"var\":[\"a\"]}", + "rule": { "val": ["a"] }, + "data": null, + "result": null + }, + { "description": "{\"var\":\"a\"}", "rule": {"val": "a"}, "data": {"a": 1}, "result": 1 }, + { "description": "{\"var\":\"b\"}", "rule": {"val": "b"}, "data": {"a": 1}, "result": null }, + { "description": "{\"var\":\"a\"}", "rule": {"val": "a"}, "data": null, "result": null }, + { + "description": "{\"var\":\"a.b\"}", + "rule": { "val": ["a", "b"] }, + "data": { "a": {"b": "c"} }, + "result": "c" + }, + { + "description": "{\"var\":\"a.q\"}", + "rule": { "val": ["a", "q"] }, + "data": { "a": {"b": "c"} }, + "result": null + }, + { + "description": "{\"var\":1}", + "rule": {"val": 1}, + "data": ["apple", "banana"], + "result": "banana" + }, + { + "description": "{\"var\":\"1\"}", + "rule": {"val": "1"}, + "data": ["apple", "banana"], + "result": "banana" + }, + { + "description": "{\"var\":\"1.1\"}", + "rule": { "val": [1, 1] }, + "data": [ "apple", ["banana", "beer"] ], + "result": "beer" + }, + { + "description": "{\"and\":[{\"<\":[{\"var\":\"temp\"},110]},{\"==\":[{\"var\":\"pie.filling\"},\"apple\"]}]}", + "rule": { + "and": [ + { + "<": [ {"val": "temp"}, 110 ] + }, + { + "==": [ + { "val": ["pie", "filling"] }, + "apple" + ] + } + ] + }, + "data": { "temp": 100, "pie": {"filling": "apple"} }, + "result": true + }, + { + "description": "{\"var\":[{\"?:\":[{\"<\":[{\"var\":\"temp\"},110]},\"pie.filling\",\"pie.eta\"]}]}", + "rule": { + "val": { + "?:": [ + { "<": [ {"val": "temp"}, 110 ] }, + ["pie", "filling"], + ["pie", "eta"] + ] + } + }, + "data": { "temp": 100, "pie": {"filling": "apple", "eta": "60s"} }, + "result": "apple" + }, + { + "description": "{\"in\":[{\"var\":\"filling\"},[\"apple\",\"cherry\"]]}", + "rule": { + "in": [ {"val": "filling"}, ["apple", "cherry"] ] + }, + "data": {"filling": "apple"}, + "result": true + }, + { + "description": "{\"var\":\"a.b.c\"}", + "rule": { "val": ["a", "b", "c"] }, + "data": null, + "result": null + }, + { + "description": "{\"var\":\"a.b.c\"}", + "rule": { "val": ["a", "b", "c"] }, + "data": {"a": null}, + "result": null + }, + { + "description": "{\"var\":\"a.b.c\"}", + "rule": { "val": ["a", "b", "c"] }, + "data": { "a": {"b": null} }, + "result": null + }, + { "description": "{\"var\":\"\"}", "rule": {"val": []}, "data": 1, "result": 1 }, + { "description": "{\"val\":[]}", "rule": {"val": []}, "data": 1, "result": 1 }, + { + "description": "{\"missing\":{\"merge\":[\"vin\",{\"if\":[{\"var\":\"financing\"},[\"apr\"],[]]}]}}", + "rule": { + "missing": { + "merge": [ + "vin", + { + "if": [ {"val": "financing"}, ["apr"], [] ] + } + ] + } + }, + "data": {"financing": true}, + "result": ["vin", "apr"] + }, + { + "description": "{\"missing\":{\"merge\":[\"vin\",{\"if\":[{\"var\":\"financing\"},[\"apr\"],[]]}]}}", + "rule": { + "missing": { + "merge": [ + "vin", + { + "if": [ {"val": "financing"}, ["apr"], [] ] + } + ] + } + }, + "data": {"financing": false}, + "result": ["vin"] + }, + { + "description": "{\"filter\":[{\"var\":\"integers\"},true]}", + "rule": { + "filter": [ {"val": "integers"}, true ] + }, + "data": { "integers": [1, 2, 3] }, + "result": [1, 2, 3] + }, + { + "description": "{\"filter\":[{\"var\":\"integers\"},false]}", + "rule": { + "filter": [ {"val": "integers"}, false ] + }, + "data": { "integers": [1, 2, 3] }, + "result": [] + }, + { + "description": "{\"filter\":[{\"var\":\"integers\"},{\">=\":[{\"var\":\"\"},2]}]}", + "rule": { + "filter": [ + {"val": "integers"}, + { + ">=": [ {"val": []}, 2 ] + } + ] + }, + "data": { "integers": [1, 2, 3] }, + "result": [2, 3] + }, + { + "description": "{\"filter\":[{\"var\":\"integers\"},{\"%\":[{\"var\":\"\"},2]}]}", + "rule": { + "filter": [ + {"val": "integers"}, + { + "%": [ {"val": []}, 2 ] + } + ] + }, + "data": { "integers": [1, 2, 3] }, + "result": [1, 3] + }, + { + "description": "{\"map\":[{\"var\":\"integers\"},{\"*\":[{\"var\":\"\"},2]}]}", + "rule": { + "map": [ + {"val": "integers"}, + { + "*": [ {"val": []}, 2 ] + } + ] + }, + "data": { "integers": [1, 2, 3] }, + "result": [2, 4, 6] + }, + { + "description": "{\"map\":[{\"var\":\"integers\"},{\"*\":[{\"var\":\"\"},2]}]}", + "rule": { + "map": [ + {"val": "integers"}, + { + "*": [ {"val": []}, 2 ] + } + ] + }, + "data": null, + "result": [] + }, + { + "description": "{\"map\":[{\"var\":\"desserts\"},{\"var\":\"qty\"}]}", + "rule": { + "map": [ {"val": "desserts"}, {"val": "qty"} ] + }, + "data": { + "desserts": [ + {"name": "apple" , "qty": 1}, + {"name": "brownie", "qty": 2}, + {"name": "cupcake", "qty": 3} + ] + }, + "result": [1, 2, 3] + }, + { + "description": "{\"reduce\":[{\"var\":\"integers\"},{\"+\":[{\"var\":\"current\"},{\"var\":\"accumulator\"}]},0]}", + "rule": { + "reduce": [ + {"val": "integers"}, + { + "+": [ {"val": "current"}, {"val": "accumulator"} ] + }, + 0 + ] + }, + "data": { "integers": [1, 2, 3, 4] }, + "result": 10 + }, + { + "description": "{\"reduce\":[{\"var\":\"integers\"},{\"+\":[{\"var\":\"current\"},{\"var\":\"accumulator\"}]},{\"var\":\"start_with\"}]}", + "rule": { + "reduce": [ + {"val": "integers"}, + { + "+": [ {"val": "current"}, {"val": "accumulator"} ] + }, + {"val": "start_with"} + ] + }, + "data": { "integers": [1, 2, 3, 4], "start_with": 59 }, + "result": 69 + }, + { + "description": "{\"reduce\":[{\"var\":\"integers\"},{\"+\":[{\"var\":\"current\"},{\"var\":\"accumulator\"}]},0]}", + "rule": { + "reduce": [ + {"val": "integers"}, + { + "+": [ {"val": "current"}, {"val": "accumulator"} ] + }, + 0 + ] + }, + "data": null, + "result": 0 + }, + { + "description": "{\"reduce\":[{\"var\":\"integers\"},{\"*\":[{\"var\":\"current\"},{\"var\":\"accumulator\"}]},1]}", + "rule": { + "reduce": [ + {"val": "integers"}, + { + "*": [ {"val": "current"}, {"val": "accumulator"} ] + }, + 1 + ] + }, + "data": { "integers": [1, 2, 3, 4] }, + "result": 24 + }, + { + "description": "{\"reduce\":[{\"var\":\"integers\"},{\"*\":[{\"var\":\"current\"},{\"var\":\"accumulator\"}]},0]}", + "rule": { + "reduce": [ + {"val": "integers"}, + { + "*": [ {"val": "current"}, {"val": "accumulator"} ] + }, + 0 + ] + }, + "data": { "integers": [1, 2, 3, 4] }, + "result": 0 + }, + { + "description": "{\"reduce\":[{\"var\":\"desserts\"},{\"+\":[{\"var\":\"accumulator\"},{\"var\":\"current.qty\"}]},0]}", + "rule": { + "reduce": [ + {"val": "desserts"}, + { + "+": [ + { "val": "accumulator" }, + { "val": ["current", "qty"] } + ] + }, + 0 + ] + }, + "data": { + "desserts": [ + {"name": "apple" , "qty": 1}, + {"name": "brownie", "qty": 2}, + {"name": "cupcake", "qty": 3} + ] + }, + "result": 6 + }, + { + "description": "{\"all\":[{\"var\":\"integers\"},{\">=\":[{\"var\":\"\"},1]}]}", + "rule": { + "all": [ + {"val": "integers"}, + { + ">=": [ {"val": []}, 1 ] + } + ] + }, + "data": { "integers": [1, 2, 3] }, + "result": true + }, + { + "description": "{\"all\":[{\"var\":\"integers\"},{\"==\":[{\"var\":\"\"},1]}]}", + "rule": { + "all": [ + {"val": "integers"}, + { + "==": [ {"val": []}, 1 ] + } + ] + }, + "data": { "integers": [1, 2, 3] }, + "result": false + }, + { + "description": "{\"all\":[{\"var\":\"integers\"},{\"<\":[{\"var\":\"\"},1]}]}", + "rule": { + "all": [ + {"val": "integers"}, + { + "<": [ {"val": []}, 1 ] + } + ] + }, + "data": { "integers": [1, 2, 3] }, + "result": false + }, + { + "description": "{\"all\":[{\"var\":\"integers\"},{\"<\":[{\"var\":\"\"},1]}]}", + "rule": { + "all": [ + {"val": "integers"}, + { + "<": [ {"val": []}, 1 ] + } + ] + }, + "data": {"integers": []}, + "result": false + }, + { + "description": "{\"all\":[{\"var\":\"items\"},{\">=\":[{\"var\":\"qty\"},1]}]}", + "rule": { + "all": [ + {"val": "items"}, + { + ">=": [ {"val": "qty"}, 1 ] + } + ] + }, + "data": { + "items": [ {"qty": 1, "sku": "apple"}, {"qty": 2, "sku": "banana"} ] + }, + "result": true + }, + { + "description": "{\"all\":[{\"var\":\"items\"},{\">\":[{\"var\":\"qty\"},1]}]}", + "rule": { + "all": [ + {"val": "items"}, + { + ">": [ {"val": "qty"}, 1 ] + } + ] + }, + "data": { + "items": [ {"qty": 1, "sku": "apple"}, {"qty": 2, "sku": "banana"} ] + }, + "result": false + }, + { + "description": "{\"all\":[{\"var\":\"items\"},{\"<\":[{\"var\":\"qty\"},1]}]}", + "rule": { + "all": [ + {"val": "items"}, + { + "<": [ {"val": "qty"}, 1 ] + } + ] + }, + "data": { + "items": [ {"qty": 1, "sku": "apple"}, {"qty": 2, "sku": "banana"} ] + }, + "result": false + }, + { + "description": "{\"all\":[{\"var\":\"items\"},{\">=\":[{\"var\":\"qty\"},1]}]}", + "rule": { + "all": [ + {"val": "items"}, + { + ">=": [ {"val": "qty"}, 1 ] + } + ] + }, + "data": {"items": []}, + "result": false + }, + { + "description": "{\"none\":[{\"var\":\"integers\"},{\">=\":[{\"var\":\"\"},1]}]}", + "rule": { + "none": [ + {"val": "integers"}, + { + ">=": [ {"val": []}, 1 ] + } + ] + }, + "data": { "integers": [1, 2, 3] }, + "result": false + }, + { + "description": "{\"none\":[{\"var\":\"integers\"},{\"==\":[{\"var\":\"\"},1]}]}", + "rule": { + "none": [ + {"val": "integers"}, + { + "==": [ {"val": []}, 1 ] + } + ] + }, + "data": { "integers": [1, 2, 3] }, + "result": false + }, + { + "description": "{\"none\":[{\"var\":\"integers\"},{\"<\":[{\"var\":\"\"},1]}]}", + "rule": { + "none": [ + {"val": "integers"}, + { + "<": [ {"val": []}, 1 ] + } + ] + }, + "data": { "integers": [1, 2, 3] }, + "result": true + }, + { + "description": "{\"none\":[{\"var\":\"integers\"},{\"<\":[{\"var\":\"\"},1]}]}", + "rule": { + "none": [ + {"val": "integers"}, + { + "<": [ {"val": []}, 1 ] + } + ] + }, + "data": {"integers": []}, + "result": true + }, + { + "description": "{\"none\":[{\"var\":\"items\"},{\">=\":[{\"var\":\"qty\"},1]}]}", + "rule": { + "none": [ + {"val": "items"}, + { + ">=": [ {"val": "qty"}, 1 ] + } + ] + }, + "data": { + "items": [ {"qty": 1, "sku": "apple"}, {"qty": 2, "sku": "banana"} ] + }, + "result": false + }, + { + "description": "{\"none\":[{\"var\":\"items\"},{\">\":[{\"var\":\"qty\"},1]}]}", + "rule": { + "none": [ + {"val": "items"}, + { + ">": [ {"val": "qty"}, 1 ] + } + ] + }, + "data": { + "items": [ {"qty": 1, "sku": "apple"}, {"qty": 2, "sku": "banana"} ] + }, + "result": false + }, + { + "description": "{\"none\":[{\"var\":\"items\"},{\"<\":[{\"var\":\"qty\"},1]}]}", + "rule": { + "none": [ + {"val": "items"}, + { + "<": [ {"val": "qty"}, 1 ] + } + ] + }, + "data": { + "items": [ {"qty": 1, "sku": "apple"}, {"qty": 2, "sku": "banana"} ] + }, + "result": true + }, + { + "description": "{\"none\":[{\"var\":\"items\"},{\">=\":[{\"var\":\"qty\"},1]}]}", + "rule": { + "none": [ + {"val": "items"}, + { + ">=": [ {"val": "qty"}, 1 ] + } + ] + }, + "data": {"items": []}, + "result": true + }, + { + "description": "{\"some\":[{\"var\":\"integers\"},{\">=\":[{\"var\":\"\"},1]}]}", + "rule": { + "some": [ + {"val": "integers"}, + { + ">=": [ {"val": []}, 1 ] + } + ] + }, + "data": { "integers": [1, 2, 3] }, + "result": true + }, + { + "description": "{\"some\":[{\"var\":\"integers\"},{\"==\":[{\"var\":\"\"},1]}]}", + "rule": { + "some": [ + {"val": "integers"}, + { + "==": [ {"val": []}, 1 ] + } + ] + }, + "data": { "integers": [1, 2, 3] }, + "result": true + }, + { + "description": "{\"some\":[{\"var\":\"integers\"},{\"<\":[{\"var\":\"\"},1]}]}", + "rule": { + "some": [ + {"val": "integers"}, + { + "<": [ {"val": []}, 1 ] + } + ] + }, + "data": { "integers": [1, 2, 3] }, + "result": false + }, + { + "description": "{\"some\":[{\"var\":\"integers\"},{\"<\":[{\"var\":\"\"},1]}]}", + "rule": { + "some": [ + {"val": "integers"}, + { + "<": [ {"val": []}, 1 ] + } + ] + }, + "data": {"integers": []}, + "result": false + }, + { + "description": "{\"some\":[{\"var\":\"items\"},{\">=\":[{\"var\":\"qty\"},1]}]}", + "rule": { + "some": [ + {"val": "items"}, + { + ">=": [ {"val": "qty"}, 1 ] + } + ] + }, + "data": { + "items": [ {"qty": 1, "sku": "apple"}, {"qty": 2, "sku": "banana"} ] + }, + "result": true + }, + { + "description": "{\"some\":[{\"var\":\"items\"},{\">\":[{\"var\":\"qty\"},1]}]}", + "rule": { + "some": [ + {"val": "items"}, + { + ">": [ {"val": "qty"}, 1 ] + } + ] + }, + "data": { + "items": [ {"qty": 1, "sku": "apple"}, {"qty": 2, "sku": "banana"} ] + }, + "result": true + }, + { + "description": "{\"some\":[{\"var\":\"items\"},{\"<\":[{\"var\":\"qty\"},1]}]}", + "rule": { + "some": [ + {"val": "items"}, + { + "<": [ {"val": "qty"}, 1 ] + } + ] + }, + "data": { + "items": [ {"qty": 1, "sku": "apple"}, {"qty": 2, "sku": "banana"} ] + }, + "result": false + }, + { + "description": "{\"some\":[{\"var\":\"items\"},{\">=\":[{\"var\":\"qty\"},1]}]}", + "rule": { + "some": [ + {"val": "items"}, + { + ">=": [ {"val": "qty"}, 1 ] + } + ] + }, + "data": {"items": []}, + "result": false + } +] diff --git a/suites/val.json b/suites/val.json new file mode 100644 index 0000000..d52eb61 --- /dev/null +++ b/suites/val.json @@ -0,0 +1,102 @@ +[ + "Test Specification for val", + { + "description": "Fetches a value", + "rule": { "val": "hello" }, + "data": { "hello" : 0 }, + "result": 0 + }, + { + "description": "Fetches a nested value", + "rule": { "val": ["hello", "world"] }, + "data": { "hello" : { "world": 1 } }, + "result": 1 + }, + { + "description": "Fetches a value from an empty key", + "rule": { "val": "" }, + "data": { "" : 1 }, + "result": 1 + }, + { + "description": "Fetches a value from a nested empty key", + "rule": { "val": ["", ""] }, + "data": { "" : { "": 2 } }, + "result": 2 + }, + { + "description": "Fetches a value from an array", + "rule": { "val": [1] }, + "data": [1, 2], + "result": 2 + }, + { + "description": "Fetches a value from an array in an object", + "rule": { "val": ["arr", 1] }, + "data": { "arr": [1, 2] }, + "result": 2 + }, + { + "description": "Fetches a value from a doubly nested empty key", + "rule": { "val": ["", "", ""] }, + "data": { "" : { "": { "": 3 } } }, + "result": 3 + }, + { + "description": "Fetches a value from a key that is purely a dot", + "rule": { "val": "." }, + "data": { "." : 20 }, + "result": 20 + }, + { + "description": "Fetching a value from null returns null", + "rule": { "val": "hello" }, + "data": { "hello" : null }, + "result": null + }, + { + "description": "Fetching a value from a null fetched value returns null", + "rule": { "val": ["hello", "world"] }, + "data": { "hello" : null }, + "result": null + }, + { + "description": "Fetches the entire context", + "rule": { "val": [] }, + "data": { "": 21 }, + "result": { "": 21 } + }, + { + "description": "Fetches the entire context for a nested key", + "rule": { "val": "" }, + "data": { "": { "": 22 } }, + "result": { "": 22 } + }, + { + "description": "Using val in a map", + "rule": { "map": [[1,2,3], { "+": [{ "val": [] }, 1] }] }, + "data": null, + "result": [2,3,4] + }, + "Testing out scopes", + { + "description": "Climb up to get adder", + "rule": { "map": [[1,2,3], { "+": [{ "val": [] }, { "val": [[-2], "adder"] }] }] }, + "data": { "adder": 10 }, + "result": [11,12,13] + }, + { + "description": "Climb up to get index", + "rule": { "map": [[1,2,3], { "+": [{ "val": [] }, { "val": [[-1], "index"] }] }] }, + "data": { "adder": 10 }, + "result": [1,3,5] + }, + { + "description": "Nested get adder", + "rule": { + "map": [["Test"], { "map": [[1,2,3], { "+": [{"val": []}, {"val": [[-4], "adder"]}] }]} ] + }, + "data": { "adder": 10 }, + "result": [[11,12,13]] + } +] \ No newline at end of file diff --git a/suites/vars.json b/suites/vars.json deleted file mode 100644 index 06ba8be..0000000 --- a/suites/vars.json +++ /dev/null @@ -1,81 +0,0 @@ -[ - "Test Specification for Handling esoteric path traversal", - { - "description": "Fetches a value from an empty key", - "rule": { "var": "." }, - "data": { "" : 1 }, - "result": 1 - }, - { - "description": "Fetches a value from a nested empty key", - "rule": { "var": ".." }, - "data": { "" : { "": 2 } }, - "result": 2 - }, - { - "description": "Fetches a value from a doubly nested empty key", - "rule": { "var": "..." }, - "data": { "" : { "": { "": 3 } } }, - "result": 3 - }, - { - "description": "Fetches a value from a key that is purely a dot", - "rule": { "var": "\\." }, - "data": { "." : 20 }, - "result": 20 - }, - { - "description": "Fetches a value from a key with a dot in it", - "rule": { "var": "\\.key" }, - "data": { ".key" : 4 }, - "result": 4 - }, - { - "description":"Fetches a value from a key with a dot in it (2)", - "rule": { "var": "hello\\.world" }, - "data": { "hello.world" : 5 }, - "result": 5 - }, - { - "description": "Fetches a value from a key inside an empty key with a dot in it", - "rule": { "var": ".\\.key" }, - "data": { "": { ".key" : 6 } }, - "result": 6 - }, - { - "description": "Going a few levels deep", - "rule": { "var": "..\\.key." }, - "data": { "": { "": { ".key": { "": 7 }} }}, - "result": 7 - }, - { - "description": "Escape / as well, which is useful for the scope proposal", - "rule": { "var": "\\/" }, - "data": { "/" : 8 }, - "result": 8 - }, - { - "description": "Though / doesn't inherently need to be escaped", - "rule": { "var": "/" }, - "data": { "/" : 9 }, - "result": 9 - }, - { - "description": "Dot then empty key", - "rule": { "var": "\\.." }, - "data": { "." : { "" : 10 } }, - "result": 10 - }, - { - "description": "Empty key then dot", - "rule": { "var": ".\\." }, - "data": { "" : { "." : 11 } }, - "result": 11 - }, - { - "description": "Can use backslack in name, too", - "rule": { "var": "\\\\.Hello" }, - "data": { "\\" : { "Hello" : 12 } }, - "result": 12 - } -] \ No newline at end of file