Skip to content

Add a json-logic-js shim and some type annotations to create a better drop-in experience #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions asyncLogic.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,11 @@ class AsyncLogicEngine {
*
* If it detects that a bunch of dynamic objects are being passed in, and it doesn't see the same object,
* it will disable the interpreted optimization.
*
* @template T
* @param {*} logic The logic to be executed
* @param {*} data The data being passed in to the logic to be executed against.
* @param {{ above?: any }} options Options for the invocation
* @returns {Promise}
* @returns {Promise<T>}
*/
async run (logic, data = {}, options = {}) {
const { above = [] } = options
Expand Down Expand Up @@ -176,6 +176,7 @@ class AsyncLogicEngine {
// Note: In the past, it used .map and Promise.all; this can be changed in the future
// if we want it to run concurrently.
for (let i = 0; i < logic.length; i++) res[i] = await this.run(logic[i], data, { above })
// @ts-expect-error Will figure out how to type this correctly soon from direct JS
return res
}

Expand All @@ -185,10 +186,10 @@ class AsyncLogicEngine {
}

/**
*
* @template T
* @param {*} logic The logic to be built.
* @param {{ top?: Boolean, above?: any }} options
* @returns {Promise<Function>}
* @returns {Promise<(...args: any[]) => Promise<T>>}
*/
async build (logic, options = {}) {
const { above = [], top = true } = options
Expand Down
22 changes: 15 additions & 7 deletions bench/test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { LogicEngine, AsyncLogicEngine } from '../index.js'
import jsShim from '../shim.js'
import fs from 'fs'
import { isDeepStrictEqual } from 'util'
import jl from 'json-logic-js'
import rust from '@bestow/jsonlogic-rs'
// import rust from '@bestow/jsonlogic-rs'

const x = new LogicEngine(undefined, { compatible: true })
const y = new AsyncLogicEngine(undefined, { compatible: true })
const x = new LogicEngine()
const y = new AsyncLogicEngine()

const compatible = []
const incompatible = []
Expand All @@ -16,7 +17,6 @@ JSON.parse(fs.readFileSync('./tests.json').toString()).forEach((test) => {
try {
if (!isDeepStrictEqual(x.run(test[0], test[1]), test[2])) {
incompatible.push(test)
// console.log(test[0])
} else {
compatible.push(test)
}
Expand Down Expand Up @@ -69,13 +69,21 @@ for (let j = 0; j < tests.length; j++) {
}
console.timeEnd('json-logic-js')

console.time('json-logic-rs')
console.time('json-logic-engine-shim')
for (let j = 0; j < tests.length; j++) {
for (let i = 0; i < 1e5; i++) {
rust.apply(tests[j][0], tests[j][1])
jsShim.apply(tests[j][0], tests[j][1])
}
}
console.timeEnd('json-logic-rs')
console.timeEnd('json-logic-engine-shim')

// console.time('json-logic-rs')
// for (let j = 0; j < tests.length; j++) {
// for (let i = 0; i < 1e5; i++) {
// rust.apply(tests[j][0], tests[j][1])
// }
// }
// console.timeEnd('json-logic-rs')

x.disableInterpretedOptimization = true
console.time('le interpreted')
Expand Down
4 changes: 3 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Constants from './constants.js'
import defaultMethods from './defaultMethods.js'
import { asLogicSync, asLogicAsync } from './asLogic.js'
import { splitPath, splitPathMemoized } from './utilities/splitPath.js'
import jsonLogic from './shim.js'

export { splitPath, splitPathMemoized }
export { LogicEngine }
Expand All @@ -17,5 +18,6 @@ export { Constants }
export { defaultMethods }
export { asLogicSync }
export { asLogicAsync }
export { jsonLogic }

export default { LogicEngine, AsyncLogicEngine, Compiler, Constants, defaultMethods, asLogicSync, asLogicAsync, splitPath, splitPathMemoized }
export default { LogicEngine, AsyncLogicEngine, Compiler, Constants, defaultMethods, asLogicSync, asLogicAsync, splitPath, splitPathMemoized, jsonLogic }
10 changes: 6 additions & 4 deletions logic.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,11 @@ class LogicEngine {
*
* If it detects that a bunch of dynamic objects are being passed in, and it doesn't see the same object,
* it will disable the interpreted optimization.
*
* @template T
* @param {*} logic The logic to be executed
* @param {*} data The data being passed in to the logic to be executed against.
* @param {{ above?: any }} options Options for the invocation
* @returns {*}
* @returns {T}
*/
run (logic, data = {}, options = {}) {
const { above = [] } = options
Expand All @@ -150,13 +150,15 @@ class LogicEngine {
if (Array.isArray(logic)) {
const res = new Array(logic.length)
for (let i = 0; i < logic.length; i++) res[i] = this.run(logic[i], data, { above })
// @ts-expect-error I'll figure out how to type this correctly soon from direct JS
return res
}

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

/**
*
* @template T
* @param {*} logic The logic to be built.
* @param {{ top?: Boolean, above?: any }} options
* @returns {Function}
* @returns {(...args: any[]) => T}
*/
build (logic, options = {}) {
const { above = [], top = true } = options
Expand Down
183 changes: 183 additions & 0 deletions shim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// @ts-check
// istanbul ignore file
'use strict'
// Old Interface for json-logic-js being mapped onto json-logic-engine
import LogicEngine from './logic.js'

/**
* This is a to allow json-logic-engine to easily be used as a drop-in replacement for json-logic-js.
*
* This shim does not expose all of the functionality of `json-logic-engine`, so it is recommended to switch over
* to using `json-logic-engine`'s API directly.
*
* The direct API is a bit more efficient and allows for logic compilation and other advanced features.
* @deprecated Please use `json-logic-engine` directly instead.
*/
const jsonLogic = {
_init () {
this.engine = new LogicEngine()

// We don't know if folks are modifying the AST directly, and because this is a widespread package,
// I'm going to assume some folks might be, so let's go with the safe assumption and disable the optimizer
this.engine.disableInterpretedOptimization = true
},
/**
* Applies the data to the logic and returns the result.
* @template T
* @returns {T}
* @deprecated Please use `json-logic-engine` directly instead.
*/
apply (logic, data) {
if (!this.engine) this._init()
return this.engine.run(logic, data)
},
/**
* Adds a new operation to the engine.
* Note that this is much less performant than adding methods to the engine the new way.
* Either consider switching to using LogicEngine directly, or accessing `jsonLogic.engine.addMethod` instead.
*
* Documentation for the new style is available at: https://json-logic.github.io/json-logic-engine/docs/methods
*
* @param {string} name
* @param {((...args: any[]) => any) | Record<string, (...args: any[]) => any> } code
* @deprecated Please use `json-logic-engine` directly instead.
* @returns
*/
add_operation (name, code) {
if (!this.engine) this._init()
if (typeof code === 'object') {
for (const key in code) {
const method = code[key]
this.engine.addMethod(name + '.' + key, (args, ctx) => method.apply(ctx, args))
}
return
}

this.engine.addMethod(name, (args, ctx) => code.apply(ctx, args))
},
/**
* Removes an operation from the engine
* @param {string} name
*/
rm_operation (name) {
if (!this.engine) this._init()
this.engine.methods[name] = undefined
},
/**
* Gets the first key from an object
* { >name<: values }
*/
get_operator (logic) {
return Object.keys(logic)[0]
},
/**
* Gets the values from the logic
* { name: >values< }
*/
get_values (logic) {
const func = Object.keys(jsonLogic)[0]
return logic[func]
},
/**
* Allows you to enable the optimizer which will accelerate your logic execution,
* as long as you don't mutate the logic after it's been run once.
*
* This will allow it to cache the execution plan for the logic, speeding things up.
*/
set_optimizer (val = true) {
if (!this.engine) this._init()
this.engine.disableInterpretedOptimization = val
},
/**
* Checks if a given object looks like a logic object.
*/
is_logic (logic) {
return typeof logic === 'object' && logic !== null && !Array.isArray(logic) && Object.keys(logic).length === 1
},
/**
* Checks for which variables are used in a given logic object.
*/
uses_data (logic) {
/** @type {Set<string>} */
const collection = new Set()

if (!jsonLogic.is_logic(logic)) {
const op = jsonLogic.get_operator(logic)
let values = logic[op]

if (!Array.isArray(values)) values = [values]

if (op === 'var') collection.add(values[0])
else if (op === 'val') collection.add(values.join('.'))
else {
// Recursion!
for (const val of values) {
const subCollection = jsonLogic.uses_data(val)
for (const item of subCollection) collection.add(item)
}
}
}

return Array.from(collection)
},
// Copied directly from json-logic-js
rule_like (rule, pattern) {
if (pattern === rule) {
return true
} // TODO : Deep object equivalency?
if (pattern === '@') {
return true
} // Wildcard!
if (pattern === 'number') {
return (typeof rule === 'number')
}
if (pattern === 'string') {
return (typeof rule === 'string')
}
if (pattern === 'array') {
// !logic test might be superfluous in JavaScript
return Array.isArray(rule) && !jsonLogic.is_logic(rule)
}

if (jsonLogic.is_logic(pattern)) {
if (jsonLogic.is_logic(rule)) {
const patternOp = jsonLogic.get_operator(pattern)
const ruleOp = jsonLogic.get_operator(rule)

if (patternOp === '@' || patternOp === ruleOp) {
// echo "\nOperators match, go deeper\n";
return jsonLogic.rule_like(
jsonLogic.get_values(rule),
jsonLogic.get_values(pattern)
)
}
}
return false // pattern is logic, rule isn't, can't be eq
}

if (Array.isArray(pattern)) {
if (Array.isArray(rule)) {
if (pattern.length !== rule.length) {
return false
}
/*
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)
*/
for (let i = 0; i < pattern.length; i += 1) {
// If any fail, we fail
if (!jsonLogic.rule_like(rule[i], pattern[i])) {
return false
}
}
return true // If they *all* passed, we pass
} else {
return false // Pattern is array, rule isn't
}
}

// Not logic, not array, not a === match for rule.
return false
}
}

export default jsonLogic
Loading