Skip to content

Commit 3c17cf8

Browse files
committed
2 parents c795d05 + bde09eb commit 3c17cf8

File tree

6 files changed

+149
-36
lines changed

6 files changed

+149
-36
lines changed

asyncLogic.js

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ class AsyncLogicEngine {
1919
/**
2020
*
2121
* @param {Object} methods An object that stores key-value pairs between the names of the commands & the functions they execute.
22-
* @param {{ yieldSupported?: Boolean, disableInline?: Boolean }} options
22+
* @param {{ yieldSupported?: Boolean, disableInline?: Boolean, permissive?: boolean }} options
2323
*/
2424
constructor (
2525
methods = defaultMethods,
26-
options = { yieldSupported: false, disableInline: false }
26+
options = { yieldSupported: false, disableInline: false, permissive: false }
2727
) {
2828
this.methods = { ...methods }
29+
/** @type {{yieldSupported?: Boolean, disableInline?: Boolean, permissive?: boolean}} */
2930
this.options = { ...options }
3031
this.disableInline = options.disableInline
3132
this.async = true
@@ -34,22 +35,22 @@ class AsyncLogicEngine {
3435

3536
/**
3637
* An internal method used to parse through the JSON Logic at a lower level.
37-
* @param {String} func The name of the function being executed
38-
* @param {*} data The data to traverse / execute upon
38+
* @param {*} logic The logic being executed.
3939
* @param {*} context The context of the logic being run (input to the function.)
4040
* @param {*} above The context above (can be used for handlebars-style data traversal.)
41-
* @returns {Promise}
41+
* @returns {Promise<{ func: string, result: * }>}
4242
*/
43-
async _parse (func, data, context, above) {
43+
async _parse (logic, context, above) {
44+
const [func] = Object.keys(logic)
45+
const data = logic[func]
4446
if (this.methods[func]) {
4547
if (typeof this.methods[func] === 'function') {
4648
const input = await this.run(data, context, { above })
4749
if (this.options.yieldSupported && (await checkYield(input))) {
48-
return input
50+
return { result: input, func }
4951
}
5052
const result = await this.methods[func](input, context, above, this)
51-
52-
return Array.isArray(result) ? Promise.all(result) : result
53+
return { result: Array.isArray(result) ? Promise.all(result) : result, func }
5354
}
5455

5556
if (typeof this.methods[func] === 'object') {
@@ -61,7 +62,7 @@ class AsyncLogicEngine {
6162
: data
6263

6364
if (this.options.yieldSupported && (await checkYield(parsedData))) {
64-
return parsedData
65+
return { result: parsedData, func }
6566
}
6667

6768
const result = await (asyncMethod || method)(
@@ -70,9 +71,11 @@ class AsyncLogicEngine {
7071
above,
7172
this
7273
)
73-
return Array.isArray(result) ? Promise.all(result) : result
74+
return { result: Array.isArray(result) ? Promise.all(result) : result, func }
7475
}
7576
}
77+
if (this.options.permissive) return { result: logic, func }
78+
throw new Error(`Method '${func}' was not found in the Logic Engine.`)
7679
}
7780

7881
/**
@@ -148,8 +151,7 @@ class AsyncLogicEngine {
148151
}
149152

150153
if (logic && typeof logic === 'object') {
151-
const [func] = Object.keys(logic)
152-
const result = await this._parse(func, logic[func], data, above)
154+
const { func, result } = await this._parse(logic, data, above)
153155

154156
if (this.options.yieldSupported && (await checkYield(result))) {
155157
if (result instanceof YieldStructure) {

compiler.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,13 @@ import asyncIterators from './async_iterators.js'
3636
* @param {*} x
3737
* @returns
3838
*/
39-
function isPrimitive (x) {
39+
function isPrimitive (x, preserveObject) {
4040
if (typeof x === 'number' && (x === Infinity || x === -Infinity || Number.isNaN(x))) return false
4141
return (
4242
x === null ||
4343
x === undefined ||
44-
['Number', 'String', 'Boolean', 'Object'].includes(x.constructor.name)
44+
['Number', 'String', 'Boolean'].includes(x.constructor.name) ||
45+
(!preserveObject && x.constructor.name === 'Object')
4546
)
4647
}
4748

@@ -245,8 +246,8 @@ function buildString (method, buildState = {}) {
245246
values = [],
246247
engine
247248
} = buildState
248-
function pushValue (value) {
249-
if (isPrimitive(value)) return JSON.stringify(value)
249+
function pushValue (value, preserveObject = false) {
250+
if (isPrimitive(value, preserveObject)) return JSON.stringify(value)
250251
values.push(value)
251252
return `values[${values.length - 1}]`
252253
}
@@ -271,7 +272,11 @@ function buildString (method, buildState = {}) {
271272
buildState.useContext || (engine.methods[func] || {}).useContext
272273

273274
if (method && typeof method === 'object') {
274-
if (!engine.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`)
275+
if (!engine.methods[func]) {
276+
// If we are in permissive mode, we will just return the object.
277+
if (engine.options.permissive) return pushValue(method, true)
278+
throw new Error(`Method '${func}' was not found in the Logic Engine.`)
279+
}
275280
functions[func] = functions[func] || 2
276281

277282
if (
@@ -280,7 +285,7 @@ function buildString (method, buildState = {}) {
280285
isDeterministic(method, engine, buildState)
281286
) {
282287
if (isDeepSync(method, engine)) {
283-
return pushValue((engine.fallback || engine).run(method))
288+
return pushValue((engine.fallback || engine).run(method), true)
284289
} else if (!buildState.avoidInlineAsync) {
285290
processing.push(engine.run(method).then((i) => pushValue(i)))
286291
return `__%%%${processing.length - 1}%%%__`

defaultMethods.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ function isDeterministic (method, engine, buildState) {
1616
if (method && typeof method === 'object') {
1717
const func = Object.keys(method)[0]
1818
const lower = method[func]
19+
20+
if (!engine.methods[func] && engine.options.permissive) return true
21+
1922
if (engine.methods[func].traverse === false) {
2023
return typeof engine.methods[func].deterministic === 'function'
2124
? engine.methods[func].deterministic(lower, buildState)

general.test.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
2+
import assert from 'assert'
3+
import { LogicEngine, AsyncLogicEngine } from './index.js'
4+
5+
const normalEngines = [
6+
new LogicEngine(),
7+
new AsyncLogicEngine(),
8+
new LogicEngine(undefined, { yieldSupported: true }),
9+
new AsyncLogicEngine(undefined, { yieldSupported: true })
10+
]
11+
12+
const permissiveEngines = [
13+
new LogicEngine(undefined, { permissive: true }),
14+
new AsyncLogicEngine(undefined, { permissive: true }),
15+
new LogicEngine(undefined, { yieldSupported: true, permissive: true }),
16+
new AsyncLogicEngine(undefined, { yieldSupported: true, permissive: true })
17+
]
18+
19+
async function testEngineAsync (engine, rule, data, expected, matcher = 'deepStrictEqual') {
20+
// run
21+
if (expected === Error) {
22+
try {
23+
await engine.run(rule, data)
24+
throw new Error('Should have failed')
25+
} catch (e) {}
26+
} else {
27+
const result = await engine.run(rule, data)
28+
assert[matcher](result, expected)
29+
}
30+
31+
// build
32+
if (expected === Error) {
33+
try {
34+
const built = await engine.build(rule)
35+
await built(data)
36+
throw new Error('Should have failed')
37+
} catch (e) {}
38+
} else {
39+
const built = await engine.build(rule)
40+
const builtResult = await built(data)
41+
assert[matcher](builtResult, expected)
42+
}
43+
}
44+
45+
function testEngine (engine, rule, data, expected, matcher = 'deepStrictEqual') {
46+
if (engine instanceof AsyncLogicEngine) {
47+
return testEngineAsync(engine, rule, data, expected, matcher)
48+
}
49+
50+
// run
51+
if (expected === Error) {
52+
try {
53+
engine.run(rule, data)
54+
throw new Error('Should have failed')
55+
} catch (e) {}
56+
} else {
57+
const result = engine.run(rule, data)
58+
assert[matcher](result, expected)
59+
}
60+
61+
// build
62+
if (expected === Error) {
63+
try {
64+
const built = engine.build(rule)
65+
built(data)
66+
throw new Error('Should have failed')
67+
} catch (e) {}
68+
} else {
69+
const built = engine.build(rule)
70+
const builtResult = built(data)
71+
assert[matcher](builtResult, expected)
72+
}
73+
}
74+
75+
describe('Various Test Cases', () => {
76+
it('Should fail when an unrecognized method is used.', async () => {
77+
for (const engine of normalEngines) await testEngine(engine, { unknown: true }, {}, Error)
78+
})
79+
80+
it('Should return the object when an unrecognized method is used.', async () => {
81+
for (const engine of permissiveEngines) {
82+
await testEngine(engine, { unknown: true }, {}, { unknown: true })
83+
84+
await testEngine(engine, {
85+
if: [true, { unknown: true, unknown2: 2 }, 5]
86+
}, {}, { unknown: true, unknown2: 2 })
87+
88+
const obj = { unknown: true, unknown2: 2 }
89+
90+
// test with deterministic function returning a passively preserved element.
91+
await testEngine(engine, {
92+
if: [true, obj, 5]
93+
}, {}, obj, 'equal')
94+
95+
// test with a non-deterministic function returning a passively preserved element.
96+
await testEngine(engine, {
97+
if: [{ var: 'data' }, obj, 5]
98+
}, {
99+
data: true
100+
}, obj, 'equal')
101+
}
102+
})
103+
})

logic.js

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,45 +16,46 @@ class LogicEngine {
1616
/**
1717
*
1818
* @param {Object} methods An object that stores key-value pairs between the names of the commands & the functions they execute.
19-
* @param {{ yieldSupported?: Boolean, disableInline?: Boolean }} options
19+
* @param {{ yieldSupported?: Boolean, disableInline?: Boolean, permissive?: boolean }} options
2020
*/
2121
constructor (
2222
methods = defaultMethods,
23-
options = { yieldSupported: false, disableInline: false }
23+
options = { yieldSupported: false, disableInline: false, permissive: false }
2424
) {
2525
this.disableInline = options.disableInline
2626
this.methods = { ...methods }
27+
/** @type {{yieldSupported?: Boolean, disableInline?: Boolean, permissive?: boolean}} */
2728
this.options = { ...options }
2829
}
2930

3031
/**
3132
* An internal method used to parse through the JSON Logic at a lower level.
32-
* @param {String} func The name of the function being executed
33-
* @param {*} data The data to traverse / execute upon
33+
* @param {*} logic The logic being executed.
3434
* @param {*} context The context of the logic being run (input to the function.)
3535
* @param {*} above The context above (can be used for handlebars-style data traversal.)
36-
* @returns {*}
36+
* @returns {{ result: *, func: string }}
3737
*/
38-
_parse (func, data, context, above) {
38+
_parse (logic, context, above) {
39+
const [func] = Object.keys(logic)
40+
const data = logic[func]
3941
if (this.methods[func]) {
4042
if (typeof this.methods[func] === 'function') {
4143
const input = this.run(data, context, { above })
42-
if (this.options.yieldSupported && checkYield(input)) return input
43-
return this.methods[func](input, context, above, this)
44+
if (this.options.yieldSupported && checkYield(input)) return { result: input, func }
45+
return { result: this.methods[func](input, context, above, this), func }
4446
}
4547
if (typeof this.methods[func] === 'object') {
4648
const { method, traverse } = this.methods[func]
47-
const shouldTraverse =
48-
typeof traverse === 'undefined' ? true : traverse
49+
const shouldTraverse = typeof traverse === 'undefined' ? true : traverse
4950
const parsedData = shouldTraverse
5051
? this.run(data, context, { above })
5152
: data
52-
if (this.options.yieldSupported && checkYield(parsedData)) {
53-
return parsedData
54-
}
55-
return method(parsedData, context, above, this)
53+
if (this.options.yieldSupported && checkYield(parsedData)) return { result: parsedData, func }
54+
return { result: method(parsedData, context, above, this), func }
5655
}
5756
}
57+
if (this.options.permissive) return { result: logic, func }
58+
throw new Error(`Method '${func}' was not found in the Logic Engine.`)
5859
}
5960

6061
/**
@@ -111,8 +112,7 @@ class LogicEngine {
111112
return result
112113
}
113114
if (logic && typeof logic === 'object') {
114-
const [func] = Object.keys(logic)
115-
const result = this._parse(func, logic[func], data, above)
115+
const { func, result } = this._parse(logic, data, above)
116116
if (this.options.yieldSupported && checkYield(result)) {
117117
if (result instanceof YieldStructure) {
118118
if (result._input) {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-logic-engine",
3-
"version": "1.2.0",
3+
"version": "1.2.3",
44
"description": "Construct complex rules with JSON & process them.",
55
"main": "./dist/cjs/index.js",
66
"module": "./dist/esm/index.js",

0 commit comments

Comments
 (0)