From d06312412ff513757f8bea15329347899c71d555 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Tue, 19 Mar 2024 10:29:36 -0500 Subject: [PATCH 1/2] Changes how permissive mode is implemented, allows override of data detection --- asyncLogic.js | 68 +++++++++++++++++++++++++------------------ compiler.js | 7 +++-- customEngines.test.js | 45 ++++++++++++++++++++++++++++ defaultMethods.js | 3 +- logic.js | 46 +++++++++++++++++------------ package.json | 2 +- 6 files changed, 119 insertions(+), 52 deletions(-) create mode 100644 customEngines.test.js diff --git a/asyncLogic.js b/asyncLogic.js index 976a9a4..bac8da3 100644 --- a/asyncLogic.js +++ b/asyncLogic.js @@ -26,11 +26,18 @@ class AsyncLogicEngine { options = { yieldSupported: false, disableInline: false, permissive: false } ) { this.methods = { ...methods } - /** @type {{yieldSupported?: Boolean, disableInline?: Boolean, permissive?: boolean}} */ - this.options = { ...options } + /** @type {{yieldSupported?: Boolean, disableInline?: Boolean }} */ + this.options = { yieldSupported: options.yieldSupported, disableInline: options.disableInline } this.disableInline = options.disableInline this.async = true this.fallback = new LogicEngine(methods, options) + + if (!this.isData) { + if (!options.permissive) this.isData = () => false + else this.isData = (data, key) => !(key in this.methods) + } + + this.fallback.isData = this.isData } /** @@ -43,39 +50,42 @@ class AsyncLogicEngine { async _parse (logic, context, above) { const [func] = Object.keys(logic) const data = logic[func] - if (this.methods[func]) { - if (typeof this.methods[func] === 'function') { - const input = await this.run(data, context, { above }) - if (this.options.yieldSupported && (await checkYield(input))) { - return { result: input, func } - } - const result = await this.methods[func](input, context, above, this) - return { result: Array.isArray(result) ? Promise.all(result) : result, func } + + if (this.isData(logic, func)) return { result: logic, func } + + if (!this.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`) + + if (typeof this.methods[func] === 'function') { + const input = await this.run(data, context, { above }) + if (this.options.yieldSupported && (await checkYield(input))) { + return { result: input, func } } + const result = await this.methods[func](input, context, above, this) + return { result: Array.isArray(result) ? Promise.all(result) : result, func } + } - if (typeof this.methods[func] === 'object') { - const { asyncMethod, method, traverse } = this.methods[func] - const shouldTraverse = + if (typeof this.methods[func] === 'object') { + const { asyncMethod, method, traverse } = this.methods[func] + const shouldTraverse = typeof traverse === 'undefined' ? true : traverse - const parsedData = shouldTraverse - ? await this.run(data, context, { above }) - : data + const parsedData = shouldTraverse + ? await this.run(data, context, { above }) + : data - if (this.options.yieldSupported && (await checkYield(parsedData))) { - return { result: parsedData, func } - } - - const result = await (asyncMethod || method)( - parsedData, - context, - above, - this - ) - return { result: Array.isArray(result) ? Promise.all(result) : result, func } + if (this.options.yieldSupported && (await checkYield(parsedData))) { + return { result: parsedData, func } } + + const result = await (asyncMethod || method)( + parsedData, + context, + above, + this + ) + return { result: Array.isArray(result) ? Promise.all(result) : result, func } } - if (this.options.permissive) return { result: logic, func } - throw new Error(`Method '${func}' was not found in the Logic Engine.`) + + throw new Error(`Method '${func}' is not set up properly.`) } /** diff --git a/compiler.js b/compiler.js index 7a6bf99..7a3f92d 100644 --- a/compiler.js +++ b/compiler.js @@ -62,6 +62,9 @@ function isDeterministic (method, engine, buildState) { const func = Object.keys(method)[0] const lower = method[func] + if (engine.isData(method, func)) return true + if (!engine.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`) + if (engine.methods[func].traverse === false) { return typeof engine.methods[func].deterministic === 'function' ? engine.methods[func].deterministic(lower, buildState) @@ -274,8 +277,8 @@ function buildString (method, buildState = {}) { if (method && typeof method === 'object') { if (!func) return pushValue(method) if (!engine.methods[func]) { - // If we are in permissive mode, we will just return the object. - if (engine.options.permissive) return pushValue(method, true) + // Check if this is supposed to be "data" rather than a function. + if (engine.isData(method, func)) return pushValue(method, true) throw new Error(`Method '${func}' was not found in the Logic Engine.`) } functions[func] = functions[func] || 2 diff --git a/customEngines.test.js b/customEngines.test.js new file mode 100644 index 0000000..49f4d30 --- /dev/null +++ b/customEngines.test.js @@ -0,0 +1,45 @@ + +import { LogicEngine, AsyncLogicEngine } from './index.js' + +class DataEngine extends LogicEngine { + isData (logic, firstKey) { + if (Object.keys(logic).length > 1) return true + return !(firstKey in logic) + } +} + +class AsyncDataEngine extends AsyncLogicEngine { + isData (logic, firstKey) { + if (Object.keys(logic).length > 1) return true + return !(firstKey in logic) + } +} + +const engine = new DataEngine() +const asyncEngine = new AsyncDataEngine() + +describe('Custom Engines (isData)', () => { + const logic = { + get: [{ + xs: 10, + s: 20, + m: 30 + }, { + var: 'size' + }] + } + + const data = { + size: 's' + } + + it('Should let us override how data is detected (sync)', () => { + const f = engine.build(logic) + expect(f(data)).toEqual(20) + }) + + it('Should let us override how data is detected (async)', async () => { + const f = await asyncEngine.build(logic) + expect(await f(data)).toEqual(20) + }) +}) diff --git a/defaultMethods.js b/defaultMethods.js index 842ae80..cf24216 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -18,7 +18,8 @@ function isDeterministic (method, engine, buildState) { const func = Object.keys(method)[0] const lower = method[func] - if (!engine.methods[func] && engine.options.permissive) return true + if (engine.isData(method, func)) return true + if (!engine.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`) if (engine.methods[func].traverse === false) { return typeof engine.methods[func].deterministic === 'function' diff --git a/logic.js b/logic.js index 320ff83..296292e 100644 --- a/logic.js +++ b/logic.js @@ -24,8 +24,12 @@ class LogicEngine { ) { this.disableInline = options.disableInline this.methods = { ...methods } - /** @type {{yieldSupported?: Boolean, disableInline?: Boolean, permissive?: boolean}} */ - this.options = { ...options } + /** @type {{yieldSupported?: Boolean, disableInline?: Boolean }} */ + this.options = { yieldSupported: options.yieldSupported, disableInline: options.disableInline } + if (!this.isData) { + if (!options.permissive) this.isData = () => false + else this.isData = (data, key) => !(key in this.methods) + } } /** @@ -38,24 +42,28 @@ class LogicEngine { _parse (logic, context, above) { const [func] = Object.keys(logic) const data = logic[func] - if (this.methods[func]) { - if (typeof this.methods[func] === 'function') { - const input = this.run(data, context, { above }) - if (this.options.yieldSupported && checkYield(input)) return { result: input, func } - return { result: this.methods[func](input, context, above, this), func } - } - if (typeof this.methods[func] === 'object') { - const { method, traverse } = this.methods[func] - const shouldTraverse = typeof traverse === 'undefined' ? true : traverse - const parsedData = shouldTraverse - ? this.run(data, context, { above }) - : data - if (this.options.yieldSupported && checkYield(parsedData)) return { result: parsedData, func } - return { result: method(parsedData, context, above, this), func } - } + + if (this.isData(logic, func)) return { result: logic, func } + + if (!this.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`) + + if (typeof this.methods[func] === 'function') { + const input = this.run(data, context, { above }) + if (this.options.yieldSupported && checkYield(input)) return { result: input, func } + return { result: this.methods[func](input, context, above, this), func } + } + + if (typeof this.methods[func] === 'object') { + const { method, traverse } = this.methods[func] + const shouldTraverse = typeof traverse === 'undefined' ? true : traverse + const parsedData = shouldTraverse + ? this.run(data, context, { above }) + : data + if (this.options.yieldSupported && checkYield(parsedData)) return { result: parsedData, func } + return { result: method(parsedData, context, above, this), func } } - if (this.options.permissive) return { result: logic, func } - throw new Error(`Method '${func}' was not found in the Logic Engine.`) + + throw new Error(`Method '${func}' is not set up properly.`) } /** diff --git a/package.json b/package.json index ef3bf8f..d3ccb2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "json-logic-engine", - "version": "1.3.1", + "version": "1.3.2", "description": "Construct complex rules with JSON & process them.", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", From 77343ab6567211d06f8a7b3ea52d199899a0c716 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Tue, 19 Mar 2024 10:38:28 -0500 Subject: [PATCH 2/2] Fix silly typo --- customEngines.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/customEngines.test.js b/customEngines.test.js index 49f4d30..d44d158 100644 --- a/customEngines.test.js +++ b/customEngines.test.js @@ -4,14 +4,14 @@ import { LogicEngine, AsyncLogicEngine } from './index.js' class DataEngine extends LogicEngine { isData (logic, firstKey) { if (Object.keys(logic).length > 1) return true - return !(firstKey in logic) + return !(firstKey in this.methods) } } class AsyncDataEngine extends AsyncLogicEngine { isData (logic, firstKey) { if (Object.keys(logic).length > 1) return true - return !(firstKey in logic) + return !(firstKey in this.methods) } }