├── .eslintrc.json ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── Readme.md ├── asLogic.js ├── asLogic.test.js ├── async.test.js ├── asyncLogic.js ├── async_iterators.js ├── async_optimizer.js ├── bench ├── format-tests.js ├── package-lock.json ├── package.json ├── rules.js ├── test.js └── tests.json ├── build.test.js ├── compatible.test.js ├── compiler.js ├── constants.js ├── customEngines.test.js ├── defaultMethods.js ├── general.test.js ├── index.js ├── legacy.js ├── logic.js ├── modules.test.js ├── optimizer.js ├── package.json ├── perf.js ├── perf2.js ├── perf3.js ├── perf4.js ├── suites ├── additional.json ├── arithmetic │ ├── divide.extra.json │ ├── divide.json │ ├── max.json │ ├── min.json │ ├── minus.extra.json │ ├── minus.json │ ├── modulo.extra.json │ ├── modulo.json │ ├── multiply.extra.json │ ├── multiply.json │ ├── plus.extra.json │ └── plus.json ├── chained.json ├── coalesce.json ├── comparison │ ├── greaterThan.json │ ├── greaterThanEquals.json │ ├── lessThan.json │ ├── lessThanEquals.json │ ├── softEquals.json │ ├── softNotEquals.json │ ├── strictEquals.json │ └── strictNotEquals.json ├── compatible.json ├── control │ ├── and.json │ ├── if.json │ └── or.json ├── empty-objects.json ├── exists.json ├── iterators.extra.json ├── length.json ├── preserve.json ├── scopes.json ├── throw.json ├── truthiness.json ├── try.extra.json ├── try.json ├── unknown-operators.json ├── val-compat.json ├── val.extra.json └── val.json ├── test.js ├── utilities ├── chainingSupported.js ├── coerceArray.js ├── countArguments.js ├── declareSync.js ├── downgrade.js ├── omitUndefined.js ├── splitPath.js └── traverseCopy.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2020": true, 6 | "jest": true 7 | }, 8 | "extends": [ 9 | "standard" 10 | ], 11 | "parserOptions": { 12 | "ecmaVersion": 11 13 | }, 14 | "rules": { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x, 16.x, 18.x, 20.x, 22.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'yarn' 29 | - run: yarn 30 | - run: yarn test 31 | - name: Coveralls 32 | uses: coverallsapp/github-action@master 33 | with: 34 | github-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | experiments/ 2 | node_modules/ 3 | .nyc_output 4 | coverage 5 | .vscode 6 | .idea 7 | .npmrc 8 | bench/node_modules/ 9 | dist/ 10 | *.d.ts 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .nyc_output 3 | coverage 4 | .vscode 5 | .npmrc 6 | bench/node_modules/ 7 | bench/ 8 | .github/ 9 | .eslintrc.json 10 | perf*.js 11 | *.test.js 12 | experiments/ 13 | suites/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jesse Mitchell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # JSON Logic Engine 2 | 3 | [![JavaScript Style Guide](https://cdn.rawgit.com/standard/standard/master/badge.svg)](https://github.com/standard/standard) 4 | 5 | [![npm version](https://badge.fury.io/js/json-logic-engine.svg)](https://badge.fury.io/js/json-logic-engine) [![Coverage Status](https://coveralls.io/repos/github/TotalTechGeek/json-logic-engine/badge.svg?branch=master)](https://coveralls.io/github/TotalTechGeek/json-logic-engine?branch=master) [![Build Status](https://travis-ci.com/TotalTechGeek/json-logic-engine.svg?branch=master)](https://travis-ci.com/TotalTechGeek/json-logic-engine) 6 | 7 | 8 | ![Logo](https://raw.githubusercontent.com/gist/TotalTechGeek/22d699b6d7cb0f7fa1c37fdb0c427e60/raw/63bd743ce7720b7337ac30ae09cbb1b8e12f3a5b/json-logic-engine.svg) 9 | 10 | 11 | ### Fast, Powerful, and Persistable Logic 12 | 13 | Have you ever needed the ability to write a custom set of logic or set of rules for a particular customer? Or needed to be able to configure a piece of logic on the fly? 14 | 15 | JSON Logic might be your solution! Designed with a lisp-like syntax, JSON Logic makes it easy to write safe instructions that can be persisted into a database, and shared between the front-end and back-end. 16 | 17 | Check out our [Documentation Here](https://json-logic.github.io/json-logic-engine/). 18 | 19 | 20 | The engine supports both synchronous & asynchronous operations, and can use function compilation to keep your logic performant at scale. 21 | 22 | Examples: 23 | 24 | The premise is the logic engine traverses the document you pass in, and each "object" is interpreted as an instruction for the engine to run. 25 | 26 | ```js 27 | logic.run({ 28 | '+': [1,2,3,4,5] 29 | }) // 15 30 | ``` 31 | 32 | If you wanted to start factoring variables, you can pass a data object into it, and reference them using the "var" instruction: 33 | 34 | ```js 35 | logic.run({ 36 | '+': [11, { var: 'a' }] 37 | }, { 38 | 'a': 17 39 | }) // 28 40 | ``` 41 | 42 | The engine will also allow you to reference variables that are several layers deep: 43 | 44 | ```js 45 | logic.run({ 46 | '+': [{ var: 'a.b.c' }, 5] 47 | }, { 48 | a: { b: { c: 7 } } 49 | }) // 12 50 | ``` 51 | 52 | Let's explore some slightly more complex logic: 53 | 54 | ```js 55 | logic.run({ 56 | 'reduce': [{ var: 'x' }, { '+': [{ var: 'current' }, { var: 'accumulator' }] }, 0] 57 | }, { 58 | 'x': [1,2,3,4,5] 59 | }) // 15 60 | ``` 61 | 62 | In this example, we run the reduce operation on a variable called "x", and we set up instructions to add the "current" value to the "accumulator", which we have set to 0. 63 | 64 | Similarly, you can also do `map` operations: 65 | 66 | ```js 67 | logic.run({ 68 | 'map': [[1,2,3,4,5], { '+': [{ var: '' }, 1] }] 69 | }) // [2,3,4,5,6] 70 | ``` 71 | 72 | If `var` is left as an empty string, it will assume you're referring to the whole variable that is accessible at the current layer it is looking at. 73 | 74 | Example of a map accessing variables of the objects in the array: 75 | 76 | ```js 77 | logic.run({ 78 | 'map': [{var : 'x'}, { '+': [{ var: 'a' }, 1] }] 79 | }, 80 | { 81 | 'x': [{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }] 82 | }) // [2,3,4,5] 83 | ``` 84 | 85 | You can easily nest different operations in each other, like so: 86 | 87 | ```js 88 | logic.run({ 89 | max: [200, { 90 | '*': [12, {var: 'a' }] 91 | }] 92 | }, { 93 | a: 16 94 | }) // 200 95 | ``` 96 | 97 | The engine also supports Handlebars-esque style traversal of data when you use the iterative control structures. 98 | 99 | For example: 100 | 101 | ```js 102 | logic.run({ 103 | 'map': [{var : 'x'}, { '+': [{ var: 'a' }, { var: '../../adder'}] }] 104 | }, 105 | { 106 | 'x': [{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }], 107 | 'adder': 7 108 | }) // [8, 9, 10, 11] 109 | ``` 110 | --- 111 | 112 | 113 | This library was developed to be (for the most part) a drop-in replacement for the popular npm module [`json-logic-js`](https://github.com/jwadhams/json-logic-js). 114 | 115 | The intention of the library is to keep the functionality very similar to the original, while adding a few useful utilities. 116 | 117 | -------------------------------------------------------------------------------- /asLogic.js: -------------------------------------------------------------------------------- 1 | import LogicEngine from './logic.js' 2 | import AsyncLogicEngine from './asyncLogic.js' 3 | 4 | /** 5 | * @param {string[]} keep 6 | * @param {{ [key:string]: any }} obj 7 | */ 8 | function pick (keep, obj) { 9 | return Object.keys(obj).reduce((acc, i) => { 10 | if (keep.includes(i)) acc[i] = obj[i] 11 | return acc 12 | }, {}) 13 | } 14 | 15 | /** 16 | * Takes functions and makes it possible to use them in a locked-down json-logic document. 17 | * @param {{ [key: string]: (...args: any[]) => any }} functions Functions to import into the engine. 18 | * @param {string[]} keep Methods to keep from the original logic engine 19 | * @returns {(...args: any[]) => (...args: any[]) => any} 20 | */ 21 | export function asLogicSync (functions, keep = ['var'], engine = new LogicEngine()) { 22 | engine.methods = pick(keep, engine.methods) 23 | engine.addMethod('list', i => [].concat(i)) 24 | Object.keys(functions).forEach(i => engine.addMethod(i, data => functions[i](...data))) 25 | return engine.build.bind(engine) 26 | } 27 | 28 | /** 29 | * Takes functions and makes it possible to use them in a locked-down json-logic document. 30 | * If performance becomes a problem, you may wish to optimize by creating a "new AsyncLogicEngine" yourself, 31 | * and adding the methods you're using as sync / async respectively. .addMethod(name, func, { sync: true }) 32 | * This is meant to be a simple adapter. 33 | * 34 | * @param {{ [key: string]: (...args: any[]) => any }} functions 35 | * @param {string[]} keep 36 | * @returns {(...args: any[]) => Promise<(...args: any[]) => Promise>} 37 | */ 38 | export function asLogicAsync (functions, keep = ['var']) { 39 | return asLogicSync(functions, keep, new AsyncLogicEngine()) 40 | } 41 | -------------------------------------------------------------------------------- /asLogic.test.js: -------------------------------------------------------------------------------- 1 | import { asLogicSync, asLogicAsync } from './asLogic.js' 2 | 3 | const module = { 4 | hello: (name = 'World', last = '') => `Hello, ${name || 'World'}${last.length ? ' ' : ''}${last}!` 5 | } 6 | 7 | describe('asLogicSync', () => { 8 | it('Should be able to create a simple rule.', () => { 9 | const builder = asLogicSync(module) 10 | const f = builder({ 11 | hello: 'World' 12 | }) 13 | expect(f()).toBe('Hello, World!') 14 | 15 | const f2 = builder({ 16 | hello: { var: '' } 17 | }) 18 | expect(f2('Jesse')).toBe('Hello, Jesse!') 19 | 20 | const f3 = builder({ 21 | hello: null 22 | }) 23 | expect(f3()).toBe('Hello, World!') 24 | 25 | const f4 = builder({ 26 | hello: ['Jesse', 'Mitchell'] 27 | }) 28 | expect(f4()).toBe('Hello, Jesse Mitchell!') 29 | }) 30 | 31 | it('Should not allow you to create a builder with methods that do not exist', () => { 32 | const builder = asLogicSync(module) 33 | expect(() => builder({ 34 | hello: { '+': [1, 2] } 35 | })).toThrow() 36 | }) 37 | 38 | it('Should let you select logic kept', () => { 39 | const builder = asLogicSync(module, ['+']) 40 | const f = builder({ 41 | hello: { '+': [1, 2] } 42 | }) 43 | expect(f()).toBe('Hello, 3!') 44 | }) 45 | }) 46 | 47 | describe('asLogicAsync', () => { 48 | it('Should be able to create a simple rule.', async () => { 49 | const builder = asLogicAsync(module) 50 | const f = await builder({ 51 | hello: 'World' 52 | }) 53 | expect(await f()).toBe('Hello, World!') 54 | 55 | const f2 = await builder({ 56 | hello: { var: '' } 57 | }) 58 | expect(await f2('Jesse')).toBe('Hello, Jesse!') 59 | 60 | const f3 = await builder({ 61 | hello: null 62 | }) 63 | expect(await f3()).toBe('Hello, World!') 64 | 65 | const f4 = await builder({ 66 | hello: ['Jesse', 'Mitchell'] 67 | }) 68 | expect(await f4()).toBe('Hello, Jesse Mitchell!') 69 | }) 70 | 71 | it('Should not allow you to create a builder with methods that do not exist', async () => { 72 | const builder = asLogicAsync(module) 73 | try { 74 | await builder({ 75 | hello: { '+': [1, 2] } 76 | }) 77 | } catch (e) { 78 | return 79 | } 80 | throw new Error('Should have thrown an error') 81 | }) 82 | 83 | it('Should let you select logic kept', async () => { 84 | const builder = asLogicAsync(module, ['+']) 85 | const f = await builder({ 86 | hello: { '+': [1, 2] } 87 | }) 88 | expect(await f()).toBe('Hello, 3!') 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /async_iterators.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 'use strict' 3 | // Note: Each of these iterators executes synchronously, and will not "run in parallel" 4 | // I am supporting filter, reduce, some, every, map 5 | export async function filter (arr, iter) { 6 | const result = [] 7 | let index = 0 8 | for (const item of arr) { 9 | if (await iter(item, index++, arr)) result.push(item) 10 | } 11 | return result 12 | } 13 | 14 | export async function some (arr, iter) { 15 | let index = 0 16 | for (const item of arr) { 17 | if (await iter(item, index++, arr)) return true 18 | } 19 | return false 20 | } 21 | 22 | export async function every (arr, iter) { 23 | let index = 0 24 | for (const item of arr) { 25 | if (!(await iter(item, index++, arr))) return false 26 | } 27 | return true 28 | } 29 | 30 | export async function map (arr, iter) { 31 | const result = [] 32 | let index = 0 33 | for (const item of arr) { 34 | result.push(await iter(item, index++, arr)) 35 | } 36 | return result 37 | } 38 | 39 | export async function reduce (arr, iter, defaultValue) { 40 | if (arr.length === 0) { 41 | if (typeof defaultValue !== 'undefined') { 42 | return defaultValue 43 | } 44 | throw new Error('Array has no elements.') 45 | } 46 | 47 | const start = typeof defaultValue === 'undefined' ? 1 : 0 48 | let data = start ? arr[0] : defaultValue 49 | 50 | for (let i = start; i < arr.length; i++) { 51 | data = await iter(data, arr[i]) 52 | } 53 | 54 | return data 55 | } 56 | 57 | export default { 58 | filter, 59 | some, 60 | every, 61 | map, 62 | reduce 63 | } 64 | -------------------------------------------------------------------------------- /async_optimizer.js: -------------------------------------------------------------------------------- 1 | // This is the synchronous version of the optimizer; which the Async one should be based on. 2 | import { isDeterministic } from './compiler.js' 3 | import { map } from './async_iterators.js' 4 | import { isSync, Sync, OriginalImpl } from './constants.js' 5 | import declareSync from './utilities/declareSync.js' 6 | import { coerceArray } from './utilities/coerceArray.js' 7 | 8 | /** 9 | * Turns an expression like { '+': [1, 2] } into a function that can be called with data. 10 | * @param {*} logic 11 | * @param {*} engine 12 | * @param {string} methodName 13 | * @param {any[]} above 14 | * @returns A method that can be called to execute the logic. 15 | */ 16 | function getMethod (logic, engine, methodName, above) { 17 | const method = engine.methods[methodName] 18 | const called = method.asyncMethod ? method.asyncMethod : method.method ? method.method : method 19 | 20 | if (method.lazy) { 21 | if (typeof method[Sync] === 'function' && method[Sync](logic, { engine })) { 22 | const called = method.method ? method.method : method 23 | return declareSync((data, abv) => called(logic[methodName], data, abv || above, engine.fallback), true) 24 | } 25 | 26 | const args = logic[methodName] 27 | return (data, abv) => called(args, data, abv || above, engine) 28 | } 29 | 30 | let args = logic[methodName] 31 | if ((!args || typeof args !== 'object') && !method.optimizeUnary) args = [args] 32 | if (Array.isArray(args) && args.length === 1 && method.optimizeUnary && !Array.isArray(args[0])) args = args[0] 33 | 34 | if (Array.isArray(args)) { 35 | const optimizedArgs = args.map(l => optimize(l, engine, above)) 36 | 37 | if (isSync(optimizedArgs) && (method.method || method[Sync])) { 38 | const called = method.method ? method.method : method 39 | return declareSync((data, abv) => { 40 | const evaluatedArgs = optimizedArgs.map(l => typeof l === 'function' ? l(data, abv) : l) 41 | return called(evaluatedArgs, data, abv || above, engine.fallback) 42 | }, true) 43 | } 44 | 45 | return async (data, abv) => { 46 | const evaluatedArgs = await map(optimizedArgs, l => typeof l === 'function' ? l(data, abv) : l) 47 | return called(evaluatedArgs, data, abv || above, engine) 48 | } 49 | } else { 50 | const optimizedArgs = optimize(args, engine, above) 51 | 52 | if (isSync(optimizedArgs) && (method.method || method[Sync])) { 53 | const called = method.method ? method.method : method 54 | if ((methodName === 'var' || methodName === 'val') && engine.methods[methodName][OriginalImpl] && ((typeof optimizedArgs === 'string' && !optimizedArgs.includes('.') && !optimizedArgs.includes('\\')) || !optimizedArgs || typeof optimizedArgs === 'number')) { 55 | if (!optimizedArgs && methodName !== 'val') return declareSync((data) => !data || typeof data === 'undefined' || (typeof data === 'function' && !engine.allowFunctions) ? null : data) 56 | return declareSync((data) => !data || typeof data[optimizedArgs] === 'undefined' || (typeof data[optimizedArgs] === 'function' && !engine.allowFunctions) ? null : data[optimizedArgs]) 57 | } 58 | 59 | if (method.optimizeUnary) return declareSync((data, abv) => called(typeof optimizedArgs === 'function' ? optimizedArgs(data, abv) : optimizedArgs, data, abv || above, engine.fallback), true) 60 | return declareSync((data, abv) => called(coerceArray(typeof optimizedArgs === 'function' ? optimizedArgs(data, abv) : optimizedArgs), data, abv || above, engine), true) 61 | } 62 | 63 | if (method.optimizeUnary) return async (data, abv) => called(typeof optimizedArgs === 'function' ? await optimizedArgs(data, abv) : optimizedArgs, data, abv || above, engine) 64 | return async (data, abv) => called(coerceArray(typeof optimizedArgs === 'function' ? await optimizedArgs(data, abv) : optimizedArgs), data, abv || above, engine) 65 | } 66 | } 67 | 68 | /** 69 | * Processes the logic for the engine once so that it doesn't need to be traversed again. 70 | * @param {*} logic 71 | * @param {*} engine 72 | * @param {any[]} above 73 | * @returns A function that optimizes the logic for the engine in advance. 74 | */ 75 | export function optimize (logic, engine, above = []) { 76 | engine.fallback.allowFunctions = engine.allowFunctions 77 | if (Array.isArray(logic)) { 78 | const arr = logic.map(l => optimize(l, engine, above)) 79 | if (arr.every(l => typeof l !== 'function')) return arr 80 | if (isSync(arr)) return declareSync((data, abv) => arr.map(l => typeof l === 'function' ? l(data, abv) : l), true) 81 | return async (data, abv) => map(arr, l => typeof l === 'function' ? l(data, abv) : l) 82 | }; 83 | 84 | if (logic && typeof logic === 'object') { 85 | const keys = Object.keys(logic) 86 | const methodName = keys[0] 87 | 88 | if (keys.length === 0) return logic 89 | 90 | const isData = engine.isData(logic, methodName) 91 | if (isData) return () => logic 92 | 93 | // eslint-disable-next-line no-throw-literal 94 | if (keys.length > 1) throw { type: 'Unknown Operator' } 95 | 96 | // If we have a deterministic function, we can just return the result of the evaluation, 97 | // basically inlining the operation. 98 | const deterministic = !engine.disableInline && isDeterministic(logic, engine, { engine }) 99 | 100 | if (methodName in engine.methods) { 101 | const result = getMethod(logic, engine, methodName, above) 102 | if (deterministic) { 103 | let computed 104 | 105 | if (isSync(result)) { 106 | return declareSync(() => { 107 | if (!computed) computed = result() 108 | return computed 109 | }, true) 110 | } 111 | 112 | // For async, it's a little less straightforward since it could be a promise, 113 | // so we'll make it a closure. 114 | return async () => { 115 | if (!computed) computed = await result() 116 | return computed 117 | } 118 | } 119 | return result 120 | } 121 | 122 | // eslint-disable-next-line no-throw-literal 123 | throw { type: 'Unknown Operator', key: methodName } 124 | } 125 | 126 | return logic 127 | } 128 | -------------------------------------------------------------------------------- /bench/format-tests.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | const tests = JSON.parse(fs.readFileSync('tests.json', 'utf8')) 3 | fs.writeFileSync('tests.json', JSON.stringify(tests, undefined, 2)) 4 | -------------------------------------------------------------------------------- /bench/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-logic-engine-bench", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "json-logic-engine-bench", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@bestow/jsonlogic-rs": "^0.4.0", 13 | "json-logic-js": "^2.0.1", 14 | "json-rules-engine": "^7.2.1" 15 | } 16 | }, 17 | "node_modules/@bestow/jsonlogic-rs": { 18 | "version": "0.4.0", 19 | "resolved": "https://registry.npmjs.org/@bestow/jsonlogic-rs/-/jsonlogic-rs-0.4.0.tgz", 20 | "integrity": "sha512-bsVL+CnBfYi6QSOWJDUPVbPJKbSl6cQ1BvzR6Jkf22LL7GRfo0eXZ+KNf5v4dkRj/KMKC6nasKUEI7+VbE4hMg==", 21 | "license": "MIT" 22 | }, 23 | "node_modules/@jsep-plugin/assignment": { 24 | "version": "1.3.0", 25 | "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", 26 | "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", 27 | "license": "MIT", 28 | "engines": { 29 | "node": ">= 10.16.0" 30 | }, 31 | "peerDependencies": { 32 | "jsep": "^0.4.0||^1.0.0" 33 | } 34 | }, 35 | "node_modules/@jsep-plugin/regex": { 36 | "version": "1.0.4", 37 | "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", 38 | "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", 39 | "license": "MIT", 40 | "engines": { 41 | "node": ">= 10.16.0" 42 | }, 43 | "peerDependencies": { 44 | "jsep": "^0.4.0||^1.0.0" 45 | } 46 | }, 47 | "node_modules/clone": { 48 | "version": "2.1.2", 49 | "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", 50 | "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", 51 | "engines": { 52 | "node": ">=0.8" 53 | } 54 | }, 55 | "node_modules/eventemitter2": { 56 | "version": "6.4.4", 57 | "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.4.tgz", 58 | "integrity": "sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw==" 59 | }, 60 | "node_modules/hash-it": { 61 | "version": "6.0.0", 62 | "resolved": "https://registry.npmjs.org/hash-it/-/hash-it-6.0.0.tgz", 63 | "integrity": "sha512-KHzmSFx1KwyMPw0kXeeUD752q/Kfbzhy6dAZrjXV9kAIXGqzGvv8vhkUqj+2MGZldTo0IBpw6v7iWE7uxsvH0w==", 64 | "license": "MIT" 65 | }, 66 | "node_modules/jsep": { 67 | "version": "1.4.0", 68 | "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", 69 | "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", 70 | "license": "MIT", 71 | "engines": { 72 | "node": ">= 10.16.0" 73 | } 74 | }, 75 | "node_modules/json-logic-js": { 76 | "version": "2.0.1", 77 | "resolved": "https://registry.npmjs.org/json-logic-js/-/json-logic-js-2.0.1.tgz", 78 | "integrity": "sha512-J3hhqM4IY66sL8qyzU7cwLmTAt3kA6ZsYxyuZBEwhcc+OYPTmAHc64fBTXHT6K5RwFeUqJUX1tfO7wpKsUx+9A==" 79 | }, 80 | "node_modules/json-rules-engine": { 81 | "version": "7.2.1", 82 | "resolved": "https://registry.npmjs.org/json-rules-engine/-/json-rules-engine-7.2.1.tgz", 83 | "integrity": "sha512-WcAlEsFbUwnu2K++50QMpnujIn4icEBNZIMydqA6CbwE/0Dtjsq6BSEKiQlQlXirLaCXGqvXFQPCIkAWJhBZCg==", 84 | "license": "ISC", 85 | "dependencies": { 86 | "clone": "^2.1.2", 87 | "eventemitter2": "^6.4.4", 88 | "hash-it": "^6.0.0", 89 | "jsonpath-plus": "^10.2.0" 90 | }, 91 | "engines": { 92 | "node": ">=18.0.0" 93 | } 94 | }, 95 | "node_modules/jsonpath-plus": { 96 | "version": "10.2.0", 97 | "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.2.0.tgz", 98 | "integrity": "sha512-T9V+8iNYKFL2n2rF+w02LBOT2JjDnTjioaNFrxRy0Bv1y/hNsqR/EBK7Ojy2ythRHwmz2cRIls+9JitQGZC/sw==", 99 | "license": "MIT", 100 | "dependencies": { 101 | "@jsep-plugin/assignment": "^1.3.0", 102 | "@jsep-plugin/regex": "^1.0.4", 103 | "jsep": "^1.4.0" 104 | }, 105 | "bin": { 106 | "jsonpath": "bin/jsonpath-cli.js", 107 | "jsonpath-plus": "bin/jsonpath-cli.js" 108 | }, 109 | "engines": { 110 | "node": ">=18.0.0" 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /bench/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-logic-engine-bench", 3 | "version": "1.0.0", 4 | "description": "Test json-logic-engine", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Jesse Daniel Mitchell", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@bestow/jsonlogic-rs": "^0.4.0", 13 | "json-logic-js": "^2.0.1", 14 | "json-rules-engine": "^7.2.1" 15 | }, 16 | "type": "module" 17 | } 18 | -------------------------------------------------------------------------------- /bench/rules.js: -------------------------------------------------------------------------------- 1 | import jsonRulesEngine from 'json-rules-engine' 2 | import { AsyncLogicEngine } from '../index.js' 3 | const { Engine } = jsonRulesEngine 4 | 5 | const engine = new (Engine || jsonRulesEngine)() 6 | engine.addRule({ 7 | conditions: { 8 | any: [ 9 | { 10 | all: [ 11 | { 12 | fact: 'gameDuration', 13 | operator: 'equal', 14 | value: 40 15 | }, 16 | { 17 | fact: 'personalFoulCount', 18 | operator: 'greaterThanInclusive', 19 | value: 5 20 | } 21 | ] 22 | }, 23 | { 24 | all: [ 25 | { 26 | fact: 'gameDuration', 27 | operator: 'equal', 28 | value: 48 29 | }, 30 | { 31 | fact: 'personalFoulCount', 32 | operator: 'greaterThanInclusive', 33 | value: 6 34 | } 35 | ] 36 | } 37 | ] 38 | }, 39 | event: { 40 | type: 'fouledOut', 41 | params: { 42 | message: 'Player has fouled out!' 43 | } 44 | } 45 | }) 46 | const logic = new AsyncLogicEngine() 47 | const logicRules = { 48 | or: [ 49 | { 50 | and: [ 51 | { 52 | '===': [40, { var: 'gameDuration' }] 53 | }, 54 | { 55 | '>=': [{ var: 'personalFoulCount' }, 5] 56 | } 57 | ] 58 | }, 59 | { 60 | and: [ 61 | { 62 | '===': [48, { var: 'gameDuration' }] 63 | }, 64 | { 65 | '>=': [{ var: 'personalFoulCount' }, 6] 66 | } 67 | ] 68 | } 69 | ] 70 | } 71 | async function main () { 72 | const f = await logic.build(logicRules) 73 | const facts = { 74 | personalFoulCount: 6, 75 | gameDuration: 40 76 | } 77 | console.time('json-logic-engine') 78 | for (let i = 0; i < 1e6; i++) { 79 | await f(facts) 80 | } 81 | console.timeEnd('json-logic-engine') 82 | console.time('json-rules-engine') 83 | for (let i = 0; i < 1e6; i++) { 84 | await engine.run(facts) 85 | } 86 | console.timeEnd('json-rules-engine') 87 | } 88 | main() 89 | -------------------------------------------------------------------------------- /bench/test.js: -------------------------------------------------------------------------------- 1 | import { LogicEngine, AsyncLogicEngine } from '../index.js' 2 | import fs from 'fs' 3 | import { isDeepStrictEqual } from 'util' 4 | import jl from 'json-logic-js' 5 | 6 | const x = new LogicEngine() 7 | const y = new AsyncLogicEngine() 8 | const compatible = [] 9 | const incompatible = [] 10 | JSON.parse(fs.readFileSync('./tests.json').toString()).forEach((test) => { 11 | if (typeof test === 'string') { 12 | // console.log(test) 13 | } else { 14 | try { 15 | if (!isDeepStrictEqual(x.run(test[0], test[1]), test[2])) { 16 | incompatible.push(test) 17 | // console.log(test[0]) 18 | } else { 19 | compatible.push(test) 20 | } 21 | } catch (err) { 22 | // console.log(err) 23 | // console.log(test[0]) 24 | incompatible.push(test) 25 | } 26 | } 27 | }) 28 | console.log( 29 | compatible.length, 30 | incompatible.length, 31 | compatible.length / (compatible.length + incompatible.length) 32 | ) 33 | 34 | // x.optimizedMap = new WeakMap() 35 | 36 | // eslint-disable-next-line no-unused-vars 37 | const defined = [ 38 | [{ '+': [1, 2, 3, 4, 5] }, {}], 39 | [{ map: [[1, 2, 3, 4, 5], { '+': [{ var: '' }, 1] }] }, {}], 40 | [{ cat: ['Test of a ', { var: 'x' }] }, { x: 'Program' }], 41 | [ 42 | { '>': [{ var: 'x' }, 10] }, 43 | { 44 | x: 7 45 | } 46 | ], 47 | [ 48 | { and: [{ '>': [{ var: 'accountants' }, 3] }, { var: 'approvedBy.ceo' }] }, 49 | { 50 | approvedBy: { 51 | ceo: true 52 | }, 53 | accountants: 10 54 | } 55 | ], 56 | [{ '+': [{ var: '' }, 1] }, 5, 6], 57 | [{ '-': [{ var: '' }, 1] }, 7, 6], 58 | [{ '*': [{ var: 'x' }, { var: 'y' }] }, { x: 1, y: 3 }, 3] 59 | ] 60 | const tests = compatible 61 | const other = tests 62 | const built = other.map((i) => { 63 | return x.build(i[0]) 64 | }) 65 | console.time('json-logic-js') 66 | for (let j = 0; j < tests.length; j++) { 67 | for (let i = 0; i < 1e5; i++) { 68 | jl.apply(tests[j][0], tests[j][1]) 69 | } 70 | } 71 | console.timeEnd('json-logic-js') 72 | 73 | // console.time('json-logic-rs') 74 | // for (let j = 0; j < tests.length; j++) { 75 | // for (let i = 0; i < 1e5; i++) { 76 | // rust.apply(tests[j][0], tests[j][1]) 77 | // } 78 | // } 79 | // console.timeEnd('json-logic-rs') 80 | 81 | x.disableInterpretedOptimization = true 82 | console.time('le interpreted') 83 | for (let j = 0; j < other.length; j++) { 84 | for (let i = 0; i < 1e5; i++) { 85 | x.run(other[j][0], other[j][1]) 86 | } 87 | } 88 | console.timeEnd('le interpreted') 89 | 90 | x.disableInterpretedOptimization = false 91 | console.time('le interpreted (optimized)') 92 | for (let j = 0; j < other.length; j++) { 93 | for (let i = 0; i < 1e5; i++) { 94 | x.run(other[j][0], other[j][1]) 95 | } 96 | } 97 | console.timeEnd('le interpreted (optimized)') 98 | 99 | console.time('le built') 100 | for (let j = 0; j < tests.length; j++) { 101 | for (let i = 0; i < 1e5; i++) { 102 | built[j](tests[j][1]) 103 | } 104 | } 105 | console.timeEnd('le built') 106 | async function run () { 107 | const built2 = await Promise.all( 108 | other.map((i) => { 109 | return y.build(i[0]) 110 | }) 111 | ) 112 | 113 | console.time('le async interpreted') 114 | for (let j = 0; j < tests.length; j++) { 115 | for (let i = 0; i < 1e5; i++) { 116 | await y.run(tests[j][0], tests[j][1]) 117 | } 118 | } 119 | console.timeEnd('le async interpreted') 120 | 121 | console.time('le async built') 122 | for (let j = 0; j < tests.length; j++) { 123 | for (let i = 0; i < 1e5; i++) { 124 | await built2[j](tests[j][1]) 125 | } 126 | } 127 | console.timeEnd('le async built') 128 | } 129 | run() 130 | -------------------------------------------------------------------------------- /build.test.js: -------------------------------------------------------------------------------- 1 | import { LogicEngine, AsyncLogicEngine } from './index.js' 2 | 3 | function timeout (n, x) { 4 | return new Promise((resolve) => { 5 | setTimeout(() => { 6 | resolve(x) 7 | }, n) 8 | }) 9 | } 10 | 11 | [ 12 | new LogicEngine(), 13 | new AsyncLogicEngine(), 14 | new LogicEngine(undefined, { disableInline: true }), 15 | new AsyncLogicEngine(undefined, { disableInline: true }), 16 | new LogicEngine(undefined, { disableInterpretedOptimization: true, disableInline: true }), 17 | new AsyncLogicEngine(undefined, { disableInterpretedOptimization: true, disableInline: true }), 18 | new LogicEngine(undefined, { disableInterpretedOptimization: true }), 19 | new AsyncLogicEngine(undefined, { disableInterpretedOptimization: true }) 20 | ].forEach((logic) => { 21 | describe('Simple built functions', () => { 22 | test('Simple Addition', async () => { 23 | const f = await logic.build({ '+': [1, 2, 3] }) 24 | 25 | expect(await f()).toEqual(6) 26 | }) 27 | 28 | test('Simple Addition w/ Variable', async () => { 29 | const f = await logic.build({ '+': [1, 2, { var: 'x' }] }) 30 | 31 | expect(await f({ x: 3 })).toEqual(6) 32 | }) 33 | 34 | test('cat operator w/ Variable input', async () => { 35 | const f = await logic.build({ cat: { preserve: ['a', 'b', 'c'] } }) 36 | 37 | expect(await f()).toEqual('abc') 38 | }) 39 | 40 | test('Minus operator w/ String', async () => { 41 | const f = await logic.build({ '-': '5' }) 42 | 43 | expect(await f()).toEqual(-5) 44 | }) 45 | 46 | test('Minus operator w/ Infinity', async () => { 47 | const f = await logic.build({ '-': Infinity }) 48 | expect(await f()).toEqual(-Infinity) 49 | }) 50 | 51 | test('Minus operator w/ array w/ variable input', async () => { 52 | const f = await logic.build({ '-': [{ preserve: 5 }] }) 53 | expect(await f()).toEqual(-5) 54 | }) 55 | 56 | test('Minus operator w/ array w/ variable input (Infinity)', async () => { 57 | const f = await logic.build({ '-': [{ preserve: Infinity }] }) 58 | expect(await f()).toEqual(-Infinity) 59 | }) 60 | 61 | test('Minus operator w/ variable input', async () => { 62 | const f = await logic.build({ '-': { preserve: '5' } }) 63 | 64 | expect(await f()).toEqual(-5) 65 | }) 66 | 67 | test('get operator w/ deterministic input', async () => { 68 | const f = await logic.build({ get: [{ preserve: { a: 1 } }, 'a'] }) 69 | 70 | expect(await f()).toEqual(1) 71 | }) 72 | 73 | test('get operator w/ deterministic input and default', async () => { 74 | const f = await logic.build({ get: [{ preserve: {} }, 'a', 5] }) 75 | 76 | expect(await f()).toEqual(5) 77 | }) 78 | 79 | test('get operator w/ non-deterministic input', async () => { 80 | const f = await logic.build({ 81 | get: [{ eachKey: { a: { var: 'x' } } }, 'a'] 82 | }) 83 | 84 | expect(await f({ x: 1 })).toEqual(1) 85 | }) 86 | 87 | test('get operator w/ non-deterministic input and default', async () => { 88 | const f = await logic.build({ 89 | get: [{ eachKey: { a: { var: 'x' } } }, 'a', 5] 90 | }) 91 | 92 | expect(await f({})).toEqual(5) 93 | }) 94 | 95 | test('Plus operator w/ variable input', async () => { 96 | const f = await logic.build({ '+': { preserve: '5' } }) 97 | 98 | expect(await f()).toEqual(5) 99 | }) 100 | 101 | test('Multiplication operator w/ variable input', async () => { 102 | const f = await logic.build({ '*': { preserve: [1, 2] } }) 103 | 104 | expect(await f()).toEqual(2) 105 | }) 106 | 107 | test('Division operator w/ variable input', async () => { 108 | const f = await logic.build({ '/': { preserve: [1, 2] } }) 109 | 110 | expect(await f()).toEqual(1 / 2) 111 | }) 112 | 113 | test('Multiplication operator w/ array w/ variable input', async () => { 114 | const f = await logic.build({ '*': [1, { preserve: 2 }] }) 115 | 116 | expect(await f()).toEqual(2) 117 | }) 118 | 119 | test('Modulo operator w/ variable input', async () => { 120 | const f = await logic.build({ '%': { preserve: [1, 2] } }) 121 | 122 | expect(await f()).toEqual(1) 123 | }) 124 | 125 | test('Modulo operator w/ array w/ variable input', async () => { 126 | const f = await logic.build({ '%': [1, { preserve: 2 }] }) 127 | 128 | expect(await f()).toEqual(1) 129 | }) 130 | 131 | test('Division operator w/ array w/ variable input', async () => { 132 | const f = await logic.build({ '/': [1, { preserve: 2 }] }) 133 | 134 | expect(await f()).toEqual(1 / 2) 135 | }) 136 | 137 | test('min command w/ variable input', async () => { 138 | const f = await logic.build({ min: { preserve: [1, 2] } }) 139 | 140 | expect(await f()).toEqual(1) 141 | }) 142 | 143 | test('max command w/ variable input', async () => { 144 | const f = await logic.build({ max: { preserve: [1, 2] } }) 145 | 146 | expect(await f()).toEqual(2) 147 | }) 148 | }) 149 | 150 | describe('Nested structures', () => { 151 | test('Simple map (w/ handlebars traversal)', async () => { 152 | const f = await logic.build({ 153 | map: [ 154 | [1, 2, 3], 155 | { '+': [{ var: '' }, { var: '../../x' }, { preserve: 0 }] } 156 | ] 157 | }) 158 | 159 | expect(await f({ x: 1 })).toStrictEqual([2, 3, 4]) 160 | }) 161 | 162 | test('Simple eachKey', async () => { 163 | const f = await logic.build({ 164 | eachKey: { a: { var: 'x' }, b: { var: 'y' } } 165 | }) 166 | 167 | expect(await f({ x: 1, y: 2 })).toStrictEqual({ a: 1, b: 2 }) 168 | }) 169 | 170 | // test('Invalid eachKey', async () => { 171 | // expect(async () => await logic.build({ eachKey: 5 })).rejects.toThrow( 172 | // InvalidControlInput 173 | // ) 174 | // }) 175 | 176 | test('Simple deterministic eachKey', async () => { 177 | const f = await logic.build({ eachKey: { a: 1, b: { '+': [1, 1] } } }) 178 | 179 | expect(await f({ x: 1, y: 2 })).toStrictEqual({ a: 1, b: 2 }) 180 | }) 181 | }) 182 | }) 183 | 184 | const logic = new AsyncLogicEngine() 185 | logic.addMethod('as1', async ([n]) => timeout(100, n + 1), { async: true }) 186 | 187 | describe('Testing async build with full async', () => { 188 | test('Async +1', async () => { 189 | const f = await logic.build({ 190 | '+': [{ as1: 2 }, 1] 191 | }) 192 | 193 | expect(await f()).toBe(4) 194 | }) 195 | 196 | test('Simple async map (w/ handlebars traversal)', async () => { 197 | const f = await logic.build({ 198 | map: [ 199 | [1, 2, 3], 200 | { '+': [{ as1: { var: '' } }, { var: '../../x' }, { preserve: 0 }] } 201 | ] 202 | }) 203 | 204 | expect(await f({ x: 1 })).toStrictEqual([3, 4, 5]) 205 | }) 206 | 207 | test('Async +1, multiple calls', async () => { 208 | const f = await logic.build({ 209 | '+': [{ as1: { var: 'x' } }, 1] 210 | }) 211 | 212 | const a = f({ x: 2 }) 213 | const b = f({ x: 3 }) 214 | const c = f({ x: 4 }) 215 | const d = f({ x: 5 }) 216 | 217 | expect(await a).toBe(4) 218 | expect(await b).toBe(5) 219 | expect(await c).toBe(6) 220 | expect(await d).toBe(7) 221 | }) 222 | }) 223 | -------------------------------------------------------------------------------- /compatible.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-ex-assign */ 2 | import fs from 'fs' 3 | import { LogicEngine, AsyncLogicEngine } from './index.js' 4 | 5 | const tests = [] 6 | 7 | // get all json files from "suites" directory, 1 layer of depth 8 | const files = fs.readdirSync('./suites', { withFileTypes: true }).flatMap(i => { 9 | if (i.isDirectory()) return fs.readdirSync(`./suites/${i.name}`).map(j => `${i.name}/${j}`) 10 | return i.name 11 | }) 12 | 13 | for (const file of files) { 14 | if (file.endsWith('.json')) { 15 | tests.push(...JSON.parse(fs.readFileSync(`./suites/${file}`).toString()).filter(i => typeof i !== 'string').map(i => { 16 | if (Array.isArray(i)) return { rule: i[0], data: i[1] || null, result: i[2], description: JSON.stringify(i[0]) } 17 | return i 18 | })) 19 | } 20 | } 21 | 22 | function correction (x) { 23 | // eslint-disable-next-line no-compare-neg-zero 24 | if (x === -0) return 0 25 | if (Number.isNaN(x)) return { error: 'NaN' } 26 | return x 27 | } 28 | 29 | const engines = [] 30 | 31 | for (let i = 0; i < 8; i++) { 32 | let res = 'sync' 33 | let engine = new LogicEngine(undefined, { compatible: true }) 34 | // sync / async 35 | if (i & 1) { 36 | engine = new AsyncLogicEngine(undefined, { compatible: true }) 37 | res = 'async' 38 | } 39 | // inline / disabled 40 | if (i & 2) { 41 | engine.disableInline = true 42 | res += ' no-inline' 43 | } 44 | // optimized / not optimized 45 | if (i & 4) { 46 | engine.disableInterpretedOptimization = true 47 | res += ' no-optimized' 48 | } 49 | engines.push([engine, res]) 50 | } 51 | 52 | describe('All of the compatible tests', () => { 53 | for (const testCase of tests) { 54 | for (const engine of engines) { 55 | test(`${engine[1]} ${JSON.stringify(testCase.rule)} ${JSON.stringify( 56 | testCase.data 57 | )}`, async () => { 58 | try { 59 | let result = await engine[0].run(testCase.rule, testCase.data) 60 | if ((result || 0).toNumber) result = Number(result) 61 | if (Array.isArray(result)) result = result.map(i => (i || 0).toNumber ? Number(i) : i) 62 | expect(correction(result)).toStrictEqual(testCase.result) 63 | expect(testCase.error).toBeUndefined() 64 | } catch (err) { 65 | if (err.message && err.message.includes('expect')) throw err 66 | if (Number.isNaN(err)) err = { type: 'NaN' } 67 | else if (err.message) err = { type: err.message } 68 | expect(err).toMatchObject(testCase.error) 69 | } 70 | }) 71 | 72 | test(`${engine[1]} ${JSON.stringify(testCase.rule)} ${JSON.stringify( 73 | testCase.data 74 | )} (built)`, async () => { 75 | try { 76 | const f = await engine[0].build(testCase.rule) 77 | let result = await f(testCase.data) 78 | if ((result || 0).toNumber) result = Number(result) 79 | if (Array.isArray(result)) result = result.map(i => i.toNumber ? Number(i) : i) 80 | expect(correction(result)).toStrictEqual(testCase.result) 81 | expect(testCase.error).toBeUndefined() 82 | } catch (err) { 83 | if (err.message && err.message.includes('expect')) throw err 84 | if (Number.isNaN(err)) err = { type: 'NaN' } 85 | else if (err.message) err = { type: err.message } 86 | expect(err).toMatchObject(testCase.error) 87 | } 88 | }) 89 | } 90 | } 91 | }) 92 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 'use strict' 3 | 4 | export const Sync = Symbol.for('json_logic_sync') 5 | export const Compiled = Symbol.for('json_logic_compiled') 6 | export const OriginalImpl = Symbol.for('json_logic_original') 7 | export const Unfound = Symbol.for('json_logic_unfound') 8 | 9 | /** 10 | * Checks if an item is synchronous. 11 | * This allows us to optimize the logic a bit 12 | * further so that we don't need to await everything. 13 | * 14 | * @param {*} item 15 | * @returns {Boolean} 16 | */ 17 | export function isSync (item) { 18 | if (typeof item === 'function') return item[Sync] === true 19 | if (Array.isArray(item)) return item.every(isSync) 20 | if (item && item.asyncMethod && !item.method) return false 21 | return true 22 | } 23 | 24 | export default { 25 | Sync, 26 | OriginalImpl, 27 | isSync 28 | } 29 | -------------------------------------------------------------------------------- /customEngines.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { LogicEngine, AsyncLogicEngine } from './index.js' 3 | 4 | class DataEngine extends LogicEngine { 5 | isData (logic, firstKey) { 6 | if (Object.keys(logic).length > 1) return true 7 | return !(firstKey in this.methods) 8 | } 9 | } 10 | 11 | class AsyncDataEngine extends AsyncLogicEngine { 12 | isData (logic, firstKey) { 13 | if (Object.keys(logic).length > 1) return true 14 | return !(firstKey in this.methods) 15 | } 16 | } 17 | 18 | const engine = new DataEngine() 19 | const asyncEngine = new AsyncDataEngine() 20 | 21 | describe('Custom Engines (isData)', () => { 22 | const logic = { 23 | get: [{ 24 | xs: 10, 25 | s: 20, 26 | m: 30 27 | }, { 28 | var: 'size' 29 | }] 30 | } 31 | 32 | const data = { 33 | size: 's' 34 | } 35 | 36 | it('Should let us override how data is detected (sync)', () => { 37 | const f = engine.build(logic) 38 | expect(f(data)).toEqual(20) 39 | }) 40 | 41 | it('Should let us override how data is detected (async)', async () => { 42 | const f = await asyncEngine.build(logic) 43 | expect(await f(data)).toEqual(20) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 'use strict' 3 | 4 | import LogicEngine from './logic.js' 5 | import AsyncLogicEngine from './asyncLogic.js' 6 | import Compiler from './compiler.js' 7 | import Constants from './constants.js' 8 | import defaultMethods from './defaultMethods.js' 9 | import { asLogicSync, asLogicAsync } from './asLogic.js' 10 | import { splitPath, splitPathMemoized } from './utilities/splitPath.js' 11 | 12 | export { splitPath, splitPathMemoized } 13 | export { LogicEngine } 14 | export { AsyncLogicEngine } 15 | export { Compiler } 16 | export { Constants } 17 | export { defaultMethods } 18 | export { asLogicSync } 19 | export { asLogicAsync } 20 | 21 | export default { LogicEngine, AsyncLogicEngine, Compiler, Constants, defaultMethods, asLogicSync, asLogicAsync, splitPath, splitPathMemoized } 22 | -------------------------------------------------------------------------------- /legacy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import { buildString } from './compiler.js' 3 | import { splitPathMemoized } from './utilities/splitPath.js' 4 | import chainingSupported from './utilities/chainingSupported.js' 5 | import { Sync, OriginalImpl, Compiled } from './constants.js' 6 | 7 | /** @type {Record<'get' | 'missing' | 'missing_some' | 'var', { method: (...args) => any }>} **/ 8 | const legacyMethods = { 9 | get: { 10 | [Sync]: true, 11 | method: ([data, key, defaultValue], context, above, engine) => { 12 | const notFound = defaultValue === undefined ? null : defaultValue 13 | 14 | const subProps = splitPathMemoized(String(key)) 15 | for (let i = 0; i < subProps.length; i++) { 16 | if (data === null || data === undefined) return notFound 17 | // Descending into context 18 | data = data[subProps[i]] 19 | if (data === undefined) return notFound 20 | } 21 | 22 | if (engine.allowFunctions || typeof data[key] !== 'function') return data 23 | return null 24 | }, 25 | deterministic: true, 26 | compile: (data, buildState) => { 27 | let defaultValue = null 28 | let key = data 29 | let obj = null 30 | if (Array.isArray(data) && data.length <= 3) { 31 | obj = data[0] 32 | key = data[1] 33 | defaultValue = typeof data[2] === 'undefined' ? null : data[2] 34 | 35 | // Bail out if the key is dynamic; dynamic keys are not really optimized by this block. 36 | if (key && typeof key === 'object') return false 37 | 38 | key = key.toString() 39 | const pieces = splitPathMemoized(key) 40 | if (!chainingSupported) { 41 | return `(((a,b) => (typeof a === 'undefined' || a === null) ? b : a)(${pieces.reduce( 42 | (text, i) => `(${text}||0)[${JSON.stringify(i)}]`, 43 | `(${buildString(obj, buildState)}||0)` 44 | )}, ${buildString(defaultValue, buildState)}))` 45 | } 46 | return `((${buildString(obj, buildState)})${pieces 47 | .map((i) => `?.[${buildString(i, buildState)}]`) 48 | .join('')} ?? ${buildString(defaultValue, buildState)})` 49 | } 50 | return false 51 | } 52 | }, 53 | var: { 54 | [OriginalImpl]: true, 55 | [Sync]: true, 56 | method: (key, context, above, engine) => { 57 | let b 58 | if (Array.isArray(key)) { 59 | b = key[1] 60 | key = key[0] 61 | } 62 | let iter = 0 63 | while (typeof key === 'string' && key.startsWith('../') && iter < above.length) { 64 | context = above[iter++] 65 | key = key.substring(3) 66 | // A performance optimization that allows you to pass the previous above array without spreading it as the last argument 67 | if (iter === above.length && Array.isArray(context)) { 68 | iter = 0 69 | above = context 70 | context = above[iter++] 71 | } 72 | } 73 | 74 | const notFound = b === undefined ? null : b 75 | if (typeof key === 'undefined' || key === '' || key === null) { 76 | if (engine.allowFunctions || typeof context !== 'function') return context 77 | return null 78 | } 79 | const subProps = splitPathMemoized(String(key)) 80 | for (let i = 0; i < subProps.length; i++) { 81 | if (context === null || context === undefined) return notFound 82 | 83 | // Descending into context 84 | context = context[subProps[i]] 85 | if (context === undefined) return notFound 86 | } 87 | 88 | if (engine.allowFunctions || typeof context !== 'function') return context 89 | return null 90 | }, 91 | deterministic: (data, buildState) => buildState.insideIterator && !String(data).includes('../../'), 92 | optimizeUnary: true, 93 | compile: (data, buildState) => { 94 | let key = data 95 | let defaultValue = null 96 | if ( 97 | !key || 98 | typeof data === 'string' || 99 | typeof data === 'number' || 100 | (Array.isArray(data) && data.length <= 2) 101 | ) { 102 | if (Array.isArray(data)) { 103 | key = data[0] 104 | defaultValue = typeof data[1] === 'undefined' ? null : data[1] 105 | } 106 | 107 | if (key === '../index' && buildState.iteratorCompile) return 'index' 108 | 109 | // this counts the number of var accesses to determine if they're all just using this override. 110 | // this allows for a small optimization :) 111 | if (typeof key === 'undefined' || key === null || key === '') return 'context' 112 | if (typeof key !== 'string' && typeof key !== 'number') return false 113 | 114 | key = key.toString() 115 | if (key.includes('../')) return false 116 | 117 | const pieces = splitPathMemoized(key) 118 | 119 | // support older versions of node 120 | if (!chainingSupported) { 121 | const res = `((((a,b) => (typeof a === 'undefined' || a === null) ? b : a)(${pieces.reduce( 122 | (text, i) => `(${text}||0)[${JSON.stringify(i)}]`, 123 | '(context||0)' 124 | )}, ${buildString(defaultValue, buildState)})))` 125 | if (buildState.engine.allowFunctions) return res 126 | return `(typeof (prev = ${res}) === 'function' ? null : prev)` 127 | } 128 | const res = `(context${pieces 129 | .map((i) => `?.[${JSON.stringify(i)}]`) 130 | .join('')} ?? ${buildString(defaultValue, buildState)})` 131 | if (buildState.engine.allowFunctions) return res 132 | return `(typeof (prev = ${res}) === 'function' ? null : prev)` 133 | } 134 | return false 135 | } 136 | }, 137 | missing: { 138 | [Sync]: true, 139 | optimizeUnary: false, 140 | method: (checked, context) => { 141 | if (!checked.length) return [] 142 | 143 | // Check every item in checked 144 | const missing = [] 145 | 146 | for (let i = 0; i < checked.length; i++) { 147 | // check context for the key, exiting early if any is null 148 | const path = splitPathMemoized(String(checked[i])) 149 | let data = context 150 | let found = true 151 | for (let j = 0; j < path.length; j++) { 152 | if (!data) { 153 | found = false 154 | break 155 | } 156 | data = data[path[j]] 157 | if (data === undefined) { 158 | found = false 159 | break 160 | } 161 | } 162 | if (!found) missing.push(checked[i]) 163 | } 164 | 165 | return missing 166 | }, 167 | compile: (data, buildState) => { 168 | if (!Array.isArray(data)) return false 169 | if (data.length === 0) return buildState.compile`[]` 170 | if (data.length === 1 && typeof data[0] === 'string' && !data[0].includes('.')) return buildState.compile`(context || 0)[${data[0]}] === undefined ? [${data[0]}] : []` 171 | if (data.length === 2 && typeof data[0] === 'string' && typeof data[1] === 'string' && !data[0].includes('.') && !data[1].includes('.')) return buildState.compile`(context || 0)[${data[0]}] === undefined ? (context || 0)[${data[1]}] === undefined ? [${data[0]}, ${data[1]}] : [${data[0]}] : (context || 0)[${data[1]}] === undefined ? [${data[1]}] : []` 172 | return false 173 | }, 174 | deterministic: (data, buildState) => { 175 | if (Array.isArray(data) && data.length === 0) return true 176 | return false 177 | } 178 | }, 179 | missing_some: { 180 | [Sync]: true, 181 | optimizeUnary: false, 182 | method: ([needCount, options], context) => { 183 | const missing = legacyMethods.missing.method(options, context) 184 | if (options.length - missing.length >= needCount) return [] 185 | return missing 186 | }, 187 | compile: ([needCount, options], buildState) => { 188 | if (!Array.isArray(options)) return false 189 | let compilation = legacyMethods.missing.compile(options, buildState) 190 | if (!compilation) compilation = buildState.compile`engine.methods.missing.method(${{ [Compiled]: JSON.stringify(options) }}, context)` 191 | return buildState.compile`${options.length} - (prev = ${compilation}).length < ${needCount} ? prev : []` 192 | }, 193 | deterministic: false 194 | } 195 | } 196 | 197 | export default { ...legacyMethods } 198 | -------------------------------------------------------------------------------- /logic.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 'use strict' 3 | 4 | import defaultMethods from './defaultMethods.js' 5 | 6 | import { build } from './compiler.js' 7 | import declareSync from './utilities/declareSync.js' 8 | import omitUndefined from './utilities/omitUndefined.js' 9 | import { optimize } from './optimizer.js' 10 | import { coerceArray } from './utilities/coerceArray.js' 11 | import { OriginalImpl } from './constants.js' 12 | 13 | /** 14 | * An engine capable of running synchronous JSON Logic. 15 | */ 16 | class LogicEngine { 17 | /** 18 | * Creates a new instance of the Logic Engine. 19 | * 20 | * @param {Object} methods An object that stores key-value pairs between the names of the commands & the functions they execute. 21 | * @param {{ disableInline?: Boolean, disableInterpretedOptimization?: Boolean, permissive?: boolean }} options 22 | */ 23 | constructor ( 24 | methods = defaultMethods, 25 | options = { disableInline: false, disableInterpretedOptimization: false, permissive: false } 26 | ) { 27 | this.disableInline = options.disableInline 28 | this.disableInterpretedOptimization = options.disableInterpretedOptimization 29 | this.methods = { ...methods } 30 | 31 | this.optimizedMap = new WeakMap() 32 | this.missesSinceSeen = 0 33 | 34 | /** @type {{ disableInline?: Boolean, disableInterpretedOptimization?: Boolean }} */ 35 | this.options = { disableInline: options.disableInline, disableInterpretedOptimization: options.disableInterpretedOptimization } 36 | if (!this.isData) { 37 | if (!options.permissive) this.isData = () => false 38 | else this.isData = (data, key) => !(key in this.methods) 39 | } 40 | } 41 | 42 | /** 43 | * Determines the truthiness of a value. 44 | * You can override this method to change the way truthiness is determined. 45 | * @param {*} value 46 | * @returns 47 | */ 48 | truthy (value) { 49 | if (!value) return value 50 | // The following check could be erased, as it'd be caught by the iterator check, 51 | // but it's here for performance reasons. 52 | if (Array.isArray(value)) return value.length > 0 53 | if (typeof value === 'object') { 54 | if (value[Symbol.iterator]) { 55 | if ('length' in value && value.length === 0) return false 56 | if ('size' in value && value.size === 0) return false 57 | } 58 | if (value.constructor.name === 'Object') return Object.keys(value).length > 0 59 | } 60 | return value 61 | } 62 | 63 | /** 64 | * An internal method used to parse through the JSON Logic at a lower level. 65 | * @param {*} logic The logic being executed. 66 | * @param {*} context The context of the logic being run (input to the function.) 67 | * @param {*} above The context above (can be used for handlebars-style data traversal.) 68 | * @returns {{ result: *, func: string }} 69 | */ 70 | _parse (logic, context, above, func, length) { 71 | const data = logic[func] 72 | 73 | if (this.isData(logic, func)) return logic 74 | 75 | // eslint-disable-next-line no-throw-literal 76 | if (!this.methods[func] || length > 1) throw { type: 'Unknown Operator', key: func } 77 | 78 | // A small but useful micro-optimization for some of the most common functions. 79 | // Later on, I could define something to shut this off if var / val are redefined. 80 | if ((func === 'var' || func === 'val') && this.methods[func][OriginalImpl]) { 81 | const input = (!data || typeof data !== 'object') ? data : this.run(data, context, { above }) 82 | return this.methods[func].method(input, context, above, this, null) 83 | } 84 | 85 | if (typeof this.methods[func] === 'function') { 86 | const input = (!data || typeof data !== 'object') ? [data] : coerceArray(this.run(data, context, { above })) 87 | return this.methods[func](input, context, above, this) 88 | } 89 | 90 | if (typeof this.methods[func] === 'object') { 91 | const { method, lazy } = this.methods[func] 92 | const parsedData = !lazy ? ((!data || typeof data !== 'object') ? [data] : coerceArray(this.run(data, context, { above }))) : data 93 | return method(parsedData, context, above, this) 94 | } 95 | 96 | throw new Error(`Method '${func}' is not set up properly.`) 97 | } 98 | 99 | /** 100 | * 101 | * @param {String} name The name of the method being added. 102 | * @param {((args: any, context: any, above: any[], engine: LogicEngine) => any) |{ lazy?: Boolean, traverse?: Boolean, method: (args: any, context: any, above: any[], engine: LogicEngine) => any, deterministic?: Function | Boolean }} method 103 | * @param {{ deterministic?: Boolean, optimizeUnary?: Boolean }} annotations This is used by the compiler to help determine if it can optimize the function being generated. 104 | */ 105 | addMethod (name, method, { deterministic, optimizeUnary } = {}) { 106 | if (typeof method === 'function') method = { method, lazy: false } 107 | else method = { ...method, lazy: typeof method.traverse !== 'undefined' ? !method.traverse : method.lazy } 108 | Object.assign(method, omitUndefined({ deterministic, optimizeUnary })) 109 | this.methods[name] = declareSync(method) 110 | } 111 | 112 | /** 113 | * Adds a batch of functions to the engine 114 | * @param {String} name 115 | * @param {Object} obj 116 | * @param {{ deterministic?: Boolean, async?: Boolean, sync?: Boolean }} annotations Not recommended unless you're sure every function from the module will match these annotations. 117 | */ 118 | addModule (name, obj, annotations) { 119 | Object.getOwnPropertyNames(obj).forEach((key) => { 120 | if (typeof obj[key] === 'function' || typeof obj[key] === 'object') this.addMethod(`${name}${name ? '.' : ''}${key}`, obj[key], annotations) 121 | }) 122 | } 123 | 124 | /** 125 | * Runs the logic against the data. 126 | * 127 | * NOTE: With interpreted optimizations enabled, it will cache the execution plan for the logic for 128 | * future invocations; if you plan to modify the logic, you should disable this feature, by passing 129 | * `disableInterpretedOptimization: true` in the constructor. 130 | * 131 | * If it detects that a bunch of dynamic objects are being passed in, and it doesn't see the same object, 132 | * it will disable the interpreted optimization. 133 | * 134 | * @param {*} logic The logic to be executed 135 | * @param {*} data The data being passed in to the logic to be executed against. 136 | * @param {{ above?: any }} options Options for the invocation 137 | * @returns {*} 138 | */ 139 | run (logic, data = {}, options = {}) { 140 | const { above = [] } = options 141 | 142 | // OPTIMIZER BLOCK // 143 | if (!this.disableInterpretedOptimization && typeof logic === 'object' && logic) { 144 | if (this.missesSinceSeen > 500) { 145 | this.disableInterpretedOptimization = true 146 | this.missesSinceSeen = 0 147 | } 148 | 149 | if (!this.optimizedMap.has(logic)) { 150 | this.optimizedMap.set(logic, optimize(logic, this, above)) 151 | this.missesSinceSeen++ 152 | const grab = this.optimizedMap.get(logic) 153 | return typeof grab === 'function' ? grab(data, above) : grab 154 | } else { 155 | this.missesSinceSeen = 0 156 | const grab = this.optimizedMap.get(logic) 157 | return typeof grab === 'function' ? grab(data, above) : grab 158 | } 159 | } 160 | // END OPTIMIZER BLOCK // 161 | 162 | if (Array.isArray(logic)) { 163 | const res = new Array(logic.length) 164 | for (let i = 0; i < logic.length; i++) res[i] = this.run(logic[i], data, { above }) 165 | return res 166 | } 167 | 168 | if (logic && typeof logic === 'object') { 169 | const keys = Object.keys(logic) 170 | if (keys.length > 0) { 171 | const func = keys[0] 172 | return this._parse(logic, data, above, func, keys.length) 173 | } 174 | } 175 | 176 | return logic 177 | } 178 | 179 | /** 180 | * 181 | * @param {*} logic The logic to be built. 182 | * @param {{ top?: Boolean, above?: any }} options 183 | * @returns {Function} 184 | */ 185 | build (logic, options = {}) { 186 | const { above = [], top = true } = options 187 | const constructedFunction = build(logic, { engine: this, above }) 188 | if (top === false && constructedFunction.deterministic) return constructedFunction() 189 | return constructedFunction 190 | } 191 | } 192 | export default LogicEngine 193 | -------------------------------------------------------------------------------- /modules.test.js: -------------------------------------------------------------------------------- 1 | import { LogicEngine, AsyncLogicEngine } from './index.js' 2 | const engines = [new LogicEngine(), new AsyncLogicEngine()] 3 | describe('Trying out adding modules to the engine', () => { 4 | engines.forEach((engine) => { 5 | test('Add Math Module', async () => { 6 | engine.addModule('Math', Math, { deterministic: true, sync: true }) 7 | expect(await engine.run({ 'Math.ceil': 2.5 })).toBe(3) 8 | expect(await engine.run({ 'Math.floor': 2.5 })).toBe(2) 9 | }) 10 | 11 | test('Add math module with empty name', async () => { 12 | engine.addModule('', Math, { deterministic: true, sync: true }) 13 | expect(await engine.run({ ceil: 2.5 })).toBe(3) 14 | expect(await engine.run({ floor: 2.5 })).toBe(2) 15 | }) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-logic-engine", 3 | "version": "5.0.0", 4 | "description": "Construct complex rules with JSON & process them.", 5 | "main": "./dist/cjs/index.js", 6 | "module": "./dist/esm/index.js", 7 | "author": "Jesse Daniel Mitchell", 8 | "repository": { 9 | "url": "https://github.com/TotalTechGeek/json-logic-engine" 10 | }, 11 | "license": "MIT", 12 | "scripts": { 13 | "test": "cross-env NODE_OPTIONS=--experimental-vm-modules nyc jest --coverage", 14 | "coverage": "coveralls < coverage/lcov.info", 15 | "prepublish": "npm run build", 16 | "build": "run-script-os", 17 | "build:win32": "rm -rf dist && rm -f *.d.ts && rm -f utilities/*.d.ts && rollup index.js --file dist/cjs/index.js --format cjs --exports named && rollup index.js --file dist/esm/index.js --format esm && echo { \"type\": \"module\" } > dist/esm/package.json && echo { \"type\": \"commonjs\" } > dist/cjs/package.json && cd dist && standard --fix */*.js && tsc ../index.js --declaration --allowJs --emitDeclarationOnly --target ESNext --moduleResolution node", 18 | "build:default": "rm -rf dist && rm -f *.d.ts && rm -f utilities/*.d.ts && rollup index.js --file dist/cjs/index.js --format cjs --exports named && rollup index.js --file dist/esm/index.js --format esm && echo '{ \"type\": \"module\" }' > dist/esm/package.json && echo '{ \"type\": \"commonjs\" }' > dist/cjs/package.json && cd dist && standard --fix */*.js && tsc ../index.js --declaration --allowJs --emitDeclarationOnly --target ESNext --moduleResolution node" 19 | }, 20 | "devDependencies": { 21 | "coveralls": "^3.1.1", 22 | "cross-env": "^7.0.3", 23 | "eslint": "^7.32.0", 24 | "eslint-config-standard": "^16.0.3", 25 | "eslint-plugin-import": "^2.24.1", 26 | "eslint-plugin-node": "^11.1.0", 27 | "eslint-plugin-promise": "^4.2.1", 28 | "jest": "^27.3.1", 29 | "nyc": "^15.1.0", 30 | "rollup": "^2.58.3", 31 | "run-script-os": "^1.1.6", 32 | "standard": "^16.0.4", 33 | "typescript": "^4.4.4" 34 | }, 35 | "engines": { 36 | "node": ">=12.22.7" 37 | }, 38 | "jest": { 39 | "testPathIgnorePatterns": [ 40 | "./bench" 41 | ] 42 | }, 43 | "exports": { 44 | ".": { 45 | "import": "./dist/esm/index.js", 46 | "require": "./dist/cjs/index.js", 47 | "types": "./index.d.ts" 48 | } 49 | }, 50 | "types": "./index.d.ts", 51 | "type": "module", 52 | "sideEffects": false, 53 | "keywords": [ 54 | "json", 55 | "logic", 56 | "jsonlogic", 57 | "rules", 58 | "sandbox", 59 | "engine" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /perf.js: -------------------------------------------------------------------------------- 1 | import { AsyncLogicEngine } from './index.js' 2 | const x = new AsyncLogicEngine(undefined) 3 | async function test () { 4 | const logic = { 5 | if: [ 6 | { 7 | '>': [{ val: 'x' }, { '+': [11, 5, { '+': [1, { val: 'y' }, 1] }, 2] }] 8 | }, 9 | { 10 | '*': [{ val: 'x' }, { '*': { map: [[2, 5, 5], { val: [] }] } }] 11 | }, 12 | { 13 | '/': [{ val: 'x' }, { '-': { map: [[100, 50, 30, 10], { val: [] }] } }] 14 | } 15 | ] 16 | } 17 | console.time('interpreted') 18 | console.log(await x.run(logic, { x: 15, y: 1 })) 19 | for (let i = 0; i < 2e6; i++) { 20 | await x.run(logic, { x: i, y: i % 20 }) 21 | } 22 | console.timeEnd('interpreted') 23 | } 24 | test() 25 | -------------------------------------------------------------------------------- /perf2.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 'use strict' 3 | 4 | import { AsyncLogicEngine } from './index.js' 5 | 6 | const x = new AsyncLogicEngine(undefined) 7 | async function main () { 8 | const logic = { 9 | if: [ 10 | { 11 | '>': [{ val: 'x' }, { '+': [11, 5, { '+': [1, { val: 'y' }, 1] }, 2] }] 12 | }, 13 | { 14 | '*': [{ val: 'x' }, { '*': { map: [[2, 5, 5], { val: [] }] } }] 15 | }, 16 | { 17 | '/': [{ val: 'x' }, { '-': { map: [[100, 50, 30, 10], { val: [] }] } }] 18 | } 19 | ] 20 | } 21 | console.time('built') 22 | const f = await x.build(logic) 23 | // console.log(f[Sync]) 24 | console.log(await f({ x: 15, y: 1 })) 25 | for (let i = 0; i < 2e6; i++) { 26 | await f({ x: i, y: i % 20 }) 27 | } 28 | console.timeEnd('built') 29 | } 30 | main() 31 | -------------------------------------------------------------------------------- /perf3.js: -------------------------------------------------------------------------------- 1 | import { LogicEngine } from './index.js' 2 | const x = new LogicEngine() 3 | const f = x.build({ '<': [{ val: [] }, 10] }) 4 | console.time('Logic Built') 5 | for (let i = 0; i < 1e6; i++) { 6 | f(i % 20) 7 | } 8 | console.timeEnd('Logic Built') 9 | function g (n) { 10 | return n < 10 11 | } 12 | console.time('Raw Code') 13 | for (let i = 0; i < 1e6; i++) { 14 | g(i % 20) 15 | } 16 | console.timeEnd('Raw Code') 17 | console.log('For 1M invocations') 18 | -------------------------------------------------------------------------------- /perf4.js: -------------------------------------------------------------------------------- 1 | 2 | import { LogicEngine } from './index.js' 3 | 4 | const optimized = new LogicEngine() 5 | const unoptimized = new LogicEngine(undefined, { disableInterpretedOptimization: true }) 6 | 7 | const dynamicRule = { '+': [1, 2, 3, { val: 'a' }] } 8 | const staticRule = { '+': [1, 2, 3, 4] } 9 | 10 | console.time('Optimized, Same Object, Dynamic') 11 | for (let i = 0; i < 1e6; i++) { 12 | optimized.run(dynamicRule, { a: i }) 13 | } 14 | console.timeEnd('Optimized, Same Object, Dynamic') 15 | 16 | console.time('Optimized, Same Object, Static') 17 | for (let i = 0; i < 1e6; i++) { 18 | optimized.run(staticRule) 19 | } 20 | console.timeEnd('Optimized, Same Object, Static') 21 | 22 | console.time('Unoptimized, Same Object, Dynamic') 23 | for (let i = 0; i < 1e6; i++) { 24 | unoptimized.run(dynamicRule, { a: i }) 25 | } 26 | console.timeEnd('Unoptimized, Same Object, Dynamic') 27 | 28 | console.time('Unoptimized, Same Object, Static') 29 | for (let i = 0; i < 1e6; i++) { 30 | unoptimized.run(staticRule) 31 | } 32 | console.timeEnd('Unoptimized, Same Object, Static') 33 | 34 | console.log('----') 35 | 36 | console.time('Optimized, Different Object, Dynamic') 37 | for (let i = 0; i < 1e6; i++) { 38 | optimized.run({ '+': [1, 2, 3, { val: 'a' }] }, { a: i }) 39 | } 40 | console.timeEnd('Optimized, Different Object, Dynamic') 41 | 42 | optimized.disableInterpretedOptimization = false 43 | 44 | console.time('Optimized, Different Object, Static') 45 | for (let i = 0; i < 1e6; i++) { 46 | optimized.run({ '+': [1, 2, 3, 4] }) 47 | } 48 | console.timeEnd('Optimized, Different Object, Static') 49 | 50 | console.time('Unoptimized, Different Object, Dynamic') 51 | for (let i = 0; i < 1e6; i++) { 52 | unoptimized.run({ '+': [1, 2, 3, { val: 'a' }] }, { a: i }) 53 | } 54 | console.timeEnd('Unoptimized, Different Object, Dynamic') 55 | 56 | console.time('Unoptimized, Different Object, Static') 57 | for (let i = 0; i < 1e6; i++) { 58 | unoptimized.run({ '+': [1, 2, 3, 4] }) 59 | } 60 | console.timeEnd('Unoptimized, Different Object, Static') 61 | -------------------------------------------------------------------------------- /suites/additional.json: -------------------------------------------------------------------------------- 1 | [ 2 | "# These are some tests from https://github.com/TotalTechGeek/json-logic-engine/commit/9125e91b5137938a8319de1103b0ebc5819e54e1", 3 | [ 4 | [ 5 | 1, 6 | { 7 | "val": "x" 8 | }, 9 | 3 10 | ], 11 | { 12 | "x": 2 13 | }, 14 | [ 15 | 1, 16 | 2, 17 | 3 18 | ] 19 | ], 20 | [ 21 | { 22 | "if": [ 23 | { 24 | "val": "x" 25 | }, 26 | [ 27 | { 28 | "val": "y" 29 | } 30 | ], 31 | 99 32 | ] 33 | }, 34 | { 35 | "x": true, 36 | "y": 42 37 | }, 38 | [ 39 | 42 40 | ] 41 | ], 42 | [ 43 | { 44 | "reduce": [ 45 | { 46 | "val": "integers" 47 | }, 48 | { 49 | "+": [ 50 | { 51 | "val": "current" 52 | }, 53 | { 54 | "val": "accumulator" 55 | } 56 | ] 57 | }, 58 | { 59 | "val": "start_with" 60 | } 61 | ] 62 | }, 63 | { 64 | "integers": [ 65 | 1, 66 | 2, 67 | 3, 68 | 4 69 | ], 70 | "start_with": 59 71 | }, 72 | 69 73 | ], 74 | { 75 | "description": "Simple Inlineable Val Chained", 76 | "rule": { "val": { "cat": ["te", "st"] } }, 77 | "data": { "test": 1 }, 78 | "result": 1 79 | } 80 | ] -------------------------------------------------------------------------------- /suites/arithmetic/divide.extra.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "Divide with dynamic empty array", 4 | "rule": { "/": { "preserve": [] } }, 5 | "error": { "type": "Invalid Arguments" }, 6 | "data": null 7 | }, 8 | { 9 | "description": "Divide with dynamic unary array", 10 | "rule": { "/": { "preserve": [2] } }, 11 | "result": 0.5, 12 | "decimal": true, 13 | "data": null 14 | }, 15 | { 16 | "description": "Divide with dynamic array", 17 | "rule": { "/": { "preserve": [8, 2] } }, 18 | "result": 4, 19 | "data": null 20 | } 21 | ] -------------------------------------------------------------------------------- /suites/arithmetic/divide.json: -------------------------------------------------------------------------------- 1 | [ 2 | "# Collection of Divide Operator Tests", 3 | { 4 | "description": "Divide", 5 | "rule": { "/": [4, 2] }, 6 | "result": 2, 7 | "data": null 8 | }, 9 | { 10 | "description": "Divide to Decimal", 11 | "rule": { "/": [2, 4] }, 12 | "result": 0.5, 13 | "data": null, 14 | "decimal": true 15 | }, 16 | { 17 | "description": "Divide with Multiple Operands", 18 | "rule": { "/": [8, 2, 2] }, 19 | "result": 2, 20 | "data": null 21 | }, 22 | { 23 | "description": "Divide with Multiple Operands (2)", 24 | "rule": { "/": [2, 2, 1] }, 25 | "result": 1, 26 | "data": null 27 | }, 28 | { 29 | "description": "Divide with Negative Numbers", 30 | "rule": { "/": [-1, 2] }, 31 | "result": -0.5, 32 | "data": null, 33 | "decimal": true 34 | }, 35 | { 36 | "description": "Divide with Strings", 37 | "rule": { "/": ["8", "2", "2"] }, 38 | "result": 2, 39 | "data": null 40 | }, 41 | { 42 | "description": "Divide with Booleans", 43 | "rule": { "/": [false, true] }, 44 | "result": 0, 45 | "data": null 46 | }, 47 | { 48 | "description": "Divide with Multiple Value Types", 49 | "rule": { "/": ["8", true, 2] }, 50 | "result": 4, 51 | "data": null 52 | }, 53 | { 54 | "description": "Divide with Multiple Value Types (2)", 55 | "rule": { "/": ["1", 1] }, 56 | "result": 1, 57 | "data": null 58 | }, 59 | { 60 | "description": "Divide with Single Operand (Number)", 61 | "rule": { "/": [1] }, 62 | "result": 1, 63 | "data": null 64 | }, 65 | { 66 | "description": "Divide with zero operands is an error", 67 | "rule": { "/": [] }, 68 | "error": { "type": "Invalid Arguments" }, 69 | "data": null 70 | }, 71 | { 72 | "description": "Divide with Single Operand, Direct (Number)", 73 | "rule": { "/": 1 }, 74 | "result": 1, 75 | "data": null 76 | }, 77 | { 78 | "description": "Divide with Single Operand, Direct (0)", 79 | "rule": { "/": 0 }, 80 | "error": { "type": "NaN" }, 81 | "data": null 82 | }, 83 | { 84 | "description": "Divide Operator with Single Operand (Number)", 85 | "rule": { "/": [1] }, 86 | "result": 1, 87 | "data": null 88 | }, 89 | { 90 | "description": "Divide Operator with Single Operand (Negative Number)", 91 | "rule": { "/": [-1] }, 92 | "result": -1, 93 | "data": null 94 | }, 95 | { 96 | "description": "Divide Operator with Single Operand, Direct (Number)", 97 | "rule": { "/": 1 }, 98 | "result": 1, 99 | "data": null 100 | }, 101 | { 102 | "description": "Divide Operator with Single Operand, Direct (2)", 103 | "rule": { "/": 2 }, 104 | "result": 0.5, 105 | "decimal": true, 106 | "data": null 107 | }, 108 | { 109 | "description": "Divide Operator with Single Operand, Direct (0)", 110 | "rule": { "/": 0 }, 111 | "error": { "type": "NaN" }, 112 | "data": null 113 | }, 114 | { 115 | "description": "Divide Operator with Single Operand (String)", 116 | "rule": { "/": ["1"] }, 117 | "result": 1, 118 | "data": null 119 | }, 120 | { 121 | "description": "Divide Operator with Single Operand, Direct (Negative Number String)", 122 | "rule": { "/": "-1" }, 123 | "result": -1, 124 | "data": null 125 | }, 126 | 127 | { 128 | "description": "Divide Operator with Single Operand, Direct (String 0)", 129 | "rule": { "/": "0" }, 130 | "error": { "type": "NaN" }, 131 | "data": null 132 | }, 133 | { 134 | "description": "Divide Operator with Single Operand, Direct (true)", 135 | "rule": { "/": true }, 136 | "result": 1, 137 | "data": null 138 | }, 139 | { 140 | "description": "Divide Operator with Single Operand, Direct (false)", 141 | "rule": { "/": false }, 142 | "error": { "type": "NaN" }, 143 | "data": null 144 | }, 145 | { 146 | "description": "Divide Operator with Single Operand, Direct (Empty String)", 147 | "rule": { "/": "" }, 148 | "error": { "type": "NaN" }, 149 | "data": null 150 | }, 151 | { 152 | "description": "Divide Operator with a Single Operand, Direct (null)", 153 | "rule": { "/": null }, 154 | "error": { "type": "NaN" }, 155 | "data": null 156 | }, 157 | { 158 | "description": "Divide with val", 159 | "rule": { "/": [{ "val": "x" }, { "val": "y" }] }, 160 | "data": { "x": 8, "y": 2 }, 161 | "result": 4 162 | }, 163 | { 164 | "description": "Divide by Zero", 165 | "rule": { "/": [0, 0] }, 166 | "error": { "type": "NaN" }, 167 | "data": null 168 | }, 169 | { 170 | "description": "Divide with String produces NaN", 171 | "rule": { "/": [1, "a"] }, 172 | "error": { "type": "NaN" }, 173 | "data": null 174 | }, 175 | { 176 | "description": "Divide with Array produces NaN", 177 | "rule": { "/": [1, [1]] }, 178 | "error": { "type": "NaN" }, 179 | "data": null 180 | }, 181 | { 182 | "description": "Any division by zero should return NaN", 183 | "rule": { "/": [1, 0] }, 184 | "error": { "type": "NaN" }, 185 | "data": null 186 | }, 187 | { 188 | "description": "Any division by zero should return NaN (2)", 189 | "rule": { "/": [8, 2, 0] }, 190 | "error": { "type": "NaN" }, 191 | "data": null 192 | }, 193 | { 194 | "description": "Divide can be chained with other operators", 195 | "rule": { "/": { "val": "arr" } }, 196 | "data": { "arr": [10, 5] }, 197 | "result": 2 198 | }, 199 | { 200 | "description": "Divide can be chained with other operators (2)", 201 | "rule": { "/": { "merge": [[20], 5] } }, 202 | "data": null, 203 | "result": 4 204 | }, 205 | { 206 | "description": "Divide can be chained with other operators (3)", 207 | "rule": { "/": { "map": [{ "val": "arr" }, { "val": "x" }] } }, 208 | "data": { 209 | "arr": [ 210 | { "x": 16 }, 211 | { "x": 2 }, 212 | { "x": 2 }, 213 | { "x": 2 } 214 | ] 215 | }, 216 | "result": 2 217 | } 218 | ] -------------------------------------------------------------------------------- /suites/arithmetic/max.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "Max of two numbers", 4 | "rule": { "max": [1, 2] }, 5 | "data": null, 6 | "result": 2 7 | }, 8 | { 9 | "description": "Max of two numbers (2)", 10 | "rule": { "max": [2, 1] }, 11 | "data": null, 12 | "result": 2 13 | }, 14 | { 15 | "description": "Max of three numbers", 16 | "rule": { "max": [1, 2, 3] }, 17 | "data": null, 18 | "result": 3 19 | }, 20 | { 21 | "description": "Max of three numbers (2)", 22 | "rule": { "max": [3, 2, 1] }, 23 | "data": null, 24 | "result": 3 25 | }, 26 | { 27 | "description": "Max of three numbers", 28 | "rule": { "max": [3, 3, 2] }, 29 | "data": null, 30 | "result": 3 31 | }, 32 | { 33 | "description": "Max of a multitude of numbers", 34 | "rule": { "max": [55, 33, 11, 66, 127, 25, 3] }, 35 | "data": null, 36 | "result": 127 37 | }, 38 | { 39 | "description": "Max with all negative numbers", 40 | "rule": { "max": [-1, -2, -3] }, 41 | "data": null, 42 | "result": -1 43 | }, 44 | { 45 | "description": "Max with a mix of positive and negative numbers", 46 | "rule": { "max": [-1, 2, -3] }, 47 | "data": null, 48 | "result": 2 49 | }, 50 | { 51 | "description": "Max of one number", 52 | "rule": { "max": [1] }, 53 | "data": null, 54 | "result": 1 55 | }, 56 | { 57 | "description": "Max of one number, direct", 58 | "rule": { "max": 7 }, 59 | "data": null, 60 | "result": 7 61 | }, 62 | { 63 | "description": "Max with an operator argument (showing it's not lazy)", 64 | "rule": { "max": [1, 2, 3, { "val": "a" }, 5, { "+": "3" }, 4] }, 65 | "data": { "a": 7 }, 66 | "result": 7 67 | }, 68 | { 69 | "description": "Max can chain with other operators", 70 | "rule": { "max": { "val": "arr" } }, 71 | "data": { "arr": [1, 2, 3] }, 72 | "result": 3 73 | }, 74 | { 75 | "description": "Max can chain with other operators (2)", 76 | "rule": { "max": { "val": "arr" } }, 77 | "data": { "arr": [6, 5, 4] }, 78 | "result": 6 79 | }, 80 | { 81 | "description": "Max can chain with other operators (3)", 82 | "rule": { "max": { "merge": [[1, 2], 3, [4, 5]] } }, 83 | "data": null, 84 | "result": 5 85 | }, 86 | { 87 | "description": "Max can chain with other operators (4)", 88 | "rule": { "max": { "map": [{ "val": "people" }, { "val": "age" }] } }, 89 | "data": { 90 | "people": [ 91 | { "name": "John", "age": 30 }, 92 | { "name": "Jane", "age": 25 }, 93 | { "name": "Bob", "age": 35 }, 94 | { "name": "Alice", "age": 28 } 95 | ] 96 | }, 97 | "result": 35 98 | }, 99 | { 100 | "description": "Max of a string", 101 | "rule": { "max": ["1"] }, 102 | "data": null, 103 | "error": { "type": "Invalid Arguments" } 104 | }, 105 | { 106 | "description": "Max of a string, direct", 107 | "rule": { "max": "2" }, 108 | "data": null, 109 | "error": { "type": "Invalid Arguments" } 110 | }, 111 | { 112 | "description": "Max of multiple strings", 113 | "rule": { "max": ["1", "2", "3"] }, 114 | "data": null, 115 | "error": { "type": "Invalid Arguments" } 116 | }, 117 | { 118 | "description": "Max of a string and a number", 119 | "rule": { "max": ["1", 2] }, 120 | "data": null, 121 | "error": { "type": "Invalid Arguments" } 122 | }, 123 | { 124 | "description": "Max of a string and a number (2)", 125 | "rule": { "max": [2, "1"] }, 126 | "data": null, 127 | "error": { "type": "Invalid Arguments" } 128 | }, 129 | { 130 | "description": "Max of multiple numbers and a string", 131 | "rule": { "max": [1, 2, 3, 4, 5, "3", 4] }, 132 | "data": null, 133 | "error": { "type": "Invalid Arguments" } 134 | }, 135 | { 136 | "description": "Max of a boolean", 137 | "rule": { "max": [true] }, 138 | "data": null, 139 | "error": { "type": "Invalid Arguments" } 140 | }, 141 | { 142 | "description": "Max of a boolean, direct", 143 | "rule": { "max": true }, 144 | "data": null, 145 | "error": { "type": "Invalid Arguments" } 146 | }, 147 | { 148 | "description": "Max of multiple booleans", 149 | "rule": { "max": [true, false, true] }, 150 | "data": null, 151 | "error": { "type": "Invalid Arguments" } 152 | }, 153 | { 154 | "description": "Max of boolean and a number", 155 | "rule": { "max": [true, 2] }, 156 | "data": null, 157 | "error": { "type": "Invalid Arguments" } 158 | }, 159 | { 160 | "description": "Max of boolean and a number (2)", 161 | "rule": { "max": [2, true] }, 162 | "data": null, 163 | "error": { "type": "Invalid Arguments" } 164 | }, 165 | { 166 | "description": "Max of a null", 167 | "rule": { "max": [null] }, 168 | "data": null, 169 | "error": { "type": "Invalid Arguments" } 170 | }, 171 | { 172 | "description": "Max of a null, direct", 173 | "rule": { "max": null }, 174 | "data": null, 175 | "error": { "type": "Invalid Arguments" } 176 | }, 177 | { 178 | "description": "Max of multiple nulls", 179 | "rule": { "max": [null, null, null] }, 180 | "data": null, 181 | "error": { "type": "Invalid Arguments" } 182 | }, 183 | { 184 | "description": "Max of a null and a number", 185 | "rule": { "max": [null, 2] }, 186 | "data": null, 187 | "error": { "type": "Invalid Arguments" } 188 | }, 189 | { 190 | "description": "Max of a null and a number (2)", 191 | "rule": { "max": [2, null] }, 192 | "data": null, 193 | "error": { "type": "Invalid Arguments" } 194 | }, 195 | { 196 | "description": "Max of an empty array", 197 | "rule": { "max": [[]] }, 198 | "data": null, 199 | "error": { "type": "Invalid Arguments" } 200 | }, 201 | { 202 | "description": "Max of an array with items", 203 | "rule": { "max": [[1, 2, 3]] }, 204 | "data": null, 205 | "error": { "type": "Invalid Arguments" } 206 | }, 207 | { 208 | "description": "Max with multiple arrays", 209 | "rule": { "max": [[1, 2], [3, 4], []] }, 210 | "data": null, 211 | "error": { "type": "Invalid Arguments" } 212 | }, 213 | { 214 | "description": "Max of an array and a number", 215 | "rule": { "max": [[1, 2], 2] }, 216 | "data": null, 217 | "error": { "type": "Invalid Arguments" } 218 | }, 219 | { 220 | "description": "Max of an array and a number (2)", 221 | "rule": { "max": [2, [1, 2]] }, 222 | "data": null, 223 | "error": { "type": "Invalid Arguments" } 224 | }, 225 | { 226 | "description": "Max of an object", 227 | "rule": { "max": [{}] }, 228 | "data": null, 229 | "error": { "type": "Invalid Arguments" } 230 | }, 231 | { 232 | "description": "Max of an object, direct", 233 | "rule": { "max": {} }, 234 | "data": null, 235 | "error": { "type": "Invalid Arguments" } 236 | }, 237 | { 238 | "description": "Max of multiple objects", 239 | "rule": { "max": [{}, {}, {}] }, 240 | "data": null, 241 | "error": { "type": "Invalid Arguments" } 242 | }, 243 | { 244 | "description": "Max of an object and a number", 245 | "rule": { "max": [{}, 2] }, 246 | "data": null, 247 | "error": { "type": "Invalid Arguments" } 248 | }, 249 | { 250 | "description": "Max of an object and a number (2)", 251 | "rule": { "max": [2, {}] }, 252 | "data": null, 253 | "error": { "type": "Invalid Arguments" } 254 | }, 255 | { 256 | "description": "Max with zero arguments throws", 257 | "rule": { "max": [] }, 258 | "data": null, 259 | "error": { "type": "Invalid Arguments" } 260 | } 261 | ] -------------------------------------------------------------------------------- /suites/arithmetic/min.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "Min of two numbers", 4 | "rule": { "min": [1, 2] }, 5 | "data": null, 6 | "result": 1 7 | }, 8 | { 9 | "description": "Min of two numbers (2)", 10 | "rule": { "min": [2, 1] }, 11 | "data": null, 12 | "result": 1 13 | }, 14 | { 15 | "description": "Min of three numbers", 16 | "rule": { "min": [1, 2, 3] }, 17 | "data": null, 18 | "result": 1 19 | }, 20 | { 21 | "description": "Min of three numbers (2)", 22 | "rule": { "min": [3, 2, 1] }, 23 | "data": null, 24 | "result": 1 25 | }, 26 | { 27 | "description": "Min of three numbers", 28 | "rule": { "min": [3, 3, 2] }, 29 | "data": null, 30 | "result": 2 31 | }, 32 | { 33 | "description": "Min of a multitude of numbers", 34 | "rule": { "min": [55, 33, 11, 66, 127, 25, 3] }, 35 | "data": null, 36 | "result": 3 37 | }, 38 | { 39 | "description": "Min with all negative numbers", 40 | "rule": { "min": [-1, -2, -3] }, 41 | "data": null, 42 | "result": -3 43 | }, 44 | { 45 | "description": "Min with a mix of positive and negative numbers", 46 | "rule": { "min": [-1, 2, -3] }, 47 | "data": null, 48 | "result": -3 49 | }, 50 | { 51 | "description": "Min of one number", 52 | "rule": { "min": [1] }, 53 | "data": null, 54 | "result": 1 55 | }, 56 | { 57 | "description": "Min of one number, direct", 58 | "rule": { "min": 7 }, 59 | "data": null, 60 | "result": 7 61 | }, 62 | { 63 | "description": "Min with an operator argument (showing it's not lazy)", 64 | "rule": { "min": [1, 2, 3, { "val": "a" }, 5, { "+": "3" }, 4] }, 65 | "data": { "a": 7 }, 66 | "result": 1 67 | }, 68 | { 69 | "description": "Min can chain with other operators", 70 | "rule": { "min": { "val": "arr" } }, 71 | "data": { "arr": [1, 2, 3] }, 72 | "result": 1 73 | }, 74 | { 75 | "description": "Min can chain with other operators (2)", 76 | "rule": { "min": { "val": "arr" } }, 77 | "data": { "arr": [6, 5, 4] }, 78 | "result": 4 79 | }, 80 | { 81 | "description": "Min can chain with other operators (3)", 82 | "rule": { "min": { "merge": [[1, 2], 3, [4, 5]] } }, 83 | "data": null, 84 | "result": 1 85 | }, 86 | { 87 | "description": "Min can chain with other operators (4)", 88 | "rule": { "min": { "map": [{ "val": "people" }, { "val": "age" }] } }, 89 | "data": { 90 | "people": [ 91 | { "name": "John", "age": 30 }, 92 | { "name": "Jane", "age": 25 }, 93 | { "name": "Bob", "age": 35 }, 94 | { "name": "Alice", "age": 28 } 95 | ] 96 | }, 97 | "result": 25 98 | }, 99 | { 100 | "description": "Min of a string", 101 | "rule": { "min": ["1"] }, 102 | "data": null, 103 | "error": { "type": "Invalid Arguments" } 104 | }, 105 | { 106 | "description": "Min of a string, direct", 107 | "rule": { "min": "2" }, 108 | "data": null, 109 | "error": { "type": "Invalid Arguments" } 110 | }, 111 | { 112 | "description": "Min of multiple strings", 113 | "rule": { "min": ["1", "2", "3"] }, 114 | "data": null, 115 | "error": { "type": "Invalid Arguments" } 116 | }, 117 | { 118 | "description": "Min of a string and a number", 119 | "rule": { "min": ["1", 2] }, 120 | "data": null, 121 | "error": { "type": "Invalid Arguments" } 122 | }, 123 | { 124 | "description": "Min of a string and a number (2)", 125 | "rule": { "min": [2, "1"] }, 126 | "data": null, 127 | "error": { "type": "Invalid Arguments" } 128 | }, 129 | { 130 | "description": "Min of multiple numbers and a string", 131 | "rule": { "min": [1, 2, 3, 4, 5, "3", 4] }, 132 | "data": null, 133 | "error": { "type": "Invalid Arguments" } 134 | }, 135 | { 136 | "description": "Min of a boolean", 137 | "rule": { "min": [true] }, 138 | "data": null, 139 | "error": { "type": "Invalid Arguments" } 140 | }, 141 | { 142 | "description": "Min of a boolean, direct", 143 | "rule": { "min": true }, 144 | "data": null, 145 | "error": { "type": "Invalid Arguments" } 146 | }, 147 | { 148 | "description": "Min of multiple booleans", 149 | "rule": { "min": [true, false, true] }, 150 | "data": null, 151 | "error": { "type": "Invalid Arguments" } 152 | }, 153 | { 154 | "description": "Min of boolean and a number", 155 | "rule": { "min": [true, 2] }, 156 | "data": null, 157 | "error": { "type": "Invalid Arguments" } 158 | }, 159 | { 160 | "description": "Min of boolean and a number (2)", 161 | "rule": { "min": [2, true] }, 162 | "data": null, 163 | "error": { "type": "Invalid Arguments" } 164 | }, 165 | { 166 | "description": "Min of a null", 167 | "rule": { "min": [null] }, 168 | "data": null, 169 | "error": { "type": "Invalid Arguments" } 170 | }, 171 | { 172 | "description": "Min of a null, direct", 173 | "rule": { "min": null }, 174 | "data": null, 175 | "error": { "type": "Invalid Arguments" } 176 | }, 177 | { 178 | "description": "Min of multiple nulls", 179 | "rule": { "min": [null, null, null] }, 180 | "data": null, 181 | "error": { "type": "Invalid Arguments" } 182 | }, 183 | { 184 | "description": "Min of a null and a number", 185 | "rule": { "min": [null, 2] }, 186 | "data": null, 187 | "error": { "type": "Invalid Arguments" } 188 | }, 189 | { 190 | "description": "Min of a null and a number (2)", 191 | "rule": { "min": [2, null] }, 192 | "data": null, 193 | "error": { "type": "Invalid Arguments" } 194 | }, 195 | { 196 | "description": "Min of an empty array", 197 | "rule": { "min": [[]] }, 198 | "data": null, 199 | "error": { "type": "Invalid Arguments" } 200 | }, 201 | { 202 | "description": "Min of an array with items", 203 | "rule": { "min": [[1, 2, 3]] }, 204 | "data": null, 205 | "error": { "type": "Invalid Arguments" } 206 | }, 207 | { 208 | "description": "Min with multiple arrays", 209 | "rule": { "min": [[1, 2], [3, 4], []] }, 210 | "data": null, 211 | "error": { "type": "Invalid Arguments" } 212 | }, 213 | { 214 | "description": "Min of an array and a number", 215 | "rule": { "min": [[1, 2], 2] }, 216 | "data": null, 217 | "error": { "type": "Invalid Arguments" } 218 | }, 219 | { 220 | "description": "Min of an array and a number (2)", 221 | "rule": { "min": [2, [1, 2]] }, 222 | "data": null, 223 | "error": { "type": "Invalid Arguments" } 224 | }, 225 | { 226 | "description": "Min of an object", 227 | "rule": { "min": [{}] }, 228 | "data": null, 229 | "error": { "type": "Invalid Arguments" } 230 | }, 231 | { 232 | "description": "Min of an object, direct", 233 | "rule": { "min": {} }, 234 | "data": null, 235 | "error": { "type": "Invalid Arguments" } 236 | }, 237 | { 238 | "description": "Min of multiple objects", 239 | "rule": { "min": [{}, {}, {}] }, 240 | "data": null, 241 | "error": { "type": "Invalid Arguments" } 242 | }, 243 | { 244 | "description": "Min of an object and a number", 245 | "rule": { "min": [{}, 2] }, 246 | "data": null, 247 | "error": { "type": "Invalid Arguments" } 248 | }, 249 | { 250 | "description": "Min of an object and a number (2)", 251 | "rule": { "min": [2, {}] }, 252 | "data": null, 253 | "error": { "type": "Invalid Arguments" } 254 | }, 255 | { 256 | "description": "Min with zero arguments throws", 257 | "rule": { "min": [] }, 258 | "data": null, 259 | "error": { "type": "Invalid Arguments" } 260 | } 261 | ] -------------------------------------------------------------------------------- /suites/arithmetic/minus.extra.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "Minus with dynamic empty array", 4 | "rule": { "-": { "preserve": [] }}, 5 | "error": { "type": "Invalid Arguments" }, 6 | "data": null 7 | }, 8 | { 9 | "description": "Minus with dynamic unary array", 10 | "rule": { "-": { "preserve": [7] }}, 11 | "result": -7, 12 | "data": null 13 | }, 14 | { 15 | "description": "Minus with dynamic array", 16 | "rule": { "-": { "preserve": [7, 8] }}, 17 | "result": -1, 18 | "data": null 19 | } 20 | ] -------------------------------------------------------------------------------- /suites/arithmetic/minus.json: -------------------------------------------------------------------------------- 1 | [ 2 | "# Collection of Minus Operator Tests", 3 | { 4 | "description": "Subtraction", 5 | "rule": { "-": [1, 2] }, 6 | "result": -1, 7 | "data": null 8 | }, 9 | { 10 | "description": "Subtraction (2)", 11 | "rule": { "-": [5, 12] }, 12 | "result": -7, 13 | "data": null 14 | }, 15 | { 16 | "description": "Subtraction with Multiple Operands", 17 | "rule": { "-": [1, 2, 3, 4] }, 18 | "result": -8, 19 | "data": null 20 | }, 21 | { 22 | "description": "Subtraction with Negative Numbers", 23 | "rule": { "-": [-1, 0, 5] }, 24 | "result": -6, 25 | "data": null 26 | }, 27 | { 28 | "description": "Subtraction with Strings", 29 | "rule": { "-": ["1", "2", "3"] }, 30 | "result": -4, 31 | "data": null 32 | }, 33 | { 34 | "description": "Subtraction with Booleans", 35 | "rule": { "-": [true, false, true] }, 36 | "result": 0, 37 | "data": null 38 | }, 39 | { 40 | "description": "Subtraction with Multiple Value Types", 41 | "rule": { "-": [1, "2", 3, "4", "", true, false, null] }, 42 | "result": -9, 43 | "data": null 44 | }, 45 | { 46 | "description": "Minus Operator with Single Operand (Number)", 47 | "rule": { "-": [1] }, 48 | "result": -1, 49 | "data": null 50 | }, 51 | { 52 | "description": "Minus Operator with Single Operand (Negative Number)", 53 | "rule": { "-": [-1] }, 54 | "result": 1, 55 | "data": null 56 | }, 57 | { 58 | "description": "Minus with zero operands is an error", 59 | "rule": { "-": [] }, 60 | "error": { "type": "Invalid Arguments" }, 61 | "data": null 62 | }, 63 | { 64 | "description": "Minus Operator with Single Operand, Direct (Number)", 65 | "rule": { "-": 1 }, 66 | "result": -1, 67 | "data": null 68 | }, 69 | { 70 | "description": "Minus Operator with Single Operand, Direct (0)", 71 | "rule": { "-": 0 }, 72 | "result": 0, 73 | "data": null 74 | }, 75 | { 76 | "description": "Minus Operator with Single Operand (String)", 77 | "rule": { "-": ["1"] }, 78 | "result": -1, 79 | "data": null 80 | }, 81 | { 82 | "description": "Minus Operator with Single Operand, Direct (Negative Number String)", 83 | "rule": { "-": "-1" }, 84 | "result": 1, 85 | "data": null 86 | }, 87 | 88 | { 89 | "description": "Minus Operator with Single Operand, Direct (String 0)", 90 | "rule": { "-": "0" }, 91 | "result": 0, 92 | "data": null 93 | }, 94 | { 95 | "description": "Minus Operator with Single Operand, Direct (true)", 96 | "rule": { "-": true }, 97 | "result": -1, 98 | "data": null 99 | }, 100 | { 101 | "description": "Minus Operator with Single Operand, Direct (false)", 102 | "rule": { "-": false }, 103 | "result": 0, 104 | "data": null 105 | }, 106 | { 107 | "description": "Minus Operator with Single Operand, Direct (Empty String)", 108 | "rule": { "-": "" }, 109 | "result": 0, 110 | "data": null 111 | }, 112 | { 113 | "description": "Minus Operator with a Single Operand, Direct (null)", 114 | "rule": { "-": null }, 115 | "result": 0, 116 | "data": null 117 | }, 118 | { 119 | "description": "Subtraction with val", 120 | "rule": { "-": [{ "val": "x" }, { "val": "y" }] }, 121 | "data": { "x": 1, "y": 2 }, 122 | "result": -1 123 | }, 124 | { 125 | "description": "Subtraction with string produces NaN", 126 | "rule": { "-": ["Hey", 1] }, 127 | "error": { "type": "NaN" }, 128 | "data": null 129 | }, 130 | { 131 | "description": "Subtraction with Array produces NaN", 132 | "rule": { "-": [[1], 1] }, 133 | "error": { "type": "NaN" }, 134 | "data": null 135 | }, 136 | { 137 | "description": "Minus can be chained with other operators", 138 | "rule": { "-": { "val": "arr" } }, 139 | "data": { "arr": [10, 7] }, 140 | "result": 3 141 | }, 142 | { 143 | "description": "Minus can be chained with other operators (2)", 144 | "rule": { "-": { "merge": [[1], 5] } }, 145 | "data": null, 146 | "result": -4 147 | }, 148 | { 149 | "description": "Minus can be chained with other operators (3)", 150 | "rule": { "-": { "map": [{ "val": "people" }, { "val": "age" }] } }, 151 | "data": { 152 | "people": [ 153 | { "name": "John", "age": 30 }, 154 | { "name": "Jane", "age": 25 }, 155 | { "name": "Bob", "age": 35 }, 156 | { "name": "Alice", "age": 28 } 157 | ] 158 | }, 159 | "result": -58 160 | } 161 | ] -------------------------------------------------------------------------------- /suites/arithmetic/modulo.extra.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "Modulo with dynamic array invalid", 4 | "rule": { "%": { "preserve": [] } }, 5 | "error": { "type": "Invalid Arguments" }, 6 | "data": null 7 | }, 8 | { 9 | "description": "Modulo with dynamic array invalid", 10 | "rule": { "%": { "preserve": [1] } }, 11 | "error": { "type": "Invalid Arguments" }, 12 | "data": null 13 | } 14 | ] -------------------------------------------------------------------------------- /suites/arithmetic/modulo.json: -------------------------------------------------------------------------------- 1 | [ 2 | "# Collection of Modulo Operator Tests", 3 | { 4 | "description": "Modulo", 5 | "rule": { "%": [4, 2] }, 6 | "result": 0, 7 | "data": null 8 | }, 9 | { 10 | "description": "Modulo (2)", 11 | "rule": { "%": [2, 2] }, 12 | "result": 0, 13 | "data": null 14 | }, 15 | { 16 | "description": "Modulo (3)", 17 | "rule": { "%": [3, 2] }, 18 | "result": 1, 19 | "data": null 20 | }, 21 | { 22 | "description": "Modulo with a Decimal Operand", 23 | "rule": { "%": [1, 0.5] }, 24 | "result": 0, 25 | "data": null, 26 | "decimal": true 27 | }, 28 | { 29 | "description": "Modulo with Multiple Operands", 30 | "rule": { "%": [8, 6, 3] }, 31 | "result": 2, 32 | "data": null 33 | }, 34 | { 35 | "description": "Modulo with Multiple Operands (2)", 36 | "rule": { "%": [2, 2, 1] }, 37 | "result": 0, 38 | "data": null 39 | }, 40 | { 41 | "description": "Modulo with Negative Numbers", 42 | "rule": { "%": [-1, 2] }, 43 | "result": -1, 44 | "data": null 45 | }, 46 | { 47 | "description": "Modulo with Strings", 48 | "rule": { "%": ["8", "2"] }, 49 | "result": 0, 50 | "data": null 51 | }, 52 | { 53 | "description": "Modulo with Booleans", 54 | "rule": { "%": [false, true] }, 55 | "result": 0, 56 | "data": null 57 | }, 58 | { 59 | "description": "Modulo with Multiple Value Types", 60 | "rule": { "%": ["8", 3, true] }, 61 | "result": 0, 62 | "data": null 63 | }, 64 | { 65 | "description": "Modulo with Multiple Value Types (2)", 66 | "rule": { "%": ["1", 1] }, 67 | "result": 0, 68 | "data": null 69 | }, 70 | { 71 | "description": "Modulo with Single Operand (Number)", 72 | "rule": { "%": [1] }, 73 | "error": { "type": "Invalid Arguments"}, 74 | "data": null 75 | }, 76 | { 77 | "description": "Modulo with Single Operand, Direct (Number)", 78 | "rule": { "%": 1 }, 79 | "error": { "type": "Invalid Arguments"}, 80 | "data": null 81 | }, 82 | { 83 | "description": "Modulo with Single Operand, Direct (0)", 84 | "rule": { "%": 0 }, 85 | "error": { "type": "Invalid Arguments"}, 86 | "data": null 87 | }, 88 | { 89 | "description": "Modulo Operator with Single Operand (Number)", 90 | "rule": { "%": [1] }, 91 | "error": { "type": "Invalid Arguments"}, 92 | "data": null 93 | }, 94 | { 95 | "description": "Modulo Operator with Single Operand (Negative Number)", 96 | "rule": { "%": [-1] }, 97 | "error": { "type": "Invalid Arguments"}, 98 | "data": null 99 | }, 100 | { 101 | "description": "Modulo with zero operands is an error", 102 | "rule": { "%": [] }, 103 | "error": { "type": "Invalid Arguments" }, 104 | "data": null 105 | }, 106 | { 107 | "description": "Modulo Operator with Single Operand, Direct (Number)", 108 | "rule": { "%": 1 }, 109 | "error": { "type": "Invalid Arguments"}, 110 | "data": null 111 | }, 112 | { 113 | "description": "Modulo Operator with Single Operand, Direct (0)", 114 | "rule": { "%": 0 }, 115 | "error": { "type": "Invalid Arguments"}, 116 | "data": null 117 | }, 118 | { 119 | "description": "Modulo Operator with Single Operand (String)", 120 | "rule": { "%": ["1"] }, 121 | "error": { "type": "Invalid Arguments"}, 122 | "data": null 123 | }, 124 | { 125 | "description": "Modulo Operator with Single Operand, Direct (Negative Number String)", 126 | "rule": { "%": "-1" }, 127 | "error": { "type": "Invalid Arguments"}, 128 | "data": null 129 | }, 130 | 131 | { 132 | "description": "Modulo Operator with Single Operand, Direct (String 0)", 133 | "rule": { "%": "0" }, 134 | "error": { "type": "Invalid Arguments"}, 135 | "data": null 136 | }, 137 | { 138 | "description": "Modulo Operator with Single Operand, Direct (true)", 139 | "rule": { "%": true }, 140 | "error": { "type": "Invalid Arguments"}, 141 | "data": null 142 | }, 143 | { 144 | "description": "Modulo Operator with Single Operand, Direct (false)", 145 | "rule": { "%": false }, 146 | "error": { "type": "Invalid Arguments"}, 147 | "data": null 148 | }, 149 | { 150 | "description": "Modulo Operator with Single Operand, Direct (Empty String)", 151 | "rule": { "%": "" }, 152 | "error": { "type": "Invalid Arguments"}, 153 | "data": null 154 | }, 155 | { 156 | "description": "Modulo Operator with a Single Operand, Direct (null)", 157 | "rule": { "%": null }, 158 | "error": { "type": "Invalid Arguments"}, 159 | "data": null 160 | }, 161 | { 162 | "description": "Modulo with val", 163 | "rule": { "%": [{ "val": "x" }, { "val": "y" }] }, 164 | "data": { "x": 11, "y": 6 }, 165 | "result": 5 166 | }, 167 | { 168 | "description": "Modulo with string produces NaN", 169 | "rule": { "%": ["Hey", 1] }, 170 | "error": { "type": "NaN" }, 171 | "data": null 172 | }, 173 | { 174 | "description": "Modulo with array produces NaN", 175 | "rule": { "%": [[1], 1] }, 176 | "error": { "type": "NaN" }, 177 | "data": null 178 | }, 179 | { 180 | "description": "Modulo with a negative dividend", 181 | "rule": { "%": [-8, 3] }, 182 | "result": -2, 183 | "data": null 184 | }, 185 | { 186 | "description": "Modulo with a negative divisor", 187 | "rule": { "%": [8, -3] }, 188 | "result": 2, 189 | "data": null 190 | }, 191 | { 192 | "description": "Modulo can be chained with other operators", 193 | "rule": { "%": { "val": "arr" } }, 194 | "data": { "arr": [10, 3] }, 195 | "result": 1 196 | }, 197 | { 198 | "description": "Modulo can be chained with other operators (2)", 199 | "rule": { "%": { "merge": [[20], 3] } }, 200 | "data": null, 201 | "result": 2 202 | }, 203 | { 204 | "description": "Modulo can be chained with other operators (3)", 205 | "rule": { "%": { "map": [{ "val": "arr" }, { "val": "x" }] } }, 206 | "data": { 207 | "arr": [ 208 | { "x": 17 }, 209 | { "x": 4 } 210 | ] 211 | }, 212 | "result": 1 213 | } 214 | ] -------------------------------------------------------------------------------- /suites/arithmetic/multiply.extra.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "Multiply with dynamic empty array", 4 | "rule": { "*": { "preserve": [] } }, 5 | "result": 1, 6 | "data": null 7 | }, 8 | { 9 | "description": "Multiply with dynamic unary array", 10 | "rule": { "*": { "preserve": [2] } }, 11 | "result": 2, 12 | "data": null 13 | }, 14 | { 15 | "description": "Multiply with dynamic array", 16 | "rule": { "*": { "preserve": [8, 2] } }, 17 | "result": 16, 18 | "data": null 19 | } 20 | ] -------------------------------------------------------------------------------- /suites/arithmetic/multiply.json: -------------------------------------------------------------------------------- 1 | [ 2 | "# Collection of Multiply Operator Tests", 3 | { 4 | "description": "Multiply", 5 | "rule": { "*": [3, 2] }, 6 | "result": 6, 7 | "data": null 8 | }, 9 | { 10 | "description": "Multiply with Multiple Operands", 11 | "rule": { "*": [5, 10, 2] }, 12 | "result": 100, 13 | "data": null 14 | }, 15 | { 16 | "description": "Multiply with Multiple Operands (2)", 17 | "rule": { "*": [2, 2, 2] }, 18 | "result": 8, 19 | "data": null 20 | }, 21 | { 22 | "description": "Multiply with Negative Numbers", 23 | "rule": { "*": [-1, 2, 5] }, 24 | "result": -10, 25 | "data": null 26 | }, 27 | { 28 | "description": "Multiply with Strings", 29 | "rule": { "*": ["1", "2", "3"] }, 30 | "result": 6, 31 | "data": null 32 | }, 33 | { 34 | "description": "Multiply with Booleans", 35 | "rule": { "*": [true, false, true] }, 36 | "result": 0, 37 | "data": null 38 | }, 39 | { 40 | "description": "Multiply with Multiple Value Types", 41 | "rule": { "*": [1, "2", 3, "4", true] }, 42 | "result": 24, 43 | "data": null 44 | }, 45 | { 46 | "description": "Multiply with Multiple Value Types (2)", 47 | "rule": { "*": [1, "1"] }, 48 | "result": 1, 49 | "data": null 50 | }, 51 | { 52 | "description": "Multiply with Multiple Value Types (3)", 53 | "rule": { "*": ["1", null] }, 54 | "result": 0, 55 | "data": null 56 | }, 57 | { 58 | "description": "Multiply with Single Operand (Number)", 59 | "rule": { "*": [1] }, 60 | "result": 1, 61 | "data": null 62 | }, 63 | { 64 | "description": "Multiply with Single Operand, Direct (Number)", 65 | "rule": { "*": 1 }, 66 | "result": 1, 67 | "data": null 68 | }, 69 | { 70 | "description": "Multiply with Single Operand, Direct (0)", 71 | "rule": { "*": 0 }, 72 | "result": 0, 73 | "data": null 74 | }, 75 | { 76 | "description": "Multiply Operator with Single Operand (Number)", 77 | "rule": { "*": [1] }, 78 | "result": 1, 79 | "data": null 80 | }, 81 | { 82 | "description": "Multiply Operator with Single Operand (Negative Number)", 83 | "rule": { "*": [-1] }, 84 | "result": -1, 85 | "data": null 86 | }, 87 | { 88 | "description": "Multiply with zero operands is 1 (Product)", 89 | "rule": { "*": [] }, 90 | "result": 1, 91 | "data": null 92 | }, 93 | { 94 | "description": "Multiply Operator with Single Operand, Direct (Number)", 95 | "rule": { "*": 1 }, 96 | "result": 1, 97 | "data": null 98 | }, 99 | { 100 | "description": "Multiply Operator with Single Operand, Direct (0)", 101 | "rule": { "*": 0 }, 102 | "result": 0, 103 | "data": null 104 | }, 105 | { 106 | "description": "Multiply Operator with Single Operand (String)", 107 | "rule": { "*": ["1"] }, 108 | "result": 1, 109 | "data": null 110 | }, 111 | { 112 | "description": "Multiply Operator with Single Operand, Direct (Negative Number String)", 113 | "rule": { "*": "-1" }, 114 | "result": -1, 115 | "data": null 116 | }, 117 | 118 | { 119 | "description": "Multiply Operator with Single Operand, Direct (String 0)", 120 | "rule": { "*": "0" }, 121 | "result": 0, 122 | "data": null 123 | }, 124 | { 125 | "description": "Multiply Operator with Single Operand, Direct (true)", 126 | "rule": { "*": true }, 127 | "result": 1, 128 | "data": null 129 | }, 130 | { 131 | "description": "Multiply Operator with Single Operand, Direct (false)", 132 | "rule": { "*": false }, 133 | "result": 0, 134 | "data": null 135 | }, 136 | { 137 | "description": "Multiply Operator with Single Operand, Direct (Empty String)", 138 | "rule": { "*": "" }, 139 | "result": 0, 140 | "data": null 141 | }, 142 | { 143 | "description": "Multiply Operator with a Single Operand, Direct (null)", 144 | "rule": { "*": null }, 145 | "result": 0, 146 | "data": null 147 | }, 148 | { 149 | "description": "Multiply with val", 150 | "rule": { "*": [{ "val": "x" }, { "val": "y" }] }, 151 | "data": { "x": 8, "y": 2 }, 152 | "result": 16 153 | }, 154 | { 155 | "description": "Multiply with string produces NaN", 156 | "rule": { "*": ["Hey", 1] }, 157 | "error": { "type": "NaN" }, 158 | "data": null 159 | }, 160 | { 161 | "description": "Multiply with a single string produces NaN", 162 | "rule": { "*": ["Hey"] }, 163 | "error": { "type": "NaN" }, 164 | "data": null 165 | }, 166 | { 167 | "description": "Multiply with Array produces NaN", 168 | "rule": { "*": [[1], 1] }, 169 | "error": { "type": "NaN" }, 170 | "data": null 171 | }, 172 | { 173 | "description": "Multiply can be chained with other operators", 174 | "rule": { "*": { "val": "arr" } }, 175 | "data": { "arr": [1, 2, 3] }, 176 | "result": 6 177 | }, 178 | { 179 | "description": "Multiply can be chained with other operators (2)", 180 | "rule": { "*": { "merge": [[1, 2], 3, [4, 5]] } }, 181 | "data": null, 182 | "result": 120 183 | }, 184 | { 185 | "description": "Multiply can be chained with other operators (3)", 186 | "rule": { "*": { "map": [{ "val": "arr" }, { "val": "x" }] } }, 187 | "data": { 188 | "arr": [ 189 | { "x": 30 }, 190 | { "x": 25 }, 191 | { "x": 35 }, 192 | { "x": 28 } 193 | ] 194 | }, 195 | "result": 735000 196 | } 197 | ] -------------------------------------------------------------------------------- /suites/arithmetic/plus.extra.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "Plus operator with empty array dynamic", 4 | "rule": { "+": { "preserve": [] } }, 5 | "result": 0, 6 | "data": null 7 | }, 8 | { 9 | "description": "Plus operator with unary array dynamic", 10 | "rule": { "+": { "preserve": [7] } }, 11 | "result": 7, 12 | "data": null 13 | }, 14 | { 15 | "description": "Plus operator with array dynamic", 16 | "rule": { "+": { "preserve": [7, 8] } }, 17 | "result": 15, 18 | "data": null 19 | } 20 | ] -------------------------------------------------------------------------------- /suites/arithmetic/plus.json: -------------------------------------------------------------------------------- 1 | [ 2 | "# Collection of Plus Operator Tests", 3 | { 4 | "description": "Addition", 5 | "rule": { "+": [1, 2] }, 6 | "result": 3, 7 | "data": null 8 | }, 9 | { 10 | "description": "Addition (2)", 11 | "rule": { "+": [5, 12] }, 12 | "result": 17, 13 | "data": null 14 | }, 15 | { 16 | "description": "Addition with Multiple Operands", 17 | "rule": { "+": [1, 2, 3, 4] }, 18 | "result": 10, 19 | "data": null 20 | }, 21 | { 22 | "description": "Addition with Negative Numbers", 23 | "rule": { "+": [-1, 0, 5] }, 24 | "result": 4, 25 | "data": null 26 | }, 27 | { 28 | "description": "Addition with Strings", 29 | "rule": { "+": ["1", "2", "3"] }, 30 | "result": 6, 31 | "data": null 32 | }, 33 | { 34 | "description": "Addition with Booleans", 35 | "rule": { "+": [true, false, true] }, 36 | "result": 2, 37 | "data": null 38 | }, 39 | { 40 | "description": "Addition with Multiple Value Types", 41 | "rule": { "+": [1, "2", 3, "4", "", true, false, null] }, 42 | "result": 11, 43 | "data": null 44 | }, 45 | { 46 | "description": "Plus Operator with Single Operand (Number)", 47 | "rule": { "+": [1] }, 48 | "result": 1, 49 | "data": null 50 | }, 51 | { 52 | "description": "Plus Operator with Single Operand (Negative Number)", 53 | "rule": { "+": [-1] }, 54 | "result": -1, 55 | "data": null 56 | }, 57 | { 58 | "description": "Plus Operator with zero operands is zero", 59 | "rule": { "+": [] }, 60 | "result": 0, 61 | "data": null 62 | }, 63 | { 64 | "description": "Plus Operator with Single Operand, Direct (Number)", 65 | "rule": { "+": 1 }, 66 | "result": 1, 67 | "data": null 68 | }, 69 | { 70 | "description": "Plus Operator with Single Operand, Direct (0)", 71 | "rule": { "+": 0 }, 72 | "result": 0, 73 | "data": null 74 | }, 75 | { 76 | "description": "Plus Operator with Single Operand (String)", 77 | "rule": { "+": ["1"] }, 78 | "result": 1, 79 | "data": null 80 | }, 81 | { 82 | "description": "Plus Operator with Single Operand, Direct (Negative Number String)", 83 | "rule": { "+": "-1" }, 84 | "result": -1, 85 | "data": null 86 | }, 87 | { 88 | "description": "Plus Operator with Single Operand, Direct (String Decimal)", 89 | "rule": { "+": "1.5" }, 90 | "result": 1.5, 91 | "data": null, 92 | "decimal": true 93 | }, 94 | { 95 | "description": "Plus Operator with Single Operand, Direct (String Negative Decimal)", 96 | "rule": { "+": "-1.5" }, 97 | "result": -1.5, 98 | "data": null, 99 | "decimal": true 100 | }, 101 | { 102 | "description": "Plus Operator with Single Operand, Direct (String 0.5)", 103 | "rule": { "+": "0.5" }, 104 | "result": 0.5, 105 | "data": null, 106 | "decimal": true 107 | }, 108 | { 109 | "description": "Plus Operator with Single Operand, Direct (String 1e2)", 110 | "rule": { "+": "1e2" }, 111 | "result": 100, 112 | "data": null 113 | }, 114 | { 115 | "description": "Plus Operator with Single Operand, Direct (String 0)", 116 | "rule": { "+": "0" }, 117 | "result": 0, 118 | "data": null 119 | }, 120 | { 121 | "description": "Plus Operator with Single Operand, Direct (true)", 122 | "rule": { "+": true }, 123 | "result": 1, 124 | "data": null 125 | }, 126 | { 127 | "description": "Plus Operator with Single Operand, Direct (false)", 128 | "rule": { "+": false }, 129 | "result": 0, 130 | "data": null 131 | }, 132 | { 133 | "description": "Plus Operator with Single Operand, Direct (Empty String)", 134 | "rule": { "+": "" }, 135 | "result": 0, 136 | "data": null 137 | }, 138 | { 139 | "description": "Plus Operator with a Single Operand, Direct (null)", 140 | "rule": { "+": null }, 141 | "result": 0, 142 | "data": null 143 | }, 144 | { 145 | "description": "Addition with val", 146 | "rule": { "+": [{ "val": "x" }, { "val": "y" }] }, 147 | "result": 3, 148 | "data": { "x": 1, "y": 2 } 149 | }, 150 | { 151 | "description": "Addition with string produces NaN", 152 | "rule": { "+": ["Hey", 1] }, 153 | "error": { "type": "NaN" }, 154 | "data": null 155 | }, 156 | { 157 | "description": "Addition with Array produces NaN", 158 | "rule": { "+": [[1], 1] }, 159 | "error": { "type": "NaN" }, 160 | "data": null 161 | }, 162 | { 163 | "description": "Addition with Array from context produces NaN", 164 | "rule": { "+": [{ "val": "x" }, 1] }, 165 | "error": { "type": "NaN" }, 166 | "data": { "x": [1] } 167 | }, 168 | { 169 | "description": "Addition with Object produces NaN", 170 | "rule": { "+": [{ "val": "x" }, 1] }, 171 | "error": { "type": "NaN" }, 172 | "data": { "x": {} } 173 | }, 174 | { 175 | "description": "Plus Operator with Single Operand, Invalid String Produces NaN", 176 | "rule": { "+": "Hello" }, 177 | "error": { "type": "NaN" }, 178 | "data": null 179 | }, 180 | { 181 | "description": "Plus Operator with Single Operand, Array Input Produces NaN", 182 | "rule": { "+": [[1]] }, 183 | "error": { "type": "NaN" }, 184 | "data": null 185 | }, 186 | { 187 | "description": "Plus Operator with Single Operand, Object Input Produces NaN", 188 | "rule": { "+": [{}] }, 189 | "error": { "type": "NaN" }, 190 | "data": null 191 | }, 192 | { 193 | "description": "Plus Operator with Single Operand, Direct Object Input Produces NaN", 194 | "rule": { "+": {} }, 195 | "error": { "type": "NaN" }, 196 | "data": null 197 | }, 198 | { 199 | "description": "Plus can be chained with other operators", 200 | "rule": { "+": { "val": "arr" } }, 201 | "data": { "arr": [1, 2, 3] }, 202 | "result": 6 203 | }, 204 | { 205 | "description": "Plus can be chained with other operators (2)", 206 | "rule": { "+": { "merge": [[1, 2], 3, [4, 5]] } }, 207 | "data": null, 208 | "result": 15 209 | }, 210 | { 211 | "description": "Plus can be chained with other operators (3)", 212 | "rule": { "+": { "map": [{ "val": "people" }, { "val": "age" }] } }, 213 | "data": { 214 | "people": [ 215 | { "name": "John", "age": 30 }, 216 | { "name": "Jane", "age": 25 }, 217 | { "name": "Bob", "age": 35 }, 218 | { "name": "Alice", "age": 28 } 219 | ] 220 | }, 221 | "result": 118 222 | } 223 | ] -------------------------------------------------------------------------------- /suites/chained.json: -------------------------------------------------------------------------------- 1 | [ 2 | "# These are tests from https://github.com/orgs/json-logic/discussions/2", 3 | { 4 | "description": "Standard Max", 5 | "rule": { "max": [1, 2, 3] }, 6 | "data": {}, 7 | "result": 3 8 | }, 9 | { 10 | "description": "Standard Max, Single Argument Sugared", 11 | "rule": { "max": 1 }, 12 | "data": {}, 13 | "result": 1 14 | }, 15 | { 16 | "description": "Max with Logic Chaining", 17 | "rule": { "max": { "val": "data" } }, 18 | "data": { "data": [1, 2, 3] }, 19 | "result": 3 20 | }, 21 | { 22 | "description": "Cat with Logic Chaining", 23 | "rule": { "cat": { "merge": [["Hello "], ["World", "!"]] } }, 24 | "data": {}, 25 | "result": "Hello World!" 26 | }, 27 | { 28 | "description": "Cat with Logic Chaining (Simple)", 29 | "rule": { "cat": { "val": "text" } }, 30 | "data": { "text": ["Hello ", "World", "!"] }, 31 | "result": "Hello World!" 32 | }, 33 | { 34 | "rule": { 35 | "max": { 36 | "map": [{ 37 | "filter": [ 38 | { "val": "people" }, 39 | { "===": [{ "val": "department" }, "Engineering"] } 40 | ]}, 41 | { "val": "salary" } 42 | ] 43 | } 44 | }, 45 | "data": { 46 | "people": [ 47 | { "name": "Jay Ortiz", "salary": 100414, "department": "Engineering"}, 48 | { "name": "Louisa Hall", "salary": 133601, "department": "Sales"}, 49 | { "name": "Kyle Carlson", "salary": 139803, "department": "Sales"}, 50 | { "name": "Grace Ortiz", "salary": 147068, "department": "Engineering"}, 51 | { "name": "Isabelle Harrington", "salary": 112704, "department": "Marketing"}, 52 | { "name": "Harold Moore", "salary": 125221, "department": "Sales"}, 53 | { "name": "Clarence Schultz", "salary": 127985, "department": "Sales"}, 54 | { "name": "Jesse Keller", "salary": 149212, "department": "Engineering"}, 55 | { "name": "Phillip Holland", "salary": 105888, "department": "Marketing"}, 56 | { "name": "Mason Sullivan", "salary": 147161, "department": "Engineering" } 57 | ] 58 | }, 59 | "result": 149212, 60 | "description": "Max with Logic Chaining (Complex)" 61 | }, 62 | { 63 | "description": "Addition Chained w/ Merge", 64 | "rule": { "+": { "merge": [{ "val": "x" }, { "val": "y" }] }}, 65 | "result": 6, 66 | "data": { "x": [1, 2], "y": 3 } 67 | } 68 | ] -------------------------------------------------------------------------------- /suites/coalesce.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Test Specification for ??", 3 | { 4 | "description": "Coalesces a string alone", 5 | "rule": { "??": ["hello"] }, 6 | "data": null, 7 | "result": "hello" 8 | }, 9 | { 10 | "description": "Coalesces a number alone", 11 | "rule": { "??": [1] }, 12 | "data": null, 13 | "result": 1 14 | }, 15 | { 16 | "description": "Coalesces a boolean alone", 17 | "rule": { "??": [true] }, 18 | "data": null, 19 | "result": true 20 | }, 21 | { 22 | "description": "Coalesces an object from context alone", 23 | "rule": { "??": [{ "val": "person" }]}, 24 | "data": { "person": { "name": "John" } }, 25 | "result": { "name": "John" } 26 | }, 27 | { 28 | "description": "Empty behavior", 29 | "rule": { "??": [] }, 30 | "data": null, 31 | "result": null 32 | }, 33 | { 34 | "description": "Coalesces a string with nulls before", 35 | "rule": { "??": [null, "hello"] }, 36 | "data": null, 37 | "result": "hello" 38 | }, 39 | { 40 | "description": "Coalesces a string with multiple nulls before", 41 | "rule": { "??": [null, null, null, "hello"] }, 42 | "data": null, 43 | "result": "hello" 44 | }, 45 | { 46 | "description": "Coalesces a string with nulls after", 47 | "rule": { "??": ["hello", null] }, 48 | "data": null, 49 | "result": "hello" 50 | }, 51 | { 52 | "description": "Coalesces a string with nulls both before and after", 53 | "rule": { "??": [null, "hello", null] }, 54 | "data": null, 55 | "result": "hello" 56 | }, 57 | { 58 | "description": "Coalesces a number with nulls both before and after", 59 | "rule": { "??": [null, 1, null] }, 60 | "data": null, 61 | "result": 1 62 | }, 63 | { 64 | "description": "Uses the first non-null value", 65 | "rule": { "??": [null, 1, "hello"] }, 66 | "data": null, 67 | "result": 1 68 | }, 69 | { 70 | "description": "Uses the first non-null value, even if it is false", 71 | "rule": { "??": [null, false, "hello"] }, 72 | "data": null, 73 | "result": false 74 | }, 75 | { 76 | "description": "Uses the first non-null value from context", 77 | "rule": { "??": [{ "val": ["person", "name"] }, { "val": "name" }, "Unknown Name"] }, 78 | "data": { "person": { "name": "John" }, "name": "Jane" }, 79 | "result": "John" 80 | }, 81 | { 82 | "description": "Uses the first non-null value from context (with person undefined)", 83 | "rule": { "??": [{ "val": ["person", "name"] }, { "val": "name" }, "Unknown Name"] }, 84 | "data": { "name": "Jane" }, 85 | "result": "Jane" 86 | }, 87 | { 88 | "description": "Uses the first non-null value from context (without any context)", 89 | "rule": { "??": [{ "val": ["person", "name"] }, { "val": "name" }, "Unknown Name"] }, 90 | "data": {}, 91 | "result": "Unknown Name" 92 | } 93 | ] -------------------------------------------------------------------------------- /suites/comparison/greaterThan.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "> with 3 arguments", 4 | "rule": { ">": [3, 2, 1] }, 5 | "data": {}, 6 | "result": true 7 | }, 8 | { 9 | "description": "> with 3 arguments failing", 10 | "rule": { ">": [3, 2, 3] }, 11 | "data": {}, 12 | "result": false 13 | }, 14 | { 15 | "description": "> is lazily evaluated", 16 | "rule": { ">": [2, 3, { "throw": "Not Lazy" }] }, 17 | "data": {}, 18 | "result": false 19 | }, 20 | { 21 | "description": "> with 1 argument", 22 | "rule": { ">": [1] }, 23 | "data": {}, 24 | "error": { "type": "Invalid Arguments" } 25 | }, 26 | { 27 | "description": "> with direct argument", 28 | "rule": { ">": 1 }, 29 | "data": {}, 30 | "error": { "type": "Invalid Arguments" } 31 | }, 32 | { 33 | "description": "> with empty argument", 34 | "rule": { ">": [] }, 35 | "data": {}, 36 | "error": { "type": "Invalid Arguments" } 37 | }, 38 | { 39 | "description": "> with (false, true)", 40 | "rule": { ">": [false, true] }, 41 | "data": {}, 42 | "result": false 43 | }, 44 | { 45 | "description": "> with (true, false)", 46 | "rule": { ">": [true, false] }, 47 | "data": {}, 48 | "result": true 49 | }, 50 | { 51 | "description": "> with (false, false)", 52 | "rule": { ">": [false, false] }, 53 | "data": {}, 54 | "result": false 55 | }, 56 | { 57 | "description": "> with (true, true)", 58 | "rule": { ">": [true, true] }, 59 | "data": {}, 60 | "result": false 61 | }, 62 | { 63 | "description": "> with (a, b)", 64 | "rule": { ">": ["a", "b"] }, 65 | "data": {}, 66 | "result": false 67 | }, 68 | { 69 | "description": "> with (b, a)", 70 | "rule": { ">": ["b", "a"] }, 71 | "data": {}, 72 | "result": true 73 | }, 74 | { 75 | "description": "> with (a, a)", 76 | "rule": { ">": ["a", "a"] }, 77 | "data": {}, 78 | "result": false 79 | }, 80 | { 81 | "description": "> with (2024-01-01, 2024-01-02)", 82 | "rule": { ">": ["2024-01-01", "2024-01-02"] }, 83 | "data": {}, 84 | "result": false 85 | }, 86 | { 87 | "description": "> with (2024-01-02, 2024-01-01)", 88 | "rule": { ">": ["2024-01-02", "2024-01-01"] }, 89 | "data": {}, 90 | "result": true 91 | }, 92 | { 93 | "description": "> with (2024-01-01, 2024-01-01)", 94 | "rule": { ">": ["2024-01-01", "2024-01-01"] }, 95 | "data": {}, 96 | "result": false 97 | }, 98 | { 99 | "description": "> with (2023n, 2024-01-01s)", 100 | "rule": { ">": [2023, "2024-01-01"] }, 101 | "data": {}, 102 | "error": { "type": "NaN" } 103 | }, 104 | { 105 | "description": "> with (5s, 3n)", 106 | "rule": { ">": ["5", 3] }, 107 | "data": {}, 108 | "result": true 109 | }, 110 | { 111 | "description": "> with (21s, 3n)", 112 | "rule": { ">": ["21", 3] }, 113 | "data": {}, 114 | "result": true 115 | }, 116 | { 117 | "description": "> with (21n, 3s)", 118 | "rule": { ">": [21, "3"] }, 119 | "data": {}, 120 | "result": true 121 | }, 122 | { 123 | "description": "> with (3s, 21n)", 124 | "rule": { ">": ["3", 21] }, 125 | "data": {}, 126 | "result": false 127 | }, 128 | { 129 | "description": "> with (3n, 21s)", 130 | "rule": { ">": [3, "21"] }, 131 | "data": {}, 132 | "result": false 133 | }, 134 | { 135 | "description": "> with (3n, 3s)", 136 | "rule": { ">": [3, "3"] }, 137 | "data": {}, 138 | "result": false 139 | }, 140 | { 141 | "description": "> with (3s, 3n)", 142 | "rule": { ">": ["3", 3] }, 143 | "data": {}, 144 | "result": false 145 | }, 146 | { 147 | "description": "> with (1n, As)", 148 | "rule": { ">": [1, "A"] }, 149 | "data": {}, 150 | "error": { "type": "NaN" } 151 | }, 152 | { 153 | "description": "> with (null, 1)", 154 | "rule": { ">": [null, 1] }, 155 | "data": {}, 156 | "result": false 157 | }, 158 | { 159 | "description": "> with (1, null)", 160 | "rule": { ">": [1, null] }, 161 | "data": {}, 162 | "result": true 163 | }, 164 | { 165 | "description": "> with (null, 0)", 166 | "rule": { ">": [null, 0] }, 167 | "data": {}, 168 | "result": false 169 | }, 170 | { 171 | "description": "> with (null, true)", 172 | "rule": { ">": [null, true] }, 173 | "data": {}, 174 | "result": false 175 | }, 176 | { 177 | "description": "> with (1, true)", 178 | "rule": { ">": [1, true] }, 179 | "data": {}, 180 | "result": false 181 | }, 182 | { 183 | "description": "> with (0, false)", 184 | "rule": { ">": [0, false] }, 185 | "data": {}, 186 | "result": false 187 | }, 188 | { 189 | "description": "> with [] and [1]", 190 | "rule": { ">": [[], [1]] }, 191 | "data": {}, 192 | "error": { "type": "NaN" } 193 | }, 194 | { 195 | "description": "> with [1] and 5", 196 | "rule": { ">": [[1], 5] }, 197 | "data": {}, 198 | "error": { "type": "NaN" } 199 | }, 200 | { 201 | "description": "> with {} and 1", 202 | "rule": { ">": [{}, 1] }, 203 | "data": {}, 204 | "error": { "type": "NaN" } 205 | }, 206 | { 207 | "description": "> with 1 and {}", 208 | "rule": { ">": [1, {}] }, 209 | "data": {}, 210 | "error": { "type": "NaN" } 211 | } 212 | ] -------------------------------------------------------------------------------- /suites/comparison/greaterThanEquals.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": ">= with 3 arguments", 4 | "rule": { ">=": [3, 3, 1] }, 5 | "data": {}, 6 | "result": true 7 | }, 8 | { 9 | "description": ">= with 3 arguments failing", 10 | "rule": { ">=": [3, 3, 4] }, 11 | "data": {}, 12 | "result": false 13 | }, 14 | { 15 | "description": ">= is lazily evaluated", 16 | "rule": { ">=": [2, 3, { "throw": "Not Lazy" }] }, 17 | "data": {}, 18 | "result": false 19 | }, 20 | { 21 | "description": ">= with 1 argument", 22 | "rule": { ">=": [1] }, 23 | "data": {}, 24 | "error": { "type": "Invalid Arguments" } 25 | }, 26 | { 27 | "description": ">= with direct argument", 28 | "rule": { ">=": 1 }, 29 | "data": {}, 30 | "error": { "type": "Invalid Arguments" } 31 | }, 32 | { 33 | "description": ">= with empty argument", 34 | "rule": { ">=": [] }, 35 | "data": {}, 36 | "error": { "type": "Invalid Arguments" } 37 | }, 38 | { 39 | "description": ">= with (false, true)", 40 | "rule": { ">=": [false, true] }, 41 | "data": {}, 42 | "result": false 43 | }, 44 | { 45 | "description": ">= with (true, false)", 46 | "rule": { ">=": [true, false] }, 47 | "data": {}, 48 | "result": true 49 | }, 50 | { 51 | "description": ">= with (false, false)", 52 | "rule": { ">=": [false, false] }, 53 | "data": {}, 54 | "result": true 55 | }, 56 | { 57 | "description": ">= with (true, true)", 58 | "rule": { ">=": [true, true] }, 59 | "data": {}, 60 | "result": true 61 | }, 62 | { 63 | "description": ">= with (a, b)", 64 | "rule": { ">=": ["a", "b"] }, 65 | "data": {}, 66 | "result": false 67 | }, 68 | { 69 | "description": ">= with (b, a)", 70 | "rule": { ">=": ["b", "a"] }, 71 | "data": {}, 72 | "result": true 73 | }, 74 | { 75 | "description": ">= with (a, a)", 76 | "rule": { ">=": ["a", "a"] }, 77 | "data": {}, 78 | "result": true 79 | }, 80 | { 81 | "description": ">= with (2024-01-01, 2024-01-02)", 82 | "rule": { ">=": ["2024-01-01", "2024-01-02"] }, 83 | "data": {}, 84 | "result": false 85 | }, 86 | { 87 | "description": ">= with (2024-01-02, 2024-01-01)", 88 | "rule": { ">=": ["2024-01-02", "2024-01-01"] }, 89 | "data": {}, 90 | "result": true 91 | }, 92 | { 93 | "description": ">= with (2024-01-01, 2024-01-01)", 94 | "rule": { ">=": ["2024-01-01", "2024-01-01"] }, 95 | "data": {}, 96 | "result": true 97 | }, 98 | { 99 | "description": ">= with (1n, As)", 100 | "rule": { ">=": [1, "A"] }, 101 | "data": {}, 102 | "error": { "type": "NaN" } 103 | }, 104 | { 105 | "description": ">= with (1n, As)", 106 | "rule": { ">=": [1, "A"] }, 107 | "data": {}, 108 | "error": { "type": "NaN" } 109 | }, 110 | { 111 | "description": ">= with (null, 1)", 112 | "rule": { ">=": [null, 1] }, 113 | "data": {}, 114 | "result": false 115 | }, 116 | { 117 | "description": ">= with (1, null)", 118 | "rule": { ">=": [1, null] }, 119 | "data": {}, 120 | "result": true 121 | }, 122 | { 123 | "description": ">= with (null, 0)", 124 | "rule": { ">=": [null, 0] }, 125 | "data": {}, 126 | "result": true 127 | }, 128 | { 129 | "description": ">= with (null, true)", 130 | "rule": { ">=": [null, true] }, 131 | "data": {}, 132 | "result": false 133 | }, 134 | { 135 | "description": ">= with (1, true)", 136 | "rule": { ">=": [1, true] }, 137 | "data": {}, 138 | "result": true 139 | }, 140 | { 141 | "description": ">= with (0, false)", 142 | "rule": { ">=": [0, false] }, 143 | "data": {}, 144 | "result": true 145 | }, 146 | { 147 | "description": ">= with [] and [1]", 148 | "rule": { ">=": [[], [1]] }, 149 | "data": {}, 150 | "error": { "type": "NaN" } 151 | }, 152 | { 153 | "description": ">= with [1] and 5", 154 | "rule": { ">=": [[1], 5] }, 155 | "data": {}, 156 | "error": { "type": "NaN" } 157 | }, 158 | { 159 | "description": ">= with {} and 1", 160 | "rule": { ">=": [{}, 1] }, 161 | "data": {}, 162 | "error": { "type": "NaN" } 163 | }, 164 | { 165 | "description": ">= with 1 and {}", 166 | "rule": { ">=": [1, {}] }, 167 | "data": {}, 168 | "error": { "type": "NaN" } 169 | } 170 | ] -------------------------------------------------------------------------------- /suites/comparison/lessThan.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "< with 3 arguments", 4 | "rule": { "<": [1, 2, 3] }, 5 | "data": {}, 6 | "result": true 7 | }, 8 | { 9 | "description": "< is lazily evaluated", 10 | "rule": { "<": [3, 2, { "throw": "Not Lazy" }] }, 11 | "data": {}, 12 | "result": false 13 | }, 14 | { 15 | "description": "< with 1 argument", 16 | "rule": { "<": [1] }, 17 | "data": {}, 18 | "error": { "type": "Invalid Arguments" } 19 | }, 20 | { 21 | "description": "< with direct argument", 22 | "rule": { "<": 1 }, 23 | "data": {}, 24 | "error": { "type": "Invalid Arguments" } 25 | }, 26 | { 27 | "description": "< with empty argument", 28 | "rule": { "<": [] }, 29 | "data": {}, 30 | "error": { "type": "Invalid Arguments" } 31 | }, 32 | { 33 | "description": "< with (false, true)", 34 | "rule": { "<": [false, true] }, 35 | "data": {}, 36 | "result": true 37 | }, 38 | { 39 | "description": "< with (true, false)", 40 | "rule": { "<": [true, false] }, 41 | "data": {}, 42 | "result": false 43 | }, 44 | { 45 | "description": "< with (false, false)", 46 | "rule": { "<": [false, false] }, 47 | "data": {}, 48 | "result": false 49 | }, 50 | { 51 | "description": "< with (true, true)", 52 | "rule": { "<": [true, true] }, 53 | "data": {}, 54 | "result": false 55 | }, 56 | { 57 | "description": "< with (a, b)", 58 | "rule": { "<": ["a", "b"] }, 59 | "data": {}, 60 | "result": true 61 | }, 62 | { 63 | "description": "< with (b, a)", 64 | "rule": { "<": ["b", "a"] }, 65 | "data": {}, 66 | "result": false 67 | }, 68 | { 69 | "description": "< with (a, a)", 70 | "rule": { "<": ["a", "a"] }, 71 | "data": {}, 72 | "result": false 73 | }, 74 | { 75 | "description": "< with (2024-01-01, 2024-01-02)", 76 | "rule": { "<": ["2024-01-01", "2024-01-02"] }, 77 | "data": {}, 78 | "result": true 79 | }, 80 | { 81 | "description": "< with (2024-01-02, 2024-01-01)", 82 | "rule": { "<": ["2024-01-02", "2024-01-01"] }, 83 | "data": {}, 84 | "result": false 85 | }, 86 | { 87 | "description": "< with (2024-01-01, 2024-01-01)", 88 | "rule": { "<": ["2024-01-01", "2024-01-01"] }, 89 | "data": {}, 90 | "result": false 91 | }, 92 | { 93 | "description": "< with (2023, 2024-01-01)", 94 | "rule": { "<": ["2023", "2024-01-01"] }, 95 | "data": {}, 96 | "result": true 97 | }, 98 | { 99 | "description": "< with (2023n, 2024-01-01s)", 100 | "rule": { "<": [2023, "2024-01-01"] }, 101 | "data": {}, 102 | "error": { "type": "NaN" } 103 | }, 104 | { 105 | "description": "< with (5s, 3n)", 106 | "rule": { "<": ["5", 3] }, 107 | "data": {}, 108 | "result": false 109 | }, 110 | { 111 | "description": "< with (21s, 3n)", 112 | "rule": { "<": ["21", 3] }, 113 | "data": {}, 114 | "result": false 115 | }, 116 | { 117 | "description": "< with (21n, 3s)", 118 | "rule": { "<": [21, "3"] }, 119 | "data": {}, 120 | "result": false 121 | }, 122 | { 123 | "description": "< with (3s, 21n)", 124 | "rule": { "<": ["3", 21] }, 125 | "data": {}, 126 | "result": true 127 | }, 128 | { 129 | "description": "< with (3n, 21s)", 130 | "rule": { "<": [3, "21"] }, 131 | "data": {}, 132 | "result": true 133 | }, 134 | { 135 | "description": "< with (3n, 3s)", 136 | "rule": { "<": [3, "3"] }, 137 | "data": {}, 138 | "result": false 139 | }, 140 | { 141 | "description": "< with (3s, 3n)", 142 | "rule": { "<": ["3", 3] }, 143 | "data": {}, 144 | "result": false 145 | }, 146 | { 147 | "description": "< with (3n, 3n)", 148 | "rule": { "<": [3, 3] }, 149 | "data": {}, 150 | "result": false 151 | }, 152 | { 153 | "description": "< with (3s, 3s)", 154 | "rule": { "<": ["3", "3"] }, 155 | "data": {}, 156 | "result": false 157 | }, 158 | { 159 | "description": "< with (3n, 3)", 160 | "rule": { "<": [3, 3] }, 161 | "data": {}, 162 | "result": false 163 | }, 164 | { 165 | "description": "< with (3, 3n)", 166 | "rule": { "<": [3, 3] }, 167 | "data": {}, 168 | "result": false 169 | }, 170 | { 171 | "description": "< with (3s, 3)", 172 | "rule": { "<": ["3", 3] }, 173 | "data": {}, 174 | "result": false 175 | }, 176 | { 177 | "description": "< with (3, 3s)", 178 | "rule": { "<": [3, "3"] }, 179 | "data": {}, 180 | "result": false 181 | }, 182 | { 183 | "description": "< with (3n, 3n)", 184 | "rule": { "<": [3, 3] }, 185 | "data": {}, 186 | "result": false 187 | }, 188 | { 189 | "description": "< with (3s, 3s)", 190 | "rule": { "<": ["3", "3"] }, 191 | "data": {}, 192 | "result": false 193 | }, 194 | { 195 | "description": "< with (3n, 3s)", 196 | "rule": { "<": [3, "3"] }, 197 | "data": {}, 198 | "result": false 199 | }, 200 | { 201 | "description": "< with (3s, 3n)", 202 | "rule": { "<": ["3", 3] }, 203 | "data": {}, 204 | "result": false 205 | }, 206 | { 207 | "description": "< with (1n, As)", 208 | "rule": { "<": [1, "A"] }, 209 | "data": {}, 210 | "error": { "type": "NaN" } 211 | }, 212 | { 213 | "description": "< with (null, 1)", 214 | "rule": { "<": [null, 1] }, 215 | "data": {}, 216 | "result": true 217 | }, 218 | { 219 | "description": "< with (1, null)", 220 | "rule": { "<": [1, null] }, 221 | "data": {}, 222 | "result": false 223 | }, 224 | { 225 | "description": "< with (null, 0)", 226 | "rule": { "<": [null, 0] }, 227 | "data": {}, 228 | "result": false 229 | }, 230 | { 231 | "description": "< with (null, true)", 232 | "rule": { "<": [null, true] }, 233 | "data": {}, 234 | "result": true 235 | }, 236 | { 237 | "description": "< with (1, true)", 238 | "rule": { "<": [1, true] }, 239 | "data": {}, 240 | "result": false 241 | }, 242 | { 243 | "description": "< with (0, false)", 244 | "rule": { "<": [0, false] }, 245 | "data": {}, 246 | "result": false 247 | }, 248 | { 249 | "description": "< with [] and [1]", 250 | "rule": { "<": [[], [1]] }, 251 | "data": {}, 252 | "error": { "type": "NaN" } 253 | }, 254 | { 255 | "description": "< with [1] and 5", 256 | "rule": { "<": [[1], 5] }, 257 | "data": {}, 258 | "error": { "type": "NaN" } 259 | }, 260 | { 261 | "description": "< with {} and 1", 262 | "rule": { "<": [{}, 1] }, 263 | "data": {}, 264 | "error": { "type": "NaN" } 265 | }, 266 | { 267 | "description": "< with 1 and {}", 268 | "rule": { "<": [1, {}] }, 269 | "data": {}, 270 | "error": { "type": "NaN" } 271 | } 272 | ] -------------------------------------------------------------------------------- /suites/comparison/lessThanEquals.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "<= is lazily evaluated", 4 | "rule": { "<=": [3, 2, { "throw": "Not Lazy" }] }, 5 | "data": {}, 6 | "result": false 7 | }, 8 | { 9 | "description": "<= with (2023n, 2024-01-01s)", 10 | "rule": { "<=": [2023, "2024-01-01"] }, 11 | "data": {}, 12 | "error": { "type": "NaN" } 13 | }, 14 | { 15 | "description": "<= with (5s, 3n)", 16 | "rule": { "<=": ["5", 3] }, 17 | "data": {}, 18 | "result": false 19 | }, 20 | { 21 | "description": "<= with (21s, 3n)", 22 | "rule": { "<=": ["21", 3] }, 23 | "data": {}, 24 | "result": false 25 | }, 26 | { 27 | "description": "<= with (21n, 3s)", 28 | "rule": { "<=": [21, "3"] }, 29 | "data": {}, 30 | "result": false 31 | }, 32 | { 33 | "description": "<= with (3s, 21n)", 34 | "rule": { "<=": ["3", 21] }, 35 | "data": {}, 36 | "result": true 37 | }, 38 | { 39 | "description": "<= with (3n, 21s)", 40 | "rule": { "<=": [3, "21"] }, 41 | "data": {}, 42 | "result": true 43 | }, 44 | { 45 | "description": "<= with (3n, 3s)", 46 | "rule": { "<=": [3, "3"] }, 47 | "data": {}, 48 | "result": true 49 | }, 50 | { 51 | "description": "<= with (3s, 3n)", 52 | "rule": { "<=": ["3", 3] }, 53 | "data": {}, 54 | "result": true 55 | }, 56 | { 57 | "description": "<= with (1n, As)", 58 | "rule": { "<=": [1, "A"] }, 59 | "data": {}, 60 | "error": { "type": "NaN" } 61 | }, 62 | { 63 | "description": "<= with (null, 1)", 64 | "rule": { "<=": [null, 1] }, 65 | "data": {}, 66 | "result": true 67 | }, 68 | { 69 | "description": "<= with (1, null)", 70 | "rule": { "<=": [1, null] }, 71 | "data": {}, 72 | "result": false 73 | }, 74 | { 75 | "description": "<= with (null, 0)", 76 | "rule": { "<=": [null, 0] }, 77 | "data": {}, 78 | "result": true 79 | }, 80 | { 81 | "description": "<= with (null, true)", 82 | "rule": { "<": [null, true] }, 83 | "data": {}, 84 | "result": true 85 | }, 86 | { 87 | "description": "<= with (1, true)", 88 | "rule": { "<=": [1, true] }, 89 | "data": {}, 90 | "result": true 91 | }, 92 | { 93 | "description": "<= with (0, false)", 94 | "rule": { "<=": [0, false] }, 95 | "data": {}, 96 | "result": true 97 | }, 98 | { 99 | "description": "<= with [] and [1]", 100 | "rule": { "<=": [[], [1]] }, 101 | "data": {}, 102 | "error": { "type": "NaN" } 103 | }, 104 | { 105 | "description": "<= with [1] and 5", 106 | "rule": { "<=": [[1], 5] }, 107 | "data": {}, 108 | "error": { "type": "NaN" } 109 | }, 110 | { 111 | "description": "<= with {} and 1", 112 | "rule": { "<=": [{}, 1] }, 113 | "data": {}, 114 | "error": { "type": "NaN" } 115 | }, 116 | { 117 | "description": "<= with 1 and {}", 118 | "rule": { "<=": [1, {}] }, 119 | "data": {}, 120 | "error": { "type": "NaN" } 121 | } 122 | ] -------------------------------------------------------------------------------- /suites/comparison/softEquals.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "== with 3 arguments", 4 | "rule": { "==": [3, 3, 3] }, 5 | "data": {}, 6 | "result": true 7 | }, 8 | { 9 | "description": "== with 3 arguments failing", 10 | "rule": { "==": [3, 2, 3] }, 11 | "data": {}, 12 | "result": false 13 | }, 14 | { 15 | "description": "== is lazily evaluated", 16 | "rule": { "==": [3, 2, { "throw": "Not Lazy" }] }, 17 | "data": {}, 18 | "result": false 19 | }, 20 | { 21 | "description": "== with 1 argument", 22 | "rule": { "==": [1] }, 23 | "data": {}, 24 | "error": { "type": "Invalid Arguments" } 25 | }, 26 | { 27 | "description": "== with direct argument", 28 | "rule": { "==": 1 }, 29 | "data": {}, 30 | "error": { "type": "Invalid Arguments" } 31 | }, 32 | { 33 | "description": "== with empty argument", 34 | "rule": { "==": [] }, 35 | "data": {}, 36 | "error": { "type": "Invalid Arguments" } 37 | }, 38 | { 39 | "description": "== with (false, true)", 40 | "rule": { "==": [false, true] }, 41 | "data": {}, 42 | "result": false 43 | }, 44 | { 45 | "description": "== with (true, false)", 46 | "rule": { "==": [true, false] }, 47 | "data": {}, 48 | "result": false 49 | }, 50 | { 51 | "description": "== with (false, false)", 52 | "rule": { "==": [false, false] }, 53 | "data": {}, 54 | "result": true 55 | }, 56 | { 57 | "description": "== with (true, true)", 58 | "rule": { "==": [true, true] }, 59 | "data": {}, 60 | "result": true 61 | }, 62 | { 63 | "description": "== with (a, b)", 64 | "rule": { "==": ["a", "b"] }, 65 | "data": {}, 66 | "result": false 67 | }, 68 | { 69 | "description": "== with (b, a)", 70 | "rule": { "==": ["b", "a"] }, 71 | "data": {}, 72 | "result": false 73 | }, 74 | { 75 | "description": "== with (a, a)", 76 | "rule": { "==": ["a", "a"] }, 77 | "data": {}, 78 | "result": true 79 | }, 80 | { 81 | "description": "== with (2024-01-01, 2024-01-02)", 82 | "rule": { "==": ["2024-01-01", "2024-01-02"] }, 83 | "data": {}, 84 | "result": false 85 | }, 86 | { 87 | "description": "== with (2024-01-02, 2024-01-01)", 88 | "rule": { "==": ["2024-01-02", "2024-01-01"] }, 89 | "data": {}, 90 | "result": false 91 | }, 92 | { 93 | "description": "== with (2024-01-01, 2024-01-01)", 94 | "rule": { "==": ["2024-01-01", "2024-01-01"] }, 95 | "data": {}, 96 | "result": true 97 | }, 98 | { 99 | "description": "== with (2023n, 2024-01-01s)", 100 | "rule": { "==": [2023, "2024-01-01"] }, 101 | "data": {}, 102 | "error": { "type": "NaN" } 103 | }, 104 | { 105 | "description": "== with (5s, 3n)", 106 | "rule": { "==": ["5", 3] }, 107 | "data": {}, 108 | "result": false 109 | }, 110 | { 111 | "description": "== with (21s, 3n)", 112 | "rule": { "==": ["21", 3] }, 113 | "data": {}, 114 | "result": false 115 | }, 116 | { 117 | "description": "== with (21n, 3s)", 118 | "rule": { "==": [21, "3"] }, 119 | "data": {}, 120 | "result": false 121 | }, 122 | { 123 | "description": "== with (3s, 21n)", 124 | "rule": { "==": ["3", 21] }, 125 | "data": {}, 126 | "result": false 127 | }, 128 | { 129 | "description": "== with (3n, 21s)", 130 | "rule": { "==": [3, "21"] }, 131 | "data": {}, 132 | "result": false 133 | }, 134 | { 135 | "description": "== with (3n, 3s)", 136 | "rule": { "==": [3, "3"] }, 137 | "data": {}, 138 | "result": true 139 | }, 140 | { 141 | "description": "== with (3s, 3n)", 142 | "rule": { "==": ["3", 3] }, 143 | "data": {}, 144 | "result": true 145 | }, 146 | { 147 | "description": "== with (1n, As)", 148 | "rule": { "==": [1, "A"] }, 149 | "data": {}, 150 | "error": { "type": "NaN" } 151 | }, 152 | { 153 | "description": "== with (null, 1)", 154 | "rule": { "==": [null, 1] }, 155 | "data": {}, 156 | "result": false 157 | }, 158 | { 159 | "description": "== with (1, null)", 160 | "rule": { "==": [1, null] }, 161 | "data": {}, 162 | "result": false 163 | }, 164 | { 165 | "description": "== with (null, 0)", 166 | "rule": { "==": [null, 0] }, 167 | "data": {}, 168 | "result": true 169 | }, 170 | { 171 | "description": "== with (null, true)", 172 | "rule": { "==": [null, true] }, 173 | "data": {}, 174 | "result": false 175 | }, 176 | { 177 | "description": "== with (1, true)", 178 | "rule": { "==": [1, true] }, 179 | "data": {}, 180 | "result": true 181 | }, 182 | { 183 | "description": "== with (0, false)", 184 | "rule": { "==": [0, false] }, 185 | "data": {}, 186 | "result": true 187 | }, 188 | { 189 | "description": "== with [] and [1]", 190 | "rule": { "==": [[], [1]] }, 191 | "data": {}, 192 | "error": { "type": "NaN" } 193 | }, 194 | { 195 | "description": "== with [1] and 5", 196 | "rule": { "==": [[1], 5] }, 197 | "data": {}, 198 | "error": { "type": "NaN" } 199 | }, 200 | { 201 | "description": "== with {} and 1", 202 | "rule": { "==": [{}, 1] }, 203 | "data": {}, 204 | "error": { "type": "NaN" } 205 | }, 206 | { 207 | "description": "== with 1 and {}", 208 | "rule": { "==": [1, {}] }, 209 | "data": {}, 210 | "error": { "type": "NaN" } 211 | } 212 | ] -------------------------------------------------------------------------------- /suites/comparison/softNotEquals.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "!= with 3 arguments", 4 | "rule": { "!=": [3, 2, 3] }, 5 | "data": {}, 6 | "result": true 7 | }, 8 | { 9 | "description": "!= is lazily evaluated", 10 | "rule": { "!=": [3, 3, { "throw": "Not Lazy" }] }, 11 | "data": {}, 12 | "result": false 13 | }, 14 | { 15 | "description": "!= with 1 argument", 16 | "rule": { "!=": [1] }, 17 | "data": {}, 18 | "error": { "type": "Invalid Arguments" } 19 | }, 20 | { 21 | "description": "!= with direct argument", 22 | "rule": { "!=": 1 }, 23 | "data": {}, 24 | "error": { "type": "Invalid Arguments" } 25 | }, 26 | { 27 | "description": "!= with empty argument", 28 | "rule": { "!=": [] }, 29 | "data": {}, 30 | "error": { "type": "Invalid Arguments" } 31 | }, 32 | { 33 | "description": "!= with (false, true)", 34 | "rule": { "!=": [false, true] }, 35 | "data": {}, 36 | "result": true 37 | }, 38 | { 39 | "description": "!= with (true, false)", 40 | "rule": { "!=": [true, false] }, 41 | "data": {}, 42 | "result": true 43 | }, 44 | { 45 | "description": "!= with (false, false)", 46 | "rule": { "!=": [false, false] }, 47 | "data": {}, 48 | "result": false 49 | }, 50 | { 51 | "description": "!= with (true, true)", 52 | "rule": { "!=": [true, true] }, 53 | "data": {}, 54 | "result": false 55 | }, 56 | { 57 | "description": "!= with (a, b)", 58 | "rule": { "!=": ["a", "b"] }, 59 | "data": {}, 60 | "result": true 61 | }, 62 | { 63 | "description": "!= with (b, a)", 64 | "rule": { "!=": ["b", "a"] }, 65 | "data": {}, 66 | "result": true 67 | }, 68 | { 69 | "description": "!= with (a, a)", 70 | "rule": { "!=": ["a", "a"] }, 71 | "data": {}, 72 | "result": false 73 | }, 74 | { 75 | "description": "!= with (2024-01-01, 2024-01-02)", 76 | "rule": { "!=": ["2024-01-01", "2024-01-02"] }, 77 | "data": {}, 78 | "result": true 79 | }, 80 | { 81 | "description": "!= with (2024-01-02, 2024-01-01)", 82 | "rule": { "!=": ["2024-01-02", "2024-01-01"] }, 83 | "data": {}, 84 | "result": true 85 | }, 86 | { 87 | "description": "!= with (2024-01-01, 2024-01-01)", 88 | "rule": { "!=": ["2024-01-01", "2024-01-01"] }, 89 | "data": {}, 90 | "result": false 91 | }, 92 | { 93 | "description": "!= with (2023n, 2024-01-01s)", 94 | "rule": { "!=": [2023, "2024-01-01"] }, 95 | "data": {}, 96 | "error": { "type": "NaN" } 97 | }, 98 | { 99 | "description": "!= with (5s, 3n)", 100 | "rule": { "!=": ["5", 3] }, 101 | "data": {}, 102 | "result": true 103 | }, 104 | { 105 | "description": "!= with (21s, 3n)", 106 | "rule": { "!=": ["21", 3] }, 107 | "data": {}, 108 | "result": true 109 | }, 110 | { 111 | "description": "!= with (21n, 3s)", 112 | "rule": { "!=": [21, "3"] }, 113 | "data": {}, 114 | "result": true 115 | }, 116 | { 117 | "description": "!= with (3s, 21n)", 118 | "rule": { "!=": ["3", 21] }, 119 | "data": {}, 120 | "result": true 121 | }, 122 | { 123 | "description": "!= with (3n, 21s)", 124 | "rule": { "!=": [3, "21"] }, 125 | "data": {}, 126 | "result": true 127 | }, 128 | { 129 | "description": "!= with (3n, 3s)", 130 | "rule": { "!=": [3, "3"] }, 131 | "data": {}, 132 | "result": false 133 | }, 134 | { 135 | "description": "!= with (3s, 3n)", 136 | "rule": { "!=": ["3", 3] }, 137 | "data": {}, 138 | "result": false 139 | }, 140 | { 141 | "description": "!= with (1n, As)", 142 | "rule": { "!=": [1, "A"] }, 143 | "data": {}, 144 | "error": { "type": "NaN" } 145 | }, 146 | { 147 | "description": "!= with (null, 1)", 148 | "rule": { "!=": [null, 1] }, 149 | "data": {}, 150 | "result": true 151 | }, 152 | { 153 | "description": "!= with (1, null)", 154 | "rule": { "!=": [1, null] }, 155 | "data": {}, 156 | "result": true 157 | }, 158 | { 159 | "description": "!= with (null, 0)", 160 | "rule": { "!=": [null, 0] }, 161 | "data": {}, 162 | "result": false 163 | }, 164 | { 165 | "description": "!= with (null, true)", 166 | "rule": { "!=": [null, true] }, 167 | "data": {}, 168 | "result": true 169 | }, 170 | { 171 | "description": "!= with (1, true)", 172 | "rule": { "!=": [1, true] }, 173 | "data": {}, 174 | "result": false 175 | }, 176 | { 177 | "description": "!= with (0, false)", 178 | "rule": { "!=": [0, false] }, 179 | "data": {}, 180 | "result": false 181 | }, 182 | { 183 | "description": "!= with [] and [1]", 184 | "rule": { "!=": [[], [1]] }, 185 | "data": {}, 186 | "error": { "type": "NaN" } 187 | }, 188 | { 189 | "description": "!= with [1] and 5", 190 | "rule": { "!=": [[1], 5] }, 191 | "data": {}, 192 | "error": { "type": "NaN" } 193 | }, 194 | { 195 | "description": "!= with {} and 1", 196 | "rule": { "!=": [{}, 1] }, 197 | "data": {}, 198 | "error": { "type": "NaN" } 199 | }, 200 | { 201 | "description": "!= with 1 and {}", 202 | "rule": { "!=": [1, {}] }, 203 | "data": {}, 204 | "error": { "type": "NaN" } 205 | } 206 | ] -------------------------------------------------------------------------------- /suites/comparison/strictEquals.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "=== with 3 arguments", 4 | "rule": { "===": [3, 3, 3] }, 5 | "data": {}, 6 | "result": true 7 | }, 8 | { 9 | "description": "=== with 3 arguments failing", 10 | "rule": { "===": [3, 3, 2] }, 11 | "data": {}, 12 | "result": false 13 | }, 14 | { 15 | "description": "=== is lazily evaluated", 16 | "rule": { "===": [3, 2, { "throw": "Not Lazy" }] }, 17 | "data": {}, 18 | "result": false 19 | }, 20 | { 21 | "description": "=== with 1 argument", 22 | "rule": { "===": [1] }, 23 | "data": {}, 24 | "error": { "type": "Invalid Arguments" } 25 | }, 26 | { 27 | "description": "=== with direct argument", 28 | "rule": { "===": 1 }, 29 | "data": {}, 30 | "error": { "type": "Invalid Arguments" } 31 | }, 32 | { 33 | "description": "=== with empty argument", 34 | "rule": { "===": [] }, 35 | "data": {}, 36 | "error": { "type": "Invalid Arguments" } 37 | }, 38 | { 39 | "description": "=== with (false, true)", 40 | "rule": { "===": [false, true] }, 41 | "data": {}, 42 | "result": false 43 | }, 44 | { 45 | "description": "=== with (true, false)", 46 | "rule": { "===": [true, false] }, 47 | "data": {}, 48 | "result": false 49 | }, 50 | { 51 | "description": "=== with (false, false)", 52 | "rule": { "===": [false, false] }, 53 | "data": {}, 54 | "result": true 55 | }, 56 | { 57 | "description": "=== with (true, true)", 58 | "rule": { "===": [true, true] }, 59 | "data": {}, 60 | "result": true 61 | }, 62 | { 63 | "description": "=== with (a, b)", 64 | "rule": { "===": ["a", "b"] }, 65 | "data": {}, 66 | "result": false 67 | }, 68 | { 69 | "description": "=== with (b, a)", 70 | "rule": { "===": ["b", "a"] }, 71 | "data": {}, 72 | "result": false 73 | }, 74 | { 75 | "description": "=== with (a, a)", 76 | "rule": { "===": ["a", "a"] }, 77 | "data": {}, 78 | "result": true 79 | }, 80 | { 81 | "description": "=== with (2024-01-01, 2024-01-02)", 82 | "rule": { "===": ["2024-01-01", "2024-01-02"] }, 83 | "data": {}, 84 | "result": false 85 | }, 86 | { 87 | "description": "=== with (2024-01-02, 2024-01-01)", 88 | "rule": { "===": ["2024-01-02", "2024-01-01"] }, 89 | "data": {}, 90 | "result": false 91 | }, 92 | { 93 | "description": "=== with (2024-01-01, 2024-01-01)", 94 | "rule": { "===": ["2024-01-01", "2024-01-01"] }, 95 | "data": {}, 96 | "result": true 97 | }, 98 | { 99 | "description": "=== with (2023n, 2024-01-01s)", 100 | "rule": { "===": [2023, "2024-01-01"] }, 101 | "data": {}, 102 | "result": false 103 | }, 104 | { 105 | "description": "=== with (5s, 3n)", 106 | "rule": { "===": ["5", 3] }, 107 | "data": {}, 108 | "result": false 109 | }, 110 | { 111 | "description": "=== with (21s, 3n)", 112 | "rule": { "===": ["21", 3] }, 113 | "data": {}, 114 | "result": false 115 | }, 116 | { 117 | "description": "=== with (21n, 3s)", 118 | "rule": { "===": [21, "3"] }, 119 | "data": {}, 120 | "result": false 121 | }, 122 | { 123 | "description": "=== with (3s, 21n)", 124 | "rule": { "===": ["3", 21] }, 125 | "data": {}, 126 | "result": false 127 | }, 128 | { 129 | "description": "=== with (3n, 21s)", 130 | "rule": { "===": [3, "21"] }, 131 | "data": {}, 132 | "result": false 133 | }, 134 | { 135 | "description": "=== with (3n, 3s)", 136 | "rule": { "===": [3, "3"] }, 137 | "data": {}, 138 | "result": false 139 | }, 140 | { 141 | "description": "=== with (3s, 3n)", 142 | "rule": { "===": ["3", 3] }, 143 | "data": {}, 144 | "result": false 145 | }, 146 | { 147 | "description": "=== with (1n, As)", 148 | "rule": { "===": [1, "A"] }, 149 | "data": {}, 150 | "result": false 151 | }, 152 | { 153 | "description": "=== with (null, 1)", 154 | "rule": { "===": [null, 1] }, 155 | "data": {}, 156 | "result": false 157 | }, 158 | { 159 | "description": "=== with (1, null)", 160 | "rule": { "===": [1, null] }, 161 | "data": {}, 162 | "result": false 163 | }, 164 | { 165 | "description": "=== with (null, 0)", 166 | "rule": { "===": [null, 0] }, 167 | "data": {}, 168 | "result": false 169 | }, 170 | { 171 | "description": "=== with (null, true)", 172 | "rule": { "===": [null, true] }, 173 | "data": {}, 174 | "result": false 175 | }, 176 | { 177 | "description": "=== with (1, true)", 178 | "rule": { "===": [1, true] }, 179 | "data": {}, 180 | "result": false 181 | }, 182 | { 183 | "description": "=== with (0, false)", 184 | "rule": { "===": [0, false] }, 185 | "data": {}, 186 | "result": false 187 | } 188 | ] -------------------------------------------------------------------------------- /suites/comparison/strictNotEquals.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "!== with 3 arguments failing", 4 | "rule": { "!==": [3, 3, 3] }, 5 | "data": {}, 6 | "result": false 7 | }, 8 | { 9 | "description": "!== is lazily evaluated", 10 | "rule": { "!==": [3, 3, { "throw": "Not Lazy" }] }, 11 | "data": {}, 12 | "result": false 13 | }, 14 | { 15 | "description": "!== with 1 argument", 16 | "rule": { "!==": [1] }, 17 | "data": {}, 18 | "error": { "type": "Invalid Arguments" } 19 | }, 20 | { 21 | "description": "!== with direct argument", 22 | "rule": { "!==": 1 }, 23 | "data": {}, 24 | "error": { "type": "Invalid Arguments" } 25 | }, 26 | { 27 | "description": "!== with empty argument", 28 | "rule": { "!==": [] }, 29 | "data": {}, 30 | "error": { "type": "Invalid Arguments" } 31 | }, 32 | { 33 | "description": "!== with (false, true)", 34 | "rule": { "!==": [false, true] }, 35 | "data": {}, 36 | "result": true 37 | }, 38 | { 39 | "description": "!== with (true, false)", 40 | "rule": { "!==": [true, false] }, 41 | "data": {}, 42 | "result": true 43 | }, 44 | { 45 | "description": "!== with (false, false)", 46 | "rule": { "!==": [false, false] }, 47 | "data": {}, 48 | "result": false 49 | }, 50 | { 51 | "description": "!== with (true, true)", 52 | "rule": { "!==": [true, true] }, 53 | "data": {}, 54 | "result": false 55 | }, 56 | { 57 | "description": "!== with (a, b)", 58 | "rule": { "!==": ["a", "b"] }, 59 | "data": {}, 60 | "result": true 61 | }, 62 | { 63 | "description": "!== with (b, a)", 64 | "rule": { "!==": ["b", "a"] }, 65 | "data": {}, 66 | "result": true 67 | }, 68 | { 69 | "description": "!== with (a, a)", 70 | "rule": { "!==": ["a", "a"] }, 71 | "data": {}, 72 | "result": false 73 | }, 74 | { 75 | "description": "!== with (2024-01-01, 2024-01-02)", 76 | "rule": { "!==": ["2024-01-01", "2024-01-02"] }, 77 | "data": {}, 78 | "result": true 79 | }, 80 | { 81 | "description": "!== with (2024-01-02, 2024-01-01)", 82 | "rule": { "!==": ["2024-01-02", "2024-01-01"] }, 83 | "data": {}, 84 | "result": true 85 | }, 86 | { 87 | "description": "!== with (2024-01-01, 2024-01-01)", 88 | "rule": { "!==": ["2024-01-01", "2024-01-01"] }, 89 | "data": {}, 90 | "result": false 91 | }, 92 | { 93 | "description": "!== with (2023n, 2024-01-01s)", 94 | "rule": { "!==": [2023, "2024-01-01"] }, 95 | "data": {}, 96 | "result": true 97 | }, 98 | { 99 | "description": "!== with (5s, 3n)", 100 | "rule": { "!==": ["5", 3] }, 101 | "data": {}, 102 | "result": true 103 | }, 104 | { 105 | "description": "!== with (21s, 3n)", 106 | "rule": { "!==": ["21", 3] }, 107 | "data": {}, 108 | "result": true 109 | }, 110 | { 111 | "description": "!== with (21n, 3s)", 112 | "rule": { "!==": [21, "3"] }, 113 | "data": {}, 114 | "result": true 115 | }, 116 | { 117 | "description": "!== with (3s, 21n)", 118 | "rule": { "!==": ["3", 21] }, 119 | "data": {}, 120 | "result": true 121 | }, 122 | { 123 | "description": "!== with (3n, 21s)", 124 | "rule": { "!==": [3, "21"] }, 125 | "data": {}, 126 | "result": true 127 | }, 128 | { 129 | "description": "!== with (3n, 3s)", 130 | "rule": { "!==": [3, "3"] }, 131 | "data": {}, 132 | "result": true 133 | }, 134 | { 135 | "description": "!== with (3s, 3n)", 136 | "rule": { "!==": ["3", 3] }, 137 | "data": {}, 138 | "result": true 139 | }, 140 | { 141 | "description": "!== with (1n, As)", 142 | "rule": { "!==": [1, "A"] }, 143 | "data": {}, 144 | "result": true 145 | }, 146 | { 147 | "description": "!== with (null, 1)", 148 | "rule": { "!==": [null, 1] }, 149 | "data": {}, 150 | "result": true 151 | }, 152 | { 153 | "description": "!== with (1, null)", 154 | "rule": { "!==": [1, null] }, 155 | "data": {}, 156 | "result": true 157 | }, 158 | { 159 | "description": "!== with (null, 0)", 160 | "rule": { "!==": [null, 0] }, 161 | "data": {}, 162 | "result": true 163 | }, 164 | { 165 | "description": "!== with (null, true)", 166 | "rule": { "!==": [null, true] }, 167 | "data": {}, 168 | "result": true 169 | }, 170 | { 171 | "description": "!== with (1, true)", 172 | "rule": { "!==": [1, true] }, 173 | "data": {}, 174 | "result": true 175 | }, 176 | { 177 | "description": "!== with (0, false)", 178 | "rule": { "!==": [0, false] }, 179 | "data": {}, 180 | "result": true 181 | } 182 | ] -------------------------------------------------------------------------------- /suites/control/and.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Basic Checks", 3 | { 4 | "description": "Two true values should be true", 5 | "rule": { "and": [true, true] }, 6 | "data": null, 7 | "result": true 8 | }, 9 | { 10 | "description": "false and true should be false", 11 | "rule": { "and": [false, true] }, 12 | "data": null, 13 | "result": false 14 | }, 15 | { 16 | "description": "true and false should be false", 17 | "rule": { "and": [true, false] }, 18 | "data": null, 19 | "result": false 20 | }, 21 | { 22 | "description": "Two false values should be false", 23 | "rule": { "and": [false, false] }, 24 | "data": null, 25 | "result": false 26 | }, 27 | { 28 | "description": "All true values should be true", 29 | "rule": { "and": [true, true, true] }, 30 | "data": null, 31 | "result": true 32 | }, 33 | { 34 | "description": "Any false value should be false", 35 | "rule": { "and": [true, true, false] }, 36 | "data": null, 37 | "result": false 38 | }, 39 | { 40 | "description": "And with a single false value should be false", 41 | "rule": { "and": [false] }, 42 | "data": null, 43 | "result": false 44 | }, 45 | { 46 | "description": "When all values are truthy, the last truthy value should be returned", 47 | "rule": { "and": [1, 2, 3] }, 48 | "data": null, 49 | "result": 3 50 | }, 51 | { 52 | "description": "When all values are truthy, the last truthy value should be returned (2)", 53 | "rule": { "and": [true, 1, {}] }, 54 | "data": null, 55 | "result": {} 56 | }, 57 | { 58 | "description": "And with a single falsey value should be return the falsy value", 59 | "rule": { "and": [[]] }, 60 | "data": null, 61 | "result": [] 62 | }, 63 | { 64 | "description": "And with a single falsey value should be return the falsy value (2)", 65 | "rule": { "and": [0] }, 66 | "data": null, 67 | "result": 0 68 | }, 69 | { 70 | "description": "And with a single falsey value should be return the falsy value (3)", 71 | "rule": { "and": [""] }, 72 | "data": null, 73 | "result": "" 74 | }, 75 | { 76 | "description": "And with a single true value should be true", 77 | "rule": { "and": [true] }, 78 | "data": null, 79 | "result": true 80 | }, 81 | { 82 | "description": "And with 2 truthy values should return the last truthy value", 83 | "rule": { "and": [1, 3] }, 84 | "data": null, 85 | "result": 3 86 | }, 87 | { 88 | "description": "And with a truthy value and a false value should return the false value", 89 | "rule": { "and": [3, false] }, 90 | "data": null, 91 | "result": false 92 | }, 93 | { 94 | "description": "And with a truthy value and a false value should return the false value (2)", 95 | "rule": { "and": [false, 3] }, 96 | "data": null, 97 | "result": false 98 | }, 99 | { 100 | "description": "Empty array is falsy, so it is returned", 101 | "rule": { "and": [[], true] }, 102 | "data": null, 103 | "result": [] 104 | }, 105 | { 106 | "description": "0 is falsy, so it is returned", 107 | "rule": { "and": [0, true] }, 108 | "data": null, 109 | "result": 0 110 | }, 111 | { 112 | "description": "Empty string is falsy, so it is returned", 113 | "rule": { "and": ["", true] }, 114 | "data": null, 115 | "result": "" 116 | }, 117 | { 118 | "description": "0 as a string is truthy, so the last truthy value is returned", 119 | "rule": { "and": ["0", true] }, 120 | "data": null, 121 | "result": true 122 | }, 123 | { 124 | "description": "And with no arguments should return null", 125 | "rule": { "and": [] }, 126 | "data": null, 127 | "result": null 128 | }, 129 | "Valid Arguments Checks", 130 | { 131 | "description": "And with non-array arguments should throw", 132 | "rule": { "and": true }, 133 | "data": null, 134 | "error": { "type": "Invalid Arguments" } 135 | }, 136 | "Short Circuiting Checks", 137 | { 138 | "description": "And will not interpret the second argument if the first is false", 139 | "rule": { "and": [false, { "throw": "Not Lazy" }] }, 140 | "data": null, 141 | "result": false 142 | }, 143 | { 144 | "description": "And will not interpret the second argument if the first is falsy", 145 | "rule": { "and": [0, { "throw": "Not Lazy" }] }, 146 | "data": null, 147 | "result": 0 148 | }, 149 | { 150 | "description": "And will not interpret the nth argument if any value before it is false", 151 | "rule": { "and": [true, 1, 2, 3, 4, [], { "throw": "Not Lazy" }] }, 152 | "data": null, 153 | "result": [] 154 | } 155 | ] -------------------------------------------------------------------------------- /suites/control/or.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Basic Checks", 3 | { 4 | "description": "Two true values should be true", 5 | "rule": { "or": [true, true] }, 6 | "data": null, 7 | "result": true 8 | }, 9 | { 10 | "description": "false or true should be true", 11 | "rule": { "or": [false, true] }, 12 | "data": null, 13 | "result": true 14 | }, 15 | { 16 | "description": "true or false should be true", 17 | "rule": { "or": [true, false] }, 18 | "data": null, 19 | "result": true 20 | }, 21 | { 22 | "description": "Two false values should be false", 23 | "rule": { "or": [false, false] }, 24 | "data": null, 25 | "result": false 26 | }, 27 | { 28 | "description": "All true values should be true", 29 | "rule": { "or": [true, true, true] }, 30 | "data": null, 31 | "result": true 32 | }, 33 | { 34 | "description": "Any true value should be true", 35 | "rule": { "or": [true, true, false] }, 36 | "data": null, 37 | "result": true 38 | }, 39 | { 40 | "description": "Or with a single true value should be true", 41 | "rule": { "or": [true] }, 42 | "data": null, 43 | "result": true 44 | }, 45 | { 46 | "description": "When all values are truthy, the first truthy value should be returned", 47 | "rule": { "or": [1, 2, 3] }, 48 | "data": null, 49 | "result": 1 50 | }, 51 | { 52 | "description": "When all values are truthy, the first truthy value should be returned (2)", 53 | "rule": { "or": [true, 1, {}] }, 54 | "data": null, 55 | "result": true 56 | }, 57 | { 58 | "description": "Or with a single falsey value should be return the falsy value", 59 | "rule": { "or": [[]] }, 60 | "data": null, 61 | "result": [] 62 | }, 63 | { 64 | "description": "Or with a single falsey value should be return the falsy value (2)", 65 | "rule": { "or": [0] }, 66 | "data": null, 67 | "result": 0 68 | }, 69 | { 70 | "description": "Or with a single falsey value should be return the falsy value (3)", 71 | "rule": { "or": [""] }, 72 | "data": null, 73 | "result": "" 74 | }, 75 | { 76 | "description": "Or with a single false value should be false", 77 | "rule": { "or": [false] }, 78 | "data": null, 79 | "result": false 80 | }, 81 | { 82 | "description": "Or with 2 truthy values should return the first truthy value", 83 | "rule": { "or": [1, 3] }, 84 | "data": null, 85 | "result": 1 86 | }, 87 | { 88 | "description": "Or with a truthy value and a false value should return the truthy value", 89 | "rule": { "or": [3, false] }, 90 | "data": null, 91 | "result": 3 92 | }, 93 | { 94 | "description": "Or with a truthy value and a false value should return the truthy value (2)", 95 | "rule": { "or": [false, 3] }, 96 | "data": null, 97 | "result": 3 98 | }, 99 | { 100 | "description": "Empty array is falsy, so it is not returned if the second argument is truthy", 101 | "rule": { "or": [[], true] }, 102 | "data": null, 103 | "result": true 104 | }, 105 | { 106 | "description": "0 is falsy, so it is not returned if the second argument is truthy", 107 | "rule": { "or": [0, true] }, 108 | "data": null, 109 | "result": true 110 | }, 111 | { 112 | "description": "Empty string is falsy, so it is not returned if the second argument is truthy", 113 | "rule": { "or": ["", true] }, 114 | "data": null, 115 | "result": true 116 | }, 117 | { 118 | "description": "0 as a string is truthy, so the first truthy value is returned", 119 | "rule": { "or": ["0", true] }, 120 | "data": null, 121 | "result": "0" 122 | }, 123 | { 124 | "description": "Or with no arguments should return null", 125 | "rule": { "or": [] }, 126 | "data": null, 127 | "result": null 128 | }, 129 | "Valid Arguments Checks", 130 | { 131 | "description": "Or with non-array arguments should throw", 132 | "rule": { "or": true }, 133 | "data": null, 134 | "error": { "type": "Invalid Arguments" } 135 | }, 136 | "Short Circuiting Checks", 137 | { 138 | "description": "Or will not interpret the second argument if the first is false", 139 | "rule": { "or": [true, { "throw": "Not Lazy" }] }, 140 | "data": null, 141 | "result": true 142 | }, 143 | { 144 | "description": "Or will not interpret the second argument if the first is falsy", 145 | "rule": { "or": [1, { "throw": "Not Lazy" }] }, 146 | "data": null, 147 | "result": 1 148 | }, 149 | { 150 | "description": "Or will not interpret the nth argument if any value before it is false", 151 | "rule": { "or": [false, 0, null, { "val": [] }, [], 4, { "throw": "Not Lazy" }] }, 152 | "data": null, 153 | "result": 4 154 | } 155 | ] -------------------------------------------------------------------------------- /suites/empty-objects.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "Empty object passes through", 4 | "rule": {}, 5 | "result": {}, 6 | "data": null 7 | }, 8 | { 9 | "description": "Empty object passes through, even embedded", 10 | "rule": [{}, {}], 11 | "result": [{}, {}], 12 | "data": null 13 | } 14 | ] -------------------------------------------------------------------------------- /suites/exists.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Test Specification for exists", 3 | { 4 | "description": "Checks if a normal key exists", 5 | "rule": { "exists": "hello" }, 6 | "data": { "hello" : 1 }, 7 | "result": true 8 | }, 9 | { 10 | "description": "Checks if a normal key exists (array)", 11 | "rule": { "exists": ["hello"] }, 12 | "data": { "hello" : 1 }, 13 | "result": true 14 | }, 15 | { 16 | "description": "Checks if a normal key exists (false)", 17 | "rule": { "exists": "hello" }, 18 | "data": { "world" : 1 }, 19 | "result": false 20 | }, 21 | { 22 | "description": "Checks if an empty key exists (true)", 23 | "rule": { "exists": [""] }, 24 | "data": { "" : 1 }, 25 | "result": true 26 | }, 27 | { 28 | "description": "Checks if an empty key exists (false)", 29 | "rule": { "exists": [""] }, 30 | "data": { "hello" : 1 }, 31 | "result": false 32 | }, 33 | { 34 | "description": "Checks if a nested key exists", 35 | "rule": { "exists": ["hello", "world"] }, 36 | "data": { "hello" : { "world": false } }, 37 | "result": true 38 | }, 39 | { 40 | "description": "Checks if a nested key exists (false)", 41 | "rule": { "exists": ["hello", "world"] }, 42 | "data": { "hello" : { "x": false } }, 43 | "result": false 44 | }, 45 | { 46 | "description": "Checks if a null value exists", 47 | "rule": { "exists": "hello" }, 48 | "data": { "hello" : null }, 49 | "result": true 50 | } 51 | ] -------------------------------------------------------------------------------- /suites/iterators.extra.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "if with dynamic arguments throws", 4 | "rule": { "if": { "preserve": [true, 1, 2] } }, 5 | "data": null, 6 | "error": { "type": "Invalid Arguments" } 7 | }, 8 | { 9 | "description": "or with dynamic arguments throws", 10 | "rule": { "or": { "preserve": [false, false, true] } }, 11 | "data": null, 12 | "error": { "type": "Invalid Arguments" } 13 | }, 14 | { 15 | "description": "and with dynamic arguments throws", 16 | "rule": { "and": { "preserve": [true, true, true] } }, 17 | "data": null, 18 | "error": { "type": "Invalid Arguments" } 19 | }, 20 | { 21 | "description": "map with dynamic arguments throws", 22 | "rule": { "map": { "preserve": [[1,2,3], { "val": [] }]} }, 23 | "data": null, 24 | "error": { "type": "Invalid Arguments" } 25 | }, 26 | { 27 | "description": "filter with dynamic arguments throws", 28 | "rule": { "filter": { "preserve": [[1,2,3], { "val": [] }]} }, 29 | "data": null, 30 | "error": { "type": "Invalid Arguments" } 31 | }, 32 | { 33 | "description": "reduce with dynamic arguments throws", 34 | "rule": { "reduce": { "preserve": [[1,2,3], { "val": [] }]} }, 35 | "data": null, 36 | "error": { "type": "Invalid Arguments" } 37 | }, 38 | { 39 | "description": "some with dynamic arguments throws", 40 | "rule": { "some": { "preserve": [[1,2,3], { "val": [] }]} }, 41 | "data": null, 42 | "error": { "type": "Invalid Arguments" } 43 | }, 44 | { 45 | "description": "all with dynamic arguments throws", 46 | "rule": { "all": { "preserve": [[1,2,3], { "val": [] }]} }, 47 | "data": null, 48 | "error": { "type": "Invalid Arguments" } 49 | }, 50 | { 51 | "description": "none with dynamic arguments throws", 52 | "rule": { "none": { "preserve": [[1,2,3], { "val": [] }]} }, 53 | "data": null, 54 | "error": { "type": "Invalid Arguments" } 55 | }, 56 | { 57 | "description": "< with dynamic arguments throws", 58 | "rule": { "<": { "preserve": [1,2,3] } }, 59 | "data": null, 60 | "error": { "type": "Invalid Arguments" } 61 | }, 62 | { 63 | "description": "<= with dynamic arguments throws", 64 | "rule": { "<=": { "preserve": [1,2,3] } }, 65 | "data": null, 66 | "error": { "type": "Invalid Arguments" } 67 | }, 68 | { 69 | "description": "> with dynamic arguments throws", 70 | "rule": { ">": { "preserve": [1,2,3] } }, 71 | "data": null, 72 | "error": { "type": "Invalid Arguments" } 73 | }, 74 | { 75 | "description": ">= with dynamic arguments throws", 76 | "rule": { ">=": { "preserve": [1,2,3] } }, 77 | "data": null, 78 | "error": { "type": "Invalid Arguments" } 79 | }, 80 | { 81 | "description": "== with dynamic arguments throws", 82 | "rule": { "==": { "preserve": [1,2,3] } }, 83 | "data": null, 84 | "error": { "type": "Invalid Arguments" } 85 | }, 86 | { 87 | "description": "!= with dynamic arguments throws", 88 | "rule": { "!=": { "preserve": [1,2,3] } }, 89 | "data": null, 90 | "error": { "type": "Invalid Arguments" } 91 | }, 92 | { 93 | "description": "=== with dynamic arguments throws", 94 | "rule": { "===": { "preserve": [1,2,3] } }, 95 | "data": null, 96 | "error": { "type": "Invalid Arguments" } 97 | }, 98 | { 99 | "description": "!== with dynamic arguments throws", 100 | "rule": { "!==": { "preserve": [1,2,3] } }, 101 | "data": null, 102 | "error": { "type": "Invalid Arguments" } 103 | }, 104 | "Any of the operators listed when called with a non-array argument, will throw", 105 | { 106 | "description": "if with static non-array argument throws", 107 | "rule": { "if": 5 }, 108 | "data": null, 109 | "error": { "type": "Invalid Arguments" } 110 | }, 111 | { 112 | "description": "or with static non-array argument throws", 113 | "rule": { "or": 5 }, 114 | "data": null, 115 | "error": { "type": "Invalid Arguments" } 116 | }, 117 | { 118 | "description": "and with static non-array argument throws", 119 | "rule": { "and": 5 }, 120 | "data": null, 121 | "error": { "type": "Invalid Arguments" } 122 | }, 123 | { 124 | "description": "map with static non-array argument throws", 125 | "rule": { "map": 5 }, 126 | "data": null, 127 | "error": { "type": "Invalid Arguments" } 128 | }, 129 | { 130 | "description": "filter with static non-array argument throws", 131 | "rule": { "filter": 5 }, 132 | "data": null, 133 | "error": { "type": "Invalid Arguments" } 134 | }, 135 | { 136 | "description": "reduce with static non-array argument throws", 137 | "rule": { "reduce": 5 }, 138 | "data": null, 139 | "error": { "type": "Invalid Arguments" } 140 | }, 141 | { 142 | "description": "some with static non-array argument throws", 143 | "rule": { "some": 5 }, 144 | "data": null, 145 | "error": { "type": "Invalid Arguments" } 146 | }, 147 | { 148 | "description": "all with static non-array argument throws", 149 | "rule": { "all": 5 }, 150 | "data": null, 151 | "error": { "type": "Invalid Arguments" } 152 | }, 153 | { 154 | "description": "none with static non-array argument throws", 155 | "rule": { "none": 5 }, 156 | "data": null, 157 | "error": { "type": "Invalid Arguments" } 158 | }, 159 | { 160 | "description": "< with static non-array argument throws", 161 | "rule": { "<": 5 }, 162 | "data": null, 163 | "error": { "type": "Invalid Arguments" } 164 | }, 165 | { 166 | "description": "<= with static non-array argument throws", 167 | "rule": { "<=": 5 }, 168 | "data": null, 169 | "error": { "type": "Invalid Arguments" } 170 | }, 171 | { 172 | "description": "> with static non-array argument throws", 173 | "rule": { ">": 5 }, 174 | "data": null, 175 | "error": { "type": "Invalid Arguments" } 176 | }, 177 | { 178 | "description": ">= with static non-array argument throws", 179 | "rule": { ">=": 5 }, 180 | "data": null, 181 | "error": { "type": "Invalid Arguments" } 182 | }, 183 | { 184 | "description": "== with static non-array argument throws", 185 | "rule": { "==": 5 }, 186 | "data": null, 187 | "error": { "type": "Invalid Arguments" } 188 | }, 189 | { 190 | "description": "!= with static non-array argument throws", 191 | "rule": { "!=": 5 }, 192 | "data": null, 193 | "error": { "type": "Invalid Arguments" } 194 | }, 195 | { 196 | "description": "=== with static non-array argument throws", 197 | "rule": { "===": 5 }, 198 | "data": null, 199 | "error": { "type": "Invalid Arguments" } 200 | }, 201 | { 202 | "description": "!== with static non-array argument throws", 203 | "rule": { "!==": 5 }, 204 | "data": null, 205 | "error": { "type": "Invalid Arguments" } 206 | } 207 | ] -------------------------------------------------------------------------------- /suites/length.json: -------------------------------------------------------------------------------- 1 | [ 2 | "# Length operator tests", 3 | { 4 | "description": "Length of a basic array", 5 | "rule": { "length": {"val": "array"} }, 6 | "data": { "array": [1, 2, 3, 4, 5] }, 7 | "result": 5 8 | }, 9 | { 10 | "description": "Length of an empty array returns 0", 11 | "rule": { "length": {"val": "emptyArray"} }, 12 | "data": { "emptyArray": [] }, 13 | "result": 0 14 | }, 15 | { 16 | "description": "Length of a string counts characters", 17 | "rule": { "length": {"val": "str"} }, 18 | "data": { "str": "hello" }, 19 | "result": 5 20 | }, 21 | { 22 | "description": "Length of an empty string returns 0", 23 | "rule": { "length": {"val": "empty"} }, 24 | "data": { "empty": "" }, 25 | "result": 0 26 | }, 27 | { 28 | "description": "Length of missing variable returns null", 29 | "rule": { "length": {"val": "missing"} }, 30 | "data": {}, 31 | "error": { "type": "Invalid Arguments" } 32 | }, 33 | { 34 | "description": "Length operator with null argument throws error", 35 | "rule": { "length": null }, 36 | "data": {}, 37 | "error": { "type": "Invalid Arguments" } 38 | }, 39 | { 40 | "description": "Length operator with numeric argument throws error", 41 | "rule": { "length": 123 }, 42 | "data": {}, 43 | "error": { "type": "Invalid Arguments" } 44 | }, 45 | { 46 | "description": "Length operator with boolean argument throws error", 47 | "rule": { "length": true }, 48 | "data": {}, 49 | "error": { "type": "Invalid Arguments" } 50 | }, 51 | { 52 | "description": "Conditional based on array length (longer than 3)", 53 | "rule": { "if": [{ ">": [{ "length": {"val": "array"} }, 3] }, "long", "short"] }, 54 | "data": { "array": [1, 2, 3, 4] }, 55 | "result": "long" 56 | }, 57 | { 58 | "description": "Conditional based on array length (not longer than 3)", 59 | "rule": { "if": [{ ">": [{ "length": {"val": "array"} }, 3] }, "long", "short"] }, 60 | "data": { "array": [1, 2, 3] }, 61 | "result": "short" 62 | }, 63 | { 64 | "description": "Length of nested array counts top level elements", 65 | "rule": { "length": {"val": "nestedArray"} }, 66 | "data": { "nestedArray": [[1, 2], [3, 4], [5, 6]] }, 67 | "result": 3 68 | }, 69 | { 70 | "description": "Length of array of objects counts objects", 71 | "rule": { "length": {"val": "objectArray"} }, 72 | "data": { "objectArray": [{"a": 1}, {"b": 2}, {"c": 3}] }, 73 | "result": 3 74 | } 75 | ] -------------------------------------------------------------------------------- /suites/preserve.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "Preserve on a string", 4 | "rule": { "preserve": "a" }, 5 | "result": "a", 6 | "data": null 7 | }, 8 | { 9 | "description": "Preserve on a number", 10 | "rule": { "preserve": 1 }, 11 | "result": 1, 12 | "data": null 13 | }, 14 | { 15 | "description": "Preserve on a zero", 16 | "rule": { "preserve": 0 }, 17 | "result": 0, 18 | "data": null 19 | }, 20 | { 21 | "description": "Preserve on a negative number", 22 | "rule": { "preserve": -1 }, 23 | "result": -1, 24 | "data": null 25 | }, 26 | { 27 | "description": "Preserve on true", 28 | "rule": { "preserve": true }, 29 | "result": true, 30 | "data": null 31 | }, 32 | { 33 | "description": "Preserve on false", 34 | "rule": { "preserve": false }, 35 | "result": false, 36 | "data": null 37 | }, 38 | { 39 | "description": "Preserve on an array", 40 | "rule": { "preserve": [1, 2] }, 41 | "result": [1, 2], 42 | "data": null 43 | }, 44 | { 45 | "description": "Preserve on a null", 46 | "rule": { "preserve": null }, 47 | "result": null, 48 | "data": null 49 | }, 50 | { 51 | "description": "Preserve on a single key object", 52 | "rule": { "preserve": { "a": 1 } }, 53 | "result": { "a": 1 }, 54 | "data": null 55 | }, 56 | { 57 | "description": "Preserve on a multi key object", 58 | "rule": { "preserve": { "a": 1, "b": 2 } }, 59 | "result": { "a": 1, "b": 2 }, 60 | "data": null 61 | }, 62 | { 63 | "description": "Preserve on an array of objects", 64 | "rule": { "preserve": [{ "a": 1 }, { "b": 2 }, { "a": 1, "b": 2 }] }, 65 | "result": [{ "a": 1 }, { "b": 2 }, { "a": 1, "b": 2 }], 66 | "data": null 67 | }, 68 | { 69 | "description": "Preserve on something that looks like an operator", 70 | "rule": { "preserve": { "+": [1, 2, 3] } }, 71 | "result": { "+": [1, 2, 3] }, 72 | "data": null 73 | }, 74 | { 75 | "description": "Preserve on an empty array", 76 | "rule": { "preserve": [] }, 77 | "result": [], 78 | "data": null 79 | }, 80 | { 81 | "description": "Preserve on an empty object", 82 | "rule": { "preserve": {} }, 83 | "result": {}, 84 | "data": null 85 | } 86 | ] -------------------------------------------------------------------------------- /suites/scopes.json: -------------------------------------------------------------------------------- 1 | [ 2 | "A proposal for handling scopes", 3 | { 4 | "description": "Map can add each number to index", 5 | "rule": { 6 | "map": [ 7 | { "val": "numbers" }, 8 | { "+": [{ "val": [[1], "index"] }, { "val": [] }]} 9 | ] 10 | }, 11 | "data": { "numbers": [1,2,3] }, 12 | "result": [1,3,5] 13 | }, 14 | { 15 | "description": "Map can add each number to value from context", 16 | "rule": { 17 | "map": [ 18 | { "val": "numbers" }, 19 | { "+": [{ "val": [[2], "value"] }, { "val": [] }]} 20 | ] 21 | }, 22 | "data": { "numbers": [1,2,3], "value": 10 }, 23 | "result": [11,12,13] 24 | }, 25 | { 26 | "description": "Filter can use parent context to filter", 27 | "rule": { 28 | "filter": [ 29 | { "val": "people" }, 30 | { "===": [{ "val": "department" }, { "val": [[2], "department"] }] } 31 | ] 32 | }, 33 | "data": { 34 | "department": "Engineering", 35 | "people": [ 36 | { "name": "Jay Ortiz", "department": "Engineering" }, 37 | { "name": "Louisa Hall", "department": "Sales" }, 38 | { "name": "Kyle Carlson", "department": "Sales" }, 39 | { "name": "Grace Ortiz", "department": "Engineering" }, 40 | { "name": "Isabelle Harrington", "department": "Marketing" }, 41 | { "name": "Harold Moore", "department": "Sales" }, 42 | { "name": "Clarence Schultz", "department": "Sales" }, 43 | { "name": "Jesse Keller", "department": "Engineering" }, 44 | { "name": "Phillip Holland", "department": "Marketing" }, 45 | { "name": "Mason Sullivan", "department": "Engineering" } 46 | ] 47 | }, 48 | "result": [ 49 | { "name": "Jay Ortiz", "department": "Engineering" }, 50 | { "name": "Grace Ortiz", "department": "Engineering" }, 51 | { "name": "Jesse Keller", "department": "Engineering" }, 52 | { "name": "Mason Sullivan", "department": "Engineering" } 53 | ] 54 | }, 55 | "The specification below depends upon escaping from another proposal", 56 | { 57 | "description": "Access an escaped key from above", 58 | "rule": { 59 | "map": [ 60 | { "val": "arr" }, 61 | { "+": [{ "val": [[2], "", "", "/"] }, { "val": [[2], "../"] }]} 62 | ] 63 | }, 64 | "data": { "arr": [1,2,3], "../": 10, "": { "": { "/": 7 }} }, 65 | "result": [17,17,17] 66 | } 67 | ] -------------------------------------------------------------------------------- /suites/throw.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "Throws hello as an error", 4 | "rule": { "throw": "hello" }, 5 | "data": null, 6 | "error": { "type": "hello" } 7 | }, 8 | { 9 | "description": "Throwing NaN produces an error object or measured equivalent (equivalence class test)", 10 | "rule": { "throw": "NaN" }, 11 | "data": null, 12 | "error": { "type": "NaN" } 13 | }, 14 | { 15 | "description": "Can throw an error object", 16 | "rule": { "throw": { "val": "x" } }, 17 | "data": { "x": { "type": "Some error" }}, 18 | "error": { "type": "Some error" } 19 | } 20 | ] -------------------------------------------------------------------------------- /suites/truthiness.json: -------------------------------------------------------------------------------- 1 | [ 2 | "# This can also be seen as a test suite for !!", 3 | "# Primitive Truthiness tests", 4 | { 5 | "description": "Truthy: true", 6 | "rule": { "!!": true }, 7 | "result": true, 8 | "data": null 9 | }, 10 | { 11 | "description": "Falsey: false", 12 | "rule": { "!!": false }, 13 | "result": false, 14 | "data": null 15 | }, 16 | { 17 | "description": "Truthy: 1", 18 | "rule": { "!!": 1 }, 19 | "result": true, 20 | "data": null 21 | }, 22 | { 23 | "description": "Falsey: 0", 24 | "rule": { "!!": 0 }, 25 | "result": false, 26 | "data": null 27 | }, 28 | { 29 | "description": "Truthy: -1", 30 | "rule": { "!!": -1 }, 31 | "result": true, 32 | "data": null 33 | }, 34 | { 35 | "description": "Falsey: []", 36 | "rule": { "!!": [[]] }, 37 | "result": false, 38 | "data": null 39 | }, 40 | { 41 | "description": "Truthy: {}, Direct", 42 | "rule": { "!!": {} }, 43 | "result": false, 44 | "data": null 45 | }, 46 | { 47 | "description": "Truthy: {}", 48 | "rule": { "!!": [{}] }, 49 | "result": false, 50 | "data": null 51 | }, 52 | { 53 | "description": "Falsey: ''", 54 | "rule": { "!!": "" }, 55 | "result": false, 56 | "data": null 57 | }, 58 | { 59 | "description": "Falsey: null", 60 | "rule": { "!!": null }, 61 | "result": false, 62 | "data": null 63 | }, 64 | "# Context Object Tests", 65 | { 66 | "description": "Truthy: Zero Key Object", 67 | "rule": { "!!": { "val": "obj" } }, 68 | "result": false, 69 | "data": { "obj": {} } 70 | }, 71 | { 72 | "description": "Truthy: Single Key Object", 73 | "rule": { "!!": { "val": "obj" } }, 74 | "result": true, 75 | "data": { "obj": { "a": 0 } } 76 | }, 77 | { 78 | "description": "Truthy: Multi Key Object", 79 | "rule": { "!!": { "val": "obj" } }, 80 | "result": true, 81 | "data": { "obj": { "a": 0, "b": 0 } } 82 | }, 83 | { 84 | "description": "Truthy: Single Key Object (Empty Key)", 85 | "rule": { "!!": { "val": "obj" } }, 86 | "result": true, 87 | "data": { "obj": { "": 0 } } 88 | } 89 | ] -------------------------------------------------------------------------------- /suites/try.extra.json: -------------------------------------------------------------------------------- 1 | [ 2 | "# Not Proposed", 3 | { 4 | "description": "Try can work further up the AST with Exceptions; Grabbing other Context value", 5 | "rule": { 6 | "try": [{ 7 | "if": [ 8 | true, 9 | { "map": [[1,2,3], {"/": [0, 0] }]}, 10 | null 11 | ] 12 | }, { "val": [[2], "fallback"] }] 13 | }, 14 | "result": "Hello", 15 | "data": { "fallback": "Hello" } 16 | } 17 | ] -------------------------------------------------------------------------------- /suites/try.json: -------------------------------------------------------------------------------- 1 | [ 2 | "# Collection of Tests for Try", 3 | { 4 | "description": "Coalesce an error", 5 | "rule": { "try": [{ "throw": "Some error" }, 1] }, 6 | "result": 1, 7 | "data": null 8 | }, 9 | { 10 | "description": "Coalesce an error emitted from an operator", 11 | "rule": { "try": [{ "/": [0, 0]}, 1] }, 12 | "result": 1, 13 | "data": { "hello": "world" } 14 | }, 15 | { 16 | "description": "Try is variadic", 17 | "rule": { "try": [{ "throw": "Some error" }, { "/": [0, 0] }, 2] }, 18 | "result": 2, 19 | "data": null 20 | }, 21 | { 22 | "description": "Panics if none of the values are valid", 23 | "rule": { "try": [{ "throw": "Some error" }, { "throw": "Some other error" }] }, 24 | "error": { "type": "Some other error" }, 25 | "data": null 26 | }, 27 | { 28 | "description": "Panics if none of the values are valid (2)", 29 | "rule": { "try": [{ "throw": "Some error" }, { "/": [0, 0] }] }, 30 | "error": { "type": "NaN" }, 31 | "data": null 32 | }, 33 | { 34 | "description": "Panics if none of the values are valid (3)", 35 | "rule": { "try": [{ "/": [0, 0] }, { "/": [1, 0] }, { "/": [2, 0] }] }, 36 | "error": { "type": "NaN" }, 37 | "data": null 38 | }, 39 | { 40 | "description": "Panics when the only argument is an error", 41 | "rule": { "try": { "throw": "Some error" } }, 42 | "error": { "type": "Some error" }, 43 | "data": null 44 | }, 45 | { 46 | "description": "Panic with an error emitted from an operator", 47 | "rule": { "try": [{ "/": [1, 0] }] }, 48 | "error": { "type": "NaN" }, 49 | "data": null 50 | }, 51 | { 52 | "description": "Panic within an iterator", 53 | "rule": { "map": [[1, 2, 3], { "try": [{ "/": [0,0] }] }] }, 54 | "error": { "type": "NaN" }, 55 | "data": null 56 | }, 57 | { 58 | "description": "Panic based on an error emitted from an if", 59 | "rule": { "try": [{ "if": [{"val": ["user", "admin"]}, true, { "throw": "Not an admin" }] }] }, 60 | "data": { "user": { "admin": false } }, 61 | "error": { "type": "Not an admin" } 62 | }, 63 | { 64 | "description": "Try can work further up the AST with Exceptions", 65 | "rule": { 66 | "try": [{ 67 | "if": [ 68 | true, 69 | { "map": [[1,2,3], {"/": [0, 0] }]}, 70 | null 71 | ] 72 | }, 10] 73 | }, 74 | "result": 10, 75 | "data": null 76 | }, 77 | { 78 | "description": "The context switches for the try coalescing to the previous error", 79 | "rule": { 80 | "try": [ 81 | { "throw": "Some error" }, 82 | { "val": "type" } 83 | ] 84 | }, 85 | "result": "Some error", 86 | "data": null 87 | }, 88 | { 89 | "description": "The context switches for the try coalescing to the previous error (2)", 90 | "rule": { 91 | "try": [ 92 | { "if": [true, { "throw": "Some error" }, null] }, 93 | { "val": "type" } 94 | ] 95 | }, 96 | "result": "Some error", 97 | "data": null 98 | }, 99 | { 100 | "description": "The context switches for the try coalescing to the previous error (3)", 101 | "rule": { 102 | "try": [ 103 | { "throw": "A" }, 104 | { "throw": "B" }, 105 | { "val": "type" } 106 | ] 107 | }, 108 | "result": "B", 109 | "data": null 110 | }, 111 | { 112 | "description": "Error can pull from an error object", 113 | "rule": { 114 | "try": [{ "throw": { "val": "x" } }, { "val": "type" }] 115 | }, 116 | "data": { "x": { "type": "Some error" }}, 117 | "result": "Some error" 118 | }, 119 | { 120 | "description": "Try can work further up the AST with Exceptions, and return the error", 121 | "rule": { 122 | "try": [{ 123 | "if": [ 124 | true, 125 | { "map": [[1,2,3], {"/": [0, 0] }]}, 126 | null 127 | ] 128 | }, { "val": "type" }] 129 | }, 130 | "result": "NaN", 131 | "data": null 132 | }, 133 | { 134 | "description": "Handles NaN Explicitly", 135 | "rule": { 136 | "try": [ 137 | { "if": [{ "/": [1, { "val": "x" }] }, { "throw": "Some error" }, null] }, 138 | { 139 | "if": [{ "===": [{ "val": "type" }, "NaN"]}, "Handled", { "throw": { "val": [] } }] 140 | } 141 | ] 142 | }, 143 | "result": "Handled", 144 | "data": { "x": 0 } 145 | }, 146 | { 147 | "description": "Did not NaN, so it errored", 148 | "rule": { 149 | "try": [ 150 | { "if": [{ "/": [1, { "val": "x" }] }, { "throw": "Some error" }, null] }, 151 | { "if": [{ "===": [{ "val": "type" }, "NaN"]}, "Handled", { "throw": { "val": [] } }] } 152 | ] 153 | }, 154 | "error": { "type": "Some error" }, 155 | "data": { "x": 1 } 156 | } 157 | ] -------------------------------------------------------------------------------- /suites/unknown-operators.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "Single key object, does not map to operator. Should fail.", 4 | "rule": { "UnknownOperator": true }, 5 | "data": null, 6 | "error": { "type": "Unknown Operator" } 7 | }, 8 | { 9 | "description": "Multi-Key Object, both keys are not operators. Should fail.", 10 | "rule": { "UnknownOperator": true, "UnknownOperator2": true }, 11 | "data": null, 12 | "error": { "type": "Unknown Operator" } 13 | }, 14 | { 15 | "description": "Multi-Key Object, one key is a valid operator (valid, invalid). Should fail.", 16 | "rule": { "+": true, "UnknownOperator2": true }, 17 | "data": null, 18 | "error": { "type": "Unknown Operator" } 19 | }, 20 | { 21 | "description": "Multi-Key Object, one key is a valid operator (invalid, valid). Should fail.", 22 | "rule": { "UnknownOperator2": true, "+": true }, 23 | "data": null, 24 | "error": { "type": "Unknown Operator" } 25 | }, 26 | { 27 | "description": "Multi-Key Object, both keys are valid operators. Should fail.", 28 | "rule": { "+": true, "-": true }, 29 | "data": null, 30 | "error": { "type": "Unknown Operator" } 31 | } 32 | ] -------------------------------------------------------------------------------- /suites/val.extra.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Testing out scopes", 3 | { 4 | "description": "Climb up to get adder", 5 | "rule": { "map": [[1,2,3], { "+": [{ "val": [] }, { "val": [[-2], "adder"] }] }] }, 6 | "data": { "adder": 10 }, 7 | "result": [11,12,13] 8 | }, 9 | { 10 | "description": "Climb up to get index", 11 | "rule": { "map": [[1,2,3], { "+": [{ "val": [] }, { "val": [[-1], "index"] }] }] }, 12 | "data": { "adder": 10 }, 13 | "result": [1,3,5] 14 | }, 15 | { 16 | "description": "Nested get adder", 17 | "rule": { 18 | "map": [["Test"], { "map": [[1,2,3], { "+": [{"val": []}, {"val": [[-4], "adder"]}] }]} ] 19 | }, 20 | "data": { "adder": 10 }, 21 | "result": [[11,12,13]] 22 | } 23 | ] -------------------------------------------------------------------------------- /suites/val.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Test Specification for val", 3 | { 4 | "description": "Fetches a value", 5 | "rule": { "val": "hello" }, 6 | "data": { "hello" : 0 }, 7 | "result": 0 8 | }, 9 | { 10 | "description": "Fetches a nested value", 11 | "rule": { "val": ["hello", "world"] }, 12 | "data": { "hello" : { "world": 1 } }, 13 | "result": 1 14 | }, 15 | { 16 | "description": "Fetches a value from an empty key", 17 | "rule": { "val": "" }, 18 | "data": { "" : 1 }, 19 | "result": 1 20 | }, 21 | { 22 | "description": "Fetches a value from a nested empty key", 23 | "rule": { "val": ["", ""] }, 24 | "data": { "" : { "": 2 } }, 25 | "result": 2 26 | }, 27 | { 28 | "description": "Fetches a value from an array", 29 | "rule": { "val": [1] }, 30 | "data": [1, 2], 31 | "result": 2 32 | }, 33 | { 34 | "description": "Fetches a value from an array in an object", 35 | "rule": { "val": ["arr", 1] }, 36 | "data": { "arr": [1, 2] }, 37 | "result": 2 38 | }, 39 | { 40 | "description": "Fetches a value from a doubly nested empty key", 41 | "rule": { "val": ["", "", ""] }, 42 | "data": { "" : { "": { "": 3 } } }, 43 | "result": 3 44 | }, 45 | { 46 | "description": "Fetches a value from a key that is purely a dot", 47 | "rule": { "val": "." }, 48 | "data": { "." : 20 }, 49 | "result": 20 50 | }, 51 | { 52 | "description": "Fetching a value from null returns null", 53 | "rule": { "val": "hello" }, 54 | "data": { "hello" : null }, 55 | "result": null 56 | }, 57 | { 58 | "description": "Fetching a value from a null fetched value returns null", 59 | "rule": { "val": ["hello", "world"] }, 60 | "data": { "hello" : null }, 61 | "result": null 62 | }, 63 | { 64 | "description": "Fetches the entire context", 65 | "rule": { "val": [] }, 66 | "data": { "": 21 }, 67 | "result": { "": 21 } 68 | }, 69 | { 70 | "description": "Fetches the entire context for a nested key", 71 | "rule": { "val": "" }, 72 | "data": { "": { "": 22 } }, 73 | "result": { "": 22 } 74 | }, 75 | { 76 | "description": "Using val in a map", 77 | "rule": { "map": [[1,2,3], { "+": [{ "val": [] }, 1] }] }, 78 | "data": null, 79 | "result": [2,3,4] 80 | } 81 | ] -------------------------------------------------------------------------------- /utilities/chainingSupported.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 'use strict' 3 | /** 4 | * Checks if optional chaining is supported for the compiler 5 | * @returns {Boolean} 6 | */ 7 | const getIsOptionalChainingSupported = () => { 8 | try { 9 | // eslint-disable-next-line no-unused-vars 10 | const test = {} 11 | // eslint-disable-next-line no-eval 12 | const isUndefined = (typeof globalThis !== 'undefined' ? globalThis : global).eval('(test) => test?.foo?.bar')(test) 13 | return isUndefined === undefined 14 | } catch (err) { 15 | return false 16 | } 17 | } 18 | export default getIsOptionalChainingSupported() 19 | -------------------------------------------------------------------------------- /utilities/coerceArray.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Coerces a value into an array. 4 | * This is used for unary value operations. 5 | */ 6 | export function coerceArray (value) { 7 | return Array.isArray(value) ? value : [value] 8 | } 9 | -------------------------------------------------------------------------------- /utilities/countArguments.js: -------------------------------------------------------------------------------- 1 | const counts = new WeakMap() 2 | 3 | /** 4 | * Counts the number of arguments a function has; paying attention to the function's signature 5 | * to avoid edge cases. 6 | * This is used to allow for compiler optimizations. 7 | * @param {(...args: any[]) => any} fn 8 | * @returns {number} 9 | */ 10 | export function countArguments (fn) { 11 | if (!fn || typeof fn !== 'function' || !fn.length) return 0 12 | if (!counts.has(fn)) counts.set(fn, _countArguments(fn)) 13 | return counts.get(fn) 14 | } 15 | 16 | /** 17 | * Counts the number of arguments a function has; paying attention to the function's signature. 18 | * This is the internal implementation that does not use a WeakMap. 19 | * @param {(...args: any[]) => any} fn 20 | * @returns {number} 21 | */ 22 | function _countArguments (fn) { 23 | if (!fn || typeof fn !== 'function' || !fn.length) return 0 24 | let fnStr = fn.toString() 25 | if (fnStr[0] !== '(' && fnStr[0] !== 'f') return 0 26 | fnStr = fnStr.substring(fnStr.indexOf('('), fnStr.indexOf('{')).replace(/=>/g, '') 27 | 28 | // regex to check for "..." or "=" 29 | const regex = /\.{3}|=/ 30 | if (regex.test(fnStr)) return 0 31 | return fn.length 32 | } 33 | -------------------------------------------------------------------------------- /utilities/declareSync.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 'use strict' 3 | 4 | import { Sync } from '../constants.js' 5 | export default function declareSync (obj, sync = true) { 6 | obj[Sync] = sync 7 | return obj 8 | } 9 | -------------------------------------------------------------------------------- /utilities/downgrade.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Used to precoerce a data value to a number, for the purposes of coalescing. 3 | * @param {any} item 4 | */ 5 | export function precoerceNumber (item) { 6 | if (Number.isNaN(item)) throw NaN 7 | if (!item) return item 8 | if (typeof item === 'object') throw NaN 9 | return item 10 | } 11 | 12 | /** 13 | * Used to assert in compiled templates that a value is an array of at least a certain size. 14 | * @param {*} arr 15 | * @param {number} size 16 | */ 17 | export function assertSize (arr, size) { 18 | // eslint-disable-next-line no-throw-literal 19 | if (!Array.isArray(arr) || arr.length < size) throw { type: 'Invalid Arguments' } 20 | return arr 21 | } 22 | 23 | /** 24 | * Used to assert in compiled templates that when a numeric comparison is made, both values are numbers. 25 | * @param {*} item 26 | * @param {*} prev 27 | * @returns {number} 28 | */ 29 | export function compareCheck (item, prev, strict) { 30 | if (strict || (typeof item === 'string' && typeof prev === 'string')) return item 31 | 32 | if (Number.isNaN(+precoerceNumber(item))) throw NaN 33 | if (Number.isNaN(+precoerceNumber(prev))) throw NaN 34 | 35 | // The following two checks allow us to handle null == 0 and 0 == null; it's honestly 36 | // kind of gross that JavaScript works this way out of the box. Like, why is 0 <= null true, 37 | // but null == false. It's just weird. 38 | if (prev === null && !item) return null 39 | if (item === null && !prev) return 0 40 | 41 | return item 42 | } 43 | -------------------------------------------------------------------------------- /utilities/omitUndefined.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 'use strict' 3 | export default (function omitUndefined (obj) { 4 | Object.keys(obj).forEach((key) => { 5 | if (obj[key] === undefined) { 6 | delete obj[key] 7 | } 8 | }) 9 | return obj 10 | }) 11 | -------------------------------------------------------------------------------- /utilities/splitPath.js: -------------------------------------------------------------------------------- 1 | const parsedPaths = new Map() 2 | 3 | /** 4 | * Splits a path string into an array of parts; lightly memoized. 5 | * It will reset the entire cache after 2048 paths, this could be improved 6 | * by implementing an LRU cache or something, but I'm trying to keep 7 | * this library fairly dep free, and the code not too cumbersome. 8 | * 9 | * Memoizing the splitPath function can be seen as cheating, but I think it's likely 10 | * that a lot of the same paths will be used for logic, so it's a good optimization. 11 | * 12 | * @param {string} str 13 | * @returns {string[]} 14 | */ 15 | export function splitPathMemoized (str) { 16 | if (parsedPaths.has(str)) return parsedPaths.get(str) 17 | if (parsedPaths.size > 2048) parsedPaths.clear() 18 | const parts = splitPath(str) 19 | parsedPaths.set(str, parts) 20 | return parts 21 | } 22 | 23 | /** 24 | * Splits a path string into an array of parts. 25 | * 26 | * @example splitPath('a.b.c') // ['a', 'b', 'c'] 27 | * @example splitPath('a\\.b.c') // ['a.b', 'c'] 28 | * @example splitPath('a\\\\.b.c') // ['a\\', 'b', 'c'] 29 | * @example splitPath('a\\\\\\.b.c') // ['a\\.b', 'c'] 30 | * @example splitPath('hello') // ['hello'] 31 | * @example splitPath('hello\\') // ['hello\\'] 32 | * @example splitPath('hello\\\\') // ['hello\\'] 33 | * 34 | * @param {string} str 35 | * @param {string} separator 36 | * @returns {string[]} 37 | */ 38 | export function splitPath (str, separator = '.', escape = '\\', up = '/') { 39 | const parts = [] 40 | let current = '' 41 | 42 | for (let i = 0; i < str.length; i++) { 43 | const char = str[i] 44 | if (char === escape) { 45 | if (str[i + 1] === separator || str[i + 1] === up) { 46 | current += str[i + 1] 47 | i++ 48 | } else if (str[i + 1] === escape) { 49 | current += escape 50 | i++ 51 | // The following else might be something tweaked in a spec. 52 | } else current += escape 53 | } else if (char === separator) { 54 | parts.push(current) 55 | current = '' 56 | } else current += char 57 | } 58 | 59 | // The if prevents me from pushing more sections than characters 60 | // This is so that "." will [''] and not ['',''] 61 | // But .h will be ['','.h'] 62 | // .. becomes ['',''], ..h becomes ['', '', 'h'] 63 | if (parts.length !== str.length) parts.push(current) 64 | return parts 65 | } 66 | -------------------------------------------------------------------------------- /utilities/traverseCopy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 'use strict' 3 | const mutateKey = (i) => i 4 | const mutateValue = (i) => i 5 | const skipCopy = (i) => false 6 | const defaultOptions = { mutateKey, mutateValue, skipCopy } 7 | /** 8 | * 9 | * @param {Object} obj 10 | * @param {Object} target 11 | * @param {{ mutateKey?: Function, mutateValue?: Function, skipCopy?: Function }} options 12 | * @returns 13 | */ 14 | function traverseCopy (obj, target = {}, options = defaultOptions) { 15 | const { mutateKey, mutateValue, skipCopy } = { 16 | ...defaultOptions, 17 | ...options 18 | } 19 | if (typeof obj === 'object' && !Array.isArray(obj) && obj) { 20 | if (skipCopy(obj)) return mutateValue(obj) 21 | Object.keys(obj).forEach((key) => { 22 | target[mutateKey(key)] = mutateValue(traverseCopy(obj[key], {}, options)) 23 | }) 24 | return target 25 | } else if (Array.isArray(obj)) { 26 | return [...obj].map((i, x) => mutateValue(traverseCopy(i, {}, options))) 27 | } else { 28 | return mutateValue(obj) 29 | } 30 | } 31 | export default traverseCopy 32 | --------------------------------------------------------------------------------