Skip to content

Commit e930703

Browse files
committed
Add a shim and some type annotations to create a better drop-in replacement experience for json-logic-js
1 parent bcc0de7 commit e930703

File tree

5 files changed

+211
-16
lines changed

5 files changed

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

0 commit comments

Comments
 (0)