Skip to content

Commit 220d13b

Browse files
Merge 71098b8 into bcc0de7
2 parents bcc0de7 + 71098b8 commit 220d13b

File tree

5 files changed

+212
-16
lines changed

5 files changed

+212
-16
lines changed

asyncLogic.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,11 @@ class AsyncLogicEngine {
144144
*
145145
* If it detects that a bunch of dynamic objects are being passed in, and it doesn't see the same object,
146146
* it will disable the interpreted optimization.
147-
*
147+
* @template T
148148
* @param {*} logic The logic to be executed
149149
* @param {*} data The data being passed in to the logic to be executed against.
150150
* @param {{ above?: any }} options Options for the invocation
151-
* @returns {Promise}
151+
* @returns {Promise<T>}
152152
*/
153153
async run (logic, data = {}, options = {}) {
154154
const { above = [] } = options
@@ -176,6 +176,7 @@ class AsyncLogicEngine {
176176
// Note: In the past, it used .map and Promise.all; this can be changed in the future
177177
// if we want it to run concurrently.
178178
for (let i = 0; i < logic.length; i++) res[i] = await this.run(logic[i], data, { above })
179+
// @ts-expect-error Will figure out how to type this correctly soon from direct JS
179180
return res
180181
}
181182

@@ -185,10 +186,10 @@ class AsyncLogicEngine {
185186
}
186187

187188
/**
188-
*
189+
* @template T
189190
* @param {*} logic The logic to be built.
190191
* @param {{ top?: Boolean, above?: any }} options
191-
* @returns {Promise<Function>}
192+
* @returns {Promise<(...args: any[]) => Promise<T>>}
192193
*/
193194
async build (logic, options = {}) {
194195
const { above = [], top = true } = options

bench/test.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { LogicEngine, AsyncLogicEngine } from '../index.js'
2+
import jsShim from '../shim.js'
23
import fs from 'fs'
34
import { isDeepStrictEqual } from 'util'
45
import jl from 'json-logic-js'
5-
import rust from '@bestow/jsonlogic-rs'
6+
// import rust from '@bestow/jsonlogic-rs'
67

7-
const x = new LogicEngine(undefined, { compatible: true })
8-
const y = new AsyncLogicEngine(undefined, { compatible: true })
8+
const x = new LogicEngine()
9+
const y = new AsyncLogicEngine()
910

1011
const compatible = []
1112
const incompatible = []
@@ -16,7 +17,6 @@ JSON.parse(fs.readFileSync('./tests.json').toString()).forEach((test) => {
1617
try {
1718
if (!isDeepStrictEqual(x.run(test[0], test[1]), test[2])) {
1819
incompatible.push(test)
19-
// console.log(test[0])
2020
} else {
2121
compatible.push(test)
2222
}
@@ -69,13 +69,21 @@ for (let j = 0; j < tests.length; j++) {
6969
}
7070
console.timeEnd('json-logic-js')
7171

72-
console.time('json-logic-rs')
72+
console.time('json-logic-engine-shim')
7373
for (let j = 0; j < tests.length; j++) {
7474
for (let i = 0; i < 1e5; i++) {
75-
rust.apply(tests[j][0], tests[j][1])
75+
jsShim.apply(tests[j][0], tests[j][1])
7676
}
7777
}
78-
console.timeEnd('json-logic-rs')
78+
console.timeEnd('json-logic-engine-shim')
79+
80+
// console.time('json-logic-rs')
81+
// for (let j = 0; j < tests.length; j++) {
82+
// for (let i = 0; i < 1e5; i++) {
83+
// rust.apply(tests[j][0], tests[j][1])
84+
// }
85+
// }
86+
// console.timeEnd('json-logic-rs')
7987

8088
x.disableInterpretedOptimization = true
8189
console.time('le interpreted')

index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Constants from './constants.js'
88
import defaultMethods from './defaultMethods.js'
99
import { asLogicSync, asLogicAsync } from './asLogic.js'
1010
import { splitPath, splitPathMemoized } from './utilities/splitPath.js'
11+
import jsonLogic from './shim.js'
1112

1213
export { splitPath, splitPathMemoized }
1314
export { LogicEngine }
@@ -17,5 +18,6 @@ export { Constants }
1718
export { defaultMethods }
1819
export { asLogicSync }
1920
export { asLogicAsync }
21+
export { jsonLogic }
2022

21-
export default { LogicEngine, AsyncLogicEngine, Compiler, Constants, defaultMethods, asLogicSync, asLogicAsync, splitPath, splitPathMemoized }
23+
export default { LogicEngine, AsyncLogicEngine, Compiler, Constants, defaultMethods, asLogicSync, asLogicAsync, splitPath, splitPathMemoized, jsonLogic }

logic.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,11 @@ class LogicEngine {
120120
*
121121
* If it detects that a bunch of dynamic objects are being passed in, and it doesn't see the same object,
122122
* it will disable the interpreted optimization.
123-
*
123+
* @template T
124124
* @param {*} logic The logic to be executed
125125
* @param {*} data The data being passed in to the logic to be executed against.
126126
* @param {{ above?: any }} options Options for the invocation
127-
* @returns {*}
127+
* @returns {T}
128128
*/
129129
run (logic, data = {}, options = {}) {
130130
const { above = [] } = options
@@ -150,13 +150,15 @@ class LogicEngine {
150150
if (Array.isArray(logic)) {
151151
const res = new Array(logic.length)
152152
for (let i = 0; i < logic.length; i++) res[i] = this.run(logic[i], data, { above })
153+
// @ts-expect-error I'll figure out how to type this correctly soon from direct JS
153154
return res
154155
}
155156

156157
if (logic && typeof logic === 'object') {
157158
const keys = Object.keys(logic)
158159
if (keys.length > 0) {
159160
const func = keys[0]
161+
// @ts-expect-error I'll figure out how to type this correctly soon from direct JS
160162
return this._parse(logic, data, above, func)
161163
}
162164
}
@@ -165,10 +167,10 @@ class LogicEngine {
165167
}
166168

167169
/**
168-
*
170+
* @template T
169171
* @param {*} logic The logic to be built.
170172
* @param {{ top?: Boolean, above?: any }} options
171-
* @returns {Function}
173+
* @returns {(...args: any[]) => T}
172174
*/
173175
build (logic, options = {}) {
174176
const { above = [], top = true } = options

shim.js

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// @ts-check
2+
// istanbul ignore file
3+
'use strict'
4+
// Old Interface for json-logic-js being mapped onto json-logic-engine
5+
import LogicEngine from './logic.js'
6+
7+
/**
8+
* This is a to allow json-logic-engine to easily be used as a drop-in replacement for json-logic-js.
9+
*
10+
* This shim does not expose all of the functionality of `json-logic-engine`, so it is recommended to switch over
11+
* to using `json-logic-engine`'s API directly.
12+
*
13+
* The direct API is a bit more efficient and allows for logic compilation and other advanced features.
14+
* @deprecated Please use `json-logic-engine` directly instead.
15+
*/
16+
const jsonLogic = {
17+
_init () {
18+
this.engine = new LogicEngine()
19+
20+
// We don't know if folks are modifying the AST directly, and because this is a widespread package,
21+
// I'm going to assume some folks might be, so let's go with the safe assumption and disable the optimizer
22+
this.engine.disableInterpretedOptimization = true
23+
},
24+
/**
25+
* Applies the data to the logic and returns the result.
26+
* @template T
27+
* @returns {T}
28+
* @deprecated Please use `json-logic-engine` directly instead.
29+
*/
30+
apply (logic, data) {
31+
if (!this.engine) this._init()
32+
return this.engine.run(logic, data)
33+
},
34+
/**
35+
* Adds a new operation to the engine.
36+
* Note that this is much less performant than adding methods to the engine the new way.
37+
* Either consider switching to using LogicEngine directly, or accessing `jsonLogic.engine.addMethod` instead.
38+
*
39+
* Documentation for the new style is available at: https://json-logic.github.io/json-logic-engine/docs/methods
40+
*
41+
* @param {string} name
42+
* @param {((...args: any[]) => any) | Record<string, (...args: any[]) => any> } code
43+
* @deprecated Please use `json-logic-engine` directly instead.
44+
* @returns
45+
*/
46+
add_operation (name, code) {
47+
if (!this.engine) this._init()
48+
if (typeof code === 'object') {
49+
for (const key in code) {
50+
const method = code[key]
51+
this.engine.addMethod(name + '.' + key, (args, ctx) => method.apply(ctx, args))
52+
}
53+
return
54+
}
55+
56+
this.engine.addMethod(name, (args, ctx) => code.apply(ctx, args))
57+
},
58+
/**
59+
* Removes an operation from the engine
60+
* @param {string} name
61+
*/
62+
rm_operation (name) {
63+
if (!this.engine) this._init()
64+
this.engine.methods[name] = undefined
65+
},
66+
/**
67+
* Gets the first key from an object
68+
* { >name<: values }
69+
*/
70+
get_operator (logic) {
71+
return Object.keys(logic)[0]
72+
},
73+
/**
74+
* Gets the values from the logic
75+
* { name: >values< }
76+
*/
77+
get_values (logic) {
78+
const func = Object.keys(jsonLogic)[0]
79+
return logic[func]
80+
},
81+
/**
82+
* Allows you to enable the optimizer which will accelerate your logic execution,
83+
* as long as you don't mutate the logic after it's been run once.
84+
*
85+
* This will allow it to cache the execution plan for the logic, speeding things up.
86+
*/
87+
set_optimizer (val = true) {
88+
if (!this.engine) this._init()
89+
this.engine.disableInterpretedOptimization = val
90+
},
91+
/**
92+
* Checks if a given object looks like a logic object.
93+
*/
94+
is_logic (logic) {
95+
return typeof logic === 'object' && logic !== null && !Array.isArray(logic) && Object.keys(logic).length === 1
96+
},
97+
/**
98+
* Checks for which variables are used in a given logic object.
99+
*/
100+
uses_data (logic) {
101+
/** @type {Set<string>} */
102+
const collection = new Set()
103+
104+
if (!jsonLogic.is_logic(logic)) {
105+
const op = jsonLogic.get_operator(logic)
106+
let values = logic[op]
107+
108+
if (!Array.isArray(values)) values = [values]
109+
110+
if (op === 'var') collection.add(values[0])
111+
else if (op === 'val') collection.add(values.join('.'))
112+
else {
113+
// Recursion!
114+
for (const val of values) {
115+
const subCollection = jsonLogic.uses_data(val)
116+
for (const item of subCollection) collection.add(item)
117+
}
118+
}
119+
}
120+
121+
return Array.from(collection)
122+
},
123+
// Copied directly from json-logic-js
124+
rule_like (rule, pattern) {
125+
if (pattern === rule) {
126+
return true
127+
} // TODO : Deep object equivalency?
128+
if (pattern === '@') {
129+
return true
130+
} // Wildcard!
131+
if (pattern === 'number') {
132+
return (typeof rule === 'number')
133+
}
134+
if (pattern === 'string') {
135+
return (typeof rule === 'string')
136+
}
137+
if (pattern === 'array') {
138+
// !logic test might be superfluous in JavaScript
139+
return Array.isArray(rule) && !jsonLogic.is_logic(rule)
140+
}
141+
142+
if (jsonLogic.is_logic(pattern)) {
143+
if (jsonLogic.is_logic(rule)) {
144+
const patternOp = jsonLogic.get_operator(pattern)
145+
const ruleOp = jsonLogic.get_operator(rule)
146+
147+
if (patternOp === '@' || patternOp === ruleOp) {
148+
// echo "\nOperators match, go deeper\n";
149+
return jsonLogic.rule_like(
150+
jsonLogic.get_values(rule),
151+
jsonLogic.get_values(pattern)
152+
)
153+
}
154+
}
155+
return false // pattern is logic, rule isn't, can't be eq
156+
}
157+
158+
if (Array.isArray(pattern)) {
159+
if (Array.isArray(rule)) {
160+
if (pattern.length !== rule.length) {
161+
return false
162+
}
163+
/*
164+
Note, array order MATTERS, because we're using this array test logic to consider arguments, where order can matter. (e.g., + is commutative, but '-' or 'if' or 'var' are NOT)
165+
*/
166+
for (let i = 0; i < pattern.length; i += 1) {
167+
// If any fail, we fail
168+
if (!jsonLogic.rule_like(rule[i], pattern[i])) {
169+
return false
170+
}
171+
}
172+
return true // If they *all* passed, we pass
173+
} else {
174+
return false // Pattern is array, rule isn't
175+
}
176+
}
177+
178+
// Not logic, not array, not a === match for rule.
179+
return false
180+
}
181+
}
182+
183+
export default jsonLogic

0 commit comments

Comments
 (0)