From c2c7a7935409505969abfd6ee2b07b0e05c73f55 Mon Sep 17 00:00:00 2001 From: Chris Pardy Date: Wed, 23 Oct 2024 11:12:00 -0400 Subject: [PATCH 1/4] Switch to Prettier and ESLint Switch to Prettier and ESLint for tooling. --- .github/workflows/deploy.yml | 41 +- .github/workflows/node.js.yml | 21 +- CHANGELOG.md | 204 ++-- README.md | 205 ++-- docs/almanac.md | 80 +- docs/engine.md | 239 +++-- docs/facts.md | 27 +- docs/rules.md | 371 +++---- docs/walkthrough.md | 159 +-- eslint.config.mjs | 23 + examples/01-hello-world.js | 40 +- examples/02-nested-boolean-logic.js | 92 +- examples/03-dynamic-facts.js | 81 +- examples/04-fact-dependency.js | 154 +-- ...optimizing-runtime-with-fact-priorities.js | 103 +- examples/06-custom-operators.js | 92 +- examples/07-rule-chaining.js | 153 +-- examples/08-fact-comparison.js | 186 ++-- examples/09-rule-results.js | 105 +- examples/10-condition-sharing.js | 108 +- examples/11-using-facts-in-events.js | 129 +-- examples/12-using-custom-almanac.js | 124 +-- examples/13-using-operator-decorators.js | 91 +- examples/support/account-api-client.js | 46 +- package.json | 35 +- src/almanac.js | 170 +-- src/condition.js | 150 +-- src/debug.js | 21 +- src/engine-default-operator-decorators.js | 50 +- src/engine-default-operators.js | 40 +- src/engine.js | 365 ++++--- src/errors.js | 8 +- src/fact.js | 72 +- src/index.js | 4 +- src/json-rules-engine.js | 16 +- src/operator-decorator.js | 34 +- src/operator-map.js | 132 +-- src/operator.js | 20 +- src/rule-result.js | 44 +- src/rule.js | 288 +++--- test/acceptance/acceptance.js | 372 +++---- test/almanac.test.js | 392 +++---- test/condition.test.js | 737 +++++++------ test/engine-all.test.js | 171 ++-- test/engine-any.test.js | 175 ++-- test/engine-cache.test.js | 110 +- test/engine-condition.test.js | 474 ++++----- test/engine-controls.test.js | 108 +- test/engine-custom-properties.test.js | 117 ++- test/engine-error-handling.test.js | 46 +- test/engine-event.test.js | 966 +++++++++--------- test/engine-fact-comparison.test.js | 206 ++-- test/engine-fact-priority.test.js | 350 ++++--- test/engine-fact.test.js | 549 +++++----- test/engine-facts-calling-facts.test.js | 160 +-- test/engine-failure.test.js | 74 +- test/engine-not.test.js | 80 +- test/engine-operator-map.test.js | 157 +-- test/engine-operator.test.js | 122 +-- test/engine-parallel-condition-cache.test.js | 144 +-- test/engine-recusive-rules.test.js | 336 +++--- test/engine-rule-priority.js | 168 +-- test/engine-run.test.js | 263 ++--- test/engine.test.js | 634 ++++++------ test/fact.test.js | 80 +- test/index.test.js | 24 +- test/operator-decorator.test.js | 65 +- test/operator.test.js | 46 +- test/performance.test.js | 104 +- test/rule.test.js | 611 +++++------ test/support/bootstrap.js | 24 +- test/support/condition-factory.js | 8 +- test/support/rule-factory.js | 40 +- types/index.d.ts | 53 +- types/index.test-d.ts | 74 +- 75 files changed, 6544 insertions(+), 5819 deletions(-) create mode 100644 eslint.config.mjs diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 305277ba..233ceeb4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,11 +5,10 @@ name: Deploy Package on: push: - branches: [ master, v6 ] + branches: [ master, v6, v8 ] jobs: build: - runs-on: ubuntu-latest strategy: @@ -17,26 +16,26 @@ jobs: node-version: [18.x, 20.x, 22.x] steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - run: npm install - - run: npm run build --if-present - - run: npm run lint - - run: npm test + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm run build --if-present + - run: npm run lint + - run: npm test deploy: needs: build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - with: - node-version: 18.x - - run: npm install - - run: npm run build --if-present - # https://github.com/marketplace/actions/npm-publish - - uses: JS-DevTools/npm-publish@v2 - with: - token: ${{ secrets.NPM_TOKEN }} + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 18.x + - run: npm install + - run: npm run build --if-present + # https://github.com/marketplace/actions/npm-publish + - uses: JS-DevTools/npm-publish@v2 + with: + token: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 92ee104b..fc4b90d3 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -5,11 +5,10 @@ name: Node.js CI on: pull_request: - branches: [ master, v6 ] + branches: [ master, v6, v8 ] jobs: build: - runs-on: ubuntu-latest strategy: @@ -17,12 +16,12 @@ jobs: node-version: [18.x, 20.x, 22.x] steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - run: npm install - - run: npm run build --if-present - - run: npm run lint - - run: npm test + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm run build --if-present + - run: npm run lint + - run: npm test diff --git a/CHANGELOG.md b/CHANGELOG.md index cc9bcf97..7c9af25d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,167 +1,209 @@ #### 6.1.0 / 2021-06-03 - * engine.removeRule() now supports removing rules by name - * Added engine.updateRule(rule) + +- engine.removeRule() now supports removing rules by name +- Added engine.updateRule(rule) #### 6.0.1 / 2021-03-09 - * Updates Typescript types to include `failureEvents` in EngineResult. + +- Updates Typescript types to include `failureEvents` in EngineResult. #### 6.0.0 / 2020-12-22 - * BREAKING CHANGES - * To continue using [selectn](https://github.com/wilmoore/selectn.js) syntax for condition `path`s, use the new `pathResolver` feature. Read more [here](./docs/rules.md#condition-helpers-custom-path-resolver). Add the following to the engine constructor: - ```js - const pathResolver = (object, path) => { - return selectn(path)(object) - } - const engine = new Engine(rules, { pathResolver }) - ``` - (fixes #205) - * Engine and Rule events `on('success')`, `on('failure')`, and Rule callbacks `onSuccess` and `onFailure` now honor returned promises; any event handler that returns a promise will be waited upon to resolve before engine execution continues. (fixes #235) - * Private `rule.event` property renamed. Use `rule.getEvent()` to avoid breaking changes in the future. - * The `success-events` fact used to store successful events has been converted to an internal data structure and will no longer appear in the almanac's facts. (fixes #187) - * NEW FEATURES - * Engine constructor now accepts a `pathResolver` option for resolving condition `path` properties. Read more [here](./docs/rules.md#condition-helpers-custom-path-resolver). (fixes #210) - * Engine.run() now returns three additional data structures: - * `failureEvents`, an array of all failed rules events. (fixes #192) - * `results`, an array of RuleResults for each successful rule (fixes #216) - * `failureResults`, an array of RuleResults for each failed rule +- BREAKING CHANGES + - To continue using [selectn](https://github.com/wilmoore/selectn.js) syntax for condition `path`s, use the new `pathResolver` feature. Read more [here](./docs/rules.md#condition-helpers-custom-path-resolver). Add the following to the engine constructor: + ```js + const pathResolver = (object, path) => { + return selectn(path)(object); + }; + const engine = new Engine(rules, { pathResolver }); + ``` + (fixes #205) + - Engine and Rule events `on('success')`, `on('failure')`, and Rule callbacks `onSuccess` and `onFailure` now honor returned promises; any event handler that returns a promise will be waited upon to resolve before engine execution continues. (fixes #235) + - Private `rule.event` property renamed. Use `rule.getEvent()` to avoid breaking changes in the future. + - The `success-events` fact used to store successful events has been converted to an internal data structure and will no longer appear in the almanac's facts. (fixes #187) +- NEW FEATURES + - Engine constructor now accepts a `pathResolver` option for resolving condition `path` properties. Read more [here](./docs/rules.md#condition-helpers-custom-path-resolver). (fixes #210) + - Engine.run() now returns three additional data structures: + - `failureEvents`, an array of all failed rules events. (fixes #192) + - `results`, an array of RuleResults for each successful rule (fixes #216) + - `failureResults`, an array of RuleResults for each failed rule #### 5.3.0 / 2020-12-02 - * Allow facts to have a value of `undefined` + +- Allow facts to have a value of `undefined` #### 5.2.0 / 2020-11-31 - * No changes; published to correct an accidental publish of untagged alpha + +- No changes; published to correct an accidental publish of untagged alpha #### 5.0.4 / 2020-09-26 - * Upgrade dependencies to latest + +- Upgrade dependencies to latest #### 5.0.3 / 2020-01-26 - * Upgrade jsonpath-plus dependency, to fix inconsistent scalar results (#175) + +- Upgrade jsonpath-plus dependency, to fix inconsistent scalar results (#175) #### 5.0.2 / 2020-01-18 -* BUGFIX: Add missing `DEBUG` log for almanac.addRuntimeFact() + +- BUGFIX: Add missing `DEBUG` log for almanac.addRuntimeFact() #### 5.0.1 / 2020-01-18 -* BUGFIX: `DEBUG` envs works with cookies disables + +- BUGFIX: `DEBUG` envs works with cookies disables #### 5.0.0 / 2019-11-29 - * BREAKING CHANGES - * Rule conditions' `path` property is now interpreted using [json-path](https://goessner.net/articles/JsonPath/) - * To continue using the old syntax (provided via [selectn](https://github.com/wilmoore/selectn.js)), `npm install selectn` as a direct dependency, and `json-rules-engine` will continue to interpret legacy paths this way. - * Any path starting with `$` will be assumed to use `json-path` syntax + +- BREAKING CHANGES + - Rule conditions' `path` property is now interpreted using [json-path](https://goessner.net/articles/JsonPath/) + - To continue using the old syntax (provided via [selectn](https://github.com/wilmoore/selectn.js)), `npm install selectn` as a direct dependency, and `json-rules-engine` will continue to interpret legacy paths this way. + - Any path starting with `$` will be assumed to use `json-path` syntax #### 4.1.0 / 2019-09-27 - * Export Typescript definitions (@brianphillips) + +- Export Typescript definitions (@brianphillips) #### 4.0.0 / 2019-08-22 - * BREAKING CHANGES - * `engine.run()` now returns a hash of events and almanac: `{ events: [], almanac: Almanac instance }`. Previously in v3, the `run()` returned the `events` array. - * For example, `const events = await engine.run()` under v3 will need to be changed to `const { events } = await engine.run()` under v4. + +- BREAKING CHANGES + - `engine.run()` now returns a hash of events and almanac: `{ events: [], almanac: Almanac instance }`. Previously in v3, the `run()` returned the `events` array. + - For example, `const events = await engine.run()` under v3 will need to be changed to `const { events } = await engine.run()` under v4. #### 3.1.0 / 2019-07-19 - * Feature: `rule.setName()` and `ruleResult.name` + +- Feature: `rule.setName()` and `ruleResult.name` #### 3.0.3 / 2019-07-15 - * Fix "localStorage.debug" not working in browsers + +- Fix "localStorage.debug" not working in browsers #### 3.0.2 / 2019-05-23 - * Fix "process" not defined error in browsers lacking node.js global shims + +- Fix "process" not defined error in browsers lacking node.js global shims #### 3.0.0 / 2019-05-17 - * BREAKING CHANGES - * Previously all conditions with undefined facts would resolve false. With this change, undefined facts values are treated as `undefined`. - * Greatly improved performance of `allowUndefinedfacts = true` engine option - * Reduce package bundle size by ~40% + +- BREAKING CHANGES + - Previously all conditions with undefined facts would resolve false. With this change, undefined facts values are treated as `undefined`. +- Greatly improved performance of `allowUndefinedfacts = true` engine option +- Reduce package bundle size by ~40% #### 2.3.5 / 2019-04-26 - * Replace debug with vanilla console.log + +- Replace debug with vanilla console.log #### 2.3.4 / 2019-04-26 - * Use Array.isArray instead of instanceof to test Array parameters to address edge cases + +- Use Array.isArray instead of instanceof to test Array parameters to address edge cases #### 2.3.3 / 2019-04-23 - * Fix rules cache not clearing after removeRule() + +- Fix rules cache not clearing after removeRule() #### 2.3.2 / 2018-12-28 - * Upgrade all dependencies to latest + +- Upgrade all dependencies to latest #### 2.3.1 / 2018-12-03 - * IE8 compatibility: replace Array.forEach with for loop (@knalbandianbrightgrove) + +- IE8 compatibility: replace Array.forEach with for loop (@knalbandianbrightgrove) #### 2.3.0 / 2018-05-03 - * Engine.removeFact() - removes fact from the engine (@SaschaDeWaal) - * Engine.removeRule() - removes rule from the engine (@SaschaDeWaal) - * Engine.removeOperator() - removes operator from the engine (@SaschaDeWaal) + +- Engine.removeFact() - removes fact from the engine (@SaschaDeWaal) +- Engine.removeRule() - removes rule from the engine (@SaschaDeWaal) +- Engine.removeOperator() - removes operator from the engine (@SaschaDeWaal) #### 2.2.0 / 2018-04-19 - * Performance: Constant facts now perform 18-26X better - * Performance: Removes await/async transpilation and json.stringify calls, significantly improving overall performance + +- Performance: Constant facts now perform 18-26X better +- Performance: Removes await/async transpilation and json.stringify calls, significantly improving overall performance #### 2.1.0 / 2018-02-19 - * Publish dist updates for 2.0.3 + +- Publish dist updates for 2.0.3 #### 2.0.3 / 2018-01-29 - * Add factResult and result to the JSON generated for Condition (@bjacobso) + +- Add factResult and result to the JSON generated for Condition (@bjacobso) #### 2.0.2 / 2017-07-24 - * Bugfix IE8 support + +- Bugfix IE8 support #### 2.0.1 / 2017-07-05 - * Bugfix rule result serialization + +- Bugfix rule result serialization #### 2.0.0 / 2017-04-21 - * Publishing 2.0.0 + +- Publishing 2.0.0 #### 2.0.0-beta2 / 2017-04-10 - * Fix fact path object checking to work with objects that have prototypes (lodash isObjectLike instead of isPlainObject) + +- Fix fact path object checking to work with objects that have prototypes (lodash isObjectLike instead of isPlainObject) #### 2.0.0-beta1 / 2017-04-09 - * Add rule results - * Document fact .path ability to parse properties containing dots - * Bump dependencies - * BREAKING CHANGES - * `engine.on('failure', (rule, almanac))` is now `engine.on('failure', (event, almanac, ruleResult))` - * `engine.on(eventType, (eventParams, engine))` is now `engine.on(eventType, (eventParams, almanac, ruleResult))` + +- Add rule results +- Document fact .path ability to parse properties containing dots +- Bump dependencies +- BREAKING CHANGES + - `engine.on('failure', (rule, almanac))` is now `engine.on('failure', (event, almanac, ruleResult))` + - `engine.on(eventType, (eventParams, engine))` is now `engine.on(eventType, (eventParams, almanac, ruleResult))` #### 1.5.1 / 2017-03-19 - * Bugfix almanac.factValue skipping interpreting condition "path" for cached facts + +- Bugfix almanac.factValue skipping interpreting condition "path" for cached facts #### 1.5.0 / 2017-03-12 - * Add fact comparison conditions + +- Add fact comparison conditions #### 1.4.0 / 2017-01-23 - * Add `allowUndefinedFacts` engine option + +- Add `allowUndefinedFacts` engine option #### 1.3.1 / 2017-01-16 - * Bump object-hash dependency to latest + +- Bump object-hash dependency to latest #### 1.3.0 / 2016-10-24 - * Rule event emissions - * Rule chaining + +- Rule event emissions +- Rule chaining #### 1.2.1 / 2016-10-22 - * Use Array.indexOf instead of Array.includes for older node version compatibility + +- Use Array.indexOf instead of Array.includes for older node version compatibility #### 1.2.0 / 2016-09-13 - * Fact path support + +- Fact path support #### 1.1.0 / 2016-09-11 - * Custom operator support + +- Custom operator support #### 1.0.4 / 2016-06-18 - * fix issue #6; runtime facts unique to each run() + +- fix issue #6; runtime facts unique to each run() #### 1.0.3 / 2016-06-15 - * fix issue #5; dependency error babel-core/register + +- fix issue #5; dependency error babel-core/register #### 1.0.0 / 2016-05-01 - * api stable; releasing 1.0 - * engine.run() now returns triggered events + +- api stable; releasing 1.0 +- engine.run() now returns triggered events #### 1.0.0-beta10 / 2016-04-16 - * Completed the 'fact-dependecy' advanced example - * Updated addFact and addRule engine methods to return 'this' for easy chaining + +- Completed the 'fact-dependecy' advanced example +- Updated addFact and addRule engine methods to return 'this' for easy chaining #### 1.0.0-beta9 / 2016-04-11 - * Completed the 'basic' example - * [BREAKING CHANGE] update engine.on('success') and engine.on('failure') to pass the current almanac instance as the second argument, rather than the engine + +- Completed the 'basic' example +- [BREAKING CHANGE] update engine.on('success') and engine.on('failure') to pass the current almanac instance as the second argument, rather than the engine diff --git a/README.md b/README.md index 77cfcdf1..90e107a4 100644 --- a/README.md +++ b/README.md @@ -8,31 +8,31 @@ A rules engine expressed in JSON -* [Synopsis](#synopsis) -* [Features](#features) -* [Installation](#installation) -* [Docs](#docs) -* [Examples](#examples) -* [Basic Example](#basic-example) -* [Advanced Example](#advanced-example) -* [Debugging](#debugging) - * [Node](#node) - * [Browser](#browser) -* [Related Projects](#related-projects) -* [License](#license) +- [Synopsis](#synopsis) +- [Features](#features) +- [Installation](#installation) +- [Docs](#docs) +- [Examples](#examples) +- [Basic Example](#basic-example) +- [Advanced Example](#advanced-example) +- [Debugging](#debugging) + - [Node](#node) + - [Browser](#browser) +- [Related Projects](#related-projects) +- [License](#license) ## Synopsis -```json-rules-engine``` is a powerful, lightweight rules engine. Rules are composed of simple json structures, making them human readable and easy to persist. +`json-rules-engine` is a powerful, lightweight rules engine. Rules are composed of simple json structures, making them human readable and easy to persist. ## Features -* Rules expressed in simple, easy to read JSON -* Full support for ```ALL``` and ```ANY``` boolean operators, including recursive nesting -* Fast by default, faster with configuration; priority levels and cache settings for fine tuning performance -* Secure; no use of eval() -* Isomorphic; runs in node and browser -* Lightweight & extendable; 17kb gzipped w/few dependencies +- Rules expressed in simple, easy to read JSON +- Full support for `ALL` and `ANY` boolean operators, including recursive nesting +- Fast by default, faster with configuration; priority levels and cache settings for fine tuning performance +- Secure; no use of eval() +- Isomorphic; runs in node and browser +- Lightweight & extendable; 17kb gzipped w/few dependencies ## Installation @@ -56,47 +56,56 @@ See the [Examples](./examples), which demonstrate the major features and capabil This example demonstrates an engine for detecting whether a basketball player has fouled out (a player who commits five personal fouls over the course of a 40-minute game, or six in a 48-minute game, fouls out). ```js -const { Engine } = require('json-rules-engine') - +const { Engine } = require("json-rules-engine"); /** * Setup a new engine */ -let engine = new Engine() +let engine = new Engine(); // define a rule for detecting the player has exceeded foul limits. Foul out any player who: // (has committed 5 fouls AND game is 40 minutes) OR (has committed 6 fouls AND game is 48 minutes) engine.addRule({ conditions: { - any: [{ - all: [{ - fact: 'gameDuration', - operator: 'equal', - value: 40 - }, { - fact: 'personalFoulCount', - operator: 'greaterThanInclusive', - value: 5 - }] - }, { - all: [{ - fact: 'gameDuration', - operator: 'equal', - value: 48 - }, { - fact: 'personalFoulCount', - operator: 'greaterThanInclusive', - value: 6 - }] - }] + any: [ + { + all: [ + { + fact: "gameDuration", + operator: "equal", + value: 40, + }, + { + fact: "personalFoulCount", + operator: "greaterThanInclusive", + value: 5, + }, + ], + }, + { + all: [ + { + fact: "gameDuration", + operator: "equal", + value: 48, + }, + { + fact: "personalFoulCount", + operator: "greaterThanInclusive", + value: 6, + }, + ], + }, + ], }, - event: { // define the event to fire when the conditions evaluate truthy - type: 'fouledOut', + event: { + // define the event to fire when the conditions evaluate truthy + type: "fouledOut", params: { - message: 'Player has fouled out!' - } - } -}) + message: "Player has fouled out!", + }, + }, +}); /** * Define facts the engine will use to evaluate the conditions above. @@ -104,15 +113,13 @@ engine.addRule({ */ let facts = { personalFoulCount: 6, - gameDuration: 40 -} + gameDuration: 40, +}; // Run the engine to evaluate -engine - .run(facts) - .then(({ events }) => { - events.map(event => console.log(event.params.message)) - }) +engine.run(facts).then(({ events }) => { + events.map((event) => console.log(event.params.message)); +}); /* * Output: @@ -127,20 +134,20 @@ This is available in the [examples](./examples/02-nested-boolean-logic.js) This example demonstates an engine for identifying employees who work for Microsoft and are taking Christmas day off. -This demonstrates an engine which uses asynchronous fact data. +This demonstrates an engine which uses asynchronous fact data. Fact information is loaded via API call during runtime, and the results are cached and recycled for all 3 conditions. It also demonstates use of the condition _path_ feature to reference properties of objects returned by facts. ```js -const { Engine } = require('json-rules-engine') +const { Engine } = require("json-rules-engine"); // example client for making asynchronous requests to an api, database, etc -import apiClient from './account-api-client' +import apiClient from "./account-api-client"; /** * Setup a new engine */ -let engine = new Engine() +let engine = new Engine(); /** * Rule for identifying microsoft employees taking pto on christmas @@ -150,31 +157,35 @@ let engine = new Engine() */ let microsoftRule = { conditions: { - all: [{ - fact: 'account-information', - operator: 'equal', - value: 'microsoft', - path: '$.company' // access the 'company' property of "account-information" - }, { - fact: 'account-information', - operator: 'in', - value: ['active', 'paid-leave'], // 'status' can be active or paid-leave - path: '$.status' // access the 'status' property of "account-information" - }, { - fact: 'account-information', - operator: 'contains', // the 'ptoDaysTaken' property (an array) must contain '2016-12-25' - value: '2016-12-25', - path: '$.ptoDaysTaken' // access the 'ptoDaysTaken' property of "account-information" - }] + all: [ + { + fact: "account-information", + operator: "equal", + value: "microsoft", + path: "$.company", // access the 'company' property of "account-information" + }, + { + fact: "account-information", + operator: "in", + value: ["active", "paid-leave"], // 'status' can be active or paid-leave + path: "$.status", // access the 'status' property of "account-information" + }, + { + fact: "account-information", + operator: "contains", // the 'ptoDaysTaken' property (an array) must contain '2016-12-25' + value: "2016-12-25", + path: "$.ptoDaysTaken", // access the 'ptoDaysTaken' property of "account-information" + }, + ], }, event: { - type: 'microsoft-christmas-pto', + type: "microsoft-christmas-pto", params: { - message: 'current microsoft employee taking christmas day off' - } - } -} -engine.addRule(microsoftRule) + message: "current microsoft employee taking christmas day off", + }, + }, +}; +engine.addRule(microsoftRule); /** * 'account-information' fact executes an api call and retrieves account data, feeding the results @@ -182,21 +193,20 @@ engine.addRule(microsoftRule) * requiring this data, only ONE api call is made. This results in much more efficient runtime performance * and fewer network requests. */ -engine.addFact('account-information', function (params, almanac) { - console.log('loading account information...') - return almanac.factValue('accountId') - .then((accountId) => { - return apiClient.getAccountInformation(accountId) - }) -}) +engine.addFact("account-information", function (params, almanac) { + console.log("loading account information..."); + return almanac.factValue("accountId").then((accountId) => { + return apiClient.getAccountInformation(accountId); + }); +}); // define fact(s) known at runtime -let facts = { accountId: 'lincoln' } -engine - .run(facts) - .then(({ events }) => { - console.log(facts.accountId + ' is a ' + events.map(event => event.params.message)) - }) +let facts = { accountId: "lincoln" }; +engine.run(facts).then(({ events }) => { + console.log( + facts.accountId + " is a " + events.map((event) => event.params.message), + ); +}); /* * OUTPUT: @@ -219,9 +229,10 @@ DEBUG=json-rules-engine ``` ### Browser + ```js // set debug flag in local storage & refresh page to see console output -localStorage.debug = 'json-rules-engine' +localStorage.debug = "json-rules-engine"; ``` ## Related Projects @@ -230,6 +241,6 @@ https://github.com/vinzdeveloper/json-rule-editor - configuration ui for json-ru rule editor 2 - ## License + [ISC](./LICENSE) diff --git a/docs/almanac.md b/docs/almanac.md index d0c38264..8028969f 100644 --- a/docs/almanac.md +++ b/docs/almanac.md @@ -1,37 +1,37 @@ # Almanac -* [Overview](#overview) -* [Methods](#methods) - * [almanac.factValue(Fact fact, Object params, String path) -> Promise](#almanacfactvaluefact-fact-object-params-string-path---promise) - * [almanac.addFact(String id, Function [definitionFunc], Object [options])](#almanacaddfactstring-id-function-definitionfunc-object-options) - * [almanac.addRuntimeFact(String factId, Mixed value)](#almanacaddruntimefactstring-factid-mixed-value) - * [almanac.getEvents(String outcome) -> Events[]](#almanacgeteventsstring-outcome---events) - * [almanac.getResults() -> RuleResults[]](#almanacgetresults---ruleresults) -* [Common Use Cases](#common-use-cases) - * [Fact dependencies](#fact-dependencies) - * [Retrieve fact values when handling events](#retrieve-fact-values-when-handling-events) - * [Rule Chaining](#rule-chaining) +- [Overview](#overview) +- [Methods](#methods) + - [almanac.factValue(Fact fact, Object params, String path) -> Promise](#almanacfactvaluefact-fact-object-params-string-path---promise) + - [almanac.addFact(String id, Function [definitionFunc], Object [options])](#almanacaddfactstring-id-function-definitionfunc-object-options) + - [almanac.addRuntimeFact(String factId, Mixed value)](#almanacaddruntimefactstring-factid-mixed-value) + - [almanac.getEvents(String outcome) -> Events[]](#almanacgeteventsstring-outcome---events) + - [almanac.getResults() -> RuleResults[]](#almanacgetresults---ruleresults) +- [Common Use Cases](#common-use-cases) + - [Fact dependencies](#fact-dependencies) + - [Retrieve fact values when handling events](#retrieve-fact-values-when-handling-events) + - [Rule Chaining](#rule-chaining) ## Overview -An almanac collects facts through an engine run cycle. As the engine computes fact values, -the results are stored in the almanac and cached. If the engine detects a fact computation has -been previously computed, it reuses the cached result from the almanac. Every time ```engine.run()``` is invoked, +An almanac collects facts through an engine run cycle. As the engine computes fact values, +the results are stored in the almanac and cached. If the engine detects a fact computation has +been previously computed, it reuses the cached result from the almanac. Every time `engine.run()` is invoked, a new almanac is instantiated. The almanac for the current engine run is available as arguments passed to the fact evaluation methods and - to the engine ```success``` event. The almanac may be used to define additional facts during runtime. +to the engine `success` event. The almanac may be used to define additional facts during runtime. ## Methods ### almanac.factValue(Fact fact, Object params, String path) -> Promise -Computes the value of the provided fact + params. If "path" is provided, it will be used as a [json-path](https://goessner.net/articles/JsonPath/) accessor on the fact's return object. +Computes the value of the provided fact + params. If "path" is provided, it will be used as a [json-path](https://goessner.net/articles/JsonPath/) accessor on the fact's return object. ```js almanac - .factValue('account-information', { accountId: 1 }, '.balance') - .then( value => console.log(value)) + .factValue("account-information", { accountId: 1 }, ".balance") + .then((value) => console.log(value)); ``` ### almanac.addFact(String id, Function [definitionFunc], Object [options]) @@ -40,26 +40,30 @@ Sets a fact in the almanac. Used in conjunction with rule and engine event emiss ```js // constant facts: -engine.addFact('speed-of-light', 299792458) +engine.addFact("speed-of-light", 299792458); // facts computed via function -engine.addFact('account-type', function getAccountType(params, almanac) { +engine.addFact("account-type", function getAccountType(params, almanac) { // ... -}) +}); // facts with options: -engine.addFact('account-type', function getAccountType(params, almanac) { - // ... -}, { cache: false, priority: 500 }) +engine.addFact( + "account-type", + function getAccountType(params, almanac) { + // ... + }, + { cache: false, priority: 500 }, +); ``` ### almanac.addRuntimeFact(String factId, Mixed value) **Deprecated** Use `almanac.addFact` instead -Sets a constant fact mid-run. Often used in conjunction with rule and engine event emissions. +Sets a constant fact mid-run. Often used in conjunction with rule and engine event emissions. ```js -almanac.addRuntimeFact('account-id', 1) +almanac.addRuntimeFact("account-id", 1); ``` ### almanac.getEvents(String outcome) -> Events[] @@ -67,11 +71,11 @@ almanac.addRuntimeFact('account-id', 1) Returns events by outcome ("success" or "failure") for the current engine run() ```js -almanac.getEvents() // all events for every rule evaluated thus far +almanac.getEvents(); // all events for every rule evaluated thus far -almanac.getEvents('success') // array of success events +almanac.getEvents("success"); // array of success events -almanac.getEvents('failure') // array of failure events +almanac.getEvents("failure"); // array of failure events ``` ### almanac.getResults() -> RuleResults[] @@ -79,20 +83,20 @@ almanac.getEvents('failure') // array of failure events Returns [rule results](./rules#rule-results) for the current engine run() ```js -almanac.getResults() +almanac.getResults(); ``` ## Common Use Cases ### Fact dependencies -The most common use of the almanac is to access data computed by other facts during runtime. This allows +The most common use of the almanac is to access data computed by other facts during runtime. This allows leveraging the engine's caching mechanisms to design more efficient rules. The [fact-dependency](../examples/04-fact-dependency.js) example demonstrates a real world application of this technique. -For example, say there were two facts: _is-funded-account_ and _account-balance_. Both facts depend on the same _account-information_ data set. -Using the Almanac, each fact can be defined to call a **base** fact responsible for loading the data. This causes the engine +For example, say there were two facts: _is-funded-account_ and _account-balance_. Both facts depend on the same _account-information_ data set. +Using the Almanac, each fact can be defined to call a **base** fact responsible for loading the data. This causes the engine to make the API call for loading account information only once per account. ```js @@ -146,10 +150,10 @@ engine.run({ accountId: 1 }) ### Retrieve fact values when handling events -When a rule evalutes truthy and its ```event``` is called, new facts may be defined by the event handler. - Note that with this technique, the rule priority becomes important; if a rule is expected to - define a fact value, it's important that rule be run prior to other rules that reference the fact. To - learn more about setting rule priorities, see the [rule documentation](./rules.md). +When a rule evalutes truthy and its `event` is called, new facts may be defined by the event handler. +Note that with this technique, the rule priority becomes important; if a rule is expected to +define a fact value, it's important that rule be run prior to other rules that reference the fact. To +learn more about setting rule priorities, see the [rule documentation](./rules.md). ```js engine.on('success', (event, almanac) => { @@ -166,7 +170,7 @@ engine.on('success', (event, almanac) => { ### Rule Chaining The `almanac.addRuntimeFact()` method may be used in conjunction with event emissions to -set fact values during runtime, effectively enabling _rule-chaining_. Note that ordering +set fact values during runtime, effectively enabling _rule-chaining_. Note that ordering of rule execution is enabled via the `priority` option, and is crucial component to propertly configuring rule chaining. diff --git a/docs/engine.md b/docs/engine.md index b944adc6..13c61934 100644 --- a/docs/engine.md +++ b/docs/engine.md @@ -2,24 +2,24 @@ The Engine stores and executes rules, emits events, and maintains state. -* [Methods](#methods) - * [constructor([Array rules], Object [options])](#constructorarray-rules-object-options) - * [Options](#options) - * [engine.addFact(String id, Function [definitionFunc], Object [options])](#engineaddfactstring-id-function-definitionfunc-object-options) - * [engine.removeFact(String id)](#engineremovefactstring-id) - * [engine.addRule(Rule instance|Object options)](#engineaddrulerule-instanceobject-options) - * [engine.updateRule(Rule instance|Object options)](#engineupdaterulerule-instanceobject-options) - * [engine.removeRule(Rule instance | String ruleId)](#engineremoverulerule-instance) - * [engine.addOperator(String operatorName, Function evaluateFunc(factValue, jsonValue))](#engineaddoperatorstring-operatorname-function-evaluatefuncfactvalue-jsonvalue) - * [engine.removeOperator(String operatorName)](#engineremoveoperatorstring-operatorname) - * [engine.addOperatorDecorator(String decoratorName, Function evaluateFunc(factValue, jsonValue, next))](#engineaddoperatordecoratorstring-decoratorname-function-evaluatefuncfactvalue-jsonvalue-next) - * [engine.removeOperatorDecorator(String decoratorName)](#engineremoveoperatordecoratorstring-decoratorname) - * [engine.setCondition(String name, Object conditions)](#enginesetconditionstring-name-object-conditions) - * [engine.removeCondition(String name)](#engineremovecondtionstring-name) - * [engine.run([Object facts], [Object options]) -> Promise ({ events: [], failureEvents: [], almanac: Almanac, results: [], failureResults: []})](#enginerunobject-facts-object-options---promise--events--failureevents--almanac-almanac-results--failureresults-) - * [engine.stop() -> Engine](#enginestop---engine) - * [engine.on('success', Function(Object event, Almanac almanac, RuleResult ruleResult))](#engineonsuccess-functionobject-event-almanac-almanac-ruleresult-ruleresult) - * [engine.on('failure', Function(Object event, Almanac almanac, RuleResult ruleResult))](#engineonfailure-functionobject-event-almanac-almanac-ruleresult-ruleresult) +- [Methods](#methods) + - [constructor([Array rules], Object [options])](#constructorarray-rules-object-options) + - [Options](#options) + - [engine.addFact(String id, Function [definitionFunc], Object [options])](#engineaddfactstring-id-function-definitionfunc-object-options) + - [engine.removeFact(String id)](#engineremovefactstring-id) + - [engine.addRule(Rule instance|Object options)](#engineaddrulerule-instanceobject-options) + - [engine.updateRule(Rule instance|Object options)](#engineupdaterulerule-instanceobject-options) + - [engine.removeRule(Rule instance | String ruleId)](#engineremoverulerule-instance) + - [engine.addOperator(String operatorName, Function evaluateFunc(factValue, jsonValue))](#engineaddoperatorstring-operatorname-function-evaluatefuncfactvalue-jsonvalue) + - [engine.removeOperator(String operatorName)](#engineremoveoperatorstring-operatorname) + - [engine.addOperatorDecorator(String decoratorName, Function evaluateFunc(factValue, jsonValue, next))](#engineaddoperatordecoratorstring-decoratorname-function-evaluatefuncfactvalue-jsonvalue-next) + - [engine.removeOperatorDecorator(String decoratorName)](#engineremoveoperatordecoratorstring-decoratorname) + - [engine.setCondition(String name, Object conditions)](#enginesetconditionstring-name-object-conditions) + - [engine.removeCondition(String name)](#engineremovecondtionstring-name) + - [engine.run([Object facts], [Object options]) -> Promise ({ events: [], failureEvents: [], almanac: Almanac, results: [], failureResults: []})](#enginerunobject-facts-object-options---promise--events--failureevents--almanac-almanac-results--failureresults-) + - [engine.stop() -> Engine](#enginestop---engine) + - [engine.on('success', Function(Object event, Almanac almanac, RuleResult ruleResult))](#engineonsuccess-functionobject-event-almanac-almanac-ruleresult-ruleresult) + - [engine.on('failure', Function(Object event, Almanac almanac, RuleResult ruleResult))](#engineonfailure-functionobject-event-almanac-almanac-ruleresult-ruleresult) ## Methods @@ -44,8 +44,8 @@ let engine = new Engine([Array rules], options) #### Options `allowUndefinedFacts` - By default, when a running engine encounters an undefined fact, -an exception is thrown. Turning this option on will cause the engine to treat -undefined facts as `undefined`. (default: false) +an exception is thrown. Turning this option on will cause the engine to treat +undefined facts as `undefined`. (default: false) `allowUndefinedConditions` - By default, when a running engine encounters a condition reference that cannot be resolved an exception is thrown. Turning @@ -60,85 +60,89 @@ as failed conditions. (default: false) ```js // constant facts: -engine.addFact('speed-of-light', 299792458) +engine.addFact("speed-of-light", 299792458); // facts computed via function -engine.addFact('account-type', function getAccountType(params, almanac) { +engine.addFact("account-type", function getAccountType(params, almanac) { // ... -}) +}); // facts with options: -engine.addFact('account-type', function getAccountType(params, almanac) { - // ... -}, { cache: false, priority: 500 }) +engine.addFact( + "account-type", + function getAccountType(params, almanac) { + // ... + }, + { cache: false, priority: 500 }, +); ``` ### engine.removeFact(String id) ```js -engine.addFact('speed-of-light', 299792458) +engine.addFact("speed-of-light", 299792458); // removes the fact -engine.removeFact('speed-of-light') +engine.removeFact("speed-of-light"); ``` ### engine.addRule(Rule instance|Object options) -Adds a rule to the engine. The engine will execute the rule upon the next ```run()``` +Adds a rule to the engine. The engine will execute the rule upon the next `run()` ```js -let Rule = require('json-rules-engine').Rule +let Rule = require("json-rules-engine").Rule; // via rule properties: engine.addRule({ conditions: {}, event: {}, - priority: 1, // optional, default: 1 + priority: 1, // optional, default: 1 onSuccess: function (event, almanac) {}, // optional onFailure: function (event, almanac) {}, // optional -}) +}); // or rule instance: -let rule = new Rule() -engine.addRule(rule) +let rule = new Rule(); +engine.addRule(rule); ``` - ### engine.removeRule(Rule instance | Any ruleName) -> Boolean +### engine.removeRule(Rule instance | Any ruleName) -> Boolean - Removes a rule from the engine, either by passing a rule object or a rule name. When removing by rule name, all rules matching the provided name will be removed. +Removes a rule from the engine, either by passing a rule object or a rule name. When removing by rule name, all rules matching the provided name will be removed. - Method returns true when rule was successfully remove, or false when not found. +Method returns true when rule was successfully remove, or false when not found. ```javascript // adds a rule -let rule = new Rule() -engine.addRule(rule) +let rule = new Rule(); +engine.addRule(rule); //remove it -engine.removeRule(rule) +engine.removeRule(rule); //or -engine.removeRule(rule.name) +engine.removeRule(rule.name); ``` - ### engine.updateRule(Rule instance|Object options) +### engine.updateRule(Rule instance|Object options) - Updates a rule in the engine. +Updates a rule in the engine. ```javascript // adds a rule -let rule = new Rule() -engine.addRule(rule) +let rule = new Rule(); +engine.addRule(rule); // change rule condition -rule.conditions.all = [] +rule.conditions.all = []; //update it in the engine -engine.updateRule(rule) +engine.updateRule(rule); ``` ### engine.addOperator(String operatorName, Function evaluateFunc(factValue, jsonValue)) -Adds a custom operator to the engine. For situations that require going beyond the generic, built-in operators (`equal`, `greaterThan`, etc). +Adds a custom operator to the engine. For situations that require going beyond the generic, built-in operators (`equal`, `greaterThan`, etc). ```js /* @@ -168,19 +172,17 @@ let rule = new Rule( See the [operator example](../examples/06-custom-operators.js) - - ### engine.removeOperator(String operatorName) Removes a operator from the engine ```javascript -engine.addOperator('startsWithLetter', (factValue, jsonValue) => { - if (!factValue.length) return false - return factValue[0].toLowerCase() === jsonValue.toLowerCase() -}) +engine.addOperator("startsWithLetter", (factValue, jsonValue) => { + if (!factValue.length) return false; + return factValue[0].toLowerCase() === jsonValue.toLowerCase(); +}); -engine.removeOperator('startsWithLetter'); +engine.removeOperator("startsWithLetter"); ``` ### engine.addOperatorDecorator(String decoratorName, Function evaluateFunc(factValue, jsonValue, next)) @@ -220,23 +222,21 @@ let rule = new Rule( See the [operator decorator example](../examples/13-using-operator-decorators.js) - - ### engine.removeOperatorDecorator(String decoratorName) Removes a operator decorator from the engine ```javascript -engine.addOperatorDecorator('first', (factValue, jsonValue, next) => { - if (!factValue.length) return false - return next(factValue[0], jsonValue) -}) +engine.addOperatorDecorator("first", (factValue, jsonValue, next) => { + if (!factValue.length) return false; + return next(factValue[0], jsonValue); +}); -engine.addOperatorDecorator('caseInsensitive', (factValue, jsonValue, next) => { - return next(factValue.toLowerCase(), jsonValue.toLowerCase()) -}) +engine.addOperatorDecorator("caseInsensitive", (factValue, jsonValue, next) => { + return next(factValue.toLowerCase(), jsonValue.toLowerCase()); +}); -engine.removeOperator('first'); +engine.removeOperator("first"); ``` ### engine.setCondition(String name, Object conditions) @@ -244,41 +244,40 @@ engine.removeOperator('first'); Adds or updates a condition to the engine. Rules may include references to this condition. Conditions must start with `all`, `any`, `not`, or reference a condition. ```javascript -engine.setCondition('validLogin', { +engine.setCondition("validLogin", { all: [ { - operator: 'notEqual', - fact: 'loginToken', - value: null + operator: "notEqual", + fact: "loginToken", + value: null, }, { - operator: 'greaterThan', - fact: 'loginToken', - path: '$.expirationTime', - value: { fact: 'now' } - } - ] + operator: "greaterThan", + fact: "loginToken", + path: "$.expirationTime", + value: { fact: "now" }, + }, + ], }); engine.addRule({ condtions: { all: [ { - condition: 'validLogin' + condition: "validLogin", }, { - operator: 'contains', - fact: 'loginToken', - path: '$.role', - value: 'admin' - } - ] + operator: "contains", + fact: "loginToken", + path: "$.role", + value: "admin", + }, + ], }, event: { - type: 'AdminAccessAllowed' - } -}) - + type: "AdminAccessAllowed", + }, +}); ``` ### engine.removeCondition(String name) @@ -286,45 +285,45 @@ engine.addRule({ Removes the condition that was previously added. ```javascript -engine.setCondition('validLogin', { +engine.setCondition("validLogin", { all: [ { - operator: 'notEqual', - fact: 'loginToken', - value: null + operator: "notEqual", + fact: "loginToken", + value: null, }, { - operator: 'greaterThan', - fact: 'loginToken', - path: '$.expirationTime', - value: { fact: 'now' } - } - ] + operator: "greaterThan", + fact: "loginToken", + path: "$.expirationTime", + value: { fact: "now" }, + }, + ], }); -engine.removeCondition('validLogin'); +engine.removeCondition("validLogin"); ``` - ### engine.run([Object facts], [Object options]) -> Promise ({ events: [], failureEvents: [], almanac: Almanac, results: [], failureResults: []}) -Runs the rules engine. Returns a promise which resolves when all rules have been run. +Runs the rules engine. Returns a promise which resolves when all rules have been run. ```js // run the engine -await engine.run() +await engine.run(); // with constant facts -await engine.run({ userId: 1 }) +await engine.run({ userId: 1 }); const { - results, // rule results for successful rules - failureResults, // rule results for failed rules - events, // array of successful rule events - failureEvents, // array of failed rule events - almanac // Almanac instance representing the run -} = await engine.run({ userId: 1 }) + results, // rule results for successful rules + failureResults, // rule results for failed rules + events, // array of successful rule events + failureEvents, // array of failed rule events + almanac, // Almanac instance representing the run +} = await engine.run({ userId: 1 }); ``` + Link to the [Almanac documentation](./almanac.md) Optionally, you may specify a specific almanac instance via the almanac property. @@ -334,39 +333,39 @@ Optionally, you may specify a specific almanac instance via the almanac property const myCustomAlmanac = new CustomAlmanac(); // run the engine with the custom almanac -await engine.run({}, { almanac: myCustomAlmanac }) +await engine.run({}, { almanac: myCustomAlmanac }); ``` ### engine.stop() -> Engine -Stops the rules engine from running the next priority set of Rules. All remaining rules will be resolved as undefined, +Stops the rules engine from running the next priority set of Rules. All remaining rules will be resolved as undefined, and no further events emitted. -Be aware that since rules of the *same* priority are evaluated in parallel(not series), other rules of +Be aware that since rules of the _same_ priority are evaluated in parallel(not series), other rules of the same priority may still emit events, even though the engine has been told to stop. ```js -engine.stop() +engine.stop(); ``` There are two generic event emissions that trigger automatically: -#### ```engine.on('success', Function(Object event, Almanac almanac, RuleResult ruleResult))``` +#### `engine.on('success', Function(Object event, Almanac almanac, RuleResult ruleResult))` Fires when a rule passes. The callback will receive the event object, the current [Almanac](./almanac.md), and the [Rule Result](./rules.md#rule-results). Any promise returned by the callback will be waited on to resolve before execution continues. ```js -engine.on('success', function(event, almanac, ruleResult) { - console.log(event) // { type: 'my-event', params: { id: 1 } -}) +engine.on("success", function (event, almanac, ruleResult) { + console.log(event); // { type: 'my-event', params: { id: 1 } +}); ``` -#### ```engine.on('failure', Function(Object event, Almanac almanac, RuleResult ruleResult))``` +#### `engine.on('failure', Function(Object event, Almanac almanac, RuleResult ruleResult))` -Companion to 'success', except fires when a rule fails. The callback will receive the event object, the current [Almanac](./almanac.md), and the [Rule Result](./rules.md#rule-results). Any promise returned by the callback will be waited on to resolve before execution continues. +Companion to 'success', except fires when a rule fails. The callback will receive the event object, the current [Almanac](./almanac.md), and the [Rule Result](./rules.md#rule-results). Any promise returned by the callback will be waited on to resolve before execution continues. ```js -engine.on('failure', function(event, almanac, ruleResult) { - console.log(event) // { type: 'my-event', params: { id: 1 } -}) +engine.on("failure", function (event, almanac, ruleResult) { + console.log(event); // { type: 'my-event', params: { id: 1 } +}); ``` diff --git a/docs/facts.md b/docs/facts.md index 588f5c4d..1f72846e 100644 --- a/docs/facts.md +++ b/docs/facts.md @@ -1,10 +1,10 @@ # Facts -Facts are methods or constants registered with the engine prior to runtime and referenced within rule conditions. Each fact method should be a pure function that may return a either computed value, or promise that resolves to a computed value. +Facts are methods or constants registered with the engine prior to runtime and referenced within rule conditions. Each fact method should be a pure function that may return a either computed value, or promise that resolves to a computed value. As rule conditions are evaluated during runtime, they retrieve fact values dynamically and use the condition _operator_ to compare the fact result with the condition _value_. -* [Methods](#methods) - * [constructor(String id, Constant|Function(Object params, Almanac almanac), [Object options]) -> instance](#constructorstring-id-constantfunctionobject-params-almanac-almanac-object-options---instance) +- [Methods](#methods) + - [constructor(String id, Constant|Function(Object params, Almanac almanac), [Object options]) -> instance](#constructorstring-id-constantfunctionobject-params-almanac-almanac-object-options---instance) ## Methods @@ -12,19 +12,24 @@ As rule conditions are evaluated during runtime, they retrieve fact values dynam ```js // constant value facts -let fact = new Fact('apiKey', '4feca34f9d67e99b8af2') +let fact = new Fact("apiKey", "4feca34f9d67e99b8af2"); // dynamic facts -let fact = new Fact('account-type', (params, almanac) => { +let fact = new Fact("account-type", (params, almanac) => { // ... -}) +}); // facts with options: -engine.addFact('account-type', (params, almanac) => { - // ... -}, { cache: false, priority: 500 }) +engine.addFact( + "account-type", + (params, almanac) => { + // ... + }, + { cache: false, priority: 500 }, +); ``` **options** -* { cache: Boolean } - Sets whether the engine should cache the result of this fact. Cache key is based on the factId and 'params' passed to it. Default: *true* -* { priority: Integer } - Sets when the fact should run in relation to other facts and conditions. The higher the priority value, the sooner the fact will run. Default: *1* + +- { cache: Boolean } - Sets whether the engine should cache the result of this fact. Cache key is based on the factId and 'params' passed to it. Default: _true_ +- { priority: Integer } - Sets when the fact should run in relation to other facts and conditions. The higher the priority value, the sooner the fact will run. Default: _1_ diff --git a/docs/rules.md b/docs/rules.md index 7f6d4238..60eca0e6 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -1,39 +1,38 @@ - # Rules -Rules contain a set of _conditions_ and a single _event_. When the engine is run, each rule condition is evaluated. If the results are truthy, the rule's _event_ is triggered. - -* [Methods](#methods) - * [constructor([Object options|String json])](#constructorobject-optionsstring-json) - * [setConditions(Array conditions)](#setconditionsarray-conditions) - * [getConditions() -> Object](#getconditions---object) - * [setEvent(Object event)](#seteventobject-event) - * [getEvent() -> Object](#getevent---object) - * [setPriority(Integer priority = 1)](#setpriorityinteger-priority--1) - * [getPriority() -> Integer](#getpriority---integer) - * [toJSON(Boolean stringify = true)](#tojsonboolean-stringify--true) -* [Conditions](#conditions) - * [Basic conditions](#basic-conditions) - * [Boolean expressions: all, any, and not](#boolean-expressions-all-any-and-not) - * [Condition Reference](#condition-reference) - * [Condition helpers: params](#condition-helpers-params) - * [Condition helpers: path](#condition-helpers-path) - * [Condition helpers: custom path resolver](#condition-helpers-custom-path-resolver) - * [Comparing facts](#comparing-facts) -* [Events](#events) - * [rule.on('success', Function(Object event, Almanac almanac, RuleResult ruleResult))](#ruleonsuccess-functionobject-event-almanac-almanac-ruleresult-ruleresult) - * [rule.on('failure', Function(Object event, Almanac almanac, RuleResult ruleResult))](#ruleonfailure-functionobject-event-almanac-almanac-ruleresult-ruleresult) -* [Operators](#operators) - * [String and Numeric operators:](#string-and-numeric-operators) - * [Numeric operators:](#numeric-operators) - * [Array operators:](#array-operators) -* [Operator Decorators](#operator-decorators) - * [Array decorators:](#array-decorators) - * [Logical decorators:](#logical-decorators) - * [Utility decorators:](#utility-decorators) - * [Decorator composition:](#decorator-composition) -* [Rule Results](#rule-results) -* [Persisting](#persisting) +Rules contain a set of _conditions_ and a single _event_. When the engine is run, each rule condition is evaluated. If the results are truthy, the rule's _event_ is triggered. + +- [Methods](#methods) + - [constructor([Object options|String json])](#constructorobject-optionsstring-json) + - [setConditions(Array conditions)](#setconditionsarray-conditions) + - [getConditions() -> Object](#getconditions---object) + - [setEvent(Object event)](#seteventobject-event) + - [getEvent() -> Object](#getevent---object) + - [setPriority(Integer priority = 1)](#setpriorityinteger-priority--1) + - [getPriority() -> Integer](#getpriority---integer) + - [toJSON(Boolean stringify = true)](#tojsonboolean-stringify--true) +- [Conditions](#conditions) + - [Basic conditions](#basic-conditions) + - [Boolean expressions: all, any, and not](#boolean-expressions-all-any-and-not) + - [Condition Reference](#condition-reference) + - [Condition helpers: params](#condition-helpers-params) + - [Condition helpers: path](#condition-helpers-path) + - [Condition helpers: custom path resolver](#condition-helpers-custom-path-resolver) + - [Comparing facts](#comparing-facts) +- [Events](#events) + - [rule.on('success', Function(Object event, Almanac almanac, RuleResult ruleResult))](#ruleonsuccess-functionobject-event-almanac-almanac-ruleresult-ruleresult) + - [rule.on('failure', Function(Object event, Almanac almanac, RuleResult ruleResult))](#ruleonfailure-functionobject-event-almanac-almanac-ruleresult-ruleresult) +- [Operators](#operators) + - [String and Numeric operators:](#string-and-numeric-operators) + - [Numeric operators:](#numeric-operators) + - [Array operators:](#array-operators) +- [Operator Decorators](#operator-decorators) + - [Array decorators:](#array-decorators) + - [Logical decorators:](#logical-decorators) + - [Utility decorators:](#utility-decorators) + - [Decorator composition:](#decorator-composition) +- [Rule Results](#rule-results) +- [Persisting](#persisting) ## Methods @@ -46,37 +45,37 @@ let options = { conditions: { all: [ { - fact: 'my-fact', - operator: 'equal', - value: 'some-value' - } - ] + fact: "my-fact", + operator: "equal", + value: "some-value", + }, + ], }, event: { - type: 'my-event', + type: "my-event", params: { - customProperty: 'customValue' - } + customProperty: "customValue", + }, }, - name: any, // optional - priority: 1, // optional, default: 1 + name: any, // optional + priority: 1, // optional, default: 1 onSuccess: function (event, almanac) {}, // optional onFailure: function (event, almanac) {}, // optional -} -let rule = new Rule(options) +}; +let rule = new Rule(options); ``` **options.conditions** : `[Object]` Rule conditions object -**options.event** : `[Object]` Sets the `.on('success')` and `on('failure')` event argument emitted whenever the rule passes. Event objects must have a ```type``` property, and an optional ```params``` property. +**options.event** : `[Object]` Sets the `.on('success')` and `on('failure')` event argument emitted whenever the rule passes. Event objects must have a `type` property, and an optional `params` property. -**options.priority** : `[Number, default 1]` Dictates when rule should be run, relative to other rules. Higher priority rules are run before lower priority rules. Rules with the same priority are run in parallel. Priority must be a positive, non-zero integer. +**options.priority** : `[Number, default 1]` Dictates when rule should be run, relative to other rules. Higher priority rules are run before lower priority rules. Rules with the same priority are run in parallel. Priority must be a positive, non-zero integer. -**options.onSuccess** : `[Function(Object event, Almanac almanac)]` Registers callback with the rule's `on('success')` listener. The rule's `event` property and the current [Almanac](./almanac.md) are passed as arguments. Any promise returned by the callback will be waited on to resolve before execution continues. +**options.onSuccess** : `[Function(Object event, Almanac almanac)]` Registers callback with the rule's `on('success')` listener. The rule's `event` property and the current [Almanac](./almanac.md) are passed as arguments. Any promise returned by the callback will be waited on to resolve before execution continues. -**options.onFailure** : `[Function(Object event, Almanac almanac)]` Registers callback with the rule's `on('failure')` listener. The rule's `event` property and the current [Almanac](./almanac.md) are passed as arguments. Any promise returned by the callback will be waited on to resolve before execution continues. +**options.onFailure** : `[Function(Object event, Almanac almanac)]` Registers callback with the rule's `on('failure')` listener. The rule's `event` property and the current [Almanac](./almanac.md) are passed as arguments. Any promise returned by the callback will be waited on to resolve before execution continues. -**options.name** : `[Any]` A way of naming your rules, allowing them to be easily identifiable in [Rule Results](#rule-results). This is usually of type `String`, but could also be `Object`, `Array`, or `Number`. Note that the name need not be unique, and that it has no impact on execution of the rule. +**options.name** : `[Any]` A way of naming your rules, allowing them to be easily identifiable in [Rule Results](#rule-results). This is usually of type `String`, but could also be `Object`, `Array`, or `Number`. Note that the name need not be unique, and that it has no impact on execution of the rule. ### setConditions(Array conditions) @@ -88,7 +87,7 @@ Retrieves rule condition set by constructor or `setCondition()` ### setEvent(Object event) -Helper for setting rule event. Alternative to passing the `event` option to the rule constructor. +Helper for setting rule event. Alternative to passing the `event` option to the rule constructor. ### getEvent() -> Object @@ -104,15 +103,15 @@ Retrieves rule priority set by constructor or `setPriority()` ### toJSON(Boolean stringify = true) -Serializes the rule into a JSON string. Often used when persisting rules. +Serializes the rule into a JSON string. Often used when persisting rules. ```js -let jsonString = rule.toJSON() // string: '{"conditions":{"all":[]},"priority":50 ... +let jsonString = rule.toJSON(); // string: '{"conditions":{"all":[]},"priority":50 ... -let rule = new Rule(jsonString) // restored rule; same conditions, priority, event +let rule = new Rule(jsonString); // restored rule; same conditions, priority, event // without stringifying -let jsonObject = rule.toJSON(false) // object: {conditions:{ all: [] }, priority: 50 ... +let jsonObject = rule.toJSON(false); // object: {conditions:{ all: [] }, priority: 50 ... ``` ## Conditions @@ -121,7 +120,7 @@ Rule conditions are a combination of facts, operators, and values that determine ### Basic conditions -The simplest form of a condition consists of a `fact`, an `operator`, and a `value`. When the engine runs, the operator is used to compare the fact against the value. +The simplest form of a condition consists of a `fact`, an `operator`, and a `value`. When the engine runs, the operator is used to compare the fact against the value. ```js // my-fact <= 1 @@ -129,58 +128,74 @@ let rule = new Rule({ conditions: { all: [ { - fact: 'my-fact', - operator: 'lessThanInclusive', - value: 1 - } - ] - } -}) + fact: "my-fact", + operator: "lessThanInclusive", + value: 1, + }, + ], + }, +}); ``` See the [hello-world](../examples/01-hello-world.js) example. ### Boolean expressions: `all`, `any`, and `not` -Each rule's conditions *must* have an `all` or `any` operator containing an array of conditions at its root, a `not` operator containing a single condition, or a condition reference. The `all` operator specifies that all conditions contained within must be truthy for the rule to be considered a `success`. The `any` operator only requires one condition to be truthy for the rule to succeed. The `not` operator will negate whatever condition it contains. +Each rule's conditions _must_ have an `all` or `any` operator containing an array of conditions at its root, a `not` operator containing a single condition, or a condition reference. The `all` operator specifies that all conditions contained within must be truthy for the rule to be considered a `success`. The `any` operator only requires one condition to be truthy for the rule to succeed. The `not` operator will negate whatever condition it contains. ```js // all: let rule = new Rule({ conditions: { all: [ - { /* condition 1 */ }, - { /* condition 2 */ }, - { /* condition n */ }, - ] - } -}) + { + /* condition 1 */ + }, + { + /* condition 2 */ + }, + { + /* condition n */ + }, + ], + }, +}); // any: let rule = new Rule({ conditions: { any: [ - { /* condition 1 */ }, - { /* condition 2 */ }, - { /* condition n */ }, + { + /* condition 1 */ + }, + { + /* condition 2 */ + }, + { + /* condition n */ + }, { not: { - all: [ /* more conditions */ ] - } - } - ] - } -}) + all: [ + /* more conditions */ + ], + }, + }, + ], + }, +}); // not: let rule = new Rule({ conditions: { - not: { /* condition */ } - } -}) + not: { + /* condition */ + }, + }, +}); ``` -Notice in the second example how `all`, `any`, and `not` can be nested within one another to produce complex boolean expressions. See the [nested-boolean-logic](../examples/02-nested-boolean-logic.js) example. +Notice in the second example how `all`, `any`, and `not` can be nested within one another to produce complex boolean expressions. See the [nested-boolean-logic](../examples/02-nested-boolean-logic.js) example. ### Condition Reference @@ -190,47 +205,51 @@ Rules may reference conditions based on their name. let rule = new Rule({ conditions: { all: [ - { condition: 'conditionName' }, - { /* additional condition */ } - ] - } -}) + { condition: "conditionName" }, + { + /* additional condition */ + }, + ], + }, +}); ``` Before running the rule the condition should be added to the engine. ```js -engine.setCondition('conditionName', { /* conditions */ }); +engine.setCondition("conditionName", { + /* conditions */ +}); ``` Conditions must start with `all`, `any`, `not`, or reference a condition. ### Condition helpers: `params` -Sometimes facts require additional input to perform calculations. For this, the `params` property is passed as an argument to the fact handler. `params` essentially functions as fact arguments, enabling fact handlers to be more generic and reusable. +Sometimes facts require additional input to perform calculations. For this, the `params` property is passed as an argument to the fact handler. `params` essentially functions as fact arguments, enabling fact handlers to be more generic and reusable. ```js // product-price retrieves any product's price based on the "productId" in "params" -engine.addFact('product-price', function (params, almanac) { +engine.addFact("product-price", function (params, almanac) { return productLoader(params.productId) // loads the "widget" product - .then(product => product.price) -}) + .then((product) => product.price); +}); // identifies whether the current widget price is above $100 let rule = new Rule({ conditions: { all: [ { - fact: 'product-price', + fact: "product-price", params: { - productId: 'widget' // specifies which product to load + productId: "widget", // specifies which product to load }, - operator: 'greaterThan', - value: 100 - } - ] - } -}) + operator: "greaterThan", + value: 100, + }, + ], + }, +}); ``` See the [dynamic-facts](../examples/03-dynamic-facts) example @@ -242,29 +261,28 @@ In the `params` example above, the dynamic fact handler loads an object, then re To address this, a `path` property may be provided to traverse fact data using [json-path](https://goessner.net/articles/JsonPath/) syntax. The example above becomes simpler, and only one fact handler must be written: ```js - // product-price retrieves any product's price based on the "productId" in "params" -engine.addFact('product-price', function (params, almanac) { +engine.addFact("product-price", function (params, almanac) { // NOTE: `then` is not required; .price is specified via "path" below - return productLoader(params.productId) -}) + return productLoader(params.productId); +}); // identifies whether the current widget price is above $100 let rule = new Rule({ conditions: { all: [ { - fact: 'product-price', - path: '$.price', + fact: "product-price", + path: "$.price", params: { - productId: 'widget' + productId: "widget", }, - operator: 'greaterThan', - value: 100 - } - ] - } -}) + operator: "greaterThan", + value: 100, + }, + ], + }, +}); ``` json-path support is provided by [jsonpath-plus](https://github.com/s3u/JSONPath) @@ -303,7 +321,7 @@ This feature may be useful in cases where the higher performance offered by simp ### Comparing facts -Sometimes it is necessary to compare facts against other facts. This can be accomplished by nesting the second fact within the `value` property. This second fact has access to the same `params` and `path` helpers as the primary fact. +Sometimes it is necessary to compare facts against other facts. This can be accomplished by nesting the second fact within the `value` property. This second fact has access to the same `params` and `path` helpers as the primary fact. ```js // identifies whether the current widget price is above a maximum @@ -312,46 +330,47 @@ let rule = new Rule({ all: [ // widget-price > budget { - fact: 'product-price', + fact: "product-price", params: { - productId: 'widget', - path: '$.price' + productId: "widget", + path: "$.price", }, - operator: 'greaterThan', + operator: "greaterThan", // "value" contains a fact value: { - fact: 'budget' // "params" and "path" helpers are available as well - } - } - ] - } -}) + fact: "budget", // "params" and "path" helpers are available as well + }, + }, + ], + }, +}); ``` + See the [fact-comparison](../examples/08-fact-comparison.js) example ## Events Listen for `success` and `failure` events emitted when rule is evaluated. -#### ```rule.on('success', Function(Object event, Almanac almanac, RuleResult ruleResult))``` +#### `rule.on('success', Function(Object event, Almanac almanac, RuleResult ruleResult))` The callback will receive the event object, the current [Almanac](./almanac.md), and the [Rule Result](./rules.md#rule-results). ```js // whenever rule is evaluated and the conditions pass, 'success' will trigger -rule.on('success', function(event, almanac, ruleResult) { - console.log(event) // { type: 'my-event', params: { id: 1 } -}) +rule.on("success", function (event, almanac, ruleResult) { + console.log(event); // { type: 'my-event', params: { id: 1 } +}); ``` -#### ```rule.on('failure', Function(Object event, Almanac almanac, RuleResult ruleResult))``` +#### `rule.on('failure', Function(Object event, Almanac almanac, RuleResult ruleResult))` -Companion to `success`, except fires when the rule fails. The callback will receive the event object, the current [Almanac](./almanac.md), and the [Rule Result](./rules.md#rule-results). +Companion to `success`, except fires when the rule fails. The callback will receive the event object, the current [Almanac](./almanac.md), and the [Rule Result](./rules.md#rule-results). ```js -engine.on('failure', function(event, almanac, ruleResult) { - console.log(event) // { type: 'my-event', params: { id: 1 } -}) +engine.on("failure", function (event, almanac, ruleResult) { + console.log(event); // { type: 'my-event', params: { id: 1 } +}); ``` ### Referencing Facts In Events @@ -361,90 +380,94 @@ With the engine option [`replaceFactsInEventParams`](./engine.md#options) the pa ```js const engine = new Engine([], { replaceFactsInEventParams: true }); engine.addRule({ - conditions: { /* ... */ }, - event: { - type: "gameover", - params: { - initials: { - fact: "currentHighScore", - path: "$.initials", - params: { foo: 'bar' } - } - } - } - }) + conditions: { + /* ... */ + }, + event: { + type: "gameover", + params: { + initials: { + fact: "currentHighScore", + path: "$.initials", + params: { foo: "bar" }, + }, + }, + }, +}); ``` See [11-using-facts-in-events.js](../examples/11-using-facts-in-events.js) for a complete example. ## Operators -Each rule condition must begin with a boolean operator(```all```, ```any```, or ```not```) at its root. +Each rule condition must begin with a boolean operator(`all`, `any`, or `not`) at its root. -The ```operator``` compares the value returned by the ```fact``` to what is stored in the ```value``` property. If the result is truthy, the condition passes. +The `operator` compares the value returned by the `fact` to what is stored in the `value` property. If the result is truthy, the condition passes. ### String and Numeric operators: - ```equal``` - _fact_ must equal _value_ +`equal` - _fact_ must equal _value_ - ```notEqual``` - _fact_ must not equal _value_ +`notEqual` - _fact_ must not equal _value_ - _these operators use strict equality (===) and inequality (!==)_ +_these operators use strict equality (===) and inequality (!==)_ ### Numeric operators: - ```lessThan``` - _fact_ must be less than _value_ +`lessThan` - _fact_ must be less than _value_ - ```lessThanInclusive```- _fact_ must be less than or equal to _value_ +`lessThanInclusive`- _fact_ must be less than or equal to _value_ - ```greaterThan``` - _fact_ must be greater than _value_ +`greaterThan` - _fact_ must be greater than _value_ - ```greaterThanInclusive```- _fact_ must be greater than or equal to _value_ +`greaterThanInclusive`- _fact_ must be greater than or equal to _value_ ### Array operators: - ```in``` - _fact_ must be included in _value_ (an array) +`in` - _fact_ must be included in _value_ (an array) - ```notIn``` - _fact_ must not be included in _value_ (an array) +`notIn` - _fact_ must not be included in _value_ (an array) - ```contains``` - _fact_ (an array) must include _value_ +`contains` - _fact_ (an array) must include _value_ - ```doesNotContain``` - _fact_ (an array) must not include _value_ +`doesNotContain` - _fact_ (an array) must not include _value_ ## Operator Decorators -Operator Decorators modify the behavior of an operator either by changing the input or the output. To specify one or more decorators prefix the name of the operator with them in the ```operator``` field and use the colon (```:```) symbol to separate decorators and the operator. For instance ```everyFact:greaterThan``` will produce an operator that checks that every element of the _fact_ is greater than the value. +Operator Decorators modify the behavior of an operator either by changing the input or the output. To specify one or more decorators prefix the name of the operator with them in the `operator` field and use the colon (`:`) symbol to separate decorators and the operator. For instance `everyFact:greaterThan` will produce an operator that checks that every element of the _fact_ is greater than the value. See [12-using-operator-decorators.js](../examples/13-using-operator-decorators.js) for an example. ### Array Decorators: - ```everyFact``` - _fact_ (an array) must have every element pass the decorated operator for _value_ +`everyFact` - _fact_ (an array) must have every element pass the decorated operator for _value_ - ```everyValue``` - _fact_ must pass the decorated operator for every element of _value_ (an array) +`everyValue` - _fact_ must pass the decorated operator for every element of _value_ (an array) - ```someFact``` - _fact_ (an array) must have at-least one element pass the decorated operator for _value_ +`someFact` - _fact_ (an array) must have at-least one element pass the decorated operator for _value_ - ```someValue``` - _fact_ must pass the decorated operator for at-least one element of _value_ (an array) +`someValue` - _fact_ must pass the decorated operator for at-least one element of _value_ (an array) ### Logical Decorators - ```not``` - negate the result of the decorated operator +`not` - negate the result of the decorated operator ### Utility Decorators - ```swap``` - Swap _fact_ and _value_ for the decorated operator + +`swap` - Swap _fact_ and _value_ for the decorated operator ### Decorator Composition -Operator Decorators can be composed by chaining them together with the colon to separate them. For example if you wanted to ensure that every number in an array was less than every number in another array you could use ```everyFact:everyValue:lessThan```. +Operator Decorators can be composed by chaining them together with the colon to separate them. For example if you wanted to ensure that every number in an array was less than every number in another array you could use `everyFact:everyValue:lessThan`. -```swap``` and ```not``` are useful when there are not symmetric or negated versions of custom operators, for instance you could check if a _value_ does not start with a letter contained in a _fact_ using the decorated custom operator ```swap:not:startsWithLetter```. This allows a single custom operator to have 4 permutations. +`swap` and `not` are useful when there are not symmetric or negated versions of custom operators, for instance you could check if a _value_ does not start with a letter contained in a _fact_ using the decorated custom operator `swap:not:startsWithLetter`. This allows a single custom operator to have 4 permutations. ## Rule Results -After a rule is evaluated, a `rule result` object is provided to the `success` and `failure` events. This argument is similar to a regular rule, and contains additional metadata about how the rule was evaluated. Rule results can be used to extract the results of individual conditions, computed fact values, and boolean logic results. `name` can be used to easily identify a given rule. +After a rule is evaluated, a `rule result` object is provided to the `success` and `failure` events. This argument is similar to a regular rule, and contains additional metadata about how the rule was evaluated. Rule results can be used to extract the results of individual conditions, computed fact values, and boolean logic results. `name` can be used to easily identify a given rule. Rule results are structured similar to rules, with two additional pieces of metadata sprinkled throughout: `result` and `factResult` + ```js { result: false, // denotes whether rule computed truthy or falsey @@ -474,14 +497,14 @@ A demonstration can be found in the [rule-results](../examples/09-rule-results.j ## Persisting -Rules may be easily converted to JSON and persisted to a database, file system, or elsewhere. To convert a rule to JSON, simply call the ```rule.toJSON()``` method. Later, a rule may be restored by feeding the json into the Rule constructor. +Rules may be easily converted to JSON and persisted to a database, file system, or elsewhere. To convert a rule to JSON, simply call the `rule.toJSON()` method. Later, a rule may be restored by feeding the json into the Rule constructor. ```js // save somewhere... -let jsonString = rule.toJSON() +let jsonString = rule.toJSON(); // ...later: -let rule = new Rule(jsonString) +let rule = new Rule(jsonString); ``` -_Why aren't "fact" methods persistable?_ This is by design, for several reasons. Firstly, facts are by definition business logic bespoke to your application, and therefore lie outside the scope of this library. Secondly, many times this request indicates a design smell; try thinking of other ways to compose the rules and facts to accomplish the same objective. Finally, persisting fact methods would involve serializing javascript code, and restoring it later via ``eval()``. +_Why aren't "fact" methods persistable?_ This is by design, for several reasons. Firstly, facts are by definition business logic bespoke to your application, and therefore lie outside the scope of this library. Secondly, many times this request indicates a design smell; try thinking of other ways to compose the rules and facts to accomplish the same objective. Finally, persisting fact methods would involve serializing javascript code, and restoring it later via `eval()`. diff --git a/docs/walkthrough.md b/docs/walkthrough.md index 21650dc4..8a48bb3b 100644 --- a/docs/walkthrough.md +++ b/docs/walkthrough.md @@ -1,63 +1,65 @@ # Walkthrough -* [Step 1: Create an Engine](#step-1-create-an-engine) -* [Step 2: Add Rules](#step-2-add-rules) -* [Step 3: Define Facts](#step-3-define-facts) -* [Step 4: Handing Events](#step-4-handing-events) -* [Step 5: Run the engine](#step-5-run-the-engine) +- [Step 1: Create an Engine](#step-1-create-an-engine) +- [Step 2: Add Rules](#step-2-add-rules) +- [Step 3: Define Facts](#step-3-define-facts) +- [Step 4: Handing Events](#step-4-handing-events) +- [Step 5: Run the engine](#step-5-run-the-engine) ## Step 1: Create an Engine ```js - let { Engine } = require('json-rules-engine'); - let engine = new Engine(); +let { Engine } = require("json-rules-engine"); +let engine = new Engine(); ``` More on engines can be found [here](./engine.md) ## Step 2: Add Rules -Rules are composed of two components: conditions and events. _Conditions_ are a set of requirements that must be met to trigger the rule's _event_. +Rules are composed of two components: conditions and events. _Conditions_ are a set of requirements that must be met to trigger the rule's _event_. ```js let event = { - type: 'young-adult-rocky-mnts', + type: "young-adult-rocky-mnts", params: { - giftCard: 'amazon', - value: 50 - } + giftCard: "amazon", + value: 50, + }, }; let conditions = { all: [ { - fact: 'age', - operator: 'greaterThanInclusive', - value: 18 - }, { - fact: 'age', - operator: 'lessThanInclusive', - value: 25 + fact: "age", + operator: "greaterThanInclusive", + value: 18, + }, + { + fact: "age", + operator: "lessThanInclusive", + value: 25, }, { any: [ { - fact: 'state', + fact: "state", params: { - country: 'us' + country: "us", }, - operator: 'equal', - value: 'CO' - }, { - fact: 'state', + operator: "equal", + value: "CO", + }, + { + fact: "state", params: { - country: 'us' + country: "us", }, - operator: 'equal', - value: 'UT' - } - ] - } - ] + operator: "equal", + value: "UT", + }, + ], + }, + ], }; engine.addRule({ conditions, event }); ``` @@ -68,7 +70,7 @@ More on rules can be found [here](./rules.md) ### Step 3: Define Facts -Facts are constant values or pure functions. Using the current example, if the engine were to be run, it would throw an exception: `Undefined fact:'age'` (note: this behavior can be disable via [engine options](./engine.md#Options)). +Facts are constant values or pure functions. Using the current example, if the engine were to be run, it would throw an exception: `Undefined fact:'age'` (note: this behavior can be disable via [engine options](./engine.md#Options)). Let's define some facts: @@ -76,50 +78,54 @@ Let's define some facts: /* * Define the 'state' fact */ -let stateFact = function(params, almanac) { +let stateFact = function (params, almanac) { // rule "params" value is passed to the fact // 'almanac' can be used to lookup other facts // via almanac.factValue() - return almanac.factValue('zip-code') - .then(zip => { - return stateLookupByZip(params.country, zip); - }); + return almanac.factValue("zip-code").then((zip) => { + return stateLookupByZip(params.country, zip); + }); }; -engine.addFact('state', stateFact); +engine.addFact("state", stateFact); /* * Define the 'age' fact */ -let ageFact = function(params, almanac) { +let ageFact = function (params, almanac) { // facts may return a promise when performing asynchronous operations // such as database calls, http requests, etc to gather data - return almanac.factValue('userId').then((userId) => { - return getUser(userId); - }).then((user) => { - return user.age; - }) + return almanac + .factValue("userId") + .then((userId) => { + return getUser(userId); + }) + .then((user) => { + return user.age; + }); }; -engine.addFact('age', ageFact); +engine.addFact("age", ageFact); /* * Define the 'zip-code' fact */ -let zipCodeFact = function(params, almanac) { - return almanac.factValue('userId').then((userId) => { - return getUser(userId); - }).then((user) => { - return user.zipCode; - }) +let zipCodeFact = function (params, almanac) { + return almanac + .factValue("userId") + .then((userId) => { + return getUser(userId); + }) + .then((user) => { + return user.zipCode; + }); }; -engine.addFact('zip-code', zipCodeFact); +engine.addFact("zip-code", zipCodeFact); ``` -Now when the engine is run, it will call the methods above whenever it encounters the ```fact: "age"``` or ```fact: "state"``` properties. +Now when the engine is run, it will call the methods above whenever it encounters the `fact: "age"` or `fact: "state"` properties. -**Important:** facts should be *pure functions*; their computed values will vary based on the ```params``` argument. By establishing facts as pure functions, it allows the rules engine to cache results throughout each ```run()```; facts called multiple times with the same ```params``` will trigger the computation once and cache the results for future calls. If fact caching not desired, this behavior can be turned off via the options; see the [docs](./facts.md). - -More on facts can be found [here](./facts.md). More on almanacs can be found [here](./almanac.md) +**Important:** facts should be _pure functions_; their computed values will vary based on the `params` argument. By establishing facts as pure functions, it allows the rules engine to cache results throughout each `run()`; facts called multiple times with the same `params` will trigger the computation once and cache the results for future calls. If fact caching not desired, this behavior can be turned off via the options; see the [docs](./facts.md). +More on facts can be found [here](./facts.md). More on almanacs can be found [here](./almanac.md) ## Step 4: Handing Events @@ -127,7 +133,7 @@ When rule conditions are met, the application needs to respond to the event that ```js // subscribe directly to the 'young-adult' event -engine.on('young-adult-rocky-mnts', (params) => { +engine.on("young-adult-rocky-mnts", (params) => { // params: { // giftCard: 'amazon', // value: 50 @@ -137,8 +143,8 @@ engine.on('young-adult-rocky-mnts', (params) => { // - OR - // subscribe to any event emitted by the engine -engine.on('success', function (event, almanac, ruleResult) { - console.log('Success event:\n', event); +engine.on("success", function (event, almanac, ruleResult) { + console.log("Success event:\n", event); // event: { // type: "young-adult-rocky-mnts", // params: { @@ -151,46 +157,51 @@ engine.on('success', function (event, almanac, ruleResult) { ## Step 5: Run the engine -Running an engine executes the rules, and fires off event events for conditions that were met. The fact results cache will be cleared with each ```run()``` +Running an engine executes the rules, and fires off event events for conditions that were met. The fact results cache will be cleared with each `run()` ```js // evaluate the rules //engine.run(); // Optionally, facts known at runtime may be passed to run() -engine.run({ userId: 1 }); // any time a rule condition requires 'userId', '1' will be returned +engine.run({ userId: 1 }); // any time a rule condition requires 'userId', '1' will be returned // run() returns a promise engine.run({ userId: 4 }).then(({ events }) => { - console.log('all rules executed; the following events were triggered: ', events.map(result => JSON.stringify(event))) + console.log( + "all rules executed; the following events were triggered: ", + events.map((result) => JSON.stringify(event)), + ); }); ``` + Helper methods (for this example) + ```js function stateLookupByZip(country, zip) { var state; switch (zip.toString()) { - case '80014': - state = 'CO'; + case "80014": + state = "CO"; break; - case '84101': - state = 'UT'; + case "84101": + state = "UT"; break; - case '90210': - state = 'CA'; + case "90210": + state = "CA"; break; default: - state = 'NY'; + state = "NY"; } return state; } var users = { - 1: {age: 22, zipCode: 80014}, - 2: {age: 16, zipCode: 80014}, - 3: {age: 35, zipCode: 84101}, - 4: {age: 23, zipCode: 90210}, + 1: { age: 22, zipCode: 80014 }, + 2: { age: 16, zipCode: 80014 }, + 3: { age: 35, zipCode: 84101 }, + 4: { age: 23, zipCode: 90210 }, }; function getUser(id) { diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..1acd9003 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,23 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default [ + { + files: ["**/*.{js,mjs,cjs,ts,mts}"], + }, + { + ignores: ["dist/"], + }, + { languageOptions: { globals: { ...globals.browser, ...globals.node } } }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + { + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" }, + ], + }, + }, +]; diff --git a/examples/01-hello-world.js b/examples/01-hello-world.js index 6a154ff2..4ec9b336 100644 --- a/examples/01-hello-world.js +++ b/examples/01-hello-world.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; /* * This is the hello-world example from the README. * @@ -9,14 +9,14 @@ * DEBUG=json-rules-engine node ./examples/01-hello-world.js */ -require('colors') -const { Engine } = require('json-rules-engine') +require("colors"); +const { Engine } = require("json-rules-engine"); -async function start () { +async function start() { /** * Setup a new engine */ - const engine = new Engine() + const engine = new Engine(); /** * Create a rule @@ -24,35 +24,37 @@ async function start () { engine.addRule({ // define the 'conditions' for when "hello world" should display conditions: { - all: [{ - fact: 'displayMessage', - operator: 'equal', - value: true - }] + all: [ + { + fact: "displayMessage", + operator: "equal", + value: true, + }, + ], }, // define the 'event' that will fire when the condition evaluates truthy event: { - type: 'message', + type: "message", params: { - data: 'hello-world!' - } - } - }) + data: "hello-world!", + }, + }, + }); /** * Define a 'displayMessage' as a constant value * Fact values do NOT need to be known at engine runtime; see the * 03-dynamic-facts.js example for how to pull in data asynchronously during runtime */ - const facts = { displayMessage: true } + const facts = { displayMessage: true }; // engine.run() evaluates the rule using the facts provided - const { events } = await engine.run(facts) + const { events } = await engine.run(facts); - events.map(event => console.log(event.params.data.green)) + events.map((event) => console.log(event.params.data.green)); } -start() +start(); /* * OUTPUT: * diff --git a/examples/02-nested-boolean-logic.js b/examples/02-nested-boolean-logic.js index 8fcc0446..3bd0c169 100644 --- a/examples/02-nested-boolean-logic.js +++ b/examples/02-nested-boolean-logic.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; /* * This example demonstates nested boolean logic - e.g. (x OR y) AND (a OR b). * @@ -9,52 +9,62 @@ * DEBUG=json-rules-engine node ./examples/02-nested-boolean-logic.js */ -require('colors') -const { Engine } = require('json-rules-engine') +require("colors"); +const { Engine } = require("json-rules-engine"); -async function start () { +async function start() { /** * Setup a new engine */ - const engine = new Engine() + const engine = new Engine(); // define a rule for detecting the player has exceeded foul limits. Foul out any player who: // (has committed 5 fouls AND game is 40 minutes) OR (has committed 6 fouls AND game is 48 minutes) engine.addRule({ conditions: { - any: [{ - all: [{ - fact: 'gameDuration', - operator: 'equal', - value: 40 - }, { - fact: 'personalFoulCount', - operator: 'greaterThanInclusive', - value: 5 - }], - name: 'short foul limit' - }, { - all: [{ - fact: 'gameDuration', - operator: 'equal', - value: 48 - }, { - not: { - fact: 'personalFoulCount', - operator: 'lessThan', - value: 6 - } - }], - name: 'long foul limit' - }] + any: [ + { + all: [ + { + fact: "gameDuration", + operator: "equal", + value: 40, + }, + { + fact: "personalFoulCount", + operator: "greaterThanInclusive", + value: 5, + }, + ], + name: "short foul limit", + }, + { + all: [ + { + fact: "gameDuration", + operator: "equal", + value: 48, + }, + { + not: { + fact: "personalFoulCount", + operator: "lessThan", + value: 6, + }, + }, + ], + name: "long foul limit", + }, + ], }, - event: { // define the event to fire when the conditions evaluate truthy - type: 'fouledOut', + event: { + // define the event to fire when the conditions evaluate truthy + type: "fouledOut", params: { - message: 'Player has fouled out!' - } - } - }) + message: "Player has fouled out!", + }, + }, + }); /** * define the facts @@ -62,14 +72,14 @@ async function start () { */ const facts = { personalFoulCount: 6, - gameDuration: 40 - } + gameDuration: 40, + }; - const { events } = await engine.run(facts) + const { events } = await engine.run(facts); - events.map(event => console.log(event.params.message.red)) + events.map((event) => console.log(event.params.message.red)); } -start() +start(); /* * OUTPUT: * diff --git a/examples/03-dynamic-facts.js b/examples/03-dynamic-facts.js index e7ddc502..2a7e65cc 100644 --- a/examples/03-dynamic-facts.js +++ b/examples/03-dynamic-facts.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; /* * This example demonstrates computing fact values at runtime, and leveraging the 'path' feature * to select object properties returned by facts @@ -10,17 +10,17 @@ * DEBUG=json-rules-engine node ./examples/03-dynamic-facts.js */ -require('colors') -const { Engine } = require('json-rules-engine') +require("colors"); +const { Engine } = require("json-rules-engine"); // example client for making asynchronous requests to an api, database, etc -const apiClient = require('./support/account-api-client') +const apiClient = require("./support/account-api-client"); -async function start () { +async function start() { /** * Setup a new engine */ - const engine = new Engine() + const engine = new Engine(); /** * Rule for identifying microsoft employees taking pto on christmas @@ -30,51 +30,56 @@ async function start () { */ const microsoftRule = { conditions: { - all: [{ - fact: 'account-information', - operator: 'equal', - value: 'microsoft', - path: '$.company' // access the 'company' property of "account-information" - }, { - fact: 'account-information', - operator: 'in', - value: ['active', 'paid-leave'], // 'status'' can be active or paid-leave - path: '$.status' // access the 'status' property of "account-information" - }, { - fact: 'account-information', - operator: 'contains', - value: '2016-12-25', - path: '$.ptoDaysTaken' // access the 'ptoDaysTaken' property of "account-information" - }] + all: [ + { + fact: "account-information", + operator: "equal", + value: "microsoft", + path: "$.company", // access the 'company' property of "account-information" + }, + { + fact: "account-information", + operator: "in", + value: ["active", "paid-leave"], // 'status'' can be active or paid-leave + path: "$.status", // access the 'status' property of "account-information" + }, + { + fact: "account-information", + operator: "contains", + value: "2016-12-25", + path: "$.ptoDaysTaken", // access the 'ptoDaysTaken' property of "account-information" + }, + ], }, event: { - type: 'microsoft-christmas-pto', + type: "microsoft-christmas-pto", params: { - message: 'current microsoft employee taking christmas day off' - } - } - } - engine.addRule(microsoftRule) + message: "current microsoft employee taking christmas day off", + }, + }, + }; + engine.addRule(microsoftRule); /** * 'account-information' fact executes an api call and retrieves account data, feeding the results * into the engine. The major advantage of this technique is that although there are THREE conditions * requiring this data, only ONE api call is made. This results in much more efficient runtime performance. */ - engine.addFact('account-information', function (params, almanac) { - return almanac.factValue('accountId') - .then(accountId => { - return apiClient.getAccountInformation(accountId) - }) - }) + engine.addFact("account-information", function (params, almanac) { + return almanac.factValue("accountId").then((accountId) => { + return apiClient.getAccountInformation(accountId); + }); + }); // define fact(s) known at runtime - const facts = { accountId: 'lincoln' } - const { events } = await engine.run(facts) + const facts = { accountId: "lincoln" }; + const { events } = await engine.run(facts); - console.log(facts.accountId + ' is a ' + events.map(event => event.params.message)) + console.log( + facts.accountId + " is a " + events.map((event) => event.params.message), + ); } -start() +start(); /* * OUTPUT: diff --git a/examples/04-fact-dependency.js b/examples/04-fact-dependency.js index bc332a9b..55e9b78d 100644 --- a/examples/04-fact-dependency.js +++ b/examples/04-fact-dependency.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; /* * This is an advanced example that demonstrates facts with dependencies * on other facts. In addition, it demonstrates facts that load data asynchronously @@ -11,15 +11,15 @@ * DEBUG=json-rules-engine node ./examples/04-fact-dependency.js */ -require('colors') -const { Engine } = require('json-rules-engine') -const accountClient = require('./support/account-api-client') +require("colors"); +const { Engine } = require("json-rules-engine"); +const accountClient = require("./support/account-api-client"); -async function start () { +async function start() { /** * Setup a new engine */ - const engine = new Engine() + const engine = new Engine(); /** * Rule for identifying microsoft employees that have been terminated. @@ -28,21 +28,24 @@ async function start () { */ const microsoftRule = { conditions: { - all: [{ - fact: 'account-information', - operator: 'equal', - value: 'microsoft', - path: '$.company' - }, { - fact: 'account-information', - operator: 'equal', - value: 'terminated', - path: '$.status' - }] + all: [ + { + fact: "account-information", + operator: "equal", + value: "microsoft", + path: "$.company", + }, + { + fact: "account-information", + operator: "equal", + value: "terminated", + path: "$.status", + }, + ], }, - event: { type: 'microsoft-terminated-employees' } - } - engine.addRule(microsoftRule) + event: { type: "microsoft-terminated-employees" }, + }; + engine.addRule(microsoftRule); /** * Rule for identifying accounts older than 5 years @@ -51,82 +54,97 @@ async function start () { */ const tenureRule = { conditions: { - all: [{ - fact: 'employee-tenure', - operator: 'greaterThanInclusive', - value: 5, - params: { - unit: 'years' - } - }] + all: [ + { + fact: "employee-tenure", + operator: "greaterThanInclusive", + value: 5, + params: { + unit: "years", + }, + }, + ], }, - event: { type: 'five-year-tenure' } - } - engine.addRule(tenureRule) + event: { type: "five-year-tenure" }, + }; + engine.addRule(tenureRule); /** * Register listeners with the engine for rule success and failure */ - let facts + let facts; engine - .on('success', event => { - console.log(facts.accountId + ' DID '.green + 'meet conditions for the ' + event.type.underline + ' rule.') - }) - .on('failure', event => { - console.log(facts.accountId + ' did ' + 'NOT'.red + ' meet conditions for the ' + event.type.underline + ' rule.') + .on("success", (event) => { + console.log( + facts.accountId + + " DID ".green + + "meet conditions for the " + + event.type.underline + + " rule.", + ); }) + .on("failure", (event) => { + console.log( + facts.accountId + + " did " + + "NOT".red + + " meet conditions for the " + + event.type.underline + + " rule.", + ); + }); /** * 'account-information' fact executes an api call and retrieves account data * - Demonstrates facts called only by other facts and never mentioned directly in a rule */ - engine.addFact('account-information', (params, almanac) => { - return almanac.factValue('accountId') - .then(accountId => { - return accountClient.getAccountInformation(accountId) - }) - }) + engine.addFact("account-information", (params, almanac) => { + return almanac.factValue("accountId").then((accountId) => { + return accountClient.getAccountInformation(accountId); + }); + }); /** * 'employee-tenure' fact retrieves account-information, and computes the duration of employment * since the account was created using 'accountInformation.createdAt' */ - engine.addFact('employee-tenure', (params, almanac) => { - return almanac.factValue('account-information') - .then(accountInformation => { - const created = new Date(accountInformation.createdAt) - const now = new Date() + engine.addFact("employee-tenure", (params, almanac) => { + return almanac + .factValue("account-information") + .then((accountInformation) => { + const created = new Date(accountInformation.createdAt); + const now = new Date(); switch (params.unit) { - case 'years': - return now.getFullYear() - created.getFullYear() - case 'milliseconds': + case "years": + return now.getFullYear() - created.getFullYear(); + case "milliseconds": default: - return now.getTime() - created.getTime() + return now.getTime() - created.getTime(); } }) - .catch(console.log) - }) + .catch(console.log); + }); // first run, using washington's facts - console.log('-- FIRST RUN --') - facts = { accountId: 'washington' } - await engine.run(facts) + console.log("-- FIRST RUN --"); + facts = { accountId: "washington" }; + await engine.run(facts); - console.log('-- SECOND RUN --') + console.log("-- SECOND RUN --"); // second run, using jefferson's facts; facts & evaluation are independent of the first run - facts = { accountId: 'jefferson' } - await engine.run(facts) + facts = { accountId: "jefferson" }; + await engine.run(facts); /* - * NOTES: - * - * - Notice that although a total of 6 conditions were evaluated using - * account-information (3 rule conditions x 2 accounts), the account-information api call - * is only called twice -- once for each account. This is due to the base fact caching the results - * for washington and jefferson after the initial data load. - */ + * NOTES: + * + * - Notice that although a total of 6 conditions were evaluated using + * account-information (3 rule conditions x 2 accounts), the account-information api call + * is only called twice -- once for each account. This is due to the base fact caching the results + * for washington and jefferson after the initial data load. + */ } -start() +start(); /* * OUTPUT: diff --git a/examples/05-optimizing-runtime-with-fact-priorities.js b/examples/05-optimizing-runtime-with-fact-priorities.js index bfb63f6d..d39ce1a6 100644 --- a/examples/05-optimizing-runtime-with-fact-priorities.js +++ b/examples/05-optimizing-runtime-with-fact-priorities.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; /* * This is an advanced example that demonstrates using fact priorities to optimize the rules engine. * @@ -9,82 +9,105 @@ * DEBUG=json-rules-engine node ./examples/05-optimizing-runtime-with-fact-priorities.js */ -require('colors') -const { Engine } = require('json-rules-engine') -const accountClient = require('./support/account-api-client') +require("colors"); +const { Engine } = require("json-rules-engine"); +const accountClient = require("./support/account-api-client"); -async function start () { +async function start() { /** * Setup a new engine */ - const engine = new Engine() + const engine = new Engine(); /** * - Demonstrates setting high performance (cpu) facts higher than low performing (network call) facts. */ const microsoftRule = { conditions: { - all: [{ - fact: 'account-information', - operator: 'equal', - value: true - }, { - fact: 'date', - operator: 'lessThan', - value: 1467331200000 // unix ts for 2016-07-01; truthy when current date is prior to 2016-07-01 - }] + all: [ + { + fact: "account-information", + operator: "equal", + value: true, + }, + { + fact: "date", + operator: "lessThan", + value: 1467331200000, // unix ts for 2016-07-01; truthy when current date is prior to 2016-07-01 + }, + ], }, - event: { type: 'microsoft-employees' } - } - engine.addRule(microsoftRule) + event: { type: "microsoft-employees" }, + }; + engine.addRule(microsoftRule); /** * Register listeners with the engine for rule success and failure */ - const facts = { accountId: 'washington' } + const facts = { accountId: "washington" }; engine - .on('success', event => { - console.log(facts.accountId + ' DID '.green + 'meet conditions for the ' + event.type.underline + ' rule.') - }) - .on('failure', event => { - console.log(facts.accountId + ' did ' + 'NOT'.red + ' meet conditions for the ' + event.type.underline + ' rule.') + .on("success", (event) => { + console.log( + facts.accountId + + " DID ".green + + "meet conditions for the " + + event.type.underline + + " rule.", + ); }) + .on("failure", (event) => { + console.log( + facts.accountId + + " did " + + "NOT".red + + " meet conditions for the " + + event.type.underline + + " rule.", + ); + }); /** * Low and High Priorities. * Facts that do not have a priority set default to 1 * @type {Integer} - Facts are run in priority from highest to lowest. */ - const HIGH = 100 - const LOW = 1 + const HIGH = 100; + const LOW = 1; /** * 'account-information' fact executes an api call - network calls are expensive, so * we set this fact to be LOW priority; it will only be evaluated after all higher priority facts * evaluate truthy */ - engine.addFact('account-information', (params, almanac) => { - // this fact will not be evaluated, because the "date" fact will fail first - console.log('Checking the "account-information" fact...') // this message will not appear - return almanac.factValue('accountId') - .then((accountId) => { - return accountClient.getAccountInformation(accountId) - }) - }, { priority: LOW }) + engine.addFact( + "account-information", + (params, almanac) => { + // this fact will not be evaluated, because the "date" fact will fail first + console.log('Checking the "account-information" fact...'); // this message will not appear + return almanac.factValue("accountId").then((accountId) => { + return accountClient.getAccountInformation(accountId); + }); + }, + { priority: LOW }, + ); /** * 'date' fact returns the current unix timestamp in ms. * Because this is cheap to compute, we set it to "HIGH" priority */ - engine.addFact('date', (params, almanac) => { - console.log('Checking the "date" fact...') - return Date.now() - }, { priority: HIGH }) + engine.addFact( + "date", + (params, almanac) => { + console.log('Checking the "date" fact...'); + return Date.now(); + }, + { priority: HIGH }, + ); // define fact(s) known at runtime - await engine.run() + await engine.run(); } -start() +start(); /* * OUTPUT: diff --git a/examples/06-custom-operators.js b/examples/06-custom-operators.js index f9169529..36511de8 100644 --- a/examples/06-custom-operators.js +++ b/examples/06-custom-operators.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; /* * This example demonstrates using custom operators. * @@ -15,88 +15,94 @@ * DEBUG=json-rules-engine node ./examples/06-custom-operators.js */ -require('colors') -const { Engine } = require('json-rules-engine') +require("colors"); +const { Engine } = require("json-rules-engine"); -async function start () { +async function start() { /** * Setup a new engine */ - const engine = new Engine() + const engine = new Engine(); /** * Define a 'startsWith' custom operator, for use in later rules */ - engine.addOperator('startsWith', (factValue, jsonValue) => { - if (!factValue.length) return false - return factValue[0].toLowerCase() === jsonValue.toLowerCase() - }) + engine.addOperator("startsWith", (factValue, jsonValue) => { + if (!factValue.length) return false; + return factValue[0].toLowerCase() === jsonValue.toLowerCase(); + }); /** * Add rule for detecting words that start with 'a' */ const ruleA = { conditions: { - all: [{ - fact: 'word', - operator: 'startsWith', - value: 'a' - }] + all: [ + { + fact: "word", + operator: "startsWith", + value: "a", + }, + ], }, event: { - type: 'start-with-a' - } - } - engine.addRule(ruleA) + type: "start-with-a", + }, + }; + engine.addRule(ruleA); /* - * Add rule for detecting words that start with 'b' - */ + * Add rule for detecting words that start with 'b' + */ const ruleB = { conditions: { - all: [{ - fact: 'word', - operator: 'startsWith', - value: 'b' - }] + all: [ + { + fact: "word", + operator: "startsWith", + value: "b", + }, + ], }, event: { - type: 'start-with-b' - } - } - engine.addRule(ruleB) + type: "start-with-b", + }, + }; + engine.addRule(ruleB); // utility for printing output const printEventType = { - 'start-with-a': 'start with "a"', - 'start-with-b': 'start with "b"' - } + "start-with-a": 'start with "a"', + "start-with-b": 'start with "b"', + }; /** * Register listeners with the engine for rule success and failure */ - let facts + let facts; engine - .on('success', event => { - console.log(facts.word + ' DID '.green + printEventType[event.type]) - }) - .on('failure', event => { - console.log(facts.word + ' did ' + 'NOT'.red + ' ' + printEventType[event.type]) + .on("success", (event) => { + console.log(facts.word + " DID ".green + printEventType[event.type]); }) + .on("failure", (event) => { + console.log( + facts.word + " did " + "NOT".red + " " + printEventType[event.type], + ); + }); /** * Each run() of the engine executes on an independent set of facts. We'll run twice, once per word */ // first run, using 'bacon' - facts = { word: 'bacon' } - await engine.run(facts) + facts = { word: "bacon" }; + await engine.run(facts); // second run, using 'antelope' - facts = { word: 'antelope' } - await engine.run(facts) + facts = { word: "antelope" }; + await engine.run(facts); } -start() +start(); /* * OUTPUT: diff --git a/examples/07-rule-chaining.js b/examples/07-rule-chaining.js index e086cbc5..873b08a8 100644 --- a/examples/07-rule-chaining.js +++ b/examples/07-rule-chaining.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; /* * This is an advanced example demonstrating rules that passed based off the * results of other rules by adding runtime facts. It also demonstrates @@ -11,47 +11,50 @@ * DEBUG=json-rules-engine node ./examples/07-rule-chaining.js */ -require('colors') -const { Engine } = require('json-rules-engine') -const { getAccountInformation } = require('./support/account-api-client') +require("colors"); +const { Engine } = require("json-rules-engine"); +const { getAccountInformation } = require("./support/account-api-client"); -async function start () { +async function start() { /** * Setup a new engine */ - const engine = new Engine() + const engine = new Engine(); /** * Rule for identifying people who may like screwdrivers */ const drinkRule = { conditions: { - all: [{ - fact: 'drinksOrangeJuice', - operator: 'equal', - value: true - }, { - fact: 'enjoysVodka', - operator: 'equal', - value: true - }] + all: [ + { + fact: "drinksOrangeJuice", + operator: "equal", + value: true, + }, + { + fact: "enjoysVodka", + operator: "equal", + value: true, + }, + ], }, - event: { type: 'drinks-screwdrivers' }, + event: { type: "drinks-screwdrivers" }, priority: 10, // IMPORTANT! Set a higher priority for the drinkRule, so it runs first onSuccess: async function (event, almanac) { - almanac.addFact('screwdriverAficionado', true) + almanac.addFact("screwdriverAficionado", true); // asychronous operations can be performed within callbacks // engine execution will not proceed until the returned promises is resolved - const accountId = await almanac.factValue('accountId') - const accountInfo = await getAccountInformation(accountId) - almanac.addFact('accountInfo', accountInfo) + const accountId = await almanac.factValue("accountId"); + const accountInfo = await getAccountInformation(accountId); + almanac.addFact("accountInfo", accountInfo); }, onFailure: function (event, almanac) { - almanac.addFact('screwdriverAficionado', false) - } - } - engine.addRule(drinkRule) + almanac.addFact("screwdriverAficionado", false); + }, + }; + engine.addRule(drinkRule); /** * Rule for identifying people who should be invited to a screwdriver social @@ -60,58 +63,90 @@ async function start () { */ const inviteRule = { conditions: { - all: [{ - fact: 'screwdriverAficionado', // this fact value is set when the drinkRule is evaluated - operator: 'equal', - value: true - }, { - fact: 'isSociable', - operator: 'equal', - value: true - }, { - fact: 'accountInfo', - path: '$.company', - operator: 'equal', - value: 'microsoft' - }] + all: [ + { + fact: "screwdriverAficionado", // this fact value is set when the drinkRule is evaluated + operator: "equal", + value: true, + }, + { + fact: "isSociable", + operator: "equal", + value: true, + }, + { + fact: "accountInfo", + path: "$.company", + operator: "equal", + value: "microsoft", + }, + ], }, - event: { type: 'invite-to-screwdriver-social' }, - priority: 5 // Set a lower priority for the drinkRule, so it runs later (default: 1) - } - engine.addRule(inviteRule) + event: { type: "invite-to-screwdriver-social" }, + priority: 5, // Set a lower priority for the drinkRule, so it runs later (default: 1) + }; + engine.addRule(inviteRule); /** * Register listeners with the engine for rule success and failure */ engine - .on('success', async (event, almanac) => { - const accountInfo = await almanac.factValue('accountInfo') - const accountId = await almanac.factValue('accountId') - console.log(`${accountId}(${accountInfo.company}) ` + 'DID'.green + ` meet conditions for the ${event.type.underline} rule.`) - }) - .on('failure', async (event, almanac) => { - const accountId = await almanac.factValue('accountId') - console.log(`${accountId} did ` + 'NOT'.red + ` meet conditions for the ${event.type.underline} rule.`) + .on("success", async (event, almanac) => { + const accountInfo = await almanac.factValue("accountInfo"); + const accountId = await almanac.factValue("accountId"); + console.log( + `${accountId}(${accountInfo.company}) ` + + "DID".green + + ` meet conditions for the ${event.type.underline} rule.`, + ); }) + .on("failure", async (event, almanac) => { + const accountId = await almanac.factValue("accountId"); + console.log( + `${accountId} did ` + + "NOT".red + + ` meet conditions for the ${event.type.underline} rule.`, + ); + }); // define fact(s) known at runtime - let facts = { accountId: 'washington', drinksOrangeJuice: true, enjoysVodka: true, isSociable: true, accountInfo: {} } + let facts = { + accountId: "washington", + drinksOrangeJuice: true, + enjoysVodka: true, + isSociable: true, + accountInfo: {}, + }; // first run, using washington's facts - let results = await engine.run(facts) + let results = await engine.run(facts); // isScrewdriverAficionado was a fact set by engine.run() - let isScrewdriverAficionado = results.almanac.factValue('screwdriverAficionado') - console.log(`${facts.accountId} ${isScrewdriverAficionado ? 'IS'.green : 'IS NOT'.red} a screwdriver aficionado`) + let isScrewdriverAficionado = results.almanac.factValue( + "screwdriverAficionado", + ); + console.log( + `${facts.accountId} ${isScrewdriverAficionado ? "IS".green : "IS NOT".red} a screwdriver aficionado`, + ); - facts = { accountId: 'jefferson', drinksOrangeJuice: true, enjoysVodka: false, isSociable: true, accountInfo: {} } - results = await engine.run(facts) // second run, using jefferson's facts; facts & evaluation are independent of the first run + facts = { + accountId: "jefferson", + drinksOrangeJuice: true, + enjoysVodka: false, + isSociable: true, + accountInfo: {}, + }; + results = await engine.run(facts); // second run, using jefferson's facts; facts & evaluation are independent of the first run - isScrewdriverAficionado = await results.almanac.factValue('screwdriverAficionado') - console.log(`${facts.accountId} ${isScrewdriverAficionado ? 'IS'.green : 'IS NOT'.red} a screwdriver aficionado`) + isScrewdriverAficionado = await results.almanac.factValue( + "screwdriverAficionado", + ); + console.log( + `${facts.accountId} ${isScrewdriverAficionado ? "IS".green : "IS NOT".red} a screwdriver aficionado`, + ); } -start() +start(); /* * OUTPUT: diff --git a/examples/08-fact-comparison.js b/examples/08-fact-comparison.js index 31368098..d6bb635a 100644 --- a/examples/08-fact-comparison.js +++ b/examples/08-fact-comparison.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; /* * This is a basic example demonstrating a condition that compares two facts * @@ -9,14 +9,14 @@ * DEBUG=json-rules-engine node ./examples/08-fact-comparison.js */ -require('colors') -const { Engine } = require('json-rules-engine') +require("colors"); +const { Engine } = require("json-rules-engine"); -async function start () { +async function start() { /** * Setup a new engine */ - const engine = new Engine() + const engine = new Engine(); /** * Rule for determining if account has enough money to purchase a $50 gift card product @@ -25,110 +25,134 @@ async function start () { */ const rule = { conditions: { - all: [{ - // extract 'balance' from the 'customer' account type - fact: 'account', - path: '$.balance', - params: { - accountType: 'customer' - }, + all: [ + { + // extract 'balance' from the 'customer' account type + fact: "account", + path: "$.balance", + params: { + accountType: "customer", + }, - operator: 'greaterThanInclusive', // >= + operator: "greaterThanInclusive", // >= - // "value" in this instance is an object containing a fact definition - // fact helpers "path" and "params" are supported here as well - value: { - fact: 'product', - path: '$.price', - params: { - productId: 'giftCard' - } - } - }] + // "value" in this instance is an object containing a fact definition + // fact helpers "path" and "params" are supported here as well + value: { + fact: "product", + path: "$.price", + params: { + productId: "giftCard", + }, + }, + }, + ], }, - event: { type: 'customer-can-afford-gift-card' } - } - engine.addRule(rule) + event: { type: "customer-can-afford-gift-card" }, + }; + engine.addRule(rule); - engine.addFact('account', (params, almanac) => { + engine.addFact("account", (params, almanac) => { // get account list - return almanac.factValue('accounts') - .then(accounts => { - // use "params" to filter down to the type specified, in this case the "customer" account - const customerAccount = accounts.filter(account => account.type === params.accountType) - // return the customerAccount object, which "path" will use to pull the "balance" property - return customerAccount[0] - }) - }) + return almanac.factValue("accounts").then((accounts) => { + // use "params" to filter down to the type specified, in this case the "customer" account + const customerAccount = accounts.filter( + (account) => account.type === params.accountType, + ); + // return the customerAccount object, which "path" will use to pull the "balance" property + return customerAccount[0]; + }); + }); - engine.addFact('product', (params, almanac) => { + engine.addFact("product", (params, almanac) => { // get product list - return almanac.factValue('products') - .then(products => { - // use "params" to filter down to the product specified, in this case the "giftCard" product - const product = products.filter(product => product.productId === params.productId) - // return the product object, which "path" will use to pull the "price" property - return product[0] - }) - }) + return almanac.factValue("products").then((products) => { + // use "params" to filter down to the product specified, in this case the "giftCard" product + const product = products.filter( + (product) => product.productId === params.productId, + ); + // return the product object, which "path" will use to pull the "price" property + return product[0]; + }); + }); /** * Register listeners with the engine for rule success and failure */ - let facts + let facts; engine - .on('success', (event, almanac) => { - console.log(facts.userId + ' DID '.green + 'meet conditions for the ' + event.type.underline + ' rule.') - }) - .on('failure', event => { - console.log(facts.userId + ' did ' + 'NOT'.red + ' meet conditions for the ' + event.type.underline + ' rule.') + .on("success", (event, almanac) => { + console.log( + facts.userId + + " DID ".green + + "meet conditions for the " + + event.type.underline + + " rule.", + ); }) + .on("failure", (event) => { + console.log( + facts.userId + + " did " + + "NOT".red + + " meet conditions for the " + + event.type.underline + + " rule.", + ); + }); // define fact(s) known at runtime const productList = { products: [ { - productId: 'giftCard', - price: 50 - }, { - productId: 'widget', - price: 45 - }, { - productId: 'widget-plus', - price: 800 - } - ] - } + productId: "giftCard", + price: 50, + }, + { + productId: "widget", + price: 45, + }, + { + productId: "widget-plus", + price: 800, + }, + ], + }; let userFacts = { - userId: 'washington', - accounts: [{ - type: 'customer', - balance: 500 - }, { - type: 'partner', - balance: 0 - }] - } + userId: "washington", + accounts: [ + { + type: "customer", + balance: 500, + }, + { + type: "partner", + balance: 0, + }, + ], + }; // compile facts to be fed to the engine - facts = Object.assign({}, userFacts, productList) + facts = Object.assign({}, userFacts, productList); // first run, user can afford a gift card - await engine.run(facts) + await engine.run(facts); // second run; a user that cannot afford a gift card userFacts = { - userId: 'jefferson', - accounts: [{ - type: 'customer', - balance: 30 - }] - } - facts = Object.assign({}, userFacts, productList) - await engine.run(facts) + userId: "jefferson", + accounts: [ + { + type: "customer", + balance: 30, + }, + ], + }; + facts = Object.assign({}, userFacts, productList); + await engine.run(facts); } -start() +start(); /* * OUTPUT: * diff --git a/examples/09-rule-results.js b/examples/09-rule-results.js index d895ffbd..608ccf03 100644 --- a/examples/09-rule-results.js +++ b/examples/09-rule-results.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; /* * This is a basic example demonstrating how to leverage the metadata supplied by rule results * @@ -8,85 +8,94 @@ * For detailed output: * DEBUG=json-rules-engine node ./examples/09-rule-results.js */ -require('colors') -const { Engine } = require('json-rules-engine') +require("colors"); +const { Engine } = require("json-rules-engine"); -async function start () { +async function start() { /** * Setup a new engine */ - const engine = new Engine() + const engine = new Engine(); // rule for determining honor role student athletes (student has GPA >= 3.5 AND is an athlete) engine.addRule({ conditions: { - all: [{ - fact: 'athlete', - operator: 'equal', - value: true - }, { - fact: 'GPA', - operator: 'greaterThanInclusive', - value: 3.5 - }] + all: [ + { + fact: "athlete", + operator: "equal", + value: true, + }, + { + fact: "GPA", + operator: "greaterThanInclusive", + value: 3.5, + }, + ], }, - event: { // define the event to fire when the conditions evaluate truthy - type: 'honor-roll', + event: { + // define the event to fire when the conditions evaluate truthy + type: "honor-roll", params: { - message: 'Student made the athletics honor-roll' - } + message: "Student made the athletics honor-roll", + }, }, - name: 'Athlete GPA Rule' - }) + name: "Athlete GPA Rule", + }); - function render (message, ruleResult) { + function render(message, ruleResult) { // if rule succeeded, render success message if (ruleResult.result) { - return console.log(`${message}`.green) + return console.log(`${message}`.green); } // if rule failed, iterate over each failed condition to determine why the student didn't qualify for athletics honor roll - const detail = ruleResult.conditions.all.filter(condition => !condition.result) - .map(condition => { + const detail = ruleResult.conditions.all + .filter((condition) => !condition.result) + .map((condition) => { switch (condition.operator) { - case 'equal': - return `was not an ${condition.fact}` - case 'greaterThanInclusive': - return `${condition.fact} of ${condition.factResult} was too low` + case "equal": + return `was not an ${condition.fact}`; + case "greaterThanInclusive": + return `${condition.fact} of ${condition.factResult} was too low`; default: - return '' + return ""; } - }).join(' and ') - console.log(`${message} ${detail}`.red) + }) + .join(" and "); + console.log(`${message} ${detail}`.red); } /** * On success, retrieve the student's username and print rule name for display purposes, and render */ - engine.on('success', (event, almanac, ruleResult) => { - almanac.factValue('username').then(username => { - render(`${username.bold} succeeded ${ruleResult.name}! ${event.params.message}`, ruleResult) - }) - }) + engine.on("success", (event, almanac, ruleResult) => { + almanac.factValue("username").then((username) => { + render( + `${username.bold} succeeded ${ruleResult.name}! ${event.params.message}`, + ruleResult, + ); + }); + }); /** * On failure, retrieve the student's username and print rule name for display purposes, and render */ - engine.on('failure', (event, almanac, ruleResult) => { - almanac.factValue('username').then(username => { - render(`${username.bold} failed ${ruleResult.name} - `, ruleResult) - }) - }) + engine.on("failure", (event, almanac, ruleResult) => { + almanac.factValue("username").then((username) => { + render(`${username.bold} failed ${ruleResult.name} - `, ruleResult); + }); + }); // Run the engine for 5 different students await Promise.all([ - engine.run({ athlete: false, GPA: 3.9, username: 'joe' }), - engine.run({ athlete: true, GPA: 3.5, username: 'larry' }), - engine.run({ athlete: false, GPA: 3.1, username: 'jane' }), - engine.run({ athlete: true, GPA: 4.0, username: 'janet' }), - engine.run({ athlete: true, GPA: 1.1, username: 'sarah' }) - ]) + engine.run({ athlete: false, GPA: 3.9, username: "joe" }), + engine.run({ athlete: true, GPA: 3.5, username: "larry" }), + engine.run({ athlete: false, GPA: 3.1, username: "jane" }), + engine.run({ athlete: true, GPA: 4.0, username: "janet" }), + engine.run({ athlete: true, GPA: 1.1, username: "sarah" }), + ]); } -start() +start(); /* * OUTPUT: * diff --git a/examples/10-condition-sharing.js b/examples/10-condition-sharing.js index 29fe2e37..09eee8cd 100644 --- a/examples/10-condition-sharing.js +++ b/examples/10-condition-sharing.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; /* * This is an advanced example demonstrating rules that re-use a condition defined * in the engine. @@ -10,32 +10,32 @@ * DEBUG=json-rules-engine node ./examples/10-condition-sharing.js */ -require('colors') -const { Engine } = require('json-rules-engine') +require("colors"); +const { Engine } = require("json-rules-engine"); -async function start () { +async function start() { /** * Setup a new engine */ - const engine = new Engine() + const engine = new Engine(); /** * Condition that will be used to determine if a user likes screwdrivers */ - engine.setCondition('screwdriverAficionado', { + engine.setCondition("screwdriverAficionado", { all: [ { - fact: 'drinksOrangeJuice', - operator: 'equal', - value: true + fact: "drinksOrangeJuice", + operator: "equal", + value: true, }, { - fact: 'enjoysVodka', - operator: 'equal', - value: true - } - ] - }) + fact: "enjoysVodka", + operator: "equal", + value: true, + }, + ], + }); /** * Rule for identifying people who should be invited to a screwdriver social @@ -46,18 +46,18 @@ async function start () { conditions: { all: [ { - condition: 'screwdriverAficionado' + condition: "screwdriverAficionado", }, { - fact: 'isSociable', - operator: 'equal', - value: true - } - ] + fact: "isSociable", + operator: "equal", + value: true, + }, + ], }, - event: { type: 'invite-to-screwdriver-social' } - } - engine.addRule(inviteRule) + event: { type: "invite-to-screwdriver-social" }, + }; + engine.addRule(inviteRule); /** * Rule for identifying people who should be invited to the other social @@ -69,65 +69,65 @@ async function start () { all: [ { not: { - condition: 'screwdriverAficionado' - } + condition: "screwdriverAficionado", + }, }, { - fact: 'isSociable', - operator: 'equal', - value: true - } - ] + fact: "isSociable", + operator: "equal", + value: true, + }, + ], }, - event: { type: 'invite-to-other-social' } - } - engine.addRule(otherInviteRule) + event: { type: "invite-to-other-social" }, + }; + engine.addRule(otherInviteRule); /** * Register listeners with the engine for rule success and failure */ engine - .on('success', async (event, almanac) => { - const accountId = await almanac.factValue('accountId') + .on("success", async (event, almanac) => { + const accountId = await almanac.factValue("accountId"); console.log( `${accountId}` + - 'DID'.green + - ` meet conditions for the ${event.type.underline} rule.` - ) + "DID".green + + ` meet conditions for the ${event.type.underline} rule.`, + ); }) - .on('failure', async (event, almanac) => { - const accountId = await almanac.factValue('accountId') + .on("failure", async (event, almanac) => { + const accountId = await almanac.factValue("accountId"); console.log( `${accountId} did ` + - 'NOT'.red + - ` meet conditions for the ${event.type.underline} rule.` - ) - }) + "NOT".red + + ` meet conditions for the ${event.type.underline} rule.`, + ); + }); // define fact(s) known at runtime let facts = { - accountId: 'washington', + accountId: "washington", drinksOrangeJuice: true, enjoysVodka: true, - isSociable: true - } + isSociable: true, + }; // first run, using washington's facts - await engine.run(facts) + await engine.run(facts); facts = { - accountId: 'jefferson', + accountId: "jefferson", drinksOrangeJuice: true, enjoysVodka: false, isSociable: true, - accountInfo: {} - } + accountInfo: {}, + }; // second run, using jefferson's facts; facts & evaluation are independent of the first run - await engine.run(facts) + await engine.run(facts); } -start() +start(); /* * OUTPUT: diff --git a/examples/11-using-facts-in-events.js b/examples/11-using-facts-in-events.js index 1004e7ee..21e323fa 100644 --- a/examples/11-using-facts-in-events.js +++ b/examples/11-using-facts-in-events.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; /* * This is an advanced example demonstrating an event that emits the value * of a fact in it's parameters. @@ -10,18 +10,21 @@ * DEBUG=json-rules-engine node ./examples/11-using-facts-in-events.js */ -require('colors') -const { Engine, Fact } = require('json-rules-engine') +require("colors"); +const { Engine, Fact } = require("json-rules-engine"); -async function start () { +async function start() { /** * Setup a new engine */ - const engine = new Engine([], { replaceFactsInEventParams: true }) + const engine = new Engine([], { replaceFactsInEventParams: true }); // in-memory "database" - let currentHighScore = null - const currentHighScoreFact = new Fact('currentHighScore', () => currentHighScore) + let currentHighScore = null; + const currentHighScoreFact = new Fact( + "currentHighScore", + () => currentHighScore, + ); /** * Rule for when you've gotten the high score @@ -31,28 +34,28 @@ async function start () { conditions: { any: [ { - fact: 'currentHighScore', - operator: 'equal', - value: null + fact: "currentHighScore", + operator: "equal", + value: null, }, { - fact: 'score', - operator: 'greaterThan', + fact: "score", + operator: "greaterThan", value: { - fact: 'currentHighScore', - path: '$.score' - } - } - ] + fact: "currentHighScore", + path: "$.score", + }, + }, + ], }, event: { - type: 'highscore', + type: "highscore", params: { - initials: { fact: 'initials' }, - score: { fact: 'score' } - } - } - } + initials: { fact: "initials" }, + score: { fact: "score" }, + }, + }, + }; /** * Rule for when the game is over and you don't have the high score @@ -62,77 +65,77 @@ async function start () { conditions: { all: [ { - fact: 'score', - operator: 'lessThanInclusive', + fact: "score", + operator: "lessThanInclusive", value: { - fact: 'currentHighScore', - path: '$.score' - } - } - ] + fact: "currentHighScore", + path: "$.score", + }, + }, + ], }, event: { - type: 'gameover', + type: "gameover", params: { initials: { - fact: 'currentHighScore', - path: '$.initials' + fact: "currentHighScore", + path: "$.initials", }, score: { - fact: 'currentHighScore', - path: '$.score' - } - } - } - } - engine.addRule(highScoreRule) - engine.addRule(gameOverRule) - engine.addFact(currentHighScoreFact) + fact: "currentHighScore", + path: "$.score", + }, + }, + }, + }; + engine.addRule(highScoreRule); + engine.addRule(gameOverRule); + engine.addFact(currentHighScoreFact); /** * Register listeners with the engine for rule success */ engine - .on('success', async ({ params: { initials, score } }) => { - console.log(`HIGH SCORE\n${initials} - ${score}`) + .on("success", async ({ params: { initials, score } }) => { + console.log(`HIGH SCORE\n${initials} - ${score}`); }) - .on('success', ({ type, params }) => { - if (type === 'highscore') { - currentHighScore = params + .on("success", ({ type, params }) => { + if (type === "highscore") { + currentHighScore = params; } - }) + }); let facts = { - initials: 'DOG', - score: 968 - } + initials: "DOG", + score: 968, + }; // first run, without a high score - await engine.run(facts) + await engine.run(facts); - console.log('\n') + console.log("\n"); // new player facts = { - initials: 'AAA', - score: 500 - } + initials: "AAA", + score: 500, + }; // new player hasn't gotten the high score yet - await engine.run(facts) + await engine.run(facts); - console.log('\n') + console.log("\n"); facts = { - initials: 'AAA', - score: 1000 - } + initials: "AAA", + score: 1000, + }; // second run, with a high score - await engine.run(facts) + await engine.run(facts); } -start() +start(); /* * OUTPUT: diff --git a/examples/12-using-custom-almanac.js b/examples/12-using-custom-almanac.js index c94c3981..e494c7f5 100644 --- a/examples/12-using-custom-almanac.js +++ b/examples/12-using-custom-almanac.js @@ -1,89 +1,99 @@ -'use strict' +"use strict"; -require('colors') -const { Almanac, Engine } = require('json-rules-engine') +require("colors"); +const { Almanac, Engine } = require("json-rules-engine"); /** * Almanac that support piping values through named functions */ class PipedAlmanac extends Almanac { - constructor (options) { - super(options) - this.pipes = new Map() + constructor(options) { + super(options); + this.pipes = new Map(); } - addPipe (name, pipe) { - this.pipes.set(name, pipe) + addPipe(name, pipe) { + this.pipes.set(name, pipe); } - factValue (factId, params, path) { - let pipes = [] - if (params && 'pipes' in params && Array.isArray(params.pipes)) { - pipes = params.pipes - delete params.pipes + factValue(factId, params, path) { + let pipes = []; + if (params && "pipes" in params && Array.isArray(params.pipes)) { + pipes = params.pipes; + delete params.pipes; } - return super.factValue(factId, params, path).then(value => { + return super.factValue(factId, params, path).then((value) => { return pipes.reduce((value, pipeName) => { - const pipe = this.pipes.get(pipeName) + const pipe = this.pipes.get(pipeName); if (pipe) { - return pipe(value) + return pipe(value); } - return value - }, value) - }) + return value; + }, value); + }); } } -async function start () { - const engine = new Engine() - .addRule({ - conditions: { - all: [ - { - fact: 'age', - params: { - // the addOne pipe adds one to the value - pipes: ['addOne'] - }, - operator: 'greaterThanInclusive', - value: 21 - } - ] - }, - event: { - type: 'Over 21(ish)' - } - }) +async function start() { + const engine = new Engine().addRule({ + conditions: { + all: [ + { + fact: "age", + params: { + // the addOne pipe adds one to the value + pipes: ["addOne"], + }, + operator: "greaterThanInclusive", + value: 21, + }, + ], + }, + event: { + type: "Over 21(ish)", + }, + }); - engine.on('success', async (event, almanac) => { - const name = await almanac.factValue('name') - const age = await almanac.factValue('age') - console.log(`${name} is ${age} years old and ${'is'.green} ${event.type}`) - }) + engine.on("success", async (event, almanac) => { + const name = await almanac.factValue("name"); + const age = await almanac.factValue("age"); + console.log(`${name} is ${age} years old and ${"is".green} ${event.type}`); + }); - engine.on('failure', async (event, almanac) => { - const name = await almanac.factValue('name') - const age = await almanac.factValue('age') - console.log(`${name} is ${age} years old and ${'is not'.red} ${event.type}`) - }) + engine.on("failure", async (event, almanac) => { + const name = await almanac.factValue("name"); + const age = await almanac.factValue("age"); + console.log( + `${name} is ${age} years old and ${"is not".red} ${event.type}`, + ); + }); const createAlmanacWithPipes = () => { - const almanac = new PipedAlmanac() - almanac.addPipe('addOne', (v) => v + 1) - return almanac - } + const almanac = new PipedAlmanac(); + almanac.addPipe("addOne", (v) => v + 1); + return almanac; + }; // first run Bob who is less than 20 - await engine.run({ name: 'Bob', age: 19 }, { almanac: createAlmanacWithPipes() }) + await engine.run( + { name: "Bob", age: 19 }, + { almanac: createAlmanacWithPipes() }, + ); // second run Alice who is 21 - await engine.run({ name: 'Alice', age: 21 }, { almanac: createAlmanacWithPipes() }) + await engine.run( + { name: "Alice", age: 21 }, + { almanac: createAlmanacWithPipes() }, + ); // third run Chad who is 20 - await engine.run({ name: 'Chad', age: 20 }, { almanac: createAlmanacWithPipes() }) + await engine.run( + { name: "Chad", age: 20 }, + { almanac: createAlmanacWithPipes() }, + ); } -start() +start(); /* * OUTPUT: diff --git a/examples/13-using-operator-decorators.js b/examples/13-using-operator-decorators.js index 413271eb..ffc787d7 100644 --- a/examples/13-using-operator-decorators.js +++ b/examples/13-using-operator-decorators.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; /* * This example demonstrates using operator decorators. * @@ -11,14 +11,14 @@ * DEBUG=json-rules-engine node ./examples/12-using-operator-decorators.js */ -require('colors') -const { Engine } = require('json-rules-engine') +require("colors"); +const { Engine } = require("json-rules-engine"); -async function start () { +async function start() { /** * Setup a new engine */ - const engine = new Engine() + const engine = new Engine(); /** * Add a rule for validating a tag (fact) @@ -26,66 +26,75 @@ async function start () { */ const validTags = { conditions: { - all: [{ - fact: 'tags', - operator: 'everyFact:in', - value: { fact: 'validTags' } - }] + all: [ + { + fact: "tags", + operator: "everyFact:in", + value: { fact: "validTags" }, + }, + ], }, event: { - type: 'valid tags' - } - } + type: "valid tags", + }, + }; - engine.addRule(validTags) + engine.addRule(validTags); - engine.addFact('validTags', ['dev', 'staging', 'load', 'prod']) + engine.addFact("validTags", ["dev", "staging", "load", "prod"]); - let facts + let facts; engine - .on('success', event => { - console.log(facts.tags.join(', ') + ' WERE'.green + ' all ' + event.type) - }) - .on('failure', event => { - console.log(facts.tags.join(', ') + ' WERE NOT'.red + ' all ' + event.type) + .on("success", (event) => { + console.log(facts.tags.join(", ") + " WERE".green + " all " + event.type); }) + .on("failure", (event) => { + console.log( + facts.tags.join(", ") + " WERE NOT".red + " all " + event.type, + ); + }); // first run with valid tags - facts = { tags: ['dev', 'prod'] } - await engine.run(facts) + facts = { tags: ["dev", "prod"] }; + await engine.run(facts); // second run with an invalid tag - facts = { tags: ['dev', 'deleted'] } - await engine.run(facts) + facts = { tags: ["dev", "deleted"] }; + await engine.run(facts); // add a new decorator to allow for a case-insensitive match - engine.addOperatorDecorator('caseInsensitive', (factValue, jsonValue, next) => { - return next(factValue.toLowerCase(), jsonValue.toLowerCase()) - }) + engine.addOperatorDecorator( + "caseInsensitive", + (factValue, jsonValue, next) => { + return next(factValue.toLowerCase(), jsonValue.toLowerCase()); + }, + ); // new rule for case-insensitive validation const caseInsensitiveValidTags = { conditions: { - all: [{ - fact: 'tags', - // everyFact has someValue that caseInsensitive is equal - operator: 'everyFact:someValue:caseInsensitive:equal', - value: { fact: 'validTags' } - }] + all: [ + { + fact: "tags", + // everyFact has someValue that caseInsensitive is equal + operator: "everyFact:someValue:caseInsensitive:equal", + value: { fact: "validTags" }, + }, + ], }, event: { - type: 'valid tags (case insensitive)' - } - } + type: "valid tags (case insensitive)", + }, + }; - engine.addRule(caseInsensitiveValidTags) + engine.addRule(caseInsensitiveValidTags); // third run with a tag that is valid if case insensitive - facts = { tags: ['dev', 'PROD'] } - await engine.run(facts) + facts = { tags: ["dev", "PROD"] }; + await engine.run(facts); } -start() +start(); /* * OUTPUT: diff --git a/examples/support/account-api-client.js b/examples/support/account-api-client.js index 05798998..d166b721 100644 --- a/examples/support/account-api-client.js +++ b/examples/support/account-api-client.js @@ -1,39 +1,39 @@ -'use strict' +"use strict"; -require('colors') +require("colors"); const accountData = { washington: { - company: 'microsoft', - status: 'terminated', - ptoDaysTaken: ['2016-12-25', '2016-04-01'], - createdAt: '2012-02-14' + company: "microsoft", + status: "terminated", + ptoDaysTaken: ["2016-12-25", "2016-04-01"], + createdAt: "2012-02-14", }, jefferson: { - company: 'apple', - status: 'terminated', - ptoDaysTaken: ['2015-01-25'], - createdAt: '2005-04-03' + company: "apple", + status: "terminated", + ptoDaysTaken: ["2015-01-25"], + createdAt: "2005-04-03", }, lincoln: { - company: 'microsoft', - status: 'active', - ptoDaysTaken: ['2016-02-21', '2016-12-25', '2016-03-28'], - createdAt: '2015-06-26' - } -} + company: "microsoft", + status: "active", + ptoDaysTaken: ["2016-02-21", "2016-12-25", "2016-03-28"], + createdAt: "2015-06-26", + }, +}; /** * mock api client for retrieving account information */ module.exports = { getAccountInformation: (accountId) => { - const message = 'loading account information for "' + accountId + '"' - console.log(message.dim) + const message = 'loading account information for "' + accountId + '"'; + console.log(message.dim); return new Promise((resolve, reject) => { setImmediate(() => { - resolve(accountData[accountId]) - }) - }) - } -} + resolve(accountData[accountId]); + }); + }); + }, +}; diff --git a/package.json b/package.json index 8f7aa861..4e078dd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "json-rules-engine", - "version": "7.0.0", + "version": "8.0.0-alpha.1", "description": "Rules Engine expressed in simple json", "main": "dist/index.js", "types": "types/index.d.ts", @@ -10,8 +10,8 @@ "scripts": { "test": "mocha && npm run lint --silent && npm run test:types", "test:types": "tsd", - "lint": "standard --verbose --env mocha | snazzy || true", - "lint:fix": "standard --fix --env mocha", + "lint": "eslint", + "format": "prettier -w .", "prepublishOnly": "npm run build", "build": "babel --stage 1 -d dist/ src/", "watch": "babel --watch --stage 1 -d dist/ src", @@ -26,24 +26,8 @@ "engine", "rules engine" ], - "standard": { - "parser": "babel-eslint", - "ignore": [ - "/dist", - "/examples/node_modules" - ], - "globals": [ - "context", - "xcontext", - "describe", - "xdescribe", - "it", - "xit", - "before", - "beforeEach", - "expect", - "factories" - ] + "publishConfig": { + "tag": "next" }, "mocha": { "require": [ @@ -67,6 +51,7 @@ }, "homepage": "https://github.com/cachecontrol/json-rules-engine", "devDependencies": { + "@eslint/js": "^9.13.0", "babel-cli": "6.26.0", "babel-core": "6.26.3", "babel-eslint": "10.1.0", @@ -79,14 +64,16 @@ "chai-as-promised": "^7.1.1", "colors": "~1.4.0", "dirty-chai": "2.0.1", + "eslint": "^9.13.0", + "globals": "^15.11.0", "lodash": "4.17.21", "mocha": "^8.4.0", "perfy": "^1.1.5", + "prettier": "^3.3.3", "sinon": "^11.1.1", "sinon-chai": "^3.7.0", - "snazzy": "^9.0.0", - "standard": "^16.0.3", - "tsd": "^0.17.0" + "tsd": "^0.17.0", + "typescript-eslint": "^8.11.0" }, "dependencies": { "clone": "^2.1.2", diff --git a/src/almanac.js b/src/almanac.js index effc7329..37b0b3c0 100644 --- a/src/almanac.js +++ b/src/almanac.js @@ -1,13 +1,13 @@ -'use strict' +"use strict"; -import Fact from './fact' -import { UndefinedFactError } from './errors' -import debug from './debug' +import Fact from "./fact"; +import { UndefinedFactError } from "./errors"; +import debug from "./debug"; -import { JSONPath } from 'jsonpath-plus' +import { JSONPath } from "jsonpath-plus"; -function defaultPathResolver (value, path) { - return JSONPath({ path, json: value, wrap: false }) +function defaultPathResolver(value, path) { + return JSONPath({ path, json: value, wrap: false }); } /** @@ -16,45 +16,45 @@ function defaultPathResolver (value, path) { * A new almanac is used for every engine run() */ export default class Almanac { - constructor (options = {}) { - this.factMap = new Map() - this.factResultsCache = new Map() // { cacheKey: Promise } - this.allowUndefinedFacts = Boolean(options.allowUndefinedFacts) - this.pathResolver = options.pathResolver || defaultPathResolver - this.events = { success: [], failure: [] } - this.ruleResults = [] + constructor(options = {}) { + this.factMap = new Map(); + this.factResultsCache = new Map(); // { cacheKey: Promise } + this.allowUndefinedFacts = Boolean(options.allowUndefinedFacts); + this.pathResolver = options.pathResolver || defaultPathResolver; + this.events = { success: [], failure: [] }; + this.ruleResults = []; } /** * Adds a success event * @param {Object} event */ - addEvent (event, outcome) { - if (!outcome) throw new Error('outcome required: "success" | "failure"]') - this.events[outcome].push(event) + addEvent(event, outcome) { + if (!outcome) throw new Error('outcome required: "success" | "failure"]'); + this.events[outcome].push(event); } /** * retrieve successful events */ - getEvents (outcome = '') { - if (outcome) return this.events[outcome] - return this.events.success.concat(this.events.failure) + getEvents(outcome = "") { + if (outcome) return this.events[outcome]; + return this.events.success.concat(this.events.failure); } /** * Adds a rule result * @param {Object} event */ - addResult (ruleResult) { - this.ruleResults.push(ruleResult) + addResult(ruleResult) { + this.ruleResults.push(ruleResult); } /** * retrieve successful events */ - getResults () { - return this.ruleResults + getResults() { + return this.ruleResults; } /** @@ -62,17 +62,17 @@ export default class Almanac { * @param {String} factId * @return {Fact} */ - _getFact (factId) { - return this.factMap.get(factId) + _getFact(factId) { + return this.factMap.get(factId); } /** * Registers fact with the almanac * @param {[type]} fact [description] */ - _addConstantFact (fact) { - this.factMap.set(fact.id, fact) - this._setFactValue(fact, {}, fact.value) + _addConstantFact(fact) { + this.factMap.set(fact.id, fact); + this._setFactValue(fact, {}, fact.value); } /** @@ -81,13 +81,13 @@ export default class Almanac { * @param {Object} params - values for differentiating this fact value from others, used for cache key * @param {Mixed} value - computed value */ - _setFactValue (fact, params, value) { - const cacheKey = fact.getCacheKey(params) - const factValue = Promise.resolve(value) + _setFactValue(fact, params, value) { + const cacheKey = fact.getCacheKey(params); + const factValue = Promise.resolve(value); if (cacheKey) { - this.factResultsCache.set(cacheKey, factValue) + this.factResultsCache.set(cacheKey, factValue); } - return factValue + return factValue; } /** @@ -96,21 +96,21 @@ export default class Almanac { * @param {function} definitionFunc - function to be called when computing the fact value for a given rule * @param {Object} options - options to initialize the fact with. used when "id" is not a Fact instance */ - addFact (id, valueOrMethod, options) { - let factId = id - let fact + addFact(id, valueOrMethod, options) { + let factId = id; + let fact; if (id instanceof Fact) { - factId = id.id - fact = id + factId = id.id; + fact = id; } else { - fact = new Fact(id, valueOrMethod, options) + fact = new Fact(id, valueOrMethod, options); } - debug('almanac::addFact', { id: factId }) - this.factMap.set(factId, fact) + debug("almanac::addFact", { id: factId }); + this.factMap.set(factId, fact); if (fact.isConstant()) { - this._setFactValue(fact, {}, fact.value) + this._setFactValue(fact, {}, fact.value); } - return this + return this; } /** @@ -119,10 +119,10 @@ export default class Almanac { * @param {String} fact - fact identifier * @param {Mixed} value - constant value of the fact */ - addRuntimeFact (factId, value) { - debug('almanac::addRuntimeFact', { id: factId }) - const fact = new Fact(factId, value) - return this._addConstantFact(fact) + addRuntimeFact(factId, value) { + debug("almanac::addRuntimeFact", { id: factId }); + const fact = new Fact(factId, value); + return this._addConstantFact(fact); } /** @@ -133,54 +133,70 @@ export default class Almanac { * @param {String} path - object * @return {Promise} a promise which will resolve with the fact computation. */ - factValue (factId, params = {}, path = '') { - let factValuePromise - const fact = this._getFact(factId) + factValue(factId, params = {}, path = "") { + let factValuePromise; + const fact = this._getFact(factId); if (fact === undefined) { if (this.allowUndefinedFacts) { - return Promise.resolve(undefined) + return Promise.resolve(undefined); } else { - return Promise.reject(new UndefinedFactError(`Undefined fact: ${factId}`)) + return Promise.reject( + new UndefinedFactError(`Undefined fact: ${factId}`), + ); } } if (fact.isConstant()) { - factValuePromise = Promise.resolve(fact.calculate(params, this)) + factValuePromise = Promise.resolve(fact.calculate(params, this)); } else { - const cacheKey = fact.getCacheKey(params) - const cacheVal = cacheKey && this.factResultsCache.get(cacheKey) + const cacheKey = fact.getCacheKey(params); + const cacheVal = cacheKey && this.factResultsCache.get(cacheKey); if (cacheVal) { - factValuePromise = Promise.resolve(cacheVal) - debug('almanac::factValue cache hit for fact', { id: factId }) + factValuePromise = Promise.resolve(cacheVal); + debug("almanac::factValue cache hit for fact", { id: factId }); } else { - debug('almanac::factValue cache miss, calculating', { id: factId }) - factValuePromise = this._setFactValue(fact, params, fact.calculate(params, this)) + debug("almanac::factValue cache miss, calculating", { id: factId }); + factValuePromise = this._setFactValue( + fact, + params, + fact.calculate(params, this), + ); } } if (path) { - debug('condition::evaluate extracting object', { property: path }) - return factValuePromise - .then(factValue => { - if (factValue != null && typeof factValue === 'object') { - const pathValue = this.pathResolver(factValue, path) - debug('condition::evaluate extracting object', { property: path, received: pathValue }) - return pathValue - } else { - debug('condition::evaluate could not compute object path of non-object', { path, factValue, type: typeof factValue }) - return factValue - } - }) + debug("condition::evaluate extracting object", { property: path }); + return factValuePromise.then((factValue) => { + if (factValue != null && typeof factValue === "object") { + const pathValue = this.pathResolver(factValue, path); + debug("condition::evaluate extracting object", { + property: path, + received: pathValue, + }); + return pathValue; + } else { + debug( + "condition::evaluate could not compute object path of non-object", + { path, factValue, type: typeof factValue }, + ); + return factValue; + } + }); } - return factValuePromise + return factValuePromise; } /** * Interprets value as either a primitive, or if a fact, retrieves the fact value */ - getValue (value) { - if (value != null && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, 'fact')) { // value = { fact: 'xyz' } - return this.factValue(value.fact, value.params, value.path) + getValue(value) { + if ( + value != null && + typeof value === "object" && + Object.prototype.hasOwnProperty.call(value, "fact") + ) { + // value = { fact: 'xyz' } + return this.factValue(value.fact, value.params, value.path); } - return Promise.resolve(value) + return Promise.resolve(value); } } diff --git a/src/condition.js b/src/condition.js index 28a86a36..fec9344c 100644 --- a/src/condition.js +++ b/src/condition.js @@ -1,34 +1,44 @@ -'use strict' +"use strict"; -import debug from './debug' +import debug from "./debug"; export default class Condition { - constructor (properties) { - if (!properties) throw new Error('Condition: constructor options required') - const booleanOperator = Condition.booleanOperator(properties) - Object.assign(this, properties) + constructor(properties) { + if (!properties) throw new Error("Condition: constructor options required"); + const booleanOperator = Condition.booleanOperator(properties); + Object.assign(this, properties); if (booleanOperator) { - const subConditions = properties[booleanOperator] - const subConditionsIsArray = Array.isArray(subConditions) - if (booleanOperator !== 'not' && !subConditionsIsArray) { throw new Error(`"${booleanOperator}" must be an array`) } - if (booleanOperator === 'not' && subConditionsIsArray) { throw new Error(`"${booleanOperator}" cannot be an array`) } - this.operator = booleanOperator + const subConditions = properties[booleanOperator]; + const subConditionsIsArray = Array.isArray(subConditions); + if (booleanOperator !== "not" && !subConditionsIsArray) { + throw new Error(`"${booleanOperator}" must be an array`); + } + if (booleanOperator === "not" && subConditionsIsArray) { + throw new Error(`"${booleanOperator}" cannot be an array`); + } + this.operator = booleanOperator; // boolean conditions always have a priority; default 1 - this.priority = parseInt(properties.priority, 10) || 1 + this.priority = parseInt(properties.priority, 10) || 1; if (subConditionsIsArray) { - this[booleanOperator] = subConditions.map((c) => new Condition(c)) + this[booleanOperator] = subConditions.map((c) => new Condition(c)); } else { - this[booleanOperator] = new Condition(subConditions) + this[booleanOperator] = new Condition(subConditions); + } + } else if (!Object.prototype.hasOwnProperty.call(properties, "condition")) { + if (!Object.prototype.hasOwnProperty.call(properties, "fact")) { + throw new Error('Condition: constructor "fact" property required'); + } + if (!Object.prototype.hasOwnProperty.call(properties, "operator")) { + throw new Error('Condition: constructor "operator" property required'); + } + if (!Object.prototype.hasOwnProperty.call(properties, "value")) { + throw new Error('Condition: constructor "value" property required'); } - } else if (!Object.prototype.hasOwnProperty.call(properties, 'condition')) { - if (!Object.prototype.hasOwnProperty.call(properties, 'fact')) { throw new Error('Condition: constructor "fact" property required') } - if (!Object.prototype.hasOwnProperty.call(properties, 'operator')) { throw new Error('Condition: constructor "operator" property required') } - if (!Object.prototype.hasOwnProperty.call(properties, 'value')) { throw new Error('Condition: constructor "value" property required') } // a non-boolean condition does not have a priority by default. this allows // priority to be dictated by the fact definition - if (Object.prototype.hasOwnProperty.call(properties, 'priority')) { - properties.priority = parseInt(properties.priority, 10) + if (Object.prototype.hasOwnProperty.call(properties, "priority")) { + properties.priority = parseInt(properties.priority, 10); } } } @@ -38,44 +48,44 @@ export default class Condition { * @param {Boolean} stringify - whether to return as a json string * @returns {string,object} json string or json-friendly object */ - toJSON (stringify = true) { - const props = {} + toJSON(stringify = true) { + const props = {}; if (this.priority) { - props.priority = this.priority + props.priority = this.priority; } if (this.name) { - props.name = this.name + props.name = this.name; } - const oper = Condition.booleanOperator(this) + const oper = Condition.booleanOperator(this); if (oper) { if (Array.isArray(this[oper])) { - props[oper] = this[oper].map((c) => c.toJSON(false)) + props[oper] = this[oper].map((c) => c.toJSON(false)); } else { - props[oper] = this[oper].toJSON(false) + props[oper] = this[oper].toJSON(false); } } else if (this.isConditionReference()) { - props.condition = this.condition + props.condition = this.condition; } else { - props.operator = this.operator - props.value = this.value - props.fact = this.fact + props.operator = this.operator; + props.value = this.value; + props.fact = this.fact; if (this.factResult !== undefined) { - props.factResult = this.factResult + props.factResult = this.factResult; } if (this.result !== undefined) { - props.result = this.result + props.result = this.result; } if (this.params) { - props.params = this.params + props.params = this.params; } if (this.path) { - props.path = this.path + props.path = this.path; } } if (stringify) { - return JSON.stringify(props) + return JSON.stringify(props); } - return props + return props; } /** @@ -87,34 +97,36 @@ export default class Condition { * @param {Map} operatorMap - map of available operators, keyed by operator name * @returns {Boolean} - evaluation result */ - evaluate (almanac, operatorMap) { - if (!almanac) return Promise.reject(new Error('almanac required')) - if (!operatorMap) return Promise.reject(new Error('operatorMap required')) - if (this.isBooleanOperator()) { return Promise.reject(new Error('Cannot evaluate() a boolean condition')) } + evaluate(almanac, operatorMap) { + if (!almanac) return Promise.reject(new Error("almanac required")); + if (!operatorMap) return Promise.reject(new Error("operatorMap required")); + if (this.isBooleanOperator()) { + return Promise.reject(new Error("Cannot evaluate() a boolean condition")); + } - const op = operatorMap.get(this.operator) - if (!op) { return Promise.reject(new Error(`Unknown operator: ${this.operator}`)) } + const op = operatorMap.get(this.operator); + if (!op) { + return Promise.reject(new Error(`Unknown operator: ${this.operator}`)); + } return Promise.all([ almanac.getValue(this.value), - almanac.factValue(this.fact, this.params, this.path) + almanac.factValue(this.fact, this.params, this.path), ]).then(([rightHandSideValue, leftHandSideValue]) => { - const result = op.evaluate(leftHandSideValue, rightHandSideValue) - debug( - 'condition::evaluate', { - leftHandSideValue, - operator: this.operator, - rightHandSideValue, - result - } - ) + const result = op.evaluate(leftHandSideValue, rightHandSideValue); + debug("condition::evaluate", { + leftHandSideValue, + operator: this.operator, + rightHandSideValue, + result, + }); return { result, leftHandSideValue, rightHandSideValue, - operator: this.operator - } - }) + operator: this.operator, + }; + }); } /** @@ -122,13 +134,13 @@ export default class Condition { * If the condition is not a boolean condition, the result will be 'undefined' * @return {string 'all', 'any', or 'not'} */ - static booleanOperator (condition) { - if (Object.prototype.hasOwnProperty.call(condition, 'any')) { - return 'any' - } else if (Object.prototype.hasOwnProperty.call(condition, 'all')) { - return 'all' - } else if (Object.prototype.hasOwnProperty.call(condition, 'not')) { - return 'not' + static booleanOperator(condition) { + if (Object.prototype.hasOwnProperty.call(condition, "any")) { + return "any"; + } else if (Object.prototype.hasOwnProperty.call(condition, "all")) { + return "all"; + } else if (Object.prototype.hasOwnProperty.call(condition, "not")) { + return "not"; } } @@ -137,23 +149,23 @@ export default class Condition { * Instance version of Condition.isBooleanOperator * @returns {string,undefined} - 'any', 'all', 'not' or undefined (if not a boolean condition) */ - booleanOperator () { - return Condition.booleanOperator(this) + booleanOperator() { + return Condition.booleanOperator(this); } /** * Whether the operator is boolean ('all', 'any', 'not') * @returns {Boolean} */ - isBooleanOperator () { - return Condition.booleanOperator(this) !== undefined + isBooleanOperator() { + return Condition.booleanOperator(this) !== undefined; } /** * Whether the condition represents a reference to a condition * @returns {Boolean} */ - isConditionReference () { - return Object.prototype.hasOwnProperty.call(this, 'condition') + isConditionReference() { + return Object.prototype.hasOwnProperty.call(this, "condition"); } } diff --git a/src/debug.js b/src/debug.js index d8744ca9..3e315012 100644 --- a/src/debug.js +++ b/src/debug.js @@ -1,14 +1,21 @@ - -function createDebug () { +function createDebug() { try { - if ((typeof process !== 'undefined' && process.env && process.env.DEBUG && process.env.DEBUG.match(/json-rules-engine/)) || - (typeof window !== 'undefined' && window.localStorage && window.localStorage.debug && window.localStorage.debug.match(/json-rules-engine/))) { - return console.debug.bind(console) + if ( + (typeof process !== "undefined" && + process.env && + process.env.DEBUG && + process.env.DEBUG.match(/json-rules-engine/)) || + (typeof window !== "undefined" && + window.localStorage && + window.localStorage.debug && + window.localStorage.debug.match(/json-rules-engine/)) + ) { + return console.debug.bind(console); } } catch (ex) { // Do nothing } - return () => {} + return () => {}; } -export default createDebug() +export default createDebug(); diff --git a/src/engine-default-operator-decorators.js b/src/engine-default-operator-decorators.js index 4bf83312..5ab95345 100644 --- a/src/engine-default-operator-decorators.js +++ b/src/engine-default-operator-decorators.js @@ -1,14 +1,44 @@ -'use strict' +"use strict"; -import OperatorDecorator from './operator-decorator' +import OperatorDecorator from "./operator-decorator"; -const OperatorDecorators = [] +const OperatorDecorators = []; -OperatorDecorators.push(new OperatorDecorator('someFact', (factValue, jsonValue, next) => factValue.some(fv => next(fv, jsonValue)), Array.isArray)) -OperatorDecorators.push(new OperatorDecorator('someValue', (factValue, jsonValue, next) => jsonValue.some(jv => next(factValue, jv)))) -OperatorDecorators.push(new OperatorDecorator('everyFact', (factValue, jsonValue, next) => factValue.every(fv => next(fv, jsonValue)), Array.isArray)) -OperatorDecorators.push(new OperatorDecorator('everyValue', (factValue, jsonValue, next) => jsonValue.every(jv => next(factValue, jv)))) -OperatorDecorators.push(new OperatorDecorator('swap', (factValue, jsonValue, next) => next(jsonValue, factValue))) -OperatorDecorators.push(new OperatorDecorator('not', (factValue, jsonValue, next) => !next(factValue, jsonValue))) +OperatorDecorators.push( + new OperatorDecorator( + "someFact", + (factValue, jsonValue, next) => factValue.some((fv) => next(fv, jsonValue)), + Array.isArray, + ), +); +OperatorDecorators.push( + new OperatorDecorator("someValue", (factValue, jsonValue, next) => + jsonValue.some((jv) => next(factValue, jv)), + ), +); +OperatorDecorators.push( + new OperatorDecorator( + "everyFact", + (factValue, jsonValue, next) => + factValue.every((fv) => next(fv, jsonValue)), + Array.isArray, + ), +); +OperatorDecorators.push( + new OperatorDecorator("everyValue", (factValue, jsonValue, next) => + jsonValue.every((jv) => next(factValue, jv)), + ), +); +OperatorDecorators.push( + new OperatorDecorator("swap", (factValue, jsonValue, next) => + next(jsonValue, factValue), + ), +); +OperatorDecorators.push( + new OperatorDecorator( + "not", + (factValue, jsonValue, next) => !next(factValue, jsonValue), + ), +); -export default OperatorDecorators +export default OperatorDecorators; diff --git a/src/engine-default-operators.js b/src/engine-default-operators.js index dfe33f29..f22fd549 100644 --- a/src/engine-default-operators.js +++ b/src/engine-default-operators.js @@ -1,22 +1,30 @@ -'use strict' +"use strict"; -import Operator from './operator' +import Operator from "./operator"; -const Operators = [] -Operators.push(new Operator('equal', (a, b) => a === b)) -Operators.push(new Operator('notEqual', (a, b) => a !== b)) -Operators.push(new Operator('in', (a, b) => b.indexOf(a) > -1)) -Operators.push(new Operator('notIn', (a, b) => b.indexOf(a) === -1)) +const Operators = []; +Operators.push(new Operator("equal", (a, b) => a === b)); +Operators.push(new Operator("notEqual", (a, b) => a !== b)); +Operators.push(new Operator("in", (a, b) => b.indexOf(a) > -1)); +Operators.push(new Operator("notIn", (a, b) => b.indexOf(a) === -1)); -Operators.push(new Operator('contains', (a, b) => a.indexOf(b) > -1, Array.isArray)) -Operators.push(new Operator('doesNotContain', (a, b) => a.indexOf(b) === -1, Array.isArray)) +Operators.push( + new Operator("contains", (a, b) => a.indexOf(b) > -1, Array.isArray), +); +Operators.push( + new Operator("doesNotContain", (a, b) => a.indexOf(b) === -1, Array.isArray), +); -function numberValidator (factValue) { - return Number.parseFloat(factValue).toString() !== 'NaN' +function numberValidator(factValue) { + return Number.parseFloat(factValue).toString() !== "NaN"; } -Operators.push(new Operator('lessThan', (a, b) => a < b, numberValidator)) -Operators.push(new Operator('lessThanInclusive', (a, b) => a <= b, numberValidator)) -Operators.push(new Operator('greaterThan', (a, b) => a > b, numberValidator)) -Operators.push(new Operator('greaterThanInclusive', (a, b) => a >= b, numberValidator)) +Operators.push(new Operator("lessThan", (a, b) => a < b, numberValidator)); +Operators.push( + new Operator("lessThanInclusive", (a, b) => a <= b, numberValidator), +); +Operators.push(new Operator("greaterThan", (a, b) => a > b, numberValidator)); +Operators.push( + new Operator("greaterThanInclusive", (a, b) => a >= b, numberValidator), +); -export default Operators +export default Operators; diff --git a/src/engine.js b/src/engine.js index 4cb8751e..45dffcb6 100644 --- a/src/engine.js +++ b/src/engine.js @@ -1,38 +1,38 @@ -'use strict' +"use strict"; -import Fact from './fact' -import Rule from './rule' -import Almanac from './almanac' -import EventEmitter from 'eventemitter2' -import defaultOperators from './engine-default-operators' -import defaultDecorators from './engine-default-operator-decorators' -import debug from './debug' -import Condition from './condition' -import OperatorMap from './operator-map' +import Fact from "./fact"; +import Rule from "./rule"; +import Almanac from "./almanac"; +import EventEmitter from "eventemitter2"; +import defaultOperators from "./engine-default-operators"; +import defaultDecorators from "./engine-default-operator-decorators"; +import debug from "./debug"; +import Condition from "./condition"; +import OperatorMap from "./operator-map"; -export const READY = 'READY' -export const RUNNING = 'RUNNING' -export const FINISHED = 'FINISHED' +export const READY = "READY"; +export const RUNNING = "RUNNING"; +export const FINISHED = "FINISHED"; class Engine extends EventEmitter { /** * Returns a new Engine instance * @param {Rule[]} rules - array of rules to initialize with */ - constructor (rules = [], options = {}) { - super() - this.rules = [] - this.allowUndefinedFacts = options.allowUndefinedFacts || false - this.allowUndefinedConditions = options.allowUndefinedConditions || false - this.replaceFactsInEventParams = options.replaceFactsInEventParams || false - this.pathResolver = options.pathResolver - this.operators = new OperatorMap() - this.facts = new Map() - this.conditions = new Map() - this.status = READY - rules.map(r => this.addRule(r)) - defaultOperators.map(o => this.addOperator(o)) - defaultDecorators.map(d => this.addOperatorDecorator(d)) + constructor(rules = [], options = {}) { + super(); + this.rules = []; + this.allowUndefinedFacts = options.allowUndefinedFacts || false; + this.allowUndefinedConditions = options.allowUndefinedConditions || false; + this.replaceFactsInEventParams = options.replaceFactsInEventParams || false; + this.pathResolver = options.pathResolver; + this.operators = new OperatorMap(); + this.facts = new Map(); + this.conditions = new Map(); + this.status = READY; + rules.map((r) => this.addRule(r)); + defaultOperators.map((o) => this.addOperator(o)); + defaultDecorators.map((d) => this.addOperatorDecorator(d)); } /** @@ -44,35 +44,41 @@ class Engine extends EventEmitter { * @param {string} properties.event.params - parameters to pass to the event listener * @param {Object} properties.conditions - conditions to evaluate when processing this rule */ - addRule (properties) { - if (!properties) throw new Error('Engine: addRule() requires options') + addRule(properties) { + if (!properties) throw new Error("Engine: addRule() requires options"); - let rule + let rule; if (properties instanceof Rule) { - rule = properties + rule = properties; } else { - if (!Object.prototype.hasOwnProperty.call(properties, 'event')) throw new Error('Engine: addRule() argument requires "event" property') - if (!Object.prototype.hasOwnProperty.call(properties, 'conditions')) throw new Error('Engine: addRule() argument requires "conditions" property') - rule = new Rule(properties) + if (!Object.prototype.hasOwnProperty.call(properties, "event")) + throw new Error('Engine: addRule() argument requires "event" property'); + if (!Object.prototype.hasOwnProperty.call(properties, "conditions")) + throw new Error( + 'Engine: addRule() argument requires "conditions" property', + ); + rule = new Rule(properties); } - rule.setEngine(this) - this.rules.push(rule) - this.prioritizedRules = null - return this + rule.setEngine(this); + this.rules.push(rule); + this.prioritizedRules = null; + return this; } /** * update a rule in the engine * @param {object|Rule} rule - rule definition. Must be a instance of Rule */ - updateRule (rule) { - const ruleIndex = this.rules.findIndex(ruleInEngine => ruleInEngine.name === rule.name) + updateRule(rule) { + const ruleIndex = this.rules.findIndex( + (ruleInEngine) => ruleInEngine.name === rule.name, + ); if (ruleIndex > -1) { - this.rules.splice(ruleIndex, 1) - this.addRule(rule) - this.prioritizedRules = null + this.rules.splice(ruleIndex, 1); + this.addRule(rule); + this.prioritizedRules = null; } else { - throw new Error('Engine: updateRule() rule not found') + throw new Error("Engine: updateRule() rule not found"); } } @@ -80,22 +86,24 @@ class Engine extends EventEmitter { * Remove a rule from the engine * @param {object|Rule|string} rule - rule definition. Must be a instance of Rule */ - removeRule (rule) { - let ruleRemoved = false + removeRule(rule) { + let ruleRemoved = false; if (!(rule instanceof Rule)) { - const filteredRules = this.rules.filter(ruleInEngine => ruleInEngine.name !== rule) - ruleRemoved = filteredRules.length !== this.rules.length - this.rules = filteredRules + const filteredRules = this.rules.filter( + (ruleInEngine) => ruleInEngine.name !== rule, + ); + ruleRemoved = filteredRules.length !== this.rules.length; + this.rules = filteredRules; } else { - const index = this.rules.indexOf(rule) + const index = this.rules.indexOf(rule); if (index > -1) { - ruleRemoved = Boolean(this.rules.splice(index, 1).length) + ruleRemoved = Boolean(this.rules.splice(index, 1).length); } } if (ruleRemoved) { - this.prioritizedRules = null + this.prioritizedRules = null; } - return ruleRemoved + return ruleRemoved; } /** @@ -104,14 +112,22 @@ class Engine extends EventEmitter { * @param {string} name - the name of the condition to be referenced by rules. * @param {object} conditions - the conditions to use when the condition is referenced. */ - setCondition (name, conditions) { - if (!name) throw new Error('Engine: setCondition() requires name') - if (!conditions) throw new Error('Engine: setCondition() requires conditions') - if (!Object.prototype.hasOwnProperty.call(conditions, 'all') && !Object.prototype.hasOwnProperty.call(conditions, 'any') && !Object.prototype.hasOwnProperty.call(conditions, 'not') && !Object.prototype.hasOwnProperty.call(conditions, 'condition')) { - throw new Error('"conditions" root must contain a single instance of "all", "any", "not", or "condition"') + setCondition(name, conditions) { + if (!name) throw new Error("Engine: setCondition() requires name"); + if (!conditions) + throw new Error("Engine: setCondition() requires conditions"); + if ( + !Object.prototype.hasOwnProperty.call(conditions, "all") && + !Object.prototype.hasOwnProperty.call(conditions, "any") && + !Object.prototype.hasOwnProperty.call(conditions, "not") && + !Object.prototype.hasOwnProperty.call(conditions, "condition") + ) { + throw new Error( + '"conditions" root must contain a single instance of "all", "any", "not", or "condition"', + ); } - this.conditions.set(name, new Condition(conditions)) - return this + this.conditions.set(name, new Condition(conditions)); + return this; } /** @@ -119,8 +135,8 @@ class Engine extends EventEmitter { * @param {string} name - the name of the condition to remove. * @returns true if the condition existed, otherwise false */ - removeCondition (name) { - return this.conditions.delete(name) + removeCondition(name) { + return this.conditions.delete(name); } /** @@ -128,16 +144,16 @@ class Engine extends EventEmitter { * @param {string} operatorOrName - operator identifier within the condition; i.e. instead of 'equals', 'greaterThan', etc * @param {function(factValue, jsonValue)} callback - the method to execute when the operator is encountered. */ - addOperator (operatorOrName, cb) { - this.operators.addOperator(operatorOrName, cb) + addOperator(operatorOrName, cb) { + this.operators.addOperator(operatorOrName, cb); } /** * Remove a custom operator definition * @param {string} operatorOrName - operator identifier within the condition; i.e. instead of 'equals', 'greaterThan', etc */ - removeOperator (operatorOrName) { - return this.operators.removeOperator(operatorOrName) + removeOperator(operatorOrName) { + return this.operators.removeOperator(operatorOrName); } /** @@ -145,16 +161,16 @@ class Engine extends EventEmitter { * @param {string} decoratorOrName - decorator identifier within the condition; i.e. instead of 'someFact', 'everyValue', etc * @param {function(factValue, jsonValue, next)} callback - the method to execute when the decorator is encountered. */ - addOperatorDecorator (decoratorOrName, cb) { - this.operators.addOperatorDecorator(decoratorOrName, cb) + addOperatorDecorator(decoratorOrName, cb) { + this.operators.addOperatorDecorator(decoratorOrName, cb); } /** * Remove a custom operator decorator * @param {string} decoratorOrName - decorator identifier within the condition; i.e. instead of 'someFact', 'everyValue', etc */ - removeOperatorDecorator (decoratorOrName) { - return this.operators.removeOperatorDecorator(decoratorOrName) + removeOperatorDecorator(decoratorOrName) { + return this.operators.removeOperatorDecorator(decoratorOrName); } /** @@ -163,33 +179,33 @@ class Engine extends EventEmitter { * @param {function} definitionFunc - function to be called when computing the fact value for a given rule * @param {Object} options - options to initialize the fact with. used when "id" is not a Fact instance */ - addFact (id, valueOrMethod, options) { - let factId = id - let fact + addFact(id, valueOrMethod, options) { + let factId = id; + let fact; if (id instanceof Fact) { - factId = id.id - fact = id + factId = id.id; + fact = id; } else { - fact = new Fact(id, valueOrMethod, options) + fact = new Fact(id, valueOrMethod, options); } - debug('engine::addFact', { id: factId }) - this.facts.set(factId, fact) - return this + debug("engine::addFact", { id: factId }); + this.facts.set(factId, fact); + return this; } /** * Remove a fact definition to the engine. Facts are called by rules as they are evaluated. * @param {object|Fact} id - fact identifier or instance of Fact */ - removeFact (factOrId) { - let factId + removeFact(factOrId) { + let factId; if (!(factOrId instanceof Fact)) { - factId = factOrId + factId = factOrId; } else { - factId = factOrId.id + factId = factOrId.id; } - return this.facts.delete(factId) + return this.facts.delete(factId); } /** @@ -198,19 +214,21 @@ class Engine extends EventEmitter { * Each outer array element represents a single priority(integer). Inner array is * all rules with that priority. */ - prioritizeRules () { + prioritizeRules() { if (!this.prioritizedRules) { const ruleSets = this.rules.reduce((sets, rule) => { - const priority = rule.priority - if (!sets[priority]) sets[priority] = [] - sets[priority].push(rule) - return sets - }, {}) - this.prioritizedRules = Object.keys(ruleSets).sort((a, b) => { - return Number(a) > Number(b) ? -1 : 1 // order highest priority -> lowest - }).map((priority) => ruleSets[priority]) + const priority = rule.priority; + if (!sets[priority]) sets[priority] = []; + sets[priority].push(rule); + return sets; + }, {}); + this.prioritizedRules = Object.keys(ruleSets) + .sort((a, b) => { + return Number(a) > Number(b) ? -1 : 1; // order highest priority -> lowest + }) + .map((priority) => ruleSets[priority]); } - return this.prioritizedRules + return this.prioritizedRules; } /** @@ -219,9 +237,9 @@ class Engine extends EventEmitter { * the same priority may still emit events, even though the engine is in a "finished" state. * @return {Engine} */ - stop () { - this.status = FINISHED - return this + stop() { + this.status = FINISHED; + return this; } /** @@ -229,8 +247,8 @@ class Engine extends EventEmitter { * @param {string} factId - fact identifier * @return {Fact} fact instance, or undefined if no such fact exists */ - getFact (factId) { - return this.facts.get(factId) + getFact(factId) { + return this.facts.get(factId); } /** @@ -238,25 +256,45 @@ class Engine extends EventEmitter { * @param {Rule[]} array of rules to be evaluated * @return {Promise} resolves when all rules in the array have been evaluated */ - evaluateRules (ruleArray, almanac) { - return Promise.all(ruleArray.map((rule) => { - if (this.status !== RUNNING) { - debug('engine::run, skipping remaining rules', { status: this.status }) - return Promise.resolve() - } - return rule.evaluate(almanac).then((ruleResult) => { - debug('engine::run', { ruleResult: ruleResult.result }) - almanac.addResult(ruleResult) - if (ruleResult.result) { - almanac.addEvent(ruleResult.event, 'success') - return this.emitAsync('success', ruleResult.event, almanac, ruleResult) - .then(() => this.emitAsync(ruleResult.event.type, ruleResult.event.params, almanac, ruleResult)) - } else { - almanac.addEvent(ruleResult.event, 'failure') - return this.emitAsync('failure', ruleResult.event, almanac, ruleResult) + evaluateRules(ruleArray, almanac) { + return Promise.all( + ruleArray.map((rule) => { + if (this.status !== RUNNING) { + debug("engine::run, skipping remaining rules", { + status: this.status, + }); + return Promise.resolve(); } - }) - })) + return rule.evaluate(almanac).then((ruleResult) => { + debug("engine::run", { ruleResult: ruleResult.result }); + almanac.addResult(ruleResult); + if (ruleResult.result) { + almanac.addEvent(ruleResult.event, "success"); + return this.emitAsync( + "success", + ruleResult.event, + almanac, + ruleResult, + ).then(() => + this.emitAsync( + ruleResult.event.type, + ruleResult.event.params, + almanac, + ruleResult, + ), + ); + } else { + almanac.addEvent(ruleResult.event, "failure"); + return this.emitAsync( + "failure", + ruleResult.event, + almanac, + ruleResult, + ); + } + }); + }), + ); } /** @@ -265,60 +303,73 @@ class Engine extends EventEmitter { * @param {Object} runOptions - run options * @return {Promise} resolves when the engine has completed running */ - run (runtimeFacts = {}, runOptions = {}) { - debug('engine::run started') - this.status = RUNNING + run(runtimeFacts = {}, runOptions = {}) { + debug("engine::run started"); + this.status = RUNNING; - const almanac = runOptions.almanac || new Almanac({ - allowUndefinedFacts: this.allowUndefinedFacts, - pathResolver: this.pathResolver - }) + const almanac = + runOptions.almanac || + new Almanac({ + allowUndefinedFacts: this.allowUndefinedFacts, + pathResolver: this.pathResolver, + }); - this.facts.forEach(fact => { - almanac.addFact(fact) - }) + this.facts.forEach((fact) => { + almanac.addFact(fact); + }); for (const factId in runtimeFacts) { - let fact + let fact; if (runtimeFacts[factId] instanceof Fact) { - fact = runtimeFacts[factId] + fact = runtimeFacts[factId]; } else { - fact = new Fact(factId, runtimeFacts[factId]) + fact = new Fact(factId, runtimeFacts[factId]); } - almanac.addFact(fact) - debug('engine::run initialized runtime fact', { id: fact.id, value: fact.value, type: typeof fact.value }) + almanac.addFact(fact); + debug("engine::run initialized runtime fact", { + id: fact.id, + value: fact.value, + type: typeof fact.value, + }); } - const orderedSets = this.prioritizeRules() - let cursor = Promise.resolve() + const orderedSets = this.prioritizeRules(); + let cursor = Promise.resolve(); // for each rule set, evaluate in parallel, // before proceeding to the next priority set. return new Promise((resolve, reject) => { orderedSets.map((set) => { - cursor = cursor.then(() => { - return this.evaluateRules(set, almanac) - }).catch(reject) - return cursor - }) - cursor.then(() => { - this.status = FINISHED - debug('engine::run completed') - const ruleResults = almanac.getResults() - const { results, failureResults } = ruleResults.reduce((hash, ruleResult) => { - const group = ruleResult.result ? 'results' : 'failureResults' - hash[group].push(ruleResult) - return hash - }, { results: [], failureResults: [] }) + cursor = cursor + .then(() => { + return this.evaluateRules(set, almanac); + }) + .catch(reject); + return cursor; + }); + cursor + .then(() => { + this.status = FINISHED; + debug("engine::run completed"); + const ruleResults = almanac.getResults(); + const { results, failureResults } = ruleResults.reduce( + (hash, ruleResult) => { + const group = ruleResult.result ? "results" : "failureResults"; + hash[group].push(ruleResult); + return hash; + }, + { results: [], failureResults: [] }, + ); - resolve({ - almanac, - results, - failureResults, - events: almanac.getEvents('success'), - failureEvents: almanac.getEvents('failure') + resolve({ + almanac, + results, + failureResults, + events: almanac.getEvents("success"), + failureEvents: almanac.getEvents("failure"), + }); }) - }).catch(reject) - }) + .catch(reject); + }); } } -export default Engine +export default Engine; diff --git a/src/errors.js b/src/errors.js index 5f44df36..df345f03 100644 --- a/src/errors.js +++ b/src/errors.js @@ -1,8 +1,8 @@ -'use strict' +"use strict"; export class UndefinedFactError extends Error { - constructor (...props) { - super(...props) - this.code = 'UNDEFINED_FACT' + constructor(...props) { + super(...props); + this.code = "UNDEFINED_FACT"; } } diff --git a/src/fact.js b/src/fact.js index 313ba3d8..c1e72aeb 100644 --- a/src/fact.js +++ b/src/fact.js @@ -1,6 +1,6 @@ -'use strict' +"use strict"; -import hash from 'hash-it' +import hash from "hash-it"; class Fact { /** @@ -11,34 +11,34 @@ class Fact { * @param {primitive|function} valueOrMethod - constant primitive, or method to call when computing the fact's value * @return {Fact} */ - constructor (id, valueOrMethod, options) { - this.id = id - const defaultOptions = { cache: true } - if (typeof options === 'undefined') { - options = defaultOptions + constructor(id, valueOrMethod, options) { + this.id = id; + const defaultOptions = { cache: true }; + if (typeof options === "undefined") { + options = defaultOptions; } - if (typeof valueOrMethod !== 'function') { - this.value = valueOrMethod - this.type = this.constructor.CONSTANT + if (typeof valueOrMethod !== "function") { + this.value = valueOrMethod; + this.type = this.constructor.CONSTANT; } else { - this.calculationMethod = valueOrMethod - this.type = this.constructor.DYNAMIC + this.calculationMethod = valueOrMethod; + this.type = this.constructor.DYNAMIC; } - if (!this.id) throw new Error('factId required') + if (!this.id) throw new Error("factId required"); - this.priority = parseInt(options.priority || 1, 10) - this.options = Object.assign({}, defaultOptions, options) - this.cacheKeyMethod = this.defaultCacheKeys - return this + this.priority = parseInt(options.priority || 1, 10); + this.options = Object.assign({}, defaultOptions, options); + this.cacheKeyMethod = this.defaultCacheKeys; + return this; } - isConstant () { - return this.type === this.constructor.CONSTANT + isConstant() { + return this.type === this.constructor.CONSTANT; } - isDynamic () { - return this.type === this.constructor.DYNAMIC + isDynamic() { + return this.type === this.constructor.DYNAMIC; } /** @@ -47,12 +47,12 @@ class Fact { * @param {Almanac} almanac * @return {any} calculation method results */ - calculate (params, almanac) { + calculate(params, almanac) { // if constant fact w/set value, return immediately - if (Object.prototype.hasOwnProperty.call(this, 'value')) { - return this.value + if (Object.prototype.hasOwnProperty.call(this, "value")) { + return this.value; } - return this.calculationMethod(params, almanac) + return this.calculationMethod(params, almanac); } /** @@ -60,8 +60,8 @@ class Fact { * @param {object} obj - properties to generate a hash key from * @return {string} MD5 string based on the hash'd object */ - static hashFromObject (obj) { - return hash(obj) + static hashFromObject(obj) { + return hash(obj); } /** @@ -72,8 +72,8 @@ class Fact { * @param {object} params - parameters passed to fact calcution method * @return {object} id + params */ - defaultCacheKeys (id, params) { - return { params, id } + defaultCacheKeys(id, params) { + return { params, id }; } /** @@ -82,16 +82,16 @@ class Fact { * @param {object} params - parameters that would be passed to the computation method * @return {string} cache key */ - getCacheKey (params) { + getCacheKey(params) { if (this.options.cache === true) { - const cacheProperties = this.cacheKeyMethod(this.id, params) - const hash = Fact.hashFromObject(cacheProperties) - return hash + const cacheProperties = this.cacheKeyMethod(this.id, params); + const hash = Fact.hashFromObject(cacheProperties); + return hash; } } } -Fact.CONSTANT = 'CONSTANT' -Fact.DYNAMIC = 'DYNAMIC' +Fact.CONSTANT = "CONSTANT"; +Fact.DYNAMIC = "DYNAMIC"; -export default Fact +export default Fact; diff --git a/src/index.js b/src/index.js index 0fdd73f9..eb82cd8a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,3 @@ -'use strict' +"use strict"; -module.exports = require('./json-rules-engine') +module.exports = require("./json-rules-engine"); diff --git a/src/json-rules-engine.js b/src/json-rules-engine.js index bed371d5..68962583 100644 --- a/src/json-rules-engine.js +++ b/src/json-rules-engine.js @@ -1,11 +1,11 @@ -import Engine from './engine' -import Fact from './fact' -import Rule from './rule' -import Operator from './operator' -import Almanac from './almanac' -import OperatorDecorator from './operator-decorator' +import Engine from "./engine"; +import Fact from "./fact"; +import Rule from "./rule"; +import Operator from "./operator"; +import Almanac from "./almanac"; +import OperatorDecorator from "./operator-decorator"; -export { Fact, Rule, Operator, Engine, Almanac, OperatorDecorator } +export { Fact, Rule, Operator, Engine, Almanac, OperatorDecorator }; export default function (rules, options) { - return new Engine(rules, options) + return new Engine(rules, options); } diff --git a/src/operator-decorator.js b/src/operator-decorator.js index b9196222..27cd98d9 100644 --- a/src/operator-decorator.js +++ b/src/operator-decorator.js @@ -1,6 +1,6 @@ -'use strict' +"use strict"; -import Operator from './operator' +import Operator from "./operator"; export default class OperatorDecorator { /** @@ -10,13 +10,13 @@ export default class OperatorDecorator { * @param {function} [factValueValidator] - optional validator for asserting the data type of the fact * @returns {OperatorDecorator} - instance */ - constructor (name, cb, factValueValidator) { - this.name = String(name) - if (!name) throw new Error('Missing decorator name') - if (typeof cb !== 'function') throw new Error('Missing decorator callback') - this.cb = cb - this.factValueValidator = factValueValidator - if (!this.factValueValidator) this.factValueValidator = () => true + constructor(name, cb, factValueValidator) { + this.name = String(name); + if (!name) throw new Error("Missing decorator name"); + if (typeof cb !== "function") throw new Error("Missing decorator callback"); + this.cb = cb; + this.factValueValidator = factValueValidator; + if (!this.factValueValidator) this.factValueValidator = () => true; } /** @@ -24,14 +24,14 @@ export default class OperatorDecorator { * @param {Operator} operator - fact result * @returns {Operator} - whether the values pass the operator test */ - decorate (operator) { - const next = operator.evaluate.bind(operator) + decorate(operator) { + const next = operator.evaluate.bind(operator); return new Operator( - `${this.name}:${operator.name}`, - (factValue, jsonValue) => { - return this.cb(factValue, jsonValue, next) - }, - this.factValueValidator - ) + `${this.name}:${operator.name}`, + (factValue, jsonValue) => { + return this.cb(factValue, jsonValue, next); + }, + this.factValueValidator, + ); } } diff --git a/src/operator-map.js b/src/operator-map.js index 741e302c..8bc2a3e7 100644 --- a/src/operator-map.js +++ b/src/operator-map.js @@ -1,96 +1,96 @@ -'use strict' +"use strict"; -import Operator from './operator' -import OperatorDecorator from './operator-decorator' -import debug from './debug' +import Operator from "./operator"; +import OperatorDecorator from "./operator-decorator"; +import debug from "./debug"; export default class OperatorMap { - constructor () { - this.operators = new Map() - this.decorators = new Map() + constructor() { + this.operators = new Map(); + this.decorators = new Map(); } /** - * Add a custom operator definition - * @param {string} operatorOrName - operator identifier within the condition; i.e. instead of 'equals', 'greaterThan', etc - * @param {function(factValue, jsonValue)} callback - the method to execute when the operator is encountered. - */ - addOperator (operatorOrName, cb) { - let operator + * Add a custom operator definition + * @param {string} operatorOrName - operator identifier within the condition; i.e. instead of 'equals', 'greaterThan', etc + * @param {function(factValue, jsonValue)} callback - the method to execute when the operator is encountered. + */ + addOperator(operatorOrName, cb) { + let operator; if (operatorOrName instanceof Operator) { - operator = operatorOrName + operator = operatorOrName; } else { - operator = new Operator(operatorOrName, cb) + operator = new Operator(operatorOrName, cb); } - debug('operatorMap::addOperator', { name: operator.name }) - this.operators.set(operator.name, operator) + debug("operatorMap::addOperator", { name: operator.name }); + this.operators.set(operator.name, operator); } /** - * Remove a custom operator definition - * @param {string} operatorOrName - operator identifier within the condition; i.e. instead of 'equals', 'greaterThan', etc - * @param {function(factValue, jsonValue)} callback - the method to execute when the operator is encountered. - */ - removeOperator (operatorOrName) { - let operatorName + * Remove a custom operator definition + * @param {string} operatorOrName - operator identifier within the condition; i.e. instead of 'equals', 'greaterThan', etc + * @param {function(factValue, jsonValue)} callback - the method to execute when the operator is encountered. + */ + removeOperator(operatorOrName) { + let operatorName; if (operatorOrName instanceof Operator) { - operatorName = operatorOrName.name + operatorName = operatorOrName.name; } else { - operatorName = operatorOrName + operatorName = operatorOrName; } // Delete all the operators that end in :operatorName these // were decorated on-the-fly leveraging this operator - const suffix = ':' + operatorName - const operatorNames = Array.from(this.operators.keys()) + const suffix = ":" + operatorName; + const operatorNames = Array.from(this.operators.keys()); for (let i = 0; i < operatorNames.length; i++) { if (operatorNames[i].endsWith(suffix)) { - this.operators.delete(operatorNames[i]) + this.operators.delete(operatorNames[i]); } } - return this.operators.delete(operatorName) + return this.operators.delete(operatorName); } /** - * Add a custom operator decorator - * @param {string} decoratorOrName - decorator identifier within the condition; i.e. instead of 'everyFact', 'someValue', etc - * @param {function(factValue, jsonValue, next)} callback - the method to execute when the decorator is encountered. - */ - addOperatorDecorator (decoratorOrName, cb) { - let decorator + * Add a custom operator decorator + * @param {string} decoratorOrName - decorator identifier within the condition; i.e. instead of 'everyFact', 'someValue', etc + * @param {function(factValue, jsonValue, next)} callback - the method to execute when the decorator is encountered. + */ + addOperatorDecorator(decoratorOrName, cb) { + let decorator; if (decoratorOrName instanceof OperatorDecorator) { - decorator = decoratorOrName + decorator = decoratorOrName; } else { - decorator = new OperatorDecorator(decoratorOrName, cb) + decorator = new OperatorDecorator(decoratorOrName, cb); } - debug('operatorMap::addOperatorDecorator', { name: decorator.name }) - this.decorators.set(decorator.name, decorator) + debug("operatorMap::addOperatorDecorator", { name: decorator.name }); + this.decorators.set(decorator.name, decorator); } /** - * Remove a custom operator decorator - * @param {string} decoratorOrName - decorator identifier within the condition; i.e. instead of 'everyFact', 'someValue', etc - */ - removeOperatorDecorator (decoratorOrName) { - let decoratorName + * Remove a custom operator decorator + * @param {string} decoratorOrName - decorator identifier within the condition; i.e. instead of 'everyFact', 'someValue', etc + */ + removeOperatorDecorator(decoratorOrName) { + let decoratorName; if (decoratorOrName instanceof OperatorDecorator) { - decoratorName = decoratorOrName.name + decoratorName = decoratorOrName.name; } else { - decoratorName = decoratorOrName + decoratorName = decoratorOrName; } // Delete all the operators that include decoratorName: these // were decorated on-the-fly leveraging this decorator - const prefix = decoratorName + ':' - const operatorNames = Array.from(this.operators.keys()) + const prefix = decoratorName + ":"; + const operatorNames = Array.from(this.operators.keys()); for (let i = 0; i < operatorNames.length; i++) { if (operatorNames[i].includes(prefix)) { - this.operators.delete(operatorNames[i]) + this.operators.delete(operatorNames[i]); } } - return this.decorators.delete(decoratorName) + return this.decorators.delete(decoratorName); } /** @@ -98,40 +98,40 @@ export default class OperatorMap { * @param {string} name - the name of the operator including any decorators * @returns an operator or null */ - get (name) { - const decorators = [] - let opName = name + get(name) { + const decorators = []; + let opName = name; // while we don't already have this operator while (!this.operators.has(opName)) { // try splitting on the decorator symbol (:) - const firstDecoratorIndex = opName.indexOf(':') + const firstDecoratorIndex = opName.indexOf(":"); if (firstDecoratorIndex > 0) { // if there is a decorator, and it's a valid decorator - const decoratorName = opName.slice(0, firstDecoratorIndex) - const decorator = this.decorators.get(decoratorName) + const decoratorName = opName.slice(0, firstDecoratorIndex); + const decorator = this.decorators.get(decoratorName); if (!decorator) { - debug('operatorMap::get invalid decorator', { name: decoratorName }) - return null + debug("operatorMap::get invalid decorator", { name: decoratorName }); + return null; } // we're going to apply this later, use unshift since we'll apply in reverse order - decorators.unshift(decorator) + decorators.unshift(decorator); // continue looking for a known operator with the rest of the name - opName = opName.slice(firstDecoratorIndex + 1) + opName = opName.slice(firstDecoratorIndex + 1); } else { - debug('operatorMap::get no operator', { name: opName }) - return null + debug("operatorMap::get no operator", { name: opName }); + return null; } } - let op = this.operators.get(opName) + let op = this.operators.get(opName); // apply all the decorators for (let i = 0; i < decorators.length; i++) { - op = decorators[i].decorate(op) + op = decorators[i].decorate(op); // create an entry for the decorated operation so we don't need // to do this again - this.operators.set(op.name, op) + this.operators.set(op.name, op); } // return the operation - return op + return op; } } diff --git a/src/operator.js b/src/operator.js index e342ced5..b01281a9 100644 --- a/src/operator.js +++ b/src/operator.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; export default class Operator { /** @@ -8,13 +8,13 @@ export default class Operator { * @param {function} [factValueValidator] - optional validator for asserting the data type of the fact * @returns {Operator} - instance */ - constructor (name, cb, factValueValidator) { - this.name = String(name) - if (!name) throw new Error('Missing operator name') - if (typeof cb !== 'function') throw new Error('Missing operator callback') - this.cb = cb - this.factValueValidator = factValueValidator - if (!this.factValueValidator) this.factValueValidator = () => true + constructor(name, cb, factValueValidator) { + this.name = String(name); + if (!name) throw new Error("Missing operator name"); + if (typeof cb !== "function") throw new Error("Missing operator callback"); + this.cb = cb; + this.factValueValidator = factValueValidator; + if (!this.factValueValidator) this.factValueValidator = () => true; } /** @@ -23,7 +23,7 @@ export default class Operator { * @param {mixed} jsonValue - "value" property of the condition * @returns {Boolean} - whether the values pass the operator test */ - evaluate (factValue, jsonValue) { - return this.factValueValidator(factValue) && this.cb(factValue, jsonValue) + evaluate(factValue, jsonValue) { + return this.factValueValidator(factValue) && this.cb(factValue, jsonValue); } } diff --git a/src/rule-result.js b/src/rule-result.js index 09350c7e..c67b8448 100644 --- a/src/rule-result.js +++ b/src/rule-result.js @@ -1,48 +1,48 @@ -'use strict' +"use strict"; -import deepClone from 'clone' +import deepClone from "clone"; export default class RuleResult { - constructor (conditions, event, priority, name) { - this.conditions = deepClone(conditions) - this.event = deepClone(event) - this.priority = deepClone(priority) - this.name = deepClone(name) - this.result = null + constructor(conditions, event, priority, name) { + this.conditions = deepClone(conditions); + this.event = deepClone(event); + this.priority = deepClone(priority); + this.name = deepClone(name); + this.result = null; } - setResult (result) { - this.result = result + setResult(result) { + this.result = result; } - resolveEventParams (almanac) { - if (this.event.params !== null && typeof this.event.params === 'object') { - const updates = [] + resolveEventParams(almanac) { + if (this.event.params !== null && typeof this.event.params === "object") { + const updates = []; for (const key in this.event.params) { if (Object.prototype.hasOwnProperty.call(this.event.params, key)) { updates.push( almanac .getValue(this.event.params[key]) - .then((val) => (this.event.params[key] = val)) - ) + .then((val) => (this.event.params[key] = val)), + ); } } - return Promise.all(updates) + return Promise.all(updates); } - return Promise.resolve() + return Promise.resolve(); } - toJSON (stringify = true) { + toJSON(stringify = true) { const props = { conditions: this.conditions.toJSON(false), event: this.event, priority: this.priority, name: this.name, - result: this.result - } + result: this.result, + }; if (stringify) { - return JSON.stringify(props) + return JSON.stringify(props); } - return props + return props; } } diff --git a/src/rule.js b/src/rule.js index 39e5a327..4fc5e97d 100644 --- a/src/rule.js +++ b/src/rule.js @@ -1,10 +1,10 @@ -'use strict' +"use strict"; -import Condition from './condition' -import RuleResult from './rule-result' -import debug from './debug' -import deepClone from 'clone' -import EventEmitter from 'eventemitter2' +import Condition from "./condition"; +import RuleResult from "./rule-result"; +import debug from "./debug"; +import deepClone from "clone"; +import EventEmitter from "eventemitter2"; class Rule extends EventEmitter { /** @@ -18,71 +18,71 @@ class Rule extends EventEmitter { * @param {any} options.name - identifier for a particular rule, particularly valuable in RuleResult output * @return {Rule} instance */ - constructor (options) { - super() - if (typeof options === 'string') { - options = JSON.parse(options) + constructor(options) { + super(); + if (typeof options === "string") { + options = JSON.parse(options); } if (options && options.conditions) { - this.setConditions(options.conditions) + this.setConditions(options.conditions); } if (options && options.onSuccess) { - this.on('success', options.onSuccess) + this.on("success", options.onSuccess); } if (options && options.onFailure) { - this.on('failure', options.onFailure) + this.on("failure", options.onFailure); } if (options && (options.name || options.name === 0)) { - this.setName(options.name) + this.setName(options.name); } - const priority = (options && options.priority) || 1 - this.setPriority(priority) + const priority = (options && options.priority) || 1; + this.setPriority(priority); - const event = (options && options.event) || { type: 'unknown' } - this.setEvent(event) + const event = (options && options.event) || { type: "unknown" }; + this.setEvent(event); } /** * Sets the priority of the rule * @param {integer} priority (>=1) - increasing the priority causes the rule to be run prior to other rules */ - setPriority (priority) { - priority = parseInt(priority, 10) - if (priority <= 0) throw new Error('Priority must be greater than zero') - this.priority = priority - return this + setPriority(priority) { + priority = parseInt(priority, 10); + if (priority <= 0) throw new Error("Priority must be greater than zero"); + this.priority = priority; + return this; } /** * Sets the name of the rule * @param {any} name - any truthy input and zero is allowed */ - setName (name) { + setName(name) { if (!name && name !== 0) { - throw new Error('Rule "name" must be defined') + throw new Error('Rule "name" must be defined'); } - this.name = name - return this + this.name = name; + return this; } /** * Sets the conditions to run when evaluating the rule. * @param {object} conditions - conditions, root element must be a boolean operator */ - setConditions (conditions) { + setConditions(conditions) { if ( - !Object.prototype.hasOwnProperty.call(conditions, 'all') && - !Object.prototype.hasOwnProperty.call(conditions, 'any') && - !Object.prototype.hasOwnProperty.call(conditions, 'not') && - !Object.prototype.hasOwnProperty.call(conditions, 'condition') + !Object.prototype.hasOwnProperty.call(conditions, "all") && + !Object.prototype.hasOwnProperty.call(conditions, "any") && + !Object.prototype.hasOwnProperty.call(conditions, "not") && + !Object.prototype.hasOwnProperty.call(conditions, "condition") ) { throw new Error( - '"conditions" root must contain a single instance of "all", "any", "not", or "condition"' - ) + '"conditions" root must contain a single instance of "all", "any", "not", or "condition"', + ); } - this.conditions = new Condition(conditions) - return this + this.conditions = new Condition(conditions); + return this; } /** @@ -91,50 +91,50 @@ class Rule extends EventEmitter { * @param {string} event.type - event name to emit on * @param {string} event.params - parameters to emit as the argument of the event emission */ - setEvent (event) { - if (!event) throw new Error('Rule: setEvent() requires event object') - if (!Object.prototype.hasOwnProperty.call(event, 'type')) { + setEvent(event) { + if (!event) throw new Error("Rule: setEvent() requires event object"); + if (!Object.prototype.hasOwnProperty.call(event, "type")) { throw new Error( - 'Rule: setEvent() requires event object with "type" property' - ) + 'Rule: setEvent() requires event object with "type" property', + ); } this.ruleEvent = { - type: event.type - } - if (event.params) this.ruleEvent.params = event.params - return this + type: event.type, + }; + if (event.params) this.ruleEvent.params = event.params; + return this; } /** * returns the event object * @returns {Object} event */ - getEvent () { - return this.ruleEvent + getEvent() { + return this.ruleEvent; } /** * returns the priority * @returns {Number} priority */ - getPriority () { - return this.priority + getPriority() { + return this.priority; } /** * returns the event object * @returns {Object} event */ - getConditions () { - return this.conditions + getConditions() { + return this.conditions; } /** * returns the engine object * @returns {Object} engine */ - getEngine () { - return this.engine + getEngine() { + return this.engine; } /** @@ -142,22 +142,22 @@ class Rule extends EventEmitter { * @param {object} engine * @returns {Rule} */ - setEngine (engine) { - this.engine = engine - return this + setEngine(engine) { + this.engine = engine; + return this; } - toJSON (stringify = true) { + toJSON(stringify = true) { const props = { conditions: this.conditions.toJSON(false), priority: this.priority, event: this.ruleEvent, - name: this.name - } + name: this.name, + }; if (stringify) { - return JSON.stringify(props) + return JSON.stringify(props); } - return props + return props; } /** @@ -168,24 +168,24 @@ class Rule extends EventEmitter { * Each outer array element represents a single priority(integer). Inner array is * all conditions with that priority. */ - prioritizeConditions (conditions) { + prioritizeConditions(conditions) { const factSets = conditions.reduce((sets, condition) => { // if a priority has been set on this specific condition, honor that first // otherwise, use the fact's priority - let priority = condition.priority + let priority = condition.priority; if (!priority) { - const fact = this.engine.getFact(condition.fact) - priority = (fact && fact.priority) || 1 + const fact = this.engine.getFact(condition.fact); + priority = (fact && fact.priority) || 1; } - if (!sets[priority]) sets[priority] = [] - sets[priority].push(condition) - return sets - }, {}) + if (!sets[priority]) sets[priority] = []; + sets[priority].push(condition); + return sets; + }, {}); return Object.keys(factSets) .sort((a, b) => { - return Number(a) > Number(b) ? -1 : 1 // order highest priority -> lowest + return Number(a) > Number(b) ? -1 : 1; // order highest priority -> lowest }) - .map((priority) => factSets[priority]) + .map((priority) => factSets[priority]); } /** @@ -193,13 +193,13 @@ class Rule extends EventEmitter { * All evaluation is done within the context of an almanac * @return {Promise(RuleResult)} rule evaluation result */ - evaluate (almanac) { + evaluate(almanac) { const ruleResult = new RuleResult( this.conditions, this.ruleEvent, this.priority, - this.name - ) + this.name, + ); /** * Evaluates the rule conditions @@ -208,34 +208,34 @@ class Rule extends EventEmitter { */ const evaluateCondition = (condition) => { if (condition.isConditionReference()) { - return realize(condition) + return realize(condition); } else if (condition.isBooleanOperator()) { - const subConditions = condition[condition.operator] - let comparisonPromise - if (condition.operator === 'all') { - comparisonPromise = all(subConditions) - } else if (condition.operator === 'any') { - comparisonPromise = any(subConditions) + const subConditions = condition[condition.operator]; + let comparisonPromise; + if (condition.operator === "all") { + comparisonPromise = all(subConditions); + } else if (condition.operator === "any") { + comparisonPromise = any(subConditions); } else { - comparisonPromise = not(subConditions) + comparisonPromise = not(subConditions); } // for booleans, rule passing is determined by the all/any/not result return comparisonPromise.then((comparisonValue) => { - const passes = comparisonValue === true - condition.result = passes - return passes - }) + const passes = comparisonValue === true; + condition.result = passes; + return passes; + }); } else { return condition .evaluate(almanac, this.engine.operators) .then((evaluationResult) => { - const passes = evaluationResult.result - condition.factResult = evaluationResult.leftHandSideValue - condition.result = passes - return passes - }) + const passes = evaluationResult.result; + condition.factResult = evaluationResult.leftHandSideValue; + condition.result = passes; + return passes; + }); } - } + }; /** * Evalutes an array of conditions, using an 'every' or 'some' array operation @@ -244,15 +244,15 @@ class Rule extends EventEmitter { * @return {Promise(boolean)} whether conditions evaluated truthy or falsey based on condition evaluation + method */ const evaluateConditions = (conditions, method) => { - if (!Array.isArray(conditions)) conditions = [conditions] + if (!Array.isArray(conditions)) conditions = [conditions]; return Promise.all( - conditions.map((condition) => evaluateCondition(condition)) + conditions.map((condition) => evaluateCondition(condition)), ).then((conditionResults) => { - debug('rule::evaluateConditions', { results: conditionResults }) - return method.call(conditionResults, (result) => result === true) - }) - } + debug("rule::evaluateConditions", { results: conditionResults }); + return method.call(conditionResults, (result) => result === true); + }); + }; /** * Evaluates a set of conditions based on an 'all', 'any', or 'not' operator. @@ -266,28 +266,28 @@ class Rule extends EventEmitter { */ const prioritizeAndRun = (conditions, operator) => { if (conditions.length === 0) { - return Promise.resolve(true) + return Promise.resolve(true); } if (conditions.length === 1) { // no prioritizing is necessary, just evaluate the single condition // 'all' and 'any' will give the same results with a single condition so no method is necessary // this also covers the 'not' case which should only ever have a single condition - return evaluateCondition(conditions[0]) + return evaluateCondition(conditions[0]); } - const orderedSets = this.prioritizeConditions(conditions) - let cursor = Promise.resolve(operator === 'all') + const orderedSets = this.prioritizeConditions(conditions); + let cursor = Promise.resolve(operator === "all"); // use for() loop over Array.forEach to support IE8 without polyfill for (let i = 0; i < orderedSets.length; i++) { - const set = orderedSets[i] + const set = orderedSets[i]; cursor = cursor.then((setResult) => { // rely on the short-circuiting behavior of || and && to avoid evaluating subsequent conditions - return operator === 'any' - ? (setResult || evaluateConditions(set, Array.prototype.some)) - : (setResult && evaluateConditions(set, Array.prototype.every)) - }) + return operator === "any" + ? setResult || evaluateConditions(set, Array.prototype.some) + : setResult && evaluateConditions(set, Array.prototype.every); + }); } - return cursor - } + return cursor; + }; /** * Runs an 'any' boolean operator on an array of conditions @@ -295,8 +295,8 @@ class Rule extends EventEmitter { * @return {Promise(boolean)} condition evaluation result */ const any = (conditions) => { - return prioritizeAndRun(conditions, 'any') - } + return prioritizeAndRun(conditions, "any"); + }; /** * Runs an 'all' boolean operator on an array of conditions @@ -304,8 +304,8 @@ class Rule extends EventEmitter { * @return {Promise(boolean)} condition evaluation result */ const all = (conditions) => { - return prioritizeAndRun(conditions, 'all') - } + return prioritizeAndRun(conditions, "all"); + }; /** * Runs a 'not' boolean operator on a single condition @@ -313,8 +313,8 @@ class Rule extends EventEmitter { * @return {Promise(boolean)} condition evaluation result */ const not = (condition) => { - return prioritizeAndRun([condition], 'not').then((result) => !result) - } + return prioritizeAndRun([condition], "not").then((result) => !result); + }; /** * Dereferences the condition reference and then evaluates it. @@ -322,59 +322,63 @@ class Rule extends EventEmitter { * @returns {Promise(boolean)} condition evaluation result */ const realize = (conditionReference) => { - const condition = this.engine.conditions.get(conditionReference.condition) + const condition = this.engine.conditions.get( + conditionReference.condition, + ); if (!condition) { if (this.engine.allowUndefinedConditions) { // undefined conditions always fail - conditionReference.result = false - return Promise.resolve(false) + conditionReference.result = false; + return Promise.resolve(false); } else { throw new Error( - `No condition ${conditionReference.condition} exists` - ) + `No condition ${conditionReference.condition} exists`, + ); } } else { // project the referenced condition onto reference object and evaluate it. - delete conditionReference.condition - Object.assign(conditionReference, deepClone(condition)) - return evaluateCondition(conditionReference) + delete conditionReference.condition; + Object.assign(conditionReference, deepClone(condition)); + return evaluateCondition(conditionReference); } - } + }; /** * Emits based on rule evaluation result, and decorates ruleResult with 'result' property * @param {RuleResult} ruleResult */ const processResult = (result) => { - ruleResult.setResult(result) - let processEvent = Promise.resolve() + ruleResult.setResult(result); + let processEvent = Promise.resolve(); if (this.engine.replaceFactsInEventParams) { - processEvent = ruleResult.resolveEventParams(almanac) + processEvent = ruleResult.resolveEventParams(almanac); } - const event = result ? 'success' : 'failure' - return processEvent.then(() => this.emitAsync(event, ruleResult.event, almanac, ruleResult)).then( - () => ruleResult - ) - } + const event = result ? "success" : "failure"; + return processEvent + .then(() => + this.emitAsync(event, ruleResult.event, almanac, ruleResult), + ) + .then(() => ruleResult); + }; if (ruleResult.conditions.any) { return any(ruleResult.conditions.any).then((result) => - processResult(result) - ) + processResult(result), + ); } else if (ruleResult.conditions.all) { return all(ruleResult.conditions.all).then((result) => - processResult(result) - ) + processResult(result), + ); } else if (ruleResult.conditions.not) { return not(ruleResult.conditions.not).then((result) => - processResult(result) - ) + processResult(result), + ); } else { - return realize( - ruleResult.conditions - ).then((result) => processResult(result)) + return realize(ruleResult.conditions).then((result) => + processResult(result), + ); } } } -export default Rule +export default Rule; diff --git a/test/acceptance/acceptance.js b/test/acceptance/acceptance.js index fb68cc77..e2c5efbb 100644 --- a/test/acceptance/acceptance.js +++ b/test/acceptance/acceptance.js @@ -1,268 +1,274 @@ -'use strict' +"use strict"; -import sinon from 'sinon' -import { expect } from 'chai' -import { Engine } from '../../src/index' +import sinon from "sinon"; +import { expect } from "chai"; +import { Engine } from "../../src/index"; /** * acceptance tests are intended to use features that, when used in combination, * could cause integration bugs not caught by the rest of the test suite */ -describe('Acceptance', () => { - let sandbox +describe("Acceptance", () => { + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); afterEach(() => { - sandbox.restore() - }) - const factParam = 1 + sandbox.restore(); + }); + const factParam = 1; const event1 = { - type: 'event-1', + type: "event-1", params: { - eventParam: 1 - } - } + eventParam: 1, + }, + }; const event2 = { - type: 'event-2' - } + type: "event-2", + }; const expectedFirstRuleResult = { - all: [{ - fact: 'high-priority', - params: { - factParam + all: [ + { + fact: "high-priority", + params: { + factParam, + }, + operator: "contains", + path: "$.values", + value: 2, + factResult: [2], + result: true, + }, + { + fact: "low-priority", + operator: "in", + value: [2], + factResult: 2, + result: true, }, - operator: 'contains', - path: '$.values', - value: 2, - factResult: [2], - result: true - }, - { - fact: 'low-priority', - operator: 'in', - value: [2], - factResult: 2, - result: true - } ], - operator: 'all', - priority: 1 - } - let successSpy - let failureSpy - let highPrioritySpy - let lowPrioritySpy + operator: "all", + priority: 1, + }; + let successSpy; + let failureSpy; + let highPrioritySpy; + let lowPrioritySpy; - function delay (value) { - return new Promise(resolve => setTimeout(() => resolve(value), 5)) + function delay(value) { + return new Promise((resolve) => setTimeout(() => resolve(value), 5)); } - function setup (options = {}) { - const engine = new Engine() - highPrioritySpy = sandbox.spy() - lowPrioritySpy = sandbox.spy() + function setup(options = {}) { + const engine = new Engine(); + highPrioritySpy = sandbox.spy(); + lowPrioritySpy = sandbox.spy(); engine.addRule({ - name: 'first', + name: "first", priority: 10, conditions: { - all: [{ - fact: 'high-priority', - params: { - factParam + all: [ + { + fact: "high-priority", + params: { + factParam, + }, + operator: "contains", + path: "$.values", + value: options.highPriorityValue, + }, + { + fact: "low-priority", + operator: "in", + value: options.lowPriorityValue, }, - operator: 'contains', - path: '$.values', - value: options.highPriorityValue - }, { - fact: 'low-priority', - operator: 'in', - value: options.lowPriorityValue - }] + ], }, event: event1, onSuccess: async (event, almanac, ruleResults) => { - expect(ruleResults.name).to.equal('first') - expect(ruleResults.event).to.deep.equal(event1) - expect(ruleResults.priority).to.equal(10) - expect(ruleResults.conditions).to.deep.equal(expectedFirstRuleResult) + expect(ruleResults.name).to.equal("first"); + expect(ruleResults.event).to.deep.equal(event1); + expect(ruleResults.priority).to.equal(10); + expect(ruleResults.conditions).to.deep.equal(expectedFirstRuleResult); - return delay(almanac.addRuntimeFact('rule-created-fact', { array: options.highPriorityValue })) - } - }) + return delay( + almanac.addRuntimeFact("rule-created-fact", { + array: options.highPriorityValue, + }), + ); + }, + }); engine.addRule({ - name: 'second', + name: "second", priority: 1, conditions: { - all: [{ - fact: 'high-priority', - params: { - factParam + all: [ + { + fact: "high-priority", + params: { + factParam, + }, + operator: "containsDivisibleValuesOf", + path: "$.values", + value: { + fact: "rule-created-fact", + path: "$.array", // set by 'success' of first rule + }, }, - operator: 'containsDivisibleValuesOf', - path: '$.values', - value: { - fact: 'rule-created-fact', - path: '$.array' // set by 'success' of first rule - } - }] + ], }, - event: event2 - }) + event: event2, + }); - engine.addOperator('containsDivisibleValuesOf', (factValue, jsonValue) => { - return factValue.some(v => v % jsonValue === 0) - }) + engine.addOperator("containsDivisibleValuesOf", (factValue, jsonValue) => { + return factValue.some((v) => v % jsonValue === 0); + }); - engine.addFact('high-priority', async function (params, almanac) { - highPrioritySpy(params) - const idx = await almanac.factValue('sub-fact') - return delay({ values: [idx + params.factParam] }) // { values: [baseIndex + factParam] } - }, { priority: 2 }) + engine.addFact( + "high-priority", + async function (params, almanac) { + highPrioritySpy(params); + const idx = await almanac.factValue("sub-fact"); + return delay({ values: [idx + params.factParam] }); // { values: [baseIndex + factParam] } + }, + { priority: 2 }, + ); - engine.addFact('low-priority', async function (params, almanac) { - lowPrioritySpy(params) - const idx = await almanac.factValue('sub-fact') - return delay(idx + 1) // baseIndex + 1 - }, { priority: 1 }) + engine.addFact( + "low-priority", + async function (params, almanac) { + lowPrioritySpy(params); + const idx = await almanac.factValue("sub-fact"); + return delay(idx + 1); // baseIndex + 1 + }, + { priority: 1 }, + ); - engine.addFact('sub-fact', async function (params, almanac) { - const baseIndex = await almanac.factValue('baseIndex') - return delay(baseIndex) - }) - successSpy = sandbox.spy() - failureSpy = sandbox.spy() - engine.on('success', successSpy) - engine.on('failure', failureSpy) + engine.addFact("sub-fact", async function (params, almanac) { + const baseIndex = await almanac.factValue("baseIndex"); + return delay(baseIndex); + }); + successSpy = sandbox.spy(); + failureSpy = sandbox.spy(); + engine.on("success", successSpy); + engine.on("failure", failureSpy); - return engine + return engine; } - it('succeeds', async () => { + it("succeeds", async () => { const engine = setup({ highPriorityValue: 2, - lowPriorityValue: [2] - }) + lowPriorityValue: [2], + }); - const { - results, - failureResults, - events, - failureEvents - } = await engine.run({ baseIndex: 1 }) + const { results, failureResults, events, failureEvents } = await engine.run( + { baseIndex: 1 }, + ); // results - expect(results.length).to.equal(2) + expect(results.length).to.equal(2); expect(results[0]).to.deep.equal({ conditions: { all: [ { - fact: 'high-priority', - factResult: [ - 2 - ], - operator: 'contains', + fact: "high-priority", + factResult: [2], + operator: "contains", params: { - factParam: 1 + factParam: 1, }, - path: '$.values', + path: "$.values", result: true, - value: 2 + value: 2, }, { - fact: 'low-priority', + fact: "low-priority", factResult: 2, - operator: 'in', + operator: "in", result: true, - value: [ - 2 - ] - } + value: [2], + }, ], - operator: 'all', - priority: 1 + operator: "all", + priority: 1, }, event: { params: { - eventParam: 1 + eventParam: 1, }, - type: 'event-1' + type: "event-1", }, - name: 'first', + name: "first", priority: 10, - result: true - }) + result: true, + }); expect(results[1]).to.deep.equal({ conditions: { all: [ { - fact: 'high-priority', - factResult: [ - 2 - ], - operator: 'containsDivisibleValuesOf', + fact: "high-priority", + factResult: [2], + operator: "containsDivisibleValuesOf", params: { - factParam: 1 + factParam: 1, }, - path: '$.values', + path: "$.values", result: true, value: { - fact: 'rule-created-fact', - path: '$.array' - } - } + fact: "rule-created-fact", + path: "$.array", + }, + }, ], - operator: 'all', - priority: 1 + operator: "all", + priority: 1, }, event: { - type: 'event-2' + type: "event-2", }, - name: 'second', + name: "second", priority: 1, - result: true - }) - expect(failureResults).to.be.empty() + result: true, + }); + expect(failureResults).to.be.empty(); // events - expect(failureEvents.length).to.equal(0) - expect(events.length).to.equal(2) - expect(events[0]).to.deep.equal(event1) - expect(events[1]).to.deep.equal(event2) + expect(failureEvents.length).to.equal(0); + expect(events.length).to.equal(2); + expect(events[0]).to.deep.equal(event1); + expect(events[1]).to.deep.equal(event2); // callbacks - expect(successSpy).to.have.been.calledTwice() - expect(successSpy).to.have.been.calledWith(event1) - expect(successSpy).to.have.been.calledWith(event2) - expect(highPrioritySpy).to.have.been.calledBefore(lowPrioritySpy) - expect(failureSpy).to.not.have.been.called() - }) + expect(successSpy).to.have.been.calledTwice(); + expect(successSpy).to.have.been.calledWith(event1); + expect(successSpy).to.have.been.calledWith(event2); + expect(highPrioritySpy).to.have.been.calledBefore(lowPrioritySpy); + expect(failureSpy).to.not.have.been.called(); + }); - it('fails', async () => { + it("fails", async () => { const engine = setup({ highPriorityValue: 2, - lowPriorityValue: [3] // falsey - }) + lowPriorityValue: [3], // falsey + }); - const { - results, - failureResults, - events, - failureEvents - } = await engine.run({ baseIndex: 1, 'rule-created-fact': '' }) + const { results, failureResults, events, failureEvents } = await engine.run( + { baseIndex: 1, "rule-created-fact": "" }, + ); - expect(results.length).to.equal(0) - expect(failureResults.length).to.equal(2) - expect(failureResults.every(rr => rr.result === false)).to.be.true() + expect(results.length).to.equal(0); + expect(failureResults.length).to.equal(2); + expect(failureResults.every((rr) => rr.result === false)).to.be.true(); - expect(events.length).to.equal(0) - expect(failureEvents.length).to.equal(2) - expect(failureSpy).to.have.been.calledTwice() - expect(failureSpy).to.have.been.calledWith(event1) - expect(failureSpy).to.have.been.calledWith(event2) - expect(highPrioritySpy).to.have.been.calledBefore(lowPrioritySpy) - expect(successSpy).to.not.have.been.called() - }) -}) + expect(events.length).to.equal(0); + expect(failureEvents.length).to.equal(2); + expect(failureSpy).to.have.been.calledTwice(); + expect(failureSpy).to.have.been.calledWith(event1); + expect(failureSpy).to.have.been.calledWith(event2); + expect(highPrioritySpy).to.have.been.calledBefore(lowPrioritySpy); + expect(successSpy).to.not.have.been.called(); + }); +}); diff --git a/test/almanac.test.js b/test/almanac.test.js index b9701839..e8b16506 100644 --- a/test/almanac.test.js +++ b/test/almanac.test.js @@ -1,200 +1,216 @@ -import { Fact } from '../src/index' -import Almanac from '../src/almanac' -import sinon from 'sinon' - -describe('Almanac', () => { - let almanac - let factSpy - let sandbox +import { Fact } from "../src/index"; +import Almanac from "../src/almanac"; +import sinon from "sinon"; + +describe("Almanac", () => { + let almanac; + let factSpy; + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); beforeEach(() => { - factSpy = sandbox.spy() - }) + factSpy = sandbox.spy(); + }); afterEach(() => { - sandbox.restore() - }) - - describe('properties', () => { - it('has methods for managing facts', () => { - almanac = new Almanac() - expect(almanac).to.have.property('factValue') - }) - - it('adds runtime facts', () => { - almanac = new Almanac() - almanac.addFact('modelId', 'XYZ') - expect(almanac.factMap.get('modelId').value).to.equal('XYZ') - }) - }) - - describe('addFact', () => { - it('supports runtime facts as key => values', () => { - almanac = new Almanac() - almanac.addFact('fact1', 3) - return expect(almanac.factValue('fact1')).to.eventually.equal(3) - }) - - it('supporrts runtime facts as dynamic callbacks', async () => { - almanac = new Almanac() - almanac.addFact('fact1', () => { - factSpy() - return Promise.resolve(3) - }) - await expect(almanac.factValue('fact1')).to.eventually.equal(3) - await expect(factSpy).to.have.been.calledOnce() - }) - - it('supports runtime fact instances', () => { - const fact = new Fact('fact1', 3) - almanac = new Almanac() - almanac.addFact(fact) - return expect(almanac.factValue('fact1')).to.eventually.equal(fact.value) - }) - }) - - describe('addEvent() / getEvents()', () => { + sandbox.restore(); + }); + + describe("properties", () => { + it("has methods for managing facts", () => { + almanac = new Almanac(); + expect(almanac).to.have.property("factValue"); + }); + + it("adds runtime facts", () => { + almanac = new Almanac(); + almanac.addFact("modelId", "XYZ"); + expect(almanac.factMap.get("modelId").value).to.equal("XYZ"); + }); + }); + + describe("addFact", () => { + it("supports runtime facts as key => values", () => { + almanac = new Almanac(); + almanac.addFact("fact1", 3); + return expect(almanac.factValue("fact1")).to.eventually.equal(3); + }); + + it("supporrts runtime facts as dynamic callbacks", async () => { + almanac = new Almanac(); + almanac.addFact("fact1", () => { + factSpy(); + return Promise.resolve(3); + }); + await expect(almanac.factValue("fact1")).to.eventually.equal(3); + await expect(factSpy).to.have.been.calledOnce(); + }); + + it("supports runtime fact instances", () => { + const fact = new Fact("fact1", 3); + almanac = new Almanac(); + almanac.addFact(fact); + return expect(almanac.factValue("fact1")).to.eventually.equal(fact.value); + }); + }); + + describe("addEvent() / getEvents()", () => { const event = {}; - ['success', 'failure'].forEach(outcome => { + ["success", "failure"].forEach((outcome) => { it(`manages ${outcome} events`, () => { - almanac = new Almanac() - expect(almanac.getEvents(outcome)).to.be.empty() - almanac.addEvent(event, outcome) - expect(almanac.getEvents(outcome)).to.have.a.lengthOf(1) - expect(almanac.getEvents(outcome)[0]).to.equal(event) - }) - - it('getEvent() filters when outcome provided, or returns all events', () => { - almanac = new Almanac() - almanac.addEvent(event, 'success') - almanac.addEvent(event, 'failure') - expect(almanac.getEvents('success')).to.have.a.lengthOf(1) - expect(almanac.getEvents('failure')).to.have.a.lengthOf(1) - expect(almanac.getEvents()).to.have.a.lengthOf(2) - }) - }) - }) - - describe('arguments', () => { + almanac = new Almanac(); + expect(almanac.getEvents(outcome)).to.be.empty(); + almanac.addEvent(event, outcome); + expect(almanac.getEvents(outcome)).to.have.a.lengthOf(1); + expect(almanac.getEvents(outcome)[0]).to.equal(event); + }); + + it("getEvent() filters when outcome provided, or returns all events", () => { + almanac = new Almanac(); + almanac.addEvent(event, "success"); + almanac.addEvent(event, "failure"); + expect(almanac.getEvents("success")).to.have.a.lengthOf(1); + expect(almanac.getEvents("failure")).to.have.a.lengthOf(1); + expect(almanac.getEvents()).to.have.a.lengthOf(2); + }); + }); + }); + + describe("arguments", () => { beforeEach(() => { - const fact = new Fact('foo', async (params, facts) => { - if (params.userId) return params.userId - return 'unknown' - }) - almanac = new Almanac() - almanac.addFact(fact) - }) - - it('allows parameters to be passed to the fact', async () => { - return expect(almanac.factValue('foo')).to.eventually.equal('unknown') - }) - - it('allows parameters to be passed to the fact', async () => { - return expect(almanac.factValue('foo', { userId: 1 })).to.eventually.equal(1) - }) - - it('throws an exception if it encounters an undefined fact', () => { - return expect(almanac.factValue('bar')).to.be.rejectedWith(/Undefined fact: bar/) - }) - }) - - describe('addRuntimeFact', () => { - it('adds a key/value pair to the factMap as a fact instance', () => { - almanac = new Almanac() - almanac.addRuntimeFact('factId', 'factValue') - expect(almanac.factMap.get('factId').value).to.equal('factValue') - }) - }) - - describe('_addConstantFact', () => { - it('adds fact instances to the factMap', () => { - const fact = new Fact('factId', 'factValue') - almanac = new Almanac() - almanac._addConstantFact(fact) - expect(almanac.factMap.get(fact.id).value).to.equal(fact.value) - }) - }) - - describe('_getFact', _ => { - it('retrieves the fact object', () => { - const fact = new Fact('id', 1) - almanac = new Almanac() - almanac.addFact(fact) - expect(almanac._getFact('id')).to.equal(fact) - }) - }) - - describe('_setFactValue()', () => { - function expectFactResultsCache (expected) { - const promise = almanac.factResultsCache.values().next().value - expect(promise).to.be.instanceof(Promise) - promise.then(value => expect(value).to.equal(expected)) - return promise + const fact = new Fact("foo", async (params, facts) => { + if (params.userId) return params.userId; + return "unknown"; + }); + almanac = new Almanac(); + almanac.addFact(fact); + }); + + it("allows parameters to be passed to the fact", async () => { + return expect(almanac.factValue("foo")).to.eventually.equal("unknown"); + }); + + it("allows parameters to be passed to the fact", async () => { + return expect( + almanac.factValue("foo", { userId: 1 }), + ).to.eventually.equal(1); + }); + + it("throws an exception if it encounters an undefined fact", () => { + return expect(almanac.factValue("bar")).to.be.rejectedWith( + /Undefined fact: bar/, + ); + }); + }); + + describe("addRuntimeFact", () => { + it("adds a key/value pair to the factMap as a fact instance", () => { + almanac = new Almanac(); + almanac.addRuntimeFact("factId", "factValue"); + expect(almanac.factMap.get("factId").value).to.equal("factValue"); + }); + }); + + describe("_addConstantFact", () => { + it("adds fact instances to the factMap", () => { + const fact = new Fact("factId", "factValue"); + almanac = new Almanac(); + almanac._addConstantFact(fact); + expect(almanac.factMap.get(fact.id).value).to.equal(fact.value); + }); + }); + + describe("_getFact", (_) => { + it("retrieves the fact object", () => { + const fact = new Fact("id", 1); + almanac = new Almanac(); + almanac.addFact(fact); + expect(almanac._getFact("id")).to.equal(fact); + }); + }); + + describe("_setFactValue()", () => { + function expectFactResultsCache(expected) { + const promise = almanac.factResultsCache.values().next().value; + expect(promise).to.be.instanceof(Promise); + promise.then((value) => expect(value).to.equal(expected)); + return promise; } - function setup (f = new Fact('id', 1)) { - fact = f - almanac = new Almanac() - almanac.addFact(fact) + function setup(f = new Fact("id", 1)) { + fact = f; + almanac = new Almanac(); + almanac.addFact(fact); } - let fact - const FACT_VALUE = 2 - - it('updates the fact results and returns a promise', (done) => { - setup() - almanac._setFactValue(fact, {}, FACT_VALUE) - expectFactResultsCache(FACT_VALUE).then(_ => done()).catch(done) - }) - - it('honors facts with caching disabled', (done) => { - setup(new Fact('id', 1, { cache: false })) - const promise = almanac._setFactValue(fact, {}, FACT_VALUE) - expect(almanac.factResultsCache.values().next().value).to.be.undefined() - promise.then(value => expect(value).to.equal(FACT_VALUE)).then(_ => done()).catch(done) - }) - }) - - describe('factValue()', () => { + let fact; + const FACT_VALUE = 2; + + it("updates the fact results and returns a promise", (done) => { + setup(); + almanac._setFactValue(fact, {}, FACT_VALUE); + expectFactResultsCache(FACT_VALUE) + .then((_) => done()) + .catch(done); + }); + + it("honors facts with caching disabled", (done) => { + setup(new Fact("id", 1, { cache: false })); + const promise = almanac._setFactValue(fact, {}, FACT_VALUE); + expect(almanac.factResultsCache.values().next().value).to.be.undefined(); + promise + .then((value) => expect(value).to.equal(FACT_VALUE)) + .then((_) => done()) + .catch(done); + }); + }); + + describe("factValue()", () => { it('allows "path" to be specified to traverse the fact data with json-path', async () => { - const fact = new Fact('foo', { - users: [{ - name: 'George' - }, { - name: 'Thomas' - }] - }) - almanac = new Almanac() - almanac.addFact(fact) - const result = await almanac.factValue('foo', null, '$..name') - expect(result).to.deep.equal(['George', 'Thomas']) - }) - - describe('caching', () => { - function setup (factOptions) { - const fact = new Fact('foo', async (params, facts) => { - factSpy() - return 'unknown' - }, factOptions) - almanac = new Almanac() - almanac.addFact(fact) + const fact = new Fact("foo", { + users: [ + { + name: "George", + }, + { + name: "Thomas", + }, + ], + }); + almanac = new Almanac(); + almanac.addFact(fact); + const result = await almanac.factValue("foo", null, "$..name"); + expect(result).to.deep.equal(["George", "Thomas"]); + }); + + describe("caching", () => { + function setup(factOptions) { + const fact = new Fact( + "foo", + async (params, facts) => { + factSpy(); + return "unknown"; + }, + factOptions, + ); + almanac = new Almanac(); + almanac.addFact(fact); } - it('evaluates the fact every time when fact caching is off', () => { - setup({ cache: false }) - almanac.factValue('foo') - almanac.factValue('foo') - almanac.factValue('foo') - expect(factSpy).to.have.been.calledThrice() - }) - - it('evaluates the fact once when fact caching is on', () => { - setup({ cache: true }) - almanac.factValue('foo') - almanac.factValue('foo') - almanac.factValue('foo') - expect(factSpy).to.have.been.calledOnce() - }) - }) - }) -}) + it("evaluates the fact every time when fact caching is off", () => { + setup({ cache: false }); + almanac.factValue("foo"); + almanac.factValue("foo"); + almanac.factValue("foo"); + expect(factSpy).to.have.been.calledThrice(); + }); + + it("evaluates the fact once when fact caching is on", () => { + setup({ cache: true }); + almanac.factValue("foo"); + almanac.factValue("foo"); + almanac.factValue("foo"); + expect(factSpy).to.have.been.calledOnce(); + }); + }); + }); +}); diff --git a/test/condition.test.js b/test/condition.test.js index cd1d8f54..891b0270 100644 --- a/test/condition.test.js +++ b/test/condition.test.js @@ -1,380 +1,509 @@ -'use strict' +"use strict"; -import Condition from '../src/condition' -import defaultOperators from '../src/engine-default-operators' -import Almanac from '../src/almanac' -import Fact from '../src/fact' +import Condition from "../src/condition"; +import defaultOperators from "../src/engine-default-operators"; +import Almanac from "../src/almanac"; +import Fact from "../src/fact"; -const operators = new Map() -defaultOperators.forEach(o => operators.set(o.name, o)) +const operators = new Map(); +defaultOperators.forEach((o) => operators.set(o.name, o)); -function condition () { +function condition() { return { - all: [{ - id: '6ed20017-375f-40c9-a1d2-6d7e0f4733c5', - name: 'team participation in form', - fact: 'team_participation', - operator: 'equal', - value: 50, - path: '.metrics[0].forum-posts' - }] - } + all: [ + { + id: "6ed20017-375f-40c9-a1d2-6d7e0f4733c5", + name: "team participation in form", + fact: "team_participation", + operator: "equal", + value: 50, + path: ".metrics[0].forum-posts", + }, + ], + }; } -describe('Condition', () => { - describe('constructor', () => { - it('fact conditions have properties', () => { - const properties = condition() - const subject = new Condition(properties.all[0]) - expect(subject).to.have.property('fact') - expect(subject).to.have.property('operator') - expect(subject).to.have.property('value') - expect(subject).to.have.property('path') - expect(subject).to.have.property('name') - }) - - it('boolean conditions have properties', () => { - const properties = condition() - const subject = new Condition(properties) - expect(subject).to.have.property('operator') - expect(subject).to.have.property('priority') - expect(subject.priority).to.equal(1) - }) - }) - - describe('toJSON', () => { - it('converts the condition into a json string', () => { +describe("Condition", () => { + describe("constructor", () => { + it("fact conditions have properties", () => { + const properties = condition(); + const subject = new Condition(properties.all[0]); + expect(subject).to.have.property("fact"); + expect(subject).to.have.property("operator"); + expect(subject).to.have.property("value"); + expect(subject).to.have.property("path"); + expect(subject).to.have.property("name"); + }); + + it("boolean conditions have properties", () => { + const properties = condition(); + const subject = new Condition(properties); + expect(subject).to.have.property("operator"); + expect(subject).to.have.property("priority"); + expect(subject.priority).to.equal(1); + }); + }); + + describe("toJSON", () => { + it("converts the condition into a json string", () => { const properties = factories.condition({ - fact: 'age', + fact: "age", value: { - fact: 'weight', + fact: "weight", params: { - unit: 'lbs' + unit: "lbs", }, - path: '.value' - } - }) - const condition = new Condition(properties) - const json = condition.toJSON() - expect(json).to.equal('{"operator":"equal","value":{"fact":"weight","params":{"unit":"lbs"},"path":".value"},"fact":"age"}') - }) + path: ".value", + }, + }); + const condition = new Condition(properties); + const json = condition.toJSON(); + expect(json).to.equal( + '{"operator":"equal","value":{"fact":"weight","params":{"unit":"lbs"},"path":".value"},"fact":"age"}', + ); + }); it('converts "not" conditions', () => { const properties = { not: { ...factories.condition({ - fact: 'age', + fact: "age", value: { - fact: 'weight', + fact: "weight", params: { - unit: 'lbs' + unit: "lbs", }, - path: '.value' - } - }) - } - } - const condition = new Condition(properties) - const json = condition.toJSON() - expect(json).to.equal('{"priority":1,"not":{"operator":"equal","value":{"fact":"weight","params":{"unit":"lbs"},"path":".value"},"fact":"age"}}') - }) - }) - - describe('evaluate', () => { + path: ".value", + }, + }), + }, + }; + const condition = new Condition(properties); + const json = condition.toJSON(); + expect(json).to.equal( + '{"priority":1,"not":{"operator":"equal","value":{"fact":"weight","params":{"unit":"lbs"},"path":".value"},"fact":"age"}}', + ); + }); + }); + + describe("evaluate", () => { const conditionBase = factories.condition({ - fact: 'age', - value: 50 - }) - let condition - let almanac - function setup (options, factValue) { - const properties = Object.assign({}, conditionBase, options) - condition = new Condition(properties) - const fact = new Fact(conditionBase.fact, factValue) - almanac = new Almanac() - almanac.addFact(fact) + fact: "age", + value: 50, + }); + let condition; + let almanac; + function setup(options, factValue) { + const properties = Object.assign({}, conditionBase, options); + condition = new Condition(properties); + const fact = new Fact(conditionBase.fact, factValue); + almanac = new Almanac(); + almanac.addFact(fact); } - context('validations', () => { - beforeEach(() => setup({}, 1)) - it('throws when missing an almanac', () => { - return expect(condition.evaluate(undefined, operators)).to.be.rejectedWith('almanac required') - }) - it('throws when missing operators', () => { - return expect(condition.evaluate(almanac, undefined)).to.be.rejectedWith('operatorMap required') - }) - it('throws when run against a boolean operator', () => { - condition.all = [] - return expect(condition.evaluate(almanac, operators)).to.be.rejectedWith('Cannot evaluate() a boolean condition') - }) - }) + context("validations", () => { + beforeEach(() => setup({}, 1)); + it("throws when missing an almanac", () => { + return expect( + condition.evaluate(undefined, operators), + ).to.be.rejectedWith("almanac required"); + }); + it("throws when missing operators", () => { + return expect( + condition.evaluate(almanac, undefined), + ).to.be.rejectedWith("operatorMap required"); + }); + it("throws when run against a boolean operator", () => { + condition.all = []; + return expect( + condition.evaluate(almanac, operators), + ).to.be.rejectedWith("Cannot evaluate() a boolean condition"); + }); + }); it('evaluates "equal"', async () => { - setup({ operator: 'equal' }, 50) - expect((await condition.evaluate(almanac, operators, 50)).result).to.equal(true) - setup({ operator: 'equal' }, 5) - expect((await condition.evaluate(almanac, operators, 5)).result).to.equal(false) - }) + setup({ operator: "equal" }, 50); + expect( + (await condition.evaluate(almanac, operators, 50)).result, + ).to.equal(true); + setup({ operator: "equal" }, 5); + expect((await condition.evaluate(almanac, operators, 5)).result).to.equal( + false, + ); + }); it('evaluates "equal" to check for undefined', async () => { - condition = new Condition({ fact: 'age', operator: 'equal', value: undefined }) - let fact = new Fact('age', undefined) - almanac = new Almanac() - almanac.addFact(fact) - - expect((await condition.evaluate(almanac, operators)).result).to.equal(true) - - fact = new Fact('age', 1) - almanac = new Almanac() - almanac.addFact(fact) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - }) + condition = new Condition({ + fact: "age", + operator: "equal", + value: undefined, + }); + let fact = new Fact("age", undefined); + almanac = new Almanac(); + almanac.addFact(fact); + + expect((await condition.evaluate(almanac, operators)).result).to.equal( + true, + ); + + fact = new Fact("age", 1); + almanac = new Almanac(); + almanac.addFact(fact); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + }); it('evaluates "notEqual"', async () => { - setup({ operator: 'notEqual' }, 50) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - setup({ operator: 'notEqual' }, 5) - expect((await condition.evaluate(almanac, operators)).result).to.equal(true) - }) + setup({ operator: "notEqual" }, 50); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + setup({ operator: "notEqual" }, 5); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + true, + ); + }); it('evaluates "in"', async () => { - setup({ operator: 'in', value: [5, 10, 15, 20] }, 15) - expect((await condition.evaluate(almanac, operators)).result).to.equal(true) - setup({ operator: 'in', value: [5, 10, 15, 20] }, 99) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - }) + setup({ operator: "in", value: [5, 10, 15, 20] }, 15); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + true, + ); + setup({ operator: "in", value: [5, 10, 15, 20] }, 99); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + }); it('evaluates "contains"', async () => { - setup({ operator: 'contains', value: 10 }, [5, 10, 15]) - expect((await condition.evaluate(almanac, operators)).result).to.equal(true) - setup({ operator: 'contains', value: 10 }, [1, 2, 3]) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - }) + setup({ operator: "contains", value: 10 }, [5, 10, 15]); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + true, + ); + setup({ operator: "contains", value: 10 }, [1, 2, 3]); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + }); it('evaluates "doesNotContain"', async () => { - setup({ operator: 'doesNotContain', value: 10 }, [5, 10, 15]) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - setup({ operator: 'doesNotContain', value: 10 }, [1, 2, 3]) - expect((await condition.evaluate(almanac, operators)).result).to.equal(true) - }) + setup({ operator: "doesNotContain", value: 10 }, [5, 10, 15]); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + setup({ operator: "doesNotContain", value: 10 }, [1, 2, 3]); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + true, + ); + }); it('evaluates "notIn"', async () => { - setup({ operator: 'notIn', value: [5, 10, 15, 20] }, 15) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - setup({ operator: 'notIn', value: [5, 10, 15, 20] }, 99) - expect((await condition.evaluate(almanac, operators)).result).to.equal(true) - }) + setup({ operator: "notIn", value: [5, 10, 15, 20] }, 15); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + setup({ operator: "notIn", value: [5, 10, 15, 20] }, 99); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + true, + ); + }); it('evaluates "lessThan"', async () => { - setup({ operator: 'lessThan' }, 49) - expect((await condition.evaluate(almanac, operators)).result).to.equal(true) - setup({ operator: 'lessThan' }, 50) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - setup({ operator: 'lessThan' }, 51) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - }) + setup({ operator: "lessThan" }, 49); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + true, + ); + setup({ operator: "lessThan" }, 50); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + setup({ operator: "lessThan" }, 51); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + }); it('evaluates "lessThanInclusive"', async () => { - setup({ operator: 'lessThanInclusive' }, 49) - expect((await condition.evaluate(almanac, operators)).result).to.equal(true) - setup({ operator: 'lessThanInclusive' }, 50) - expect((await condition.evaluate(almanac, operators)).result).to.equal(true) - setup({ operator: 'lessThanInclusive' }, 51) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - }) + setup({ operator: "lessThanInclusive" }, 49); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + true, + ); + setup({ operator: "lessThanInclusive" }, 50); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + true, + ); + setup({ operator: "lessThanInclusive" }, 51); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + }); it('evaluates "greaterThan"', async () => { - setup({ operator: 'greaterThan' }, 51) - expect((await condition.evaluate(almanac, operators)).result).to.equal(true) - setup({ operator: 'greaterThan' }, 49) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - setup({ operator: 'greaterThan' }, 50) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - }) + setup({ operator: "greaterThan" }, 51); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + true, + ); + setup({ operator: "greaterThan" }, 49); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + setup({ operator: "greaterThan" }, 50); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + }); it('evaluates "greaterThanInclusive"', async () => { - setup({ operator: 'greaterThanInclusive' }, 51) - expect((await condition.evaluate(almanac, operators)).result).to.equal(true) - setup({ operator: 'greaterThanInclusive' }, 50) - expect((await condition.evaluate(almanac, operators)).result).to.equal(true) - setup({ operator: 'greaterThanInclusive' }, 49) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - }) - - describe('invalid comparisonValues', () => { - it('returns false when using contains or doesNotContain with a non-array', async () => { - setup({ operator: 'contains' }, null) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - setup({ operator: 'doesNotContain' }, null) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - }) - - it('returns false when using comparison operators with null', async () => { - setup({ operator: 'lessThan' }, null) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - setup({ operator: 'lessThanInclusive' }, null) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - setup({ operator: 'greaterThan' }, null) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - setup({ operator: 'greaterThanInclusive' }, null) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - }) - - it('returns false when using comparison operators with non-numbers', async () => { - setup({ operator: 'lessThan' }, 'non-number') - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - setup({ operator: 'lessThan' }, null) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - setup({ operator: 'lessThan' }, []) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - setup({ operator: 'lessThan' }, {}) - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - }) - }) - }) - - describe('objects', () => { - describe('.path', () => { + setup({ operator: "greaterThanInclusive" }, 51); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + true, + ); + setup({ operator: "greaterThanInclusive" }, 50); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + true, + ); + setup({ operator: "greaterThanInclusive" }, 49); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + }); + + describe("invalid comparisonValues", () => { + it("returns false when using contains or doesNotContain with a non-array", async () => { + setup({ operator: "contains" }, null); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + setup({ operator: "doesNotContain" }, null); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + }); + + it("returns false when using comparison operators with null", async () => { + setup({ operator: "lessThan" }, null); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + setup({ operator: "lessThanInclusive" }, null); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + setup({ operator: "greaterThan" }, null); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + setup({ operator: "greaterThanInclusive" }, null); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + }); + + it("returns false when using comparison operators with non-numbers", async () => { + setup({ operator: "lessThan" }, "non-number"); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + setup({ operator: "lessThan" }, null); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + setup({ operator: "lessThan" }, []); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + setup({ operator: "lessThan" }, {}); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + }); + }); + }); + + describe("objects", () => { + describe(".path", () => { it('extracts the object property values using its "path" property', async () => { - const condition = new Condition({ operator: 'equal', path: '$.[0].id', fact: 'age', value: 50 }) - const ageFact = new Fact('age', [{ id: 50 }, { id: 60 }]) - const almanac = new Almanac() - almanac.addFact(ageFact) - expect((await condition.evaluate(almanac, operators)).result).to.equal(true) - - condition.value = 100 // negative case - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - }) + const condition = new Condition({ + operator: "equal", + path: "$.[0].id", + fact: "age", + value: 50, + }); + const ageFact = new Fact("age", [{ id: 50 }, { id: 60 }]); + const almanac = new Almanac(); + almanac.addFact(ageFact); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + true, + ); + + condition.value = 100; // negative case + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + }); it('ignores "path" when non-objects are returned by the fact', async () => { - const ageFact = new Fact('age', 50) - const almanac = new Almanac() - almanac.addFact(ageFact) - - const condition = new Condition({ operator: 'equal', path: '$.[0].id', fact: 'age', value: 50 }) - expect((await condition.evaluate(almanac, operators, 50)).result).to.equal(true) - - condition.value = 100 // negative case - expect((await condition.evaluate(almanac, operators, 50)).result).to.equal(false) - }) - }) - - describe('jsonPath', () => { - it('allows json path to extract values from complex facts', async () => { - const condition = new Condition({ operator: 'contains', path: '$.phoneNumbers[*].type', fact: 'users', value: 'iPhone' }) + const ageFact = new Fact("age", 50); + const almanac = new Almanac(); + almanac.addFact(ageFact); + + const condition = new Condition({ + operator: "equal", + path: "$.[0].id", + fact: "age", + value: 50, + }); + expect( + (await condition.evaluate(almanac, operators, 50)).result, + ).to.equal(true); + + condition.value = 100; // negative case + expect( + (await condition.evaluate(almanac, operators, 50)).result, + ).to.equal(false); + }); + }); + + describe("jsonPath", () => { + it("allows json path to extract values from complex facts", async () => { + const condition = new Condition({ + operator: "contains", + path: "$.phoneNumbers[*].type", + fact: "users", + value: "iPhone", + }); const userData = { phoneNumbers: [ { - type: 'iPhone', - number: '0123-4567-8888' + type: "iPhone", + number: "0123-4567-8888", }, { - type: 'home', - number: '0123-4567-8910' - } - ] - } - - const usersFact = new Fact('users', userData) - const almanac = new Almanac() - almanac.addFact(usersFact) - expect((await condition.evaluate(almanac, operators)).result).to.equal(true) - - condition.value = 'work' // negative case - expect((await condition.evaluate(almanac, operators)).result).to.equal(false) - }) - }) - }) - - describe('boolean operators', () => { - it('throws if not not an array', () => { - const conditions = condition() - conditions.all = { foo: true } - expect(() => new Condition(conditions)).to.throw(/"all" must be an array/) - }) + type: "home", + number: "0123-4567-8910", + }, + ], + }; + + const usersFact = new Fact("users", userData); + const almanac = new Almanac(); + almanac.addFact(usersFact); + expect((await condition.evaluate(almanac, operators)).result).to.equal( + true, + ); + + condition.value = "work"; // negative case + expect((await condition.evaluate(almanac, operators)).result).to.equal( + false, + ); + }); + }); + }); + + describe("boolean operators", () => { + it("throws if not not an array", () => { + const conditions = condition(); + conditions.all = { foo: true }; + expect(() => new Condition(conditions)).to.throw( + /"all" must be an array/, + ); + }); it('throws if is an array and condition is "not"', () => { const conditions = { - not: [{ foo: true }] - } - expect(() => new Condition(conditions)).to.throw(/"not" cannot be an array/) - }) + not: [{ foo: true }], + }; + expect(() => new Condition(conditions)).to.throw( + /"not" cannot be an array/, + ); + }); it('does not throw if is not an array and condition is "not"', () => { const conditions = { not: { - fact: 'foo', - operator: 'equal', - value: 'bar' - } - } - expect(() => new Condition(conditions)).to.not.throw() - }) - }) - - describe('atomic facts', () => { - it('throws if no options are provided', () => { - expect(() => new Condition()).to.throw(/Condition: constructor options required/) - }) + fact: "foo", + operator: "equal", + value: "bar", + }, + }; + expect(() => new Condition(conditions)).to.not.throw(); + }); + }); + + describe("atomic facts", () => { + it("throws if no options are provided", () => { + expect(() => new Condition()).to.throw( + /Condition: constructor options required/, + ); + }); it('throws for a missing "operator"', () => { - const conditions = condition() - delete conditions.all[0].operator - expect(() => new Condition(conditions)).to.throw(/Condition: constructor "operator" property required/) - }) + const conditions = condition(); + delete conditions.all[0].operator; + expect(() => new Condition(conditions)).to.throw( + /Condition: constructor "operator" property required/, + ); + }); it('throws for a missing "fact"', () => { - const conditions = condition() - delete conditions.all[0].fact - expect(() => new Condition(conditions)).to.throw(/Condition: constructor "fact" property required/) - }) + const conditions = condition(); + delete conditions.all[0].fact; + expect(() => new Condition(conditions)).to.throw( + /Condition: constructor "fact" property required/, + ); + }); it('throws for a missing "value"', () => { - const conditions = condition() - delete conditions.all[0].value - expect(() => new Condition(conditions)).to.throw(/Condition: constructor "value" property required/) - }) - }) - - describe('complex conditions', () => { - function complexCondition () { + const conditions = condition(); + delete conditions.all[0].value; + expect(() => new Condition(conditions)).to.throw( + /Condition: constructor "value" property required/, + ); + }); + }); + + describe("complex conditions", () => { + function complexCondition() { return { all: [ { - fact: 'age', - operator: 'lessThan', - value: 45 + fact: "age", + operator: "lessThan", + value: 45, }, { - fact: 'pointBalance', - operator: 'greaterThanInclusive', - value: 1000 + fact: "pointBalance", + operator: "greaterThanInclusive", + value: 1000, }, { any: [ { - fact: 'gender', - operator: 'equal', - value: 'female' + fact: "gender", + operator: "equal", + value: "female", }, { - fact: 'income', - operator: 'greaterThanInclusive', - value: 50000 - } - ] - } - ] - } + fact: "income", + operator: "greaterThanInclusive", + value: 50000, + }, + ], + }, + ], + }; } - it('recursively parses nested conditions', () => { - expect(() => new Condition(complexCondition())).to.not.throw() - }) - - it('throws if a nested condition is invalid', () => { - const conditions = complexCondition() - delete conditions.all[2].any[0].fact - expect(() => new Condition(conditions)).to.throw(/Condition: constructor "fact" property required/) - }) - }) -}) + it("recursively parses nested conditions", () => { + expect(() => new Condition(complexCondition())).to.not.throw(); + }); + + it("throws if a nested condition is invalid", () => { + const conditions = complexCondition(); + delete conditions.all[2].any[0].fact; + expect(() => new Condition(conditions)).to.throw( + /Condition: constructor "fact" property required/, + ); + }); + }); +}); diff --git a/test/engine-all.test.js b/test/engine-all.test.js index d82db875..3e3c3127 100644 --- a/test/engine-all.test.js +++ b/test/engine-all.test.js @@ -1,111 +1,116 @@ -'use strict' +"use strict"; -import sinon from 'sinon' -import engineFactory from '../src/index' +import sinon from "sinon"; +import engineFactory from "../src/index"; -async function factSenior (params, engine) { - return 65 +async function factSenior(params, engine) { + return 65; } -async function factChild (params, engine) { - return 10 +async function factChild(params, engine) { + return 10; } -async function factAdult (params, engine) { - return 30 +async function factAdult(params, engine) { + return 30; } describe('Engine: "all" conditions', () => { - let engine - let sandbox + let engine; + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); afterEach(() => { - sandbox.restore() - }) + sandbox.restore(); + }); describe('supports a single "all" condition', () => { const event = { - type: 'ageTrigger', + type: "ageTrigger", params: { - demographic: 'under50' - } - } + demographic: "under50", + }, + }; const conditions = { - all: [{ - fact: 'age', - operator: 'lessThan', - value: 50 - }] - } - let eventSpy + all: [ + { + fact: "age", + operator: "lessThan", + value: 50, + }, + ], + }; + let eventSpy; beforeEach(() => { - eventSpy = sandbox.spy() - const rule = factories.rule({ conditions, event }) - engine = engineFactory() - engine.addRule(rule) - engine.on('success', eventSpy) - }) + eventSpy = sandbox.spy(); + const rule = factories.rule({ conditions, event }); + engine = engineFactory(); + engine.addRule(rule); + engine.on("success", eventSpy); + }); - it('emits when the condition is met', async () => { - engine.addFact('age', factChild) - await engine.run() - expect(eventSpy).to.have.been.calledWith(event) - }) + it("emits when the condition is met", async () => { + engine.addFact("age", factChild); + await engine.run(); + expect(eventSpy).to.have.been.calledWith(event); + }); - it('does not emit when the condition fails', () => { - engine.addFact('age', factSenior) - engine.run() - expect(eventSpy).to.not.have.been.calledWith(event) - }) - }) + it("does not emit when the condition fails", () => { + engine.addFact("age", factSenior); + engine.run(); + expect(eventSpy).to.not.have.been.calledWith(event); + }); + }); describe('supports "any" with multiple conditions', () => { const conditions = { - all: [{ - fact: 'age', - operator: 'lessThan', - value: 50 - }, { - fact: 'age', - operator: 'greaterThan', - value: 21 - }] - } + all: [ + { + fact: "age", + operator: "lessThan", + value: 50, + }, + { + fact: "age", + operator: "greaterThan", + value: 21, + }, + ], + }; const event = { - type: 'ageTrigger', + type: "ageTrigger", params: { - demographic: 'adult' - } - } - let eventSpy + demographic: "adult", + }, + }; + let eventSpy; beforeEach(() => { - eventSpy = sandbox.spy() - const rule = factories.rule({ conditions, event }) - engine = engineFactory() - engine.addRule(rule) - engine.on('success', eventSpy) - }) + eventSpy = sandbox.spy(); + const rule = factories.rule({ conditions, event }); + engine = engineFactory(); + engine.addRule(rule); + engine.on("success", eventSpy); + }); - it('emits an event when every condition is met', async () => { - engine.addFact('age', factAdult) - await engine.run() - expect(eventSpy).to.have.been.calledWith(event) - }) + it("emits an event when every condition is met", async () => { + engine.addFact("age", factAdult); + await engine.run(); + expect(eventSpy).to.have.been.calledWith(event); + }); - describe('a condition fails', () => { - it('does not emit when the first condition fails', async () => { - engine.addFact('age', factChild) - await engine.run() - expect(eventSpy).to.not.have.been.calledWith(event) - }) + describe("a condition fails", () => { + it("does not emit when the first condition fails", async () => { + engine.addFact("age", factChild); + await engine.run(); + expect(eventSpy).to.not.have.been.calledWith(event); + }); - it('does not emit when the second condition', async () => { - engine.addFact('age', factSenior) - await engine.run() - expect(eventSpy).to.not.have.been.calledWith(event) - }) - }) - }) -}) + it("does not emit when the second condition", async () => { + engine.addFact("age", factSenior); + await engine.run(); + expect(eventSpy).to.not.have.been.calledWith(event); + }); + }); + }); +}); diff --git a/test/engine-any.test.js b/test/engine-any.test.js index 1b722570..b915e6f9 100644 --- a/test/engine-any.test.js +++ b/test/engine-any.test.js @@ -1,107 +1,112 @@ -'use strict' +"use strict"; -import sinon from 'sinon' -import engineFactory from '../src/index' +import sinon from "sinon"; +import engineFactory from "../src/index"; describe('Engine: "any" conditions', () => { - let engine - let sandbox + let engine; + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); afterEach(() => { - sandbox.restore() - }) + sandbox.restore(); + }); describe('supports a single "any" condition', () => { const event = { - type: 'ageTrigger', + type: "ageTrigger", params: { - demographic: 'under50' - } - } + demographic: "under50", + }, + }; const conditions = { - any: [{ - fact: 'age', - operator: 'lessThan', - value: 50 - }] - } - let eventSpy - let ageSpy + any: [ + { + fact: "age", + operator: "lessThan", + value: 50, + }, + ], + }; + let eventSpy; + let ageSpy; beforeEach(() => { - eventSpy = sandbox.spy() - ageSpy = sandbox.stub() - const rule = factories.rule({ conditions, event }) - engine = engineFactory() - engine.addRule(rule) - engine.addFact('age', ageSpy) - engine.on('success', eventSpy) - }) + eventSpy = sandbox.spy(); + ageSpy = sandbox.stub(); + const rule = factories.rule({ conditions, event }); + engine = engineFactory(); + engine.addRule(rule); + engine.addFact("age", ageSpy); + engine.on("success", eventSpy); + }); - it('emits when the condition is met', async () => { - ageSpy.returns(10) - await engine.run() - expect(eventSpy).to.have.been.calledWith(event) - }) + it("emits when the condition is met", async () => { + ageSpy.returns(10); + await engine.run(); + expect(eventSpy).to.have.been.calledWith(event); + }); - it('does not emit when the condition fails', () => { - ageSpy.returns(75) - engine.run() - expect(eventSpy).to.not.have.been.calledWith(event) - }) - }) + it("does not emit when the condition fails", () => { + ageSpy.returns(75); + engine.run(); + expect(eventSpy).to.not.have.been.calledWith(event); + }); + }); describe('supports "any" with multiple conditions', () => { const conditions = { - any: [{ - fact: 'age', - operator: 'lessThan', - value: 50 - }, { - fact: 'segment', - operator: 'equal', - value: 'european' - }] - } + any: [ + { + fact: "age", + operator: "lessThan", + value: 50, + }, + { + fact: "segment", + operator: "equal", + value: "european", + }, + ], + }; const event = { - type: 'ageTrigger', + type: "ageTrigger", params: { - demographic: 'under50' - } - } - let eventSpy - let ageSpy - let segmentSpy + demographic: "under50", + }, + }; + let eventSpy; + let ageSpy; + let segmentSpy; beforeEach(() => { - eventSpy = sandbox.spy() - ageSpy = sandbox.stub() - segmentSpy = sandbox.stub() - const rule = factories.rule({ conditions, event }) - engine = engineFactory() - engine.addRule(rule) - engine.addFact('segment', segmentSpy) - engine.addFact('age', ageSpy) - engine.on('success', eventSpy) - }) + eventSpy = sandbox.spy(); + ageSpy = sandbox.stub(); + segmentSpy = sandbox.stub(); + const rule = factories.rule({ conditions, event }); + engine = engineFactory(); + engine.addRule(rule); + engine.addFact("segment", segmentSpy); + engine.addFact("age", ageSpy); + engine.on("success", eventSpy); + }); - it('emits an event when any condition is met', async () => { - segmentSpy.returns('north-american') - ageSpy.returns(25) - await engine.run() - expect(eventSpy).to.have.been.calledWith(event) + it("emits an event when any condition is met", async () => { + segmentSpy.returns("north-american"); + ageSpy.returns(25); + await engine.run(); + expect(eventSpy).to.have.been.calledWith(event); - segmentSpy.returns('european') - ageSpy.returns(100) - await engine.run() - expect(eventSpy).to.have.been.calledWith(event) - }) + segmentSpy.returns("european"); + ageSpy.returns(100); + await engine.run(); + expect(eventSpy).to.have.been.calledWith(event); + }); - it('does not emit when all conditions fail', async () => { - segmentSpy.returns('north-american') - ageSpy.returns(100) - await engine.run() - expect(eventSpy).to.not.have.been.calledWith(event) - }) - }) -}) + it("does not emit when all conditions fail", async () => { + segmentSpy.returns("north-american"); + ageSpy.returns(100); + await engine.run(); + expect(eventSpy).to.not.have.been.calledWith(event); + }); + }); +}); diff --git a/test/engine-cache.test.js b/test/engine-cache.test.js index 7e9d7082..56dd1840 100644 --- a/test/engine-cache.test.js +++ b/test/engine-cache.test.js @@ -1,59 +1,73 @@ -'use strict' +"use strict"; -import sinon from 'sinon' -import engineFactory from '../src/index' +import sinon from "sinon"; +import engineFactory from "../src/index"; -describe('Engine: cache', () => { - let engine - let sandbox +describe("Engine: cache", () => { + let engine; + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); afterEach(() => { - sandbox.restore() - }) + sandbox.restore(); + }); - const event = { type: 'setDrinkingFlag' } - const collegeSeniorEvent = { type: 'isCollegeSenior' } + const event = { type: "setDrinkingFlag" }; + const collegeSeniorEvent = { type: "isCollegeSenior" }; const conditions = { - any: [{ - fact: 'age', - operator: 'greaterThanInclusive', - value: 21 - }] - } + any: [ + { + fact: "age", + operator: "greaterThanInclusive", + value: 21, + }, + ], + }; - let factSpy - let eventSpy + let factSpy; + let eventSpy; const ageFact = () => { - factSpy() - return 22 - } - function setup (factOptions) { - factSpy = sandbox.spy() - eventSpy = sandbox.spy() - engine = engineFactory() - const determineDrinkingAge = factories.rule({ conditions, event, priority: 100 }) - engine.addRule(determineDrinkingAge) - const determineCollegeSenior = factories.rule({ conditions, event: collegeSeniorEvent, priority: 1 }) - engine.addRule(determineCollegeSenior) - const over20 = factories.rule({ conditions, event: collegeSeniorEvent, priority: 50 }) - engine.addRule(over20) - engine.addFact('age', ageFact, factOptions) - engine.on('success', eventSpy) + factSpy(); + return 22; + }; + function setup(factOptions) { + factSpy = sandbox.spy(); + eventSpy = sandbox.spy(); + engine = engineFactory(); + const determineDrinkingAge = factories.rule({ + conditions, + event, + priority: 100, + }); + engine.addRule(determineDrinkingAge); + const determineCollegeSenior = factories.rule({ + conditions, + event: collegeSeniorEvent, + priority: 1, + }); + engine.addRule(determineCollegeSenior); + const over20 = factories.rule({ + conditions, + event: collegeSeniorEvent, + priority: 50, + }); + engine.addRule(over20); + engine.addFact("age", ageFact, factOptions); + engine.on("success", eventSpy); } - it('loads facts once and caches the results for future use', async () => { - setup({ cache: true }) - await engine.run() - expect(eventSpy).to.have.been.calledThrice() - expect(factSpy).to.have.been.calledOnce() - }) + it("loads facts once and caches the results for future use", async () => { + setup({ cache: true }); + await engine.run(); + expect(eventSpy).to.have.been.calledThrice(); + expect(factSpy).to.have.been.calledOnce(); + }); - it('allows caching to be turned off', async () => { - setup({ cache: false }) - await engine.run() - expect(eventSpy).to.have.been.calledThrice() - expect(factSpy).to.have.been.calledThrice() - }) -}) + it("allows caching to be turned off", async () => { + setup({ cache: false }); + await engine.run(); + expect(eventSpy).to.have.been.calledThrice(); + expect(factSpy).to.have.been.calledThrice(); + }); +}); diff --git a/test/engine-condition.test.js b/test/engine-condition.test.js index 028a856e..fc49f7d9 100644 --- a/test/engine-condition.test.js +++ b/test/engine-condition.test.js @@ -1,321 +1,321 @@ -'use strict' +"use strict"; -import sinon from 'sinon' -import engineFactory from '../src/index' +import sinon from "sinon"; +import engineFactory from "../src/index"; -describe('Engine: condition', () => { - let engine - let sandbox +describe("Engine: condition", () => { + let engine; + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); afterEach(() => { - sandbox.restore() - }) + sandbox.restore(); + }); - describe('setCondition()', () => { - describe('validations', () => { + describe("setCondition()", () => { + describe("validations", () => { beforeEach(() => { - engine = engineFactory() - }) - it('throws an exception for invalid root conditions', () => { - expect(engine.setCondition.bind(engine, 'test', { foo: true })).to.throw( - /"conditions" root must contain a single instance of "all", "any", "not", or "condition"/ - ) - }) - }) - }) - - describe('undefined condition', () => { + engine = engineFactory(); + }); + it("throws an exception for invalid root conditions", () => { + expect( + engine.setCondition.bind(engine, "test", { foo: true }), + ).to.throw( + /"conditions" root must contain a single instance of "all", "any", "not", or "condition"/, + ); + }); + }); + }); + + describe("undefined condition", () => { const sendEvent = { - type: 'checkSending', + type: "checkSending", params: { - sendRetirementPayment: true - } - } + sendRetirementPayment: true, + }, + }; const sendConditions = { all: [ - { condition: 'over60' }, + { condition: "over60" }, { - fact: 'isRetired', - operator: 'equal', - value: true - } - ] - } - - describe('allowUndefinedConditions: true', () => { - let eventSpy + fact: "isRetired", + operator: "equal", + value: true, + }, + ], + }; + + describe("allowUndefinedConditions: true", () => { + let eventSpy; beforeEach(() => { - eventSpy = sandbox.spy() + eventSpy = sandbox.spy(); const sendRule = factories.rule({ conditions: sendConditions, - event: sendEvent - }) - engine = engineFactory([sendRule], { allowUndefinedConditions: true }) + event: sendEvent, + }); + engine = engineFactory([sendRule], { allowUndefinedConditions: true }); - engine.addFact('isRetired', true) - engine.on('failure', eventSpy) - }) + engine.addFact("isRetired", true); + engine.on("failure", eventSpy); + }); - it('evaluates undefined conditions as false', async () => { - await engine.run() - expect(eventSpy).to.have.been.called() - }) - }) + it("evaluates undefined conditions as false", async () => { + await engine.run(); + expect(eventSpy).to.have.been.called(); + }); + }); - describe('allowUndefinedConditions: false', () => { + describe("allowUndefinedConditions: false", () => { beforeEach(() => { const sendRule = factories.rule({ conditions: sendConditions, - event: sendEvent - }) - engine = engineFactory([sendRule], { allowUndefinedConditions: false }) + event: sendEvent, + }); + engine = engineFactory([sendRule], { allowUndefinedConditions: false }); - engine.addFact('isRetired', true) - }) + engine.addFact("isRetired", true); + }); - it('throws error during run', async () => { + it("throws error during run", async () => { try { - await engine.run() + await engine.run(); } catch (error) { - expect(error.message).to.equal('No condition over60 exists') + expect(error.message).to.equal("No condition over60 exists"); } - }) - }) - }) + }); + }); + }); - describe('supports condition shared across multiple rules', () => { - const name = 'over60' + describe("supports condition shared across multiple rules", () => { + const name = "over60"; const condition = { all: [ { - fact: 'age', - operator: 'greaterThanInclusive', - value: 60 - } - ] - } + fact: "age", + operator: "greaterThanInclusive", + value: 60, + }, + ], + }; const sendEvent = { - type: 'checkSending', + type: "checkSending", params: { - sendRetirementPayment: true - } - } + sendRetirementPayment: true, + }, + }; const sendConditions = { all: [ { condition: name }, { - fact: 'isRetired', - operator: 'equal', - value: true - } - ] - } + fact: "isRetired", + operator: "equal", + value: true, + }, + ], + }; const outreachEvent = { - type: 'triggerOutreach' - } + type: "triggerOutreach", + }; const outreachConditions = { all: [ { condition: name }, { - fact: 'requestedOutreach', - operator: 'equal', - value: true - } - ] - } - - let eventSpy - let ageSpy - let isRetiredSpy - let requestedOutreachSpy + fact: "requestedOutreach", + operator: "equal", + value: true, + }, + ], + }; + + let eventSpy; + let ageSpy; + let isRetiredSpy; + let requestedOutreachSpy; beforeEach(() => { - eventSpy = sandbox.spy() - ageSpy = sandbox.stub() - isRetiredSpy = sandbox.stub() - requestedOutreachSpy = sandbox.stub() - engine = engineFactory() + eventSpy = sandbox.spy(); + ageSpy = sandbox.stub(); + isRetiredSpy = sandbox.stub(); + requestedOutreachSpy = sandbox.stub(); + engine = engineFactory(); const sendRule = factories.rule({ conditions: sendConditions, - event: sendEvent - }) - engine.addRule(sendRule) + event: sendEvent, + }); + engine.addRule(sendRule); const outreachRule = factories.rule({ conditions: outreachConditions, - event: outreachEvent - }) - engine.addRule(outreachRule) - - engine.setCondition(name, condition) - - engine.addFact('age', ageSpy) - engine.addFact('isRetired', isRetiredSpy) - engine.addFact('requestedOutreach', requestedOutreachSpy) - engine.on('success', eventSpy) - }) - - it('emits all events when all conditions are met', async () => { - ageSpy.returns(65) - isRetiredSpy.returns(true) - requestedOutreachSpy.returns(true) - await engine.run() + event: outreachEvent, + }); + engine.addRule(outreachRule); + + engine.setCondition(name, condition); + + engine.addFact("age", ageSpy); + engine.addFact("isRetired", isRetiredSpy); + engine.addFact("requestedOutreach", requestedOutreachSpy); + engine.on("success", eventSpy); + }); + + it("emits all events when all conditions are met", async () => { + ageSpy.returns(65); + isRetiredSpy.returns(true); + requestedOutreachSpy.returns(true); + await engine.run(); expect(eventSpy) .to.have.been.calledWith(sendEvent) - .and.to.have.been.calledWith(outreachEvent) - }) - - it('expands condition in rule results', async () => { - ageSpy.returns(65) - isRetiredSpy.returns(true) - requestedOutreachSpy.returns(true) - const { results } = await engine.run() + .and.to.have.been.calledWith(outreachEvent); + }); + + it("expands condition in rule results", async () => { + ageSpy.returns(65); + isRetiredSpy.returns(true); + requestedOutreachSpy.returns(true); + const { results } = await engine.run(); const nestedCondition = { - 'conditions.all[0].all[0].fact': 'age', - 'conditions.all[0].all[0].operator': 'greaterThanInclusive', - 'conditions.all[0].all[0].value': 60 - } - expect(results[0]).to.nested.include(nestedCondition) - expect(results[1]).to.nested.include(nestedCondition) - }) - }) - - describe('nested condition', () => { - const name1 = 'over60' + "conditions.all[0].all[0].fact": "age", + "conditions.all[0].all[0].operator": "greaterThanInclusive", + "conditions.all[0].all[0].value": 60, + }; + expect(results[0]).to.nested.include(nestedCondition); + expect(results[1]).to.nested.include(nestedCondition); + }); + }); + + describe("nested condition", () => { + const name1 = "over60"; const condition1 = { all: [ { - fact: 'age', - operator: 'greaterThanInclusive', - value: 60 - } - ] - } - - const name2 = 'earlyRetirement' + fact: "age", + operator: "greaterThanInclusive", + value: 60, + }, + ], + }; + + const name2 = "earlyRetirement"; const condition2 = { all: [ { not: { condition: name1 } }, { - fact: 'isRetired', - operator: 'equal', - value: true - } - ] - } + fact: "isRetired", + operator: "equal", + value: true, + }, + ], + }; const outreachEvent = { - type: 'triggerOutreach' - } + type: "triggerOutreach", + }; const outreachConditions = { all: [ { condition: name2 }, { - fact: 'requestedOutreach', - operator: 'equal', - value: true - } - ] - } - - let eventSpy - let ageSpy - let isRetiredSpy - let requestedOutreachSpy + fact: "requestedOutreach", + operator: "equal", + value: true, + }, + ], + }; + + let eventSpy; + let ageSpy; + let isRetiredSpy; + let requestedOutreachSpy; beforeEach(() => { - eventSpy = sandbox.spy() - ageSpy = sandbox.stub() - isRetiredSpy = sandbox.stub() - requestedOutreachSpy = sandbox.stub() - engine = engineFactory() + eventSpy = sandbox.spy(); + ageSpy = sandbox.stub(); + isRetiredSpy = sandbox.stub(); + requestedOutreachSpy = sandbox.stub(); + engine = engineFactory(); const outreachRule = factories.rule({ conditions: outreachConditions, - event: outreachEvent - }) - engine.addRule(outreachRule) - - engine.setCondition(name1, condition1) - - engine.setCondition(name2, condition2) - - engine.addFact('age', ageSpy) - engine.addFact('isRetired', isRetiredSpy) - engine.addFact('requestedOutreach', requestedOutreachSpy) - engine.on('success', eventSpy) - }) - - it('emits all events when all conditions are met', async () => { - ageSpy.returns(55) - isRetiredSpy.returns(true) - requestedOutreachSpy.returns(true) - await engine.run() - expect(eventSpy).to.have.been.calledWith(outreachEvent) - }) - - it('expands condition in rule results', async () => { - ageSpy.returns(55) - isRetiredSpy.returns(true) - requestedOutreachSpy.returns(true) - const { results } = await engine.run() + event: outreachEvent, + }); + engine.addRule(outreachRule); + + engine.setCondition(name1, condition1); + + engine.setCondition(name2, condition2); + + engine.addFact("age", ageSpy); + engine.addFact("isRetired", isRetiredSpy); + engine.addFact("requestedOutreach", requestedOutreachSpy); + engine.on("success", eventSpy); + }); + + it("emits all events when all conditions are met", async () => { + ageSpy.returns(55); + isRetiredSpy.returns(true); + requestedOutreachSpy.returns(true); + await engine.run(); + expect(eventSpy).to.have.been.calledWith(outreachEvent); + }); + + it("expands condition in rule results", async () => { + ageSpy.returns(55); + isRetiredSpy.returns(true); + requestedOutreachSpy.returns(true); + const { results } = await engine.run(); const nestedCondition = { - 'conditions.all[0].all[0].not.all[0].fact': 'age', - 'conditions.all[0].all[0].not.all[0].operator': 'greaterThanInclusive', - 'conditions.all[0].all[0].not.all[0].value': 60, - 'conditions.all[0].all[1].fact': 'isRetired', - 'conditions.all[0].all[1].operator': 'equal', - 'conditions.all[0].all[1].value': true - } - expect(results[0]).to.nested.include(nestedCondition) - }) - }) - - describe('top-level condition reference', () => { + "conditions.all[0].all[0].not.all[0].fact": "age", + "conditions.all[0].all[0].not.all[0].operator": "greaterThanInclusive", + "conditions.all[0].all[0].not.all[0].value": 60, + "conditions.all[0].all[1].fact": "isRetired", + "conditions.all[0].all[1].operator": "equal", + "conditions.all[0].all[1].value": true, + }; + expect(results[0]).to.nested.include(nestedCondition); + }); + }); + + describe("top-level condition reference", () => { const sendEvent = { - type: 'checkSending', + type: "checkSending", params: { - sendRetirementPayment: true - } - } + sendRetirementPayment: true, + }, + }; - const retiredName = 'retired' + const retiredName = "retired"; const retiredCondition = { - all: [ - { fact: 'isRetired', operator: 'equal', value: true } - ] - } + all: [{ fact: "isRetired", operator: "equal", value: true }], + }; const sendConditions = { - condition: retiredName - } + condition: retiredName, + }; - let eventSpy + let eventSpy; beforeEach(() => { - eventSpy = sandbox.spy() + eventSpy = sandbox.spy(); const sendRule = factories.rule({ conditions: sendConditions, - event: sendEvent - }) - engine = engineFactory() - - engine.addRule(sendRule) - engine.setCondition(retiredName, retiredCondition) - - engine.addFact('isRetired', true) - engine.on('success', eventSpy) - }) - - it('evaluates top level conditions correctly', async () => { - await engine.run() - expect(eventSpy).to.have.been.called() - }) - }) -}) + event: sendEvent, + }); + engine = engineFactory(); + + engine.addRule(sendRule); + engine.setCondition(retiredName, retiredCondition); + + engine.addFact("isRetired", true); + engine.on("success", eventSpy); + }); + + it("evaluates top level conditions correctly", async () => { + await engine.run(); + expect(eventSpy).to.have.been.called(); + }); + }); +}); diff --git a/test/engine-controls.test.js b/test/engine-controls.test.js index ad9b727f..c450fe62 100644 --- a/test/engine-controls.test.js +++ b/test/engine-controls.test.js @@ -1,65 +1,69 @@ -'use strict' +"use strict"; -import engineFactory from '../src/index' -import sinon from 'sinon' +import engineFactory from "../src/index"; +import sinon from "sinon"; -describe('Engine: fact priority', () => { - let engine - let sandbox +describe("Engine: fact priority", () => { + let engine; + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); afterEach(() => { - sandbox.restore() - }) - const event = { type: 'adult-human-admins' } + sandbox.restore(); + }); + const event = { type: "adult-human-admins" }; - let eventSpy - let ageStub - let segmentStub + let eventSpy; + let ageStub; + let segmentStub; - function setup () { - ageStub = sandbox.stub() - segmentStub = sandbox.stub() - eventSpy = sandbox.stub() - engine = engineFactory() + function setup() { + ageStub = sandbox.stub(); + segmentStub = sandbox.stub(); + eventSpy = sandbox.stub(); + engine = engineFactory(); let conditions = { - any: [{ - fact: 'age', - operator: 'greaterThanInclusive', - value: 18 - }] - } - let rule = factories.rule({ conditions, event, priority: 100 }) - engine.addRule(rule) + any: [ + { + fact: "age", + operator: "greaterThanInclusive", + value: 18, + }, + ], + }; + let rule = factories.rule({ conditions, event, priority: 100 }); + engine.addRule(rule); conditions = { - any: [{ - fact: 'segment', - operator: 'equal', - value: 'human' - }] - } - rule = factories.rule({ conditions, event }) - engine.addRule(rule) + any: [ + { + fact: "segment", + operator: "equal", + value: "human", + }, + ], + }; + rule = factories.rule({ conditions, event }); + engine.addRule(rule); - engine.addFact('age', ageStub, { priority: 100 }) - engine.addFact('segment', segmentStub, { priority: 50 }) + engine.addFact("age", ageStub, { priority: 100 }); + engine.addFact("segment", segmentStub, { priority: 50 }); } - describe('stop()', () => { - it('stops the rules from executing', async () => { - setup() - ageStub.returns(20) // success - engine.on('success', (event) => { - eventSpy() - engine.stop() - }) - await engine.run() - expect(eventSpy).to.have.been.calledOnce() - expect(ageStub).to.have.been.calledOnce() - expect(segmentStub).to.not.have.been.called() - }) - }) -}) + describe("stop()", () => { + it("stops the rules from executing", async () => { + setup(); + ageStub.returns(20); // success + engine.on("success", (event) => { + eventSpy(); + engine.stop(); + }); + await engine.run(); + expect(eventSpy).to.have.been.calledOnce(); + expect(ageStub).to.have.been.calledOnce(); + expect(segmentStub).to.not.have.been.called(); + }); + }); +}); diff --git a/test/engine-custom-properties.test.js b/test/engine-custom-properties.test.js index 086e1f16..9753ab85 100644 --- a/test/engine-custom-properties.test.js +++ b/test/engine-custom-properties.test.js @@ -1,65 +1,70 @@ -'use strict' +"use strict"; -import engineFactory, { Fact, Rule } from '../src/index' +import engineFactory, { Fact, Rule } from "../src/index"; -describe('Engine: custom properties', () => { - let engine - const event = { type: 'generic' } +describe("Engine: custom properties", () => { + let engine; + const event = { type: "generic" }; - describe('all conditions', () => { - it('preserves custom properties set on fact', () => { - engine = engineFactory() - const fact = new Fact('age', 12) - fact.customId = 'uuid' - engine.addFact(fact) - expect(engine.facts.get('age')).to.have.property('customId') - expect(engine.facts.get('age').customId).to.equal(fact.customId) - }) + describe("all conditions", () => { + it("preserves custom properties set on fact", () => { + engine = engineFactory(); + const fact = new Fact("age", 12); + fact.customId = "uuid"; + engine.addFact(fact); + expect(engine.facts.get("age")).to.have.property("customId"); + expect(engine.facts.get("age").customId).to.equal(fact.customId); + }); - describe('conditions', () => { - it('preserves custom properties set on boolean conditions', () => { - engine = engineFactory() + describe("conditions", () => { + it("preserves custom properties set on boolean conditions", () => { + engine = engineFactory(); const conditions = { - customId: 'uuid1', - all: [{ - fact: 'age', - operator: 'greaterThanInclusive', - value: 18 - }] - } - const rule = factories.rule({ conditions, event }) - engine.addRule(rule) - expect(engine.rules[0].conditions).to.have.property('customId') - }) + customId: "uuid1", + all: [ + { + fact: "age", + operator: "greaterThanInclusive", + value: 18, + }, + ], + }; + const rule = factories.rule({ conditions, event }); + engine.addRule(rule); + expect(engine.rules[0].conditions).to.have.property("customId"); + }); - it('preserves custom properties set on regular conditions', () => { - engine = engineFactory() + it("preserves custom properties set on regular conditions", () => { + engine = engineFactory(); const conditions = { - all: [{ - customId: 'uuid', - fact: 'age', - operator: 'greaterThanInclusive', - value: 18 - }] - } - const rule = factories.rule({ conditions, event }) - engine.addRule(rule) - expect(engine.rules[0].conditions.all[0]).to.have.property('customId') - expect(engine.rules[0].conditions.all[0].customId).equal('uuid') - }) - }) + all: [ + { + customId: "uuid", + fact: "age", + operator: "greaterThanInclusive", + value: 18, + }, + ], + }; + const rule = factories.rule({ conditions, event }); + engine.addRule(rule); + expect(engine.rules[0].conditions.all[0]).to.have.property("customId"); + expect(engine.rules[0].conditions.all[0].customId).equal("uuid"); + }); + }); - it('preserves custom properties set on regular conditions', () => { - engine = engineFactory() - const rule = new Rule() - const ruleProperties = factories.rule() - rule.setPriority(ruleProperties.priority) + it("preserves custom properties set on regular conditions", () => { + engine = engineFactory(); + const rule = new Rule(); + const ruleProperties = factories.rule(); + rule + .setPriority(ruleProperties.priority) .setConditions(ruleProperties.conditions) - .setEvent(ruleProperties.event) - rule.customId = 'uuid' - engine.addRule(rule) - expect(engine.rules[0]).to.have.property('customId') - expect(engine.rules[0].customId).equal('uuid') - }) - }) -}) + .setEvent(ruleProperties.event); + rule.customId = "uuid"; + engine.addRule(rule); + expect(engine.rules[0]).to.have.property("customId"); + expect(engine.rules[0].customId).equal("uuid"); + }); + }); +}); diff --git a/test/engine-error-handling.test.js b/test/engine-error-handling.test.js index 4e85795b..871d4933 100644 --- a/test/engine-error-handling.test.js +++ b/test/engine-error-handling.test.js @@ -1,28 +1,30 @@ -'use strict' +"use strict"; -import engineFactory from '../src/index' +import engineFactory from "../src/index"; -describe('Engine: failure', () => { - let engine +describe("Engine: failure", () => { + let engine; - const event = { type: 'generic' } + const event = { type: "generic" }; const conditions = { - any: [{ - fact: 'age', - operator: 'greaterThanInclusive', - value: 21 - }] - } + any: [ + { + fact: "age", + operator: "greaterThanInclusive", + value: 21, + }, + ], + }; beforeEach(() => { - engine = engineFactory() - const determineDrinkingAgeRule = factories.rule({ conditions, event }) - engine.addRule(determineDrinkingAgeRule) - engine.addFact('age', function (params, engine) { - throw new Error('problem occurred') - }) - }) + engine = engineFactory(); + const determineDrinkingAgeRule = factories.rule({ conditions, event }); + engine.addRule(determineDrinkingAgeRule); + engine.addFact("age", function (params, engine) { + throw new Error("problem occurred"); + }); + }); - it('surfaces errors', () => { - return expect(engine.run()).to.eventually.rejectedWith(/problem occurred/) - }) -}) + it("surfaces errors", () => { + return expect(engine.run()).to.eventually.rejectedWith(/problem occurred/); + }); +}); diff --git a/test/engine-event.test.js b/test/engine-event.test.js index c929d92e..2ebd7df8 100644 --- a/test/engine-event.test.js +++ b/test/engine-event.test.js @@ -1,605 +1,611 @@ -'use strict' +"use strict"; -import engineFactory, { Fact } from '../src/index' -import Almanac from '../src/almanac' -import sinon from 'sinon' +import engineFactory, { Fact } from "../src/index"; +import Almanac from "../src/almanac"; +import sinon from "sinon"; -describe('Engine: event', () => { - let engine - let sandbox +describe("Engine: event", () => { + let engine; + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); afterEach(() => { - sandbox.restore() - }) + sandbox.restore(); + }); const event = { - type: 'setDrinkingFlag', + type: "setDrinkingFlag", params: { - canOrderDrinks: true - } - } + canOrderDrinks: true, + }, + }; /** * sets up a simple 'any' rule with 2 conditions */ - function simpleSetup () { + function simpleSetup() { const conditions = { any: [ { - name: 'over 21', - fact: 'age', - operator: 'greaterThanInclusive', - value: 21 + name: "over 21", + fact: "age", + operator: "greaterThanInclusive", + value: 21, }, { - fact: 'qualified', - operator: 'equal', - value: true - } - ] - } - engine = engineFactory() - const ruleOptions = { conditions, event, priority: 100 } - const determineDrinkingAgeRule = factories.rule(ruleOptions) - engine.addRule(determineDrinkingAgeRule) + fact: "qualified", + operator: "equal", + value: true, + }, + ], + }; + engine = engineFactory(); + const ruleOptions = { conditions, event, priority: 100 }; + const determineDrinkingAgeRule = factories.rule(ruleOptions); + engine.addRule(determineDrinkingAgeRule); // age will succeed because 21 >= 21 - engine.addFact('age', 21) + engine.addFact("age", 21); // set 'qualified' to fail. rule will succeed because of 'any' - engine.addFact('qualified', false) + engine.addFact("qualified", false); } /** * sets up a complex rule with nested conditions */ - function advancedSetup () { + function advancedSetup() { const conditions = { any: [ { - fact: 'age', - operator: 'greaterThanInclusive', - value: 21 + fact: "age", + operator: "greaterThanInclusive", + value: 21, }, { - fact: 'qualified', - operator: 'equal', - value: true + fact: "qualified", + operator: "equal", + value: true, }, { all: [ { - fact: 'zipCode', - operator: 'in', - value: [80211, 80403] + fact: "zipCode", + operator: "in", + value: [80211, 80403], }, { - fact: 'gender', - operator: 'notEqual', - value: 'female' - } - ] - } - ] - } - engine = engineFactory() - const ruleOptions = { conditions, event, priority: 100 } - const determineDrinkingAgeRule = factories.rule(ruleOptions) - engine.addRule(determineDrinkingAgeRule) + fact: "gender", + operator: "notEqual", + value: "female", + }, + ], + }, + ], + }; + engine = engineFactory(); + const ruleOptions = { conditions, event, priority: 100 }; + const determineDrinkingAgeRule = factories.rule(ruleOptions); + engine.addRule(determineDrinkingAgeRule); // rule will succeed because of 'any' - engine.addFact('age', 10) // age fails - engine.addFact('qualified', false) // qualified fails. - engine.addFact('zipCode', 80403) // zipCode succeeds - engine.addFact('gender', 'male') // gender succeeds + engine.addFact("age", 10); // age fails + engine.addFact("qualified", false); // qualified fails. + engine.addFact("zipCode", 80403); // zipCode succeeds + engine.addFact("gender", "male"); // gender succeeds } - context('engine events: simple', () => { - beforeEach(() => simpleSetup()) + context("engine events: simple", () => { + beforeEach(() => simpleSetup()); it('"success" passes the event, almanac, and results', async () => { - const failureSpy = sandbox.spy() - const successSpy = sandbox.spy() - function assertResult (ruleResult) { - expect(ruleResult.result).to.be.true() - expect(ruleResult.conditions.any[0].result).to.be.true() - expect(ruleResult.conditions.any[0].factResult).to.equal(21) - expect(ruleResult.conditions.any[0].name).to.equal('over 21') - expect(ruleResult.conditions.any[1].result).to.be.false() - expect(ruleResult.conditions.any[1].factResult).to.equal(false) + const failureSpy = sandbox.spy(); + const successSpy = sandbox.spy(); + function assertResult(ruleResult) { + expect(ruleResult.result).to.be.true(); + expect(ruleResult.conditions.any[0].result).to.be.true(); + expect(ruleResult.conditions.any[0].factResult).to.equal(21); + expect(ruleResult.conditions.any[0].name).to.equal("over 21"); + expect(ruleResult.conditions.any[1].result).to.be.false(); + expect(ruleResult.conditions.any[1].factResult).to.equal(false); } - engine.on('success', function (e, almanac, ruleResult) { - expect(e).to.eql(event) - expect(almanac).to.be.an.instanceof(Almanac) - assertResult(ruleResult) - successSpy() - }) - engine.on('failure', failureSpy) - - const { results, failureResults } = await engine.run() - - expect(failureResults).to.have.lengthOf(0) - expect(results).to.have.lengthOf(1) - assertResult(results[0]) - expect(failureSpy.callCount).to.equal(0) - expect(successSpy.callCount).to.equal(1) - }) + engine.on("success", function (e, almanac, ruleResult) { + expect(e).to.eql(event); + expect(almanac).to.be.an.instanceof(Almanac); + assertResult(ruleResult); + successSpy(); + }); + engine.on("failure", failureSpy); + + const { results, failureResults } = await engine.run(); + + expect(failureResults).to.have.lengthOf(0); + expect(results).to.have.lengthOf(1); + assertResult(results[0]); + expect(failureSpy.callCount).to.equal(0); + expect(successSpy.callCount).to.equal(1); + }); it('"event.type" passes the event parameters, almanac, and results', async () => { - const failureSpy = sandbox.spy() - const successSpy = sandbox.spy() - function assertResult (ruleResult) { - expect(ruleResult.result).to.be.true() - expect(ruleResult.conditions.any[0].result).to.be.true() - expect(ruleResult.conditions.any[0].factResult).to.equal(21) - expect(ruleResult.conditions.any[1].result).to.be.false() - expect(ruleResult.conditions.any[1].factResult).to.equal(false) + const failureSpy = sandbox.spy(); + const successSpy = sandbox.spy(); + function assertResult(ruleResult) { + expect(ruleResult.result).to.be.true(); + expect(ruleResult.conditions.any[0].result).to.be.true(); + expect(ruleResult.conditions.any[0].factResult).to.equal(21); + expect(ruleResult.conditions.any[1].result).to.be.false(); + expect(ruleResult.conditions.any[1].factResult).to.equal(false); } engine.on(event.type, function (params, almanac, ruleResult) { - expect(params).to.eql(event.params) - expect(almanac).to.be.an.instanceof(Almanac) - assertResult(ruleResult) - successSpy() - }) - engine.on('failure', failureSpy) + expect(params).to.eql(event.params); + expect(almanac).to.be.an.instanceof(Almanac); + assertResult(ruleResult); + successSpy(); + }); + engine.on("failure", failureSpy); - const { results, failureResults } = await engine.run() + const { results, failureResults } = await engine.run(); - expect(failureResults).to.have.lengthOf(0) - expect(results).to.have.lengthOf(1) - assertResult(results[0]) + expect(failureResults).to.have.lengthOf(0); + expect(results).to.have.lengthOf(1); + assertResult(results[0]); - expect(failureSpy.callCount).to.equal(0) - expect(successSpy.callCount).to.equal(1) - }) + expect(failureSpy.callCount).to.equal(0); + expect(successSpy.callCount).to.equal(1); + }); it('"failure" passes the event, almanac, and results', async () => { - const AGE = 10 - const failureSpy = sandbox.spy() - const successSpy = sandbox.spy() - function assertResult (ruleResult) { - expect(ruleResult.result).to.be.false() - expect(ruleResult.conditions.any[0].result).to.be.false() - expect(ruleResult.conditions.any[0].factResult).to.equal(AGE) - expect(ruleResult.conditions.any[1].result).to.be.false() - expect(ruleResult.conditions.any[1].factResult).to.equal(false) + const AGE = 10; + const failureSpy = sandbox.spy(); + const successSpy = sandbox.spy(); + function assertResult(ruleResult) { + expect(ruleResult.result).to.be.false(); + expect(ruleResult.conditions.any[0].result).to.be.false(); + expect(ruleResult.conditions.any[0].factResult).to.equal(AGE); + expect(ruleResult.conditions.any[1].result).to.be.false(); + expect(ruleResult.conditions.any[1].factResult).to.equal(false); } - engine.on('failure', function (e, almanac, ruleResult) { - expect(e).to.eql(event) - expect(almanac).to.be.an.instanceof(Almanac) - assertResult(ruleResult) - failureSpy() - }) - engine.on('success', successSpy) - engine.addFact('age', AGE) // age fails + engine.on("failure", function (e, almanac, ruleResult) { + expect(e).to.eql(event); + expect(almanac).to.be.an.instanceof(Almanac); + assertResult(ruleResult); + failureSpy(); + }); + engine.on("success", successSpy); + engine.addFact("age", AGE); // age fails - const { results, failureResults } = await engine.run() + const { results, failureResults } = await engine.run(); - expect(failureResults).to.have.lengthOf(1) - expect(results).to.have.lengthOf(0) - assertResult(failureResults[0]) + expect(failureResults).to.have.lengthOf(1); + expect(results).to.have.lengthOf(0); + assertResult(failureResults[0]); - expect(failureSpy.callCount).to.equal(1) - expect(successSpy.callCount).to.equal(0) - }) + expect(failureSpy.callCount).to.equal(1); + expect(successSpy.callCount).to.equal(0); + }); - it('allows facts to be added by the event handler, affecting subsequent rules', () => { - const drinkOrderParams = { wine: 'merlot', quantity: 2 } + it("allows facts to be added by the event handler, affecting subsequent rules", () => { + const drinkOrderParams = { wine: "merlot", quantity: 2 }; const drinkOrderEvent = { - type: 'offerDrink', - params: drinkOrderParams - } + type: "offerDrink", + params: drinkOrderParams, + }; const drinkOrderConditions = { any: [ { - fact: 'canOrderDrinks', - operator: 'equal', - value: true - } - ] - } + fact: "canOrderDrinks", + operator: "equal", + value: true, + }, + ], + }; const drinkOrderRule = factories.rule({ conditions: drinkOrderConditions, event: drinkOrderEvent, - priority: 1 - }) - engine.addRule(drinkOrderRule) + priority: 1, + }); + engine.addRule(drinkOrderRule); return new Promise((resolve, reject) => { - engine.on('success', function (event, almanac, ruleResult) { + engine.on("success", function (event, almanac, ruleResult) { switch (event.type) { - case 'setDrinkingFlag': + case "setDrinkingFlag": almanac.addRuntimeFact( - 'canOrderDrinks', - event.params.canOrderDrinks - ) - break - case 'offerDrink': - expect(event.params).to.eql(drinkOrderParams) - break + "canOrderDrinks", + event.params.canOrderDrinks, + ); + break; + case "offerDrink": + expect(event.params).to.eql(drinkOrderParams); + break; default: - reject(new Error('default case not expected')) + reject(new Error("default case not expected")); } - }) - engine.run().then(resolve).catch(reject) - }) - }) - }) + }); + engine.run().then(resolve).catch(reject); + }); + }); + }); - context('engine events: advanced', () => { - beforeEach(() => advancedSetup()) + context("engine events: advanced", () => { + beforeEach(() => advancedSetup()); it('"success" passes the event, almanac, and results', async () => { - const failureSpy = sandbox.spy() - const successSpy = sandbox.spy() - - function assertResult (ruleResult) { - expect(ruleResult.result).to.be.true() - expect(ruleResult.conditions.any[0].result).to.be.false() - expect(ruleResult.conditions.any[0].factResult).to.equal(10) - expect(ruleResult.conditions.any[1].result).to.be.false() - expect(ruleResult.conditions.any[1].factResult).to.equal(false) - expect(ruleResult.conditions.any[2].result).to.be.true() - expect(ruleResult.conditions.any[2].all[0].result).to.be.true() - expect(ruleResult.conditions.any[2].all[0].factResult).to.equal(80403) - expect(ruleResult.conditions.any[2].all[1].result).to.be.true() - expect(ruleResult.conditions.any[2].all[1].factResult).to.equal('male') + const failureSpy = sandbox.spy(); + const successSpy = sandbox.spy(); + + function assertResult(ruleResult) { + expect(ruleResult.result).to.be.true(); + expect(ruleResult.conditions.any[0].result).to.be.false(); + expect(ruleResult.conditions.any[0].factResult).to.equal(10); + expect(ruleResult.conditions.any[1].result).to.be.false(); + expect(ruleResult.conditions.any[1].factResult).to.equal(false); + expect(ruleResult.conditions.any[2].result).to.be.true(); + expect(ruleResult.conditions.any[2].all[0].result).to.be.true(); + expect(ruleResult.conditions.any[2].all[0].factResult).to.equal(80403); + expect(ruleResult.conditions.any[2].all[1].result).to.be.true(); + expect(ruleResult.conditions.any[2].all[1].factResult).to.equal("male"); } - engine.on('success', function (e, almanac, ruleResult) { - expect(e).to.eql(event) - expect(almanac).to.be.an.instanceof(Almanac) - assertResult(ruleResult) - successSpy() - }) - engine.on('failure', failureSpy) + engine.on("success", function (e, almanac, ruleResult) { + expect(e).to.eql(event); + expect(almanac).to.be.an.instanceof(Almanac); + assertResult(ruleResult); + successSpy(); + }); + engine.on("failure", failureSpy); - const { results, failureResults } = await engine.run() + const { results, failureResults } = await engine.run(); - assertResult(results[0]) - expect(failureResults).to.have.lengthOf(0) - expect(results).to.have.lengthOf(1) - expect(failureSpy.callCount).to.equal(0) - expect(successSpy.callCount).to.equal(1) - }) + assertResult(results[0]); + expect(failureResults).to.have.lengthOf(0); + expect(results).to.have.lengthOf(1); + expect(failureSpy.callCount).to.equal(0); + expect(successSpy.callCount).to.equal(1); + }); it('"failure" passes the event, almanac, and results', async () => { - const ZIP_CODE = 99992 - const GENDER = 'female' - const failureSpy = sandbox.spy() - const successSpy = sandbox.spy() - function assertResult (ruleResult) { - expect(ruleResult.result).to.be.false() - expect(ruleResult.conditions.any[0].result).to.be.false() - expect(ruleResult.conditions.any[0].factResult).to.equal(10) - expect(ruleResult.conditions.any[1].result).to.be.false() - expect(ruleResult.conditions.any[1].factResult).to.equal(false) - expect(ruleResult.conditions.any[2].result).to.be.false() - expect(ruleResult.conditions.any[2].all[0].result).to.be.false() + const ZIP_CODE = 99992; + const GENDER = "female"; + const failureSpy = sandbox.spy(); + const successSpy = sandbox.spy(); + function assertResult(ruleResult) { + expect(ruleResult.result).to.be.false(); + expect(ruleResult.conditions.any[0].result).to.be.false(); + expect(ruleResult.conditions.any[0].factResult).to.equal(10); + expect(ruleResult.conditions.any[1].result).to.be.false(); + expect(ruleResult.conditions.any[1].factResult).to.equal(false); + expect(ruleResult.conditions.any[2].result).to.be.false(); + expect(ruleResult.conditions.any[2].all[0].result).to.be.false(); expect(ruleResult.conditions.any[2].all[0].factResult).to.equal( - ZIP_CODE - ) - expect(ruleResult.conditions.any[2].all[1].result).to.be.false() - expect(ruleResult.conditions.any[2].all[1].factResult).to.equal(GENDER) + ZIP_CODE, + ); + expect(ruleResult.conditions.any[2].all[1].result).to.be.false(); + expect(ruleResult.conditions.any[2].all[1].factResult).to.equal(GENDER); } - engine.on('failure', function (e, almanac, ruleResult) { - expect(e).to.eql(event) - expect(almanac).to.be.an.instanceof(Almanac) - assertResult(ruleResult) - failureSpy() - }) - engine.on('success', successSpy) - engine.addFact('zipCode', ZIP_CODE) // zipCode fails - engine.addFact('gender', GENDER) // gender fails - - const { results, failureResults } = await engine.run() - - assertResult(failureResults[0]) - expect(failureResults).to.have.lengthOf(1) - expect(results).to.have.lengthOf(0) - - expect(failureSpy.callCount).to.equal(1) - expect(successSpy.callCount).to.equal(0) - }) - }) - - context('engine events: with facts', () => { + engine.on("failure", function (e, almanac, ruleResult) { + expect(e).to.eql(event); + expect(almanac).to.be.an.instanceof(Almanac); + assertResult(ruleResult); + failureSpy(); + }); + engine.on("success", successSpy); + engine.addFact("zipCode", ZIP_CODE); // zipCode fails + engine.addFact("gender", GENDER); // gender fails + + const { results, failureResults } = await engine.run(); + + assertResult(failureResults[0]); + expect(failureResults).to.have.lengthOf(1); + expect(results).to.have.lengthOf(0); + + expect(failureSpy.callCount).to.equal(1); + expect(successSpy.callCount).to.equal(0); + }); + }); + + context("engine events: with facts", () => { const eventWithFact = { - type: 'countedEnough', + type: "countedEnough", params: { - count: { fact: 'count' } - } - } + count: { fact: "count" }, + }, + }; - const expectedEvent = { type: 'countedEnough', params: { count: 5 } } + const expectedEvent = { type: "countedEnough", params: { count: 5 } }; - function setup (replaceFactsInEventParams, event = eventWithFact) { + function setup(replaceFactsInEventParams, event = eventWithFact) { const conditions = { any: [ { - fact: 'success', - operator: 'equal', - value: true - } - ] - } - - const ruleOptions = { conditions, event, priority: 100 } - const countedEnoughRule = factories.rule(ruleOptions) + fact: "success", + operator: "equal", + value: true, + }, + ], + }; + + const ruleOptions = { conditions, event, priority: 100 }; + const countedEnoughRule = factories.rule(ruleOptions); engine = engineFactory([countedEnoughRule], { - replaceFactsInEventParams - }) + replaceFactsInEventParams, + }); } - context('without flag', () => { - beforeEach(() => setup(false)) + context("without flag", () => { + beforeEach(() => setup(false)); it('"success" passes the event without resolved facts', async () => { - const successSpy = sandbox.spy() - engine.on('success', successSpy) - const { results } = await engine.run({ success: true, count: 5 }) - expect(results[0].event).to.deep.equal(eventWithFact) - expect(successSpy.firstCall.args[0]).to.deep.equal(eventWithFact) - }) - - it('failure passes the event without resolved facts', async () => { - const failureSpy = sandbox.spy() - engine.on('failure', failureSpy) - const { failureResults } = await engine.run({ success: false, count: 5 }) - expect(failureResults[0].event).to.deep.equal(eventWithFact) - expect(failureSpy.firstCall.args[0]).to.deep.equal(eventWithFact) - }) - }) - context('with flag', () => { - beforeEach(() => setup(true)) + const successSpy = sandbox.spy(); + engine.on("success", successSpy); + const { results } = await engine.run({ success: true, count: 5 }); + expect(results[0].event).to.deep.equal(eventWithFact); + expect(successSpy.firstCall.args[0]).to.deep.equal(eventWithFact); + }); + + it("failure passes the event without resolved facts", async () => { + const failureSpy = sandbox.spy(); + engine.on("failure", failureSpy); + const { failureResults } = await engine.run({ + success: false, + count: 5, + }); + expect(failureResults[0].event).to.deep.equal(eventWithFact); + expect(failureSpy.firstCall.args[0]).to.deep.equal(eventWithFact); + }); + }); + context("with flag", () => { + beforeEach(() => setup(true)); it('"success" passes the event with resolved facts', async () => { - const successSpy = sandbox.spy() - engine.on('success', successSpy) - const { results } = await engine.run({ success: true, count: 5 }) - expect(results[0].event).to.deep.equal(expectedEvent) - expect(successSpy.firstCall.args[0]).to.deep.equal(expectedEvent) - }) - - it('failure passes the event with resolved facts', async () => { - const failureSpy = sandbox.spy() - engine.on('failure', failureSpy) - const { failureResults } = await engine.run({ success: false, count: 5 }) - expect(failureResults[0].event).to.deep.equal(expectedEvent) - expect(failureSpy.firstCall.args[0]).to.deep.equal(expectedEvent) - }) - context('using fact params and path', () => { + const successSpy = sandbox.spy(); + engine.on("success", successSpy); + const { results } = await engine.run({ success: true, count: 5 }); + expect(results[0].event).to.deep.equal(expectedEvent); + expect(successSpy.firstCall.args[0]).to.deep.equal(expectedEvent); + }); + + it("failure passes the event with resolved facts", async () => { + const failureSpy = sandbox.spy(); + engine.on("failure", failureSpy); + const { failureResults } = await engine.run({ + success: false, + count: 5, + }); + expect(failureResults[0].event).to.deep.equal(expectedEvent); + expect(failureSpy.firstCall.args[0]).to.deep.equal(expectedEvent); + }); + context("using fact params and path", () => { const eventWithFactWithParamsAndPath = { - type: 'countedEnough', + type: "countedEnough", params: { count: { - fact: 'count', + fact: "count", params: { incrementBy: 5 }, - path: '$.next' - } - } - } + path: "$.next", + }, + }, + }; beforeEach(() => { - setup(true, eventWithFactWithParamsAndPath) + setup(true, eventWithFactWithParamsAndPath); engine.addFact( - new Fact('count', async ({ incrementBy }) => { + new Fact("count", async ({ incrementBy }) => { return { previous: 0, - next: incrementBy - } - }) - ) - }) + next: incrementBy, + }; + }), + ); + }); it('"success" passes the event with resolved facts', async () => { - const successSpy = sandbox.spy() - engine.on('success', successSpy) - const { results } = await engine.run({ success: true }) - expect(results[0].event).to.deep.equal(expectedEvent) - expect(successSpy.firstCall.args[0]).to.deep.equal(expectedEvent) - }) - - it('failure passes the event with resolved facts', async () => { - const failureSpy = sandbox.spy() - engine.on('failure', failureSpy) - const { failureResults } = await engine.run({ success: false }) - expect(failureResults[0].event).to.deep.equal(expectedEvent) - expect(failureSpy.firstCall.args[0]).to.deep.equal(expectedEvent) - }) - }) - }) - }) - - context('rule events: simple', () => { - beforeEach(() => simpleSetup()) - - it('the rule result is a _copy_ of the rule`s conditions, and unaffected by mutation', async () => { - const rule = engine.rules[0] - let firstPass - rule.on('success', function (e, almanac, ruleResult) { - firstPass = ruleResult - delete ruleResult.conditions.any // subsequently modify the conditions in this rule result - }) - await engine.run() + const successSpy = sandbox.spy(); + engine.on("success", successSpy); + const { results } = await engine.run({ success: true }); + expect(results[0].event).to.deep.equal(expectedEvent); + expect(successSpy.firstCall.args[0]).to.deep.equal(expectedEvent); + }); + + it("failure passes the event with resolved facts", async () => { + const failureSpy = sandbox.spy(); + engine.on("failure", failureSpy); + const { failureResults } = await engine.run({ success: false }); + expect(failureResults[0].event).to.deep.equal(expectedEvent); + expect(failureSpy.firstCall.args[0]).to.deep.equal(expectedEvent); + }); + }); + }); + }); + + context("rule events: simple", () => { + beforeEach(() => simpleSetup()); + + it("the rule result is a _copy_ of the rule`s conditions, and unaffected by mutation", async () => { + const rule = engine.rules[0]; + let firstPass; + rule.on("success", function (e, almanac, ruleResult) { + firstPass = ruleResult; + delete ruleResult.conditions.any; // subsequently modify the conditions in this rule result + }); + await engine.run(); // run the engine again, now that ruleResult.conditions was modified - let secondPass - rule.on('success', function (e, almanac, ruleResult) { - secondPass = ruleResult - }) - await engine.run() - - expect(firstPass).to.deep.equal(secondPass) // second pass was unaffected by first pass - }) - - it('on-success, it passes the event type and params', async () => { - const failureSpy = sandbox.spy() - const successSpy = sandbox.spy() - const rule = engine.rules[0] - function assertResult (ruleResult) { - expect(ruleResult.result).to.be.true() - expect(ruleResult.conditions.any[0].result).to.be.true() - expect(ruleResult.conditions.any[0].factResult).to.equal(21) - expect(ruleResult.conditions.any[1].result).to.be.false() - expect(ruleResult.conditions.any[1].factResult).to.equal(false) + let secondPass; + rule.on("success", function (e, almanac, ruleResult) { + secondPass = ruleResult; + }); + await engine.run(); + + expect(firstPass).to.deep.equal(secondPass); // second pass was unaffected by first pass + }); + + it("on-success, it passes the event type and params", async () => { + const failureSpy = sandbox.spy(); + const successSpy = sandbox.spy(); + const rule = engine.rules[0]; + function assertResult(ruleResult) { + expect(ruleResult.result).to.be.true(); + expect(ruleResult.conditions.any[0].result).to.be.true(); + expect(ruleResult.conditions.any[0].factResult).to.equal(21); + expect(ruleResult.conditions.any[1].result).to.be.false(); + expect(ruleResult.conditions.any[1].factResult).to.equal(false); } - rule.on('success', function (e, almanac, ruleResult) { - expect(e).to.eql(event) - expect(almanac).to.be.an.instanceof(Almanac) - expect(failureSpy.callCount).to.equal(0) - assertResult(ruleResult) - successSpy() - }) - rule.on('failure', failureSpy) - - const { results, failureResults } = await engine.run() - - assertResult(results[0]) - expect(failureResults).to.have.lengthOf(0) - expect(results).to.have.lengthOf(1) - - expect(successSpy.callCount).to.equal(1) - expect(failureSpy.callCount).to.equal(0) - }) - - it('on-failure, it passes the event type and params', async () => { - const AGE = 10 - const successSpy = sandbox.spy() - const failureSpy = sandbox.spy() - const rule = engine.rules[0] - function assertResult (ruleResult) { - expect(ruleResult.result).to.be.false() - expect(ruleResult.conditions.any[0].result).to.be.false() - expect(ruleResult.conditions.any[0].factResult).to.equal(AGE) - expect(ruleResult.conditions.any[1].result).to.be.false() - expect(ruleResult.conditions.any[1].factResult).to.equal(false) + rule.on("success", function (e, almanac, ruleResult) { + expect(e).to.eql(event); + expect(almanac).to.be.an.instanceof(Almanac); + expect(failureSpy.callCount).to.equal(0); + assertResult(ruleResult); + successSpy(); + }); + rule.on("failure", failureSpy); + + const { results, failureResults } = await engine.run(); + + assertResult(results[0]); + expect(failureResults).to.have.lengthOf(0); + expect(results).to.have.lengthOf(1); + + expect(successSpy.callCount).to.equal(1); + expect(failureSpy.callCount).to.equal(0); + }); + + it("on-failure, it passes the event type and params", async () => { + const AGE = 10; + const successSpy = sandbox.spy(); + const failureSpy = sandbox.spy(); + const rule = engine.rules[0]; + function assertResult(ruleResult) { + expect(ruleResult.result).to.be.false(); + expect(ruleResult.conditions.any[0].result).to.be.false(); + expect(ruleResult.conditions.any[0].factResult).to.equal(AGE); + expect(ruleResult.conditions.any[1].result).to.be.false(); + expect(ruleResult.conditions.any[1].factResult).to.equal(false); } - rule.on('failure', function (e, almanac, ruleResult) { - expect(e).to.eql(event) - expect(almanac).to.be.an.instanceof(Almanac) - expect(successSpy.callCount).to.equal(0) - assertResult(ruleResult) - failureSpy() - }) - rule.on('success', successSpy) + rule.on("failure", function (e, almanac, ruleResult) { + expect(e).to.eql(event); + expect(almanac).to.be.an.instanceof(Almanac); + expect(successSpy.callCount).to.equal(0); + assertResult(ruleResult); + failureSpy(); + }); + rule.on("success", successSpy); // both conditions will fail - engine.addFact('age', AGE) - const { results, failureResults } = await engine.run() - - assertResult(failureResults[0]) - expect(failureResults).to.have.lengthOf(1) - expect(results).to.have.lengthOf(0) - expect(failureSpy.callCount).to.equal(1) - expect(successSpy.callCount).to.equal(0) - }) - }) - - context('rule events: with facts', () => { - const expectedEvent = { type: 'countedEnough', params: { count: 5 } } + engine.addFact("age", AGE); + const { results, failureResults } = await engine.run(); + + assertResult(failureResults[0]); + expect(failureResults).to.have.lengthOf(1); + expect(results).to.have.lengthOf(0); + expect(failureSpy.callCount).to.equal(1); + expect(successSpy.callCount).to.equal(0); + }); + }); + + context("rule events: with facts", () => { + const expectedEvent = { type: "countedEnough", params: { count: 5 } }; const eventWithFact = { - type: 'countedEnough', + type: "countedEnough", params: { - count: { fact: 'count' } - } - } + count: { fact: "count" }, + }, + }; - function setup (replaceFactsInEventParams, event = eventWithFact) { + function setup(replaceFactsInEventParams, event = eventWithFact) { const conditions = { any: [ { - fact: 'success', - operator: 'equal', - value: true - } - ] - } - - const ruleOptions = { conditions, event, priority: 100 } - const countedEnoughRule = factories.rule(ruleOptions) + fact: "success", + operator: "equal", + value: true, + }, + ], + }; + + const ruleOptions = { conditions, event, priority: 100 }; + const countedEnoughRule = factories.rule(ruleOptions); engine = engineFactory([countedEnoughRule], { - replaceFactsInEventParams - }) + replaceFactsInEventParams, + }); } - context('without flag', () => { - beforeEach(() => setup(false)) + context("without flag", () => { + beforeEach(() => setup(false)); it('"success" passes the event without resolved facts', async () => { - const successSpy = sandbox.spy() - engine.rules[0].on('success', successSpy) - await engine.run({ success: true, count: 5 }) - expect(successSpy.firstCall.args[0]).to.deep.equal(eventWithFact) - }) - - it('failure passes the event without resolved facts', async () => { - const failureSpy = sandbox.spy() - engine.rules[0].on('failure', failureSpy) - await engine.run({ success: false, count: 5 }) - expect(failureSpy.firstCall.args[0]).to.deep.equal(eventWithFact) - }) - }) - context('with flag', () => { - beforeEach(() => setup(true)) + const successSpy = sandbox.spy(); + engine.rules[0].on("success", successSpy); + await engine.run({ success: true, count: 5 }); + expect(successSpy.firstCall.args[0]).to.deep.equal(eventWithFact); + }); + + it("failure passes the event without resolved facts", async () => { + const failureSpy = sandbox.spy(); + engine.rules[0].on("failure", failureSpy); + await engine.run({ success: false, count: 5 }); + expect(failureSpy.firstCall.args[0]).to.deep.equal(eventWithFact); + }); + }); + context("with flag", () => { + beforeEach(() => setup(true)); it('"success" passes the event with resolved facts', async () => { - const successSpy = sandbox.spy() - engine.rules[0].on('success', successSpy) - await engine.run({ success: true, count: 5 }) - expect(successSpy.firstCall.args[0]).to.deep.equal(expectedEvent) - }) - - it('failure passes the event with resolved facts', async () => { - const failureSpy = sandbox.spy() - engine.rules[0].on('failure', failureSpy) - await engine.run({ success: false, count: 5 }) - expect(failureSpy.firstCall.args[0]).to.deep.equal(expectedEvent) - }) - context('using fact params and path', () => { + const successSpy = sandbox.spy(); + engine.rules[0].on("success", successSpy); + await engine.run({ success: true, count: 5 }); + expect(successSpy.firstCall.args[0]).to.deep.equal(expectedEvent); + }); + + it("failure passes the event with resolved facts", async () => { + const failureSpy = sandbox.spy(); + engine.rules[0].on("failure", failureSpy); + await engine.run({ success: false, count: 5 }); + expect(failureSpy.firstCall.args[0]).to.deep.equal(expectedEvent); + }); + context("using fact params and path", () => { const eventWithFactWithParamsAndPath = { - type: 'countedEnough', + type: "countedEnough", params: { count: { - fact: 'count', + fact: "count", params: { incrementBy: 5 }, - path: '$.next' - } - } - } + path: "$.next", + }, + }, + }; beforeEach(() => { - setup(true, eventWithFactWithParamsAndPath) + setup(true, eventWithFactWithParamsAndPath); engine.addFact( - new Fact('count', async ({ incrementBy }) => { + new Fact("count", async ({ incrementBy }) => { return { previous: 0, - next: incrementBy - } - }) - ) - }) + next: incrementBy, + }; + }), + ); + }); it('"success" passes the event with resolved facts', async () => { - const successSpy = sandbox.spy() - engine.on('success', successSpy) - const { results } = await engine.run({ success: true }) - expect(results[0].event).to.deep.equal(expectedEvent) - expect(successSpy.firstCall.args[0]).to.deep.equal(expectedEvent) - }) - - it('failure passes the event with resolved facts', async () => { - const failureSpy = sandbox.spy() - engine.on('failure', failureSpy) - const { failureResults } = await engine.run({ success: false }) - expect(failureResults[0].event).to.deep.equal(expectedEvent) - expect(failureSpy.firstCall.args[0]).to.deep.equal(expectedEvent) - }) - }) - }) - }) - - context('rule events: json serializing', () => { - beforeEach(() => simpleSetup()) - it('serializes properties', async () => { - const successSpy = sandbox.spy() - const rule = engine.rules[0] - rule.on('success', successSpy) - await engine.run() - const ruleResult = successSpy.getCall(0).args[2] + const successSpy = sandbox.spy(); + engine.on("success", successSpy); + const { results } = await engine.run({ success: true }); + expect(results[0].event).to.deep.equal(expectedEvent); + expect(successSpy.firstCall.args[0]).to.deep.equal(expectedEvent); + }); + + it("failure passes the event with resolved facts", async () => { + const failureSpy = sandbox.spy(); + engine.on("failure", failureSpy); + const { failureResults } = await engine.run({ success: false }); + expect(failureResults[0].event).to.deep.equal(expectedEvent); + expect(failureSpy.firstCall.args[0]).to.deep.equal(expectedEvent); + }); + }); + }); + }); + + context("rule events: json serializing", () => { + beforeEach(() => simpleSetup()); + it("serializes properties", async () => { + const successSpy = sandbox.spy(); + const rule = engine.rules[0]; + rule.on("success", successSpy); + await engine.run(); + const ruleResult = successSpy.getCall(0).args[2]; const expected = - '{"conditions":{"priority":1,"any":[{"name":"over 21","operator":"greaterThanInclusive","value":21,"fact":"age","factResult":21,"result":true},{"operator":"equal","value":true,"fact":"qualified","factResult":false,"result":false}]},"event":{"type":"setDrinkingFlag","params":{"canOrderDrinks":true}},"priority":100,"result":true}' - expect(JSON.stringify(ruleResult)).to.equal(expected) - }) - }) -}) + '{"conditions":{"priority":1,"any":[{"name":"over 21","operator":"greaterThanInclusive","value":21,"fact":"age","factResult":21,"result":true},{"operator":"equal","value":true,"fact":"qualified","factResult":false,"result":false}]},"event":{"type":"setDrinkingFlag","params":{"canOrderDrinks":true}},"priority":100,"result":true}'; + expect(JSON.stringify(ruleResult)).to.equal(expected); + }); + }); +}); diff --git a/test/engine-fact-comparison.test.js b/test/engine-fact-comparison.test.js index 39af5903..92fe5457 100644 --- a/test/engine-fact-comparison.test.js +++ b/test/engine-fact-comparison.test.js @@ -1,121 +1,127 @@ -'use strict' +"use strict"; -import engineFactory from '../src/index' -import sinon from 'sinon' +import engineFactory from "../src/index"; +import sinon from "sinon"; -describe('Engine: fact to fact comparison', () => { - let engine - let sandbox +describe("Engine: fact to fact comparison", () => { + let engine; + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); afterEach(() => { - sandbox.restore() - }) - let eventSpy + sandbox.restore(); + }); + let eventSpy; - function setup (conditions) { - const event = { type: 'success-event' } - eventSpy = sandbox.spy() - engine = engineFactory() - const rule = factories.rule({ conditions, event }) - engine.addRule(rule) - engine.on('success', eventSpy) + function setup(conditions) { + const event = { type: "success-event" }; + eventSpy = sandbox.spy(); + engine = engineFactory(); + const rule = factories.rule({ conditions, event }); + engine.addRule(rule); + engine.on("success", eventSpy); } - context('constant facts', () => { + context("constant facts", () => { const constantCondition = { - all: [{ - fact: 'height', - operator: 'lessThanInclusive', - value: { - fact: 'width' - } - }] - } - it('allows a fact to retrieve other fact values', async () => { - setup(constantCondition) - await engine.run({ height: 1, width: 2 }) - expect(eventSpy).to.have.been.calledOnce() + all: [ + { + fact: "height", + operator: "lessThanInclusive", + value: { + fact: "width", + }, + }, + ], + }; + it("allows a fact to retrieve other fact values", async () => { + setup(constantCondition); + await engine.run({ height: 1, width: 2 }); + expect(eventSpy).to.have.been.calledOnce(); - sandbox.reset() + sandbox.reset(); - await engine.run({ height: 2, width: 1 }) // negative case - expect(eventSpy.callCount).to.equal(0) - }) - }) + await engine.run({ height: 2, width: 1 }); // negative case + expect(eventSpy.callCount).to.equal(0); + }); + }); - context('rules with parameterized conditions', () => { + context("rules with parameterized conditions", () => { const paramsCondition = { - all: [{ - fact: 'widthMultiplier', - params: { - multiplier: 2 - }, - operator: 'equal', - value: { - fact: 'heightMultiplier', + all: [ + { + fact: "widthMultiplier", params: { - multiplier: 4 - } - } - }] - } - it('honors the params', async () => { - setup(paramsCondition) - engine.addFact('heightMultiplier', async (params, almanac) => { - const height = await almanac.factValue('height') - return params.multiplier * height - }) - engine.addFact('widthMultiplier', async (params, almanac) => { - const width = await almanac.factValue('width') - return params.multiplier * width - }) - await engine.run({ height: 5, width: 10 }) - expect(eventSpy).to.have.been.calledOnce() + multiplier: 2, + }, + operator: "equal", + value: { + fact: "heightMultiplier", + params: { + multiplier: 4, + }, + }, + }, + ], + }; + it("honors the params", async () => { + setup(paramsCondition); + engine.addFact("heightMultiplier", async (params, almanac) => { + const height = await almanac.factValue("height"); + return params.multiplier * height; + }); + engine.addFact("widthMultiplier", async (params, almanac) => { + const width = await almanac.factValue("width"); + return params.multiplier * width; + }); + await engine.run({ height: 5, width: 10 }); + expect(eventSpy).to.have.been.calledOnce(); - sandbox.reset() + sandbox.reset(); - await engine.run({ height: 5, width: 9 }) // negative case - expect(eventSpy.callCount).to.equal(0) - }) - }) + await engine.run({ height: 5, width: 9 }); // negative case + expect(eventSpy.callCount).to.equal(0); + }); + }); - context('rules with parameterized conditions and path values', () => { + context("rules with parameterized conditions and path values", () => { const pathCondition = { - all: [{ - fact: 'widthMultiplier', - params: { - multiplier: 2 - }, - path: '$.feet', - operator: 'equal', - value: { - fact: 'heightMultiplier', + all: [ + { + fact: "widthMultiplier", params: { - multiplier: 4 + multiplier: 2, }, - path: '$.meters' - } - }] - } - it('honors the path', async () => { - setup(pathCondition) - engine.addFact('heightMultiplier', async (params, almanac) => { - const height = await almanac.factValue('height') - return { meters: params.multiplier * height } - }) - engine.addFact('widthMultiplier', async (params, almanac) => { - const width = await almanac.factValue('width') - return { feet: params.multiplier * width } - }) - await engine.run({ height: 5, width: 10 }) - expect(eventSpy).to.have.been.calledOnce() + path: "$.feet", + operator: "equal", + value: { + fact: "heightMultiplier", + params: { + multiplier: 4, + }, + path: "$.meters", + }, + }, + ], + }; + it("honors the path", async () => { + setup(pathCondition); + engine.addFact("heightMultiplier", async (params, almanac) => { + const height = await almanac.factValue("height"); + return { meters: params.multiplier * height }; + }); + engine.addFact("widthMultiplier", async (params, almanac) => { + const width = await almanac.factValue("width"); + return { feet: params.multiplier * width }; + }); + await engine.run({ height: 5, width: 10 }); + expect(eventSpy).to.have.been.calledOnce(); - sandbox.reset() + sandbox.reset(); - await engine.run({ height: 5, width: 9 }) // negative case - expect(eventSpy.callCount).to.equal(0) - }) - }) -}) + await engine.run({ height: 5, width: 9 }); // negative case + expect(eventSpy.callCount).to.equal(0); + }); + }); +}); diff --git a/test/engine-fact-priority.test.js b/test/engine-fact-priority.test.js index 2d37e739..a8dfe60f 100644 --- a/test/engine-fact-priority.test.js +++ b/test/engine-fact-priority.test.js @@ -1,188 +1,204 @@ -'use strict' +"use strict"; -import engineFactory from '../src/index' -import sinon from 'sinon' +import engineFactory from "../src/index"; +import sinon from "sinon"; -describe('Engine: fact priority', () => { - let engine - let sandbox +describe("Engine: fact priority", () => { + let engine; + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); afterEach(() => { - sandbox.restore() - }) - const event = { type: 'adult-human-admins' } + sandbox.restore(); + }); + const event = { type: "adult-human-admins" }; - let eventSpy - let failureSpy - let ageStub - let segmentStub - let accountTypeStub + let eventSpy; + let failureSpy; + let ageStub; + let segmentStub; + let accountTypeStub; - function setup (conditions) { - ageStub = sandbox.stub() - segmentStub = sandbox.stub() - accountTypeStub = sandbox.stub() - eventSpy = sandbox.stub() - failureSpy = sandbox.stub() + function setup(conditions) { + ageStub = sandbox.stub(); + segmentStub = sandbox.stub(); + accountTypeStub = sandbox.stub(); + eventSpy = sandbox.stub(); + failureSpy = sandbox.stub(); - engine = engineFactory() - const rule = factories.rule({ conditions, event }) - engine.addRule(rule) - engine.addFact('age', ageStub, { priority: 100 }) - engine.addFact('segment', segmentStub, { priority: 50 }) - engine.addFact('accountType', accountTypeStub, { priority: 25 }) - engine.on('success', eventSpy) - engine.on('failure', failureSpy) + engine = engineFactory(); + const rule = factories.rule({ conditions, event }); + engine.addRule(rule); + engine.addFact("age", ageStub, { priority: 100 }); + engine.addFact("segment", segmentStub, { priority: 50 }); + engine.addFact("accountType", accountTypeStub, { priority: 25 }); + engine.on("success", eventSpy); + engine.on("failure", failureSpy); } - describe('all conditions', () => { + describe("all conditions", () => { const allCondition = { - all: [{ - fact: 'age', - operator: 'greaterThanInclusive', - value: 18 - }, { - fact: 'segment', - operator: 'equal', - value: 'human' - }, { - fact: 'accountType', - operator: 'equal', - value: 'admin' - }] - } + all: [ + { + fact: "age", + operator: "greaterThanInclusive", + value: 18, + }, + { + fact: "segment", + operator: "equal", + value: "human", + }, + { + fact: "accountType", + operator: "equal", + value: "admin", + }, + ], + }; - it('stops on the first fact to fail, part 1', async () => { - setup(allCondition) - ageStub.returns(10) // fail - await engine.run() - expect(failureSpy).to.have.been.called() - expect(eventSpy).to.not.have.been.called() - expect(ageStub).to.have.been.calledOnce() - expect(segmentStub).to.not.have.been.called() - expect(accountTypeStub).to.not.have.been.called() - }) + it("stops on the first fact to fail, part 1", async () => { + setup(allCondition); + ageStub.returns(10); // fail + await engine.run(); + expect(failureSpy).to.have.been.called(); + expect(eventSpy).to.not.have.been.called(); + expect(ageStub).to.have.been.calledOnce(); + expect(segmentStub).to.not.have.been.called(); + expect(accountTypeStub).to.not.have.been.called(); + }); - it('stops on the first fact to fail, part 2', async () => { - setup(allCondition) - ageStub.returns(20) // pass - segmentStub.returns('android') // fail - await engine.run() - expect(failureSpy).to.have.been.called() - expect(eventSpy).to.not.have.been.called() - expect(ageStub).to.have.been.calledOnce() - expect(segmentStub).to.have.been.calledOnce() - expect(accountTypeStub).to.not.have.been.called() - }) + it("stops on the first fact to fail, part 2", async () => { + setup(allCondition); + ageStub.returns(20); // pass + segmentStub.returns("android"); // fail + await engine.run(); + expect(failureSpy).to.have.been.called(); + expect(eventSpy).to.not.have.been.called(); + expect(ageStub).to.have.been.calledOnce(); + expect(segmentStub).to.have.been.calledOnce(); + expect(accountTypeStub).to.not.have.been.called(); + }); - describe('sub-conditions', () => { + describe("sub-conditions", () => { const allSubCondition = { - all: [{ - fact: 'age', - operator: 'greaterThanInclusive', - value: 18 - }, { - all: [ - { - fact: 'segment', - operator: 'equal', - value: 'human' - }, { - fact: 'accountType', - operator: 'equal', - value: 'admin' - } - ] - }] - } + all: [ + { + fact: "age", + operator: "greaterThanInclusive", + value: 18, + }, + { + all: [ + { + fact: "segment", + operator: "equal", + value: "human", + }, + { + fact: "accountType", + operator: "equal", + value: "admin", + }, + ], + }, + ], + }; - it('stops after the first sub-condition fact fails', async () => { - setup(allSubCondition) - ageStub.returns(20) // pass - segmentStub.returns('android') // fail - await engine.run() - expect(failureSpy).to.have.been.called() - expect(eventSpy).to.not.have.been.called() - expect(ageStub).to.have.been.calledOnce() - expect(segmentStub).to.have.been.calledOnce() - expect(accountTypeStub).to.not.have.been.called() - }) - }) - }) + it("stops after the first sub-condition fact fails", async () => { + setup(allSubCondition); + ageStub.returns(20); // pass + segmentStub.returns("android"); // fail + await engine.run(); + expect(failureSpy).to.have.been.called(); + expect(eventSpy).to.not.have.been.called(); + expect(ageStub).to.have.been.calledOnce(); + expect(segmentStub).to.have.been.calledOnce(); + expect(accountTypeStub).to.not.have.been.called(); + }); + }); + }); - describe('any conditions', () => { + describe("any conditions", () => { const anyCondition = { - any: [{ - fact: 'age', - operator: 'greaterThanInclusive', - value: 18 - }, { - fact: 'segment', - operator: 'equal', - value: 'human' - }, { - fact: 'accountType', - operator: 'equal', - value: 'admin' - }] - } - it('complete on the first fact to succeed, part 1', async () => { - setup(anyCondition) - ageStub.returns(20) // succeed - await engine.run() - expect(eventSpy).to.have.been.calledOnce() - expect(failureSpy).to.not.have.been.called() - expect(ageStub).to.have.been.calledOnce() - expect(segmentStub).to.not.have.been.called() - expect(accountTypeStub).to.not.have.been.called() - }) + any: [ + { + fact: "age", + operator: "greaterThanInclusive", + value: 18, + }, + { + fact: "segment", + operator: "equal", + value: "human", + }, + { + fact: "accountType", + operator: "equal", + value: "admin", + }, + ], + }; + it("complete on the first fact to succeed, part 1", async () => { + setup(anyCondition); + ageStub.returns(20); // succeed + await engine.run(); + expect(eventSpy).to.have.been.calledOnce(); + expect(failureSpy).to.not.have.been.called(); + expect(ageStub).to.have.been.calledOnce(); + expect(segmentStub).to.not.have.been.called(); + expect(accountTypeStub).to.not.have.been.called(); + }); - it('short circuits on the first fact to fail, part 2', async () => { - setup(anyCondition) - ageStub.returns(10) // fail - segmentStub.returns('human') // pass - await engine.run() - expect(eventSpy).to.have.been.calledOnce() - expect(failureSpy).to.not.have.been.called() - expect(ageStub).to.have.been.calledOnce() - expect(segmentStub).to.have.been.calledOnce() - expect(accountTypeStub).to.not.have.been.called() - }) + it("short circuits on the first fact to fail, part 2", async () => { + setup(anyCondition); + ageStub.returns(10); // fail + segmentStub.returns("human"); // pass + await engine.run(); + expect(eventSpy).to.have.been.calledOnce(); + expect(failureSpy).to.not.have.been.called(); + expect(ageStub).to.have.been.calledOnce(); + expect(segmentStub).to.have.been.calledOnce(); + expect(accountTypeStub).to.not.have.been.called(); + }); - describe('sub-conditions', () => { + describe("sub-conditions", () => { const anySubCondition = { - all: [{ - fact: 'age', - operator: 'greaterThanInclusive', - value: 18 - }, { - any: [ - { - fact: 'segment', - operator: 'equal', - value: 'human' - }, { - fact: 'accountType', - operator: 'equal', - value: 'admin' - } - ] - }] - } + all: [ + { + fact: "age", + operator: "greaterThanInclusive", + value: 18, + }, + { + any: [ + { + fact: "segment", + operator: "equal", + value: "human", + }, + { + fact: "accountType", + operator: "equal", + value: "admin", + }, + ], + }, + ], + }; - it('stops after the first sub-condition fact succeeds', async () => { - setup(anySubCondition) - ageStub.returns(20) // success - segmentStub.returns('human') // success - await engine.run() - expect(failureSpy).to.not.have.been.called() - expect(eventSpy).to.have.been.called() - expect(ageStub).to.have.been.calledOnce() - expect(segmentStub).to.have.been.calledOnce() - expect(accountTypeStub).to.not.have.been.called() - }) - }) - }) -}) + it("stops after the first sub-condition fact succeeds", async () => { + setup(anySubCondition); + ageStub.returns(20); // success + segmentStub.returns("human"); // success + await engine.run(); + expect(failureSpy).to.not.have.been.called(); + expect(eventSpy).to.have.been.called(); + expect(ageStub).to.have.been.calledOnce(); + expect(segmentStub).to.have.been.calledOnce(); + expect(accountTypeStub).to.not.have.been.called(); + }); + }); + }); +}); diff --git a/test/engine-fact.test.js b/test/engine-fact.test.js index 93d9b991..61c78699 100644 --- a/test/engine-fact.test.js +++ b/test/engine-fact.test.js @@ -1,336 +1,347 @@ -'use strict' +"use strict"; -import sinon from 'sinon' -import { get } from 'lodash' -import engineFactory from '../src/index' +import sinon from "sinon"; +import { get } from "lodash"; +import engineFactory from "../src/index"; -const CHILD = 14 -const ADULT = 75 +const CHILD = 14; +const ADULT = 75; -async function eligibilityField (params, engine) { - if (params.field === 'age') { +async function eligibilityField(params, engine) { + if (params.field === "age") { if (params.eligibilityId === 1) { - return CHILD + return CHILD; } - return ADULT + return ADULT; } } -async function eligibilityData (params, engine) { +async function eligibilityData(params, engine) { const address = { - street: '123 Fake Street', + street: "123 Fake Street", state: { - abbreviation: 'CO', - name: 'Colorado' + abbreviation: "CO", + name: "Colorado", }, - zip: '80403', - 'dot.property': 'dot-property-value', + zip: "80403", + "dot.property": "dot-property-value", occupantHistory: [ - { name: 'Joe', year: 2011 }, - { name: 'Jane', year: 2013 } + { name: "Joe", year: 2011 }, + { name: "Jane", year: 2013 }, ], - currentOccupants: [ - { name: 'Larry', year: 2020 } - ] - } + currentOccupants: [{ name: "Larry", year: 2020 }], + }; if (params.eligibilityId === 1) { - return { age: CHILD, address } + return { age: CHILD, address }; } - return { age: ADULT, address } + return { age: ADULT, address }; } -describe('Engine: fact evaluation', () => { - let engine - let sandbox +describe("Engine: fact evaluation", () => { + let engine; + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); afterEach(() => { - sandbox.restore() - }) + sandbox.restore(); + }); const event = { - type: 'ageTrigger', + type: "ageTrigger", params: { - demographic: 'under50' - } - } - function baseConditions () { + demographic: "under50", + }, + }; + function baseConditions() { return { - any: [{ - fact: 'eligibilityField', - operator: 'lessThan', - params: { - eligibilityId: 1, - field: 'age' + any: [ + { + fact: "eligibilityField", + operator: "lessThan", + params: { + eligibilityId: 1, + field: "age", + }, + value: 50, }, - value: 50 - }] - } + ], + }; } - let successSpy - let failureSpy + let successSpy; + let failureSpy; beforeEach(() => { - successSpy = sandbox.spy() - failureSpy = sandbox.spy() - }) + successSpy = sandbox.spy(); + failureSpy = sandbox.spy(); + }); - function setup (conditions = baseConditions(), engineOptions = {}) { - engine = engineFactory([], engineOptions) - const rule = factories.rule({ conditions, event }) - engine.addRule(rule) - engine.addFact('eligibilityField', eligibilityField) - engine.addFact('eligibilityData', eligibilityData) - engine.on('success', successSpy) - engine.on('failure', failureSpy) + function setup(conditions = baseConditions(), engineOptions = {}) { + engine = engineFactory([], engineOptions); + const rule = factories.rule({ conditions, event }); + engine.addRule(rule); + engine.addFact("eligibilityField", eligibilityField); + engine.addFact("eligibilityData", eligibilityData); + engine.on("success", successSpy); + engine.on("failure", failureSpy); } - describe('options', () => { - describe('options.allowUndefinedFacts', () => { - it('throws when fact is undefined by default', async () => { - const conditions = Object.assign({}, baseConditions()) + describe("options", () => { + describe("options.allowUndefinedFacts", () => { + it("throws when fact is undefined by default", async () => { + const conditions = Object.assign({}, baseConditions()); conditions.any.push({ - fact: 'undefined-fact', - operator: 'equal', - value: true - }) - setup(conditions) - return expect(engine.run()).to.be.rejectedWith(/Undefined fact: undefined-fact/) - }) + fact: "undefined-fact", + operator: "equal", + value: true, + }); + setup(conditions); + return expect(engine.run()).to.be.rejectedWith( + /Undefined fact: undefined-fact/, + ); + }); - context('treats undefined facts as falsey when allowUndefinedFacts is set', () => { - it('emits "success" when the condition succeeds', async () => { - const conditions = Object.assign({}, baseConditions()) - conditions.any.push({ - fact: 'undefined-fact', - operator: 'equal', - value: true - }) - setup(conditions, { allowUndefinedFacts: true }) - await engine.run() - expect(successSpy).to.have.been.called() - expect(failureSpy).to.not.have.been.called() - }) + context( + "treats undefined facts as falsey when allowUndefinedFacts is set", + () => { + it('emits "success" when the condition succeeds', async () => { + const conditions = Object.assign({}, baseConditions()); + conditions.any.push({ + fact: "undefined-fact", + operator: "equal", + value: true, + }); + setup(conditions, { allowUndefinedFacts: true }); + await engine.run(); + expect(successSpy).to.have.been.called(); + expect(failureSpy).to.not.have.been.called(); + }); - it('emits "failure" when the condition fails', async () => { - const conditions = Object.assign({}, baseConditions()) - conditions.any.push({ - fact: 'undefined-fact', - operator: 'equal', - value: true - }) - conditions.any[0].params.eligibilityId = 2 - setup(conditions, { allowUndefinedFacts: true }) - await engine.run() - expect(successSpy).to.not.have.been.called() - expect(failureSpy).to.have.been.called() - }) - }) - }) - }) + it('emits "failure" when the condition fails', async () => { + const conditions = Object.assign({}, baseConditions()); + conditions.any.push({ + fact: "undefined-fact", + operator: "equal", + value: true, + }); + conditions.any[0].params.eligibilityId = 2; + setup(conditions, { allowUndefinedFacts: true }); + await engine.run(); + expect(successSpy).to.not.have.been.called(); + expect(failureSpy).to.have.been.called(); + }); + }, + ); + }); + }); - describe('params', () => { - it('emits when the condition is met', async () => { - setup() - await engine.run() - expect(successSpy).to.have.been.calledWith(event) - }) + describe("params", () => { + it("emits when the condition is met", async () => { + setup(); + await engine.run(); + expect(successSpy).to.have.been.calledWith(event); + }); - it('does not emit when the condition fails', async () => { - const conditions = Object.assign({}, baseConditions()) - conditions.any[0].params.eligibilityId = 2 - setup(conditions) - await engine.run() - expect(successSpy).to.not.have.been.called() - }) - }) + it("does not emit when the condition fails", async () => { + const conditions = Object.assign({}, baseConditions()); + conditions.any[0].params.eligibilityId = 2; + setup(conditions); + await engine.run(); + expect(successSpy).to.not.have.been.called(); + }); + }); - describe('path', () => { - function conditions () { + describe("path", () => { + function conditions() { return { - any: [{ - fact: 'eligibilityData', - operator: 'lessThan', - path: '$.age', - params: { - eligibilityId: 1 + any: [ + { + fact: "eligibilityData", + operator: "lessThan", + path: "$.age", + params: { + eligibilityId: 1, + }, + value: 50, }, - value: 50 - }] - } + ], + }; } - it('emits when the condition is met', async () => { - setup(conditions()) - await engine.run() - expect(successSpy).to.have.been.calledWith(event) - }) + it("emits when the condition is met", async () => { + setup(conditions()); + await engine.run(); + expect(successSpy).to.have.been.calledWith(event); + }); - it('does not emit when the condition fails', async () => { - const failureCondition = conditions() - failureCondition.any[0].params.eligibilityId = 2 - setup(failureCondition) - await engine.run() - expect(successSpy).to.not.have.been.called() - }) + it("does not emit when the condition fails", async () => { + const failureCondition = conditions(); + failureCondition.any[0].params.eligibilityId = 2; + setup(failureCondition); + await engine.run(); + expect(successSpy).to.not.have.been.called(); + }); - describe('arrays', () => { - it('can extract an array, allowing it to be used in concert with array operators', async () => { - const complexCondition = conditions() - complexCondition.any[0].path = '$.address.occupantHistory[*].year' - complexCondition.any[0].value = 2011 - complexCondition.any[0].operator = 'contains' - setup(complexCondition) - await engine.run() - expect(successSpy).to.have.been.calledWith(event) - }) + describe("arrays", () => { + it("can extract an array, allowing it to be used in concert with array operators", async () => { + const complexCondition = conditions(); + complexCondition.any[0].path = "$.address.occupantHistory[*].year"; + complexCondition.any[0].value = 2011; + complexCondition.any[0].operator = "contains"; + setup(complexCondition); + await engine.run(); + expect(successSpy).to.have.been.calledWith(event); + }); - it('can extract an array with a single element', async () => { - const complexCondition = conditions() - complexCondition.any[0].path = '$.address.currentOccupants[*].year' - complexCondition.any[0].value = 2020 - complexCondition.any[0].operator = 'contains' - setup(complexCondition) - await engine.run() - expect(successSpy).to.have.been.calledWith(event) - }) - }) + it("can extract an array with a single element", async () => { + const complexCondition = conditions(); + complexCondition.any[0].path = "$.address.currentOccupants[*].year"; + complexCondition.any[0].value = 2020; + complexCondition.any[0].operator = "contains"; + setup(complexCondition); + await engine.run(); + expect(successSpy).to.have.been.calledWith(event); + }); + }); - context('complex paths', () => { + context("complex paths", () => { it('correctly interprets "path" when dynamic facts return objects', async () => { - const complexCondition = conditions() - complexCondition.any[0].path = '$.address.occupantHistory[0].year' - complexCondition.any[0].value = 2011 - complexCondition.any[0].operator = 'equal' - setup(complexCondition) - await engine.run() - expect(successSpy).to.have.been.calledWith(event) - }) + const complexCondition = conditions(); + complexCondition.any[0].path = "$.address.occupantHistory[0].year"; + complexCondition.any[0].value = 2011; + complexCondition.any[0].operator = "equal"; + setup(complexCondition); + await engine.run(); + expect(successSpy).to.have.been.calledWith(event); + }); it('correctly interprets "path" when target object properties have dots', async () => { - const complexCondition = conditions() - complexCondition.any[0].path = '$.address.[\'dot.property\']' - complexCondition.any[0].value = 'dot-property-value' - complexCondition.any[0].operator = 'equal' - setup(complexCondition) - await engine.run() - expect(successSpy).to.have.been.calledWith(event) - }) + const complexCondition = conditions(); + complexCondition.any[0].path = "$.address.['dot.property']"; + complexCondition.any[0].value = "dot-property-value"; + complexCondition.any[0].operator = "equal"; + setup(complexCondition); + await engine.run(); + expect(successSpy).to.have.been.calledWith(event); + }); it('correctly interprets "path" with runtime fact objects', async () => { - const fact = { x: { y: 1 }, a: 2 } + const fact = { x: { y: 1 }, a: 2 }; const conditions = { - all: [{ - fact: 'x', - path: '$.y', - operator: 'equal', - value: 1 - }] - } + all: [ + { + fact: "x", + path: "$.y", + operator: "equal", + value: 1, + }, + ], + }; - engine = engineFactory([]) - const rule = factories.rule({ conditions, event }) - engine.addRule(rule) - engine.on('success', successSpy) - engine.on('failure', failureSpy) - await engine.run(fact) - expect(successSpy).to.have.been.calledWith(event) - expect(failureSpy).to.not.have.been.calledWith(event) - }) - }) + engine = engineFactory([]); + const rule = factories.rule({ conditions, event }); + engine.addRule(rule); + engine.on("success", successSpy); + engine.on("failure", failureSpy); + await engine.run(fact); + expect(successSpy).to.have.been.calledWith(event); + expect(failureSpy).to.not.have.been.calledWith(event); + }); + }); - it('does not emit when complex object paths fail the condition', async () => { - const complexCondition = conditions() - complexCondition.any[0].path = '$.address.occupantHistory[0].year' - complexCondition.any[0].value = 2010 - complexCondition.any[0].operator = 'equal' - setup(complexCondition) - await engine.run() - expect(successSpy).to.not.have.been.calledWith(event) - }) + it("does not emit when complex object paths fail the condition", async () => { + const complexCondition = conditions(); + complexCondition.any[0].path = "$.address.occupantHistory[0].year"; + complexCondition.any[0].value = 2010; + complexCondition.any[0].operator = "equal"; + setup(complexCondition); + await engine.run(); + expect(successSpy).to.not.have.been.calledWith(event); + }); - it('treats invalid object paths as undefined', async () => { - const complexCondition = conditions() - complexCondition.any[0].path = '$.invalid.object[99].path' - complexCondition.any[0].value = undefined - complexCondition.any[0].operator = 'equal' - setup(complexCondition) - await engine.run() - expect(successSpy).to.have.been.calledWith(event) - }) + it("treats invalid object paths as undefined", async () => { + const complexCondition = conditions(); + complexCondition.any[0].path = "$.invalid.object[99].path"; + complexCondition.any[0].value = undefined; + complexCondition.any[0].operator = "equal"; + setup(complexCondition); + await engine.run(); + expect(successSpy).to.have.been.calledWith(event); + }); it('ignores "path" when facts return non-objects', async () => { - setup(conditions()) + setup(conditions()); const eligibilityData = async (params, engine) => { - return CHILD - } - engine.addFact('eligibilityData', eligibilityData) - await engine.run() - expect(successSpy).to.have.been.calledWith(event) - }) + return CHILD; + }; + engine.addFact("eligibilityData", eligibilityData); + await engine.run(); + expect(successSpy).to.have.been.calledWith(event); + }); - describe('pathResolver', () => { - it('allows a custom path resolver to be registered which interprets the path property', async () => { - const fact = { x: { y: [99] }, a: 2 } + describe("pathResolver", () => { + it("allows a custom path resolver to be registered which interprets the path property", async () => { + const fact = { x: { y: [99] }, a: 2 }; const conditions = { - all: [{ - fact: 'x', - path: 'y[0]', - operator: 'equal', - value: 99 - }] - } + all: [ + { + fact: "x", + path: "y[0]", + operator: "equal", + value: 99, + }, + ], + }; const pathResolver = (value, path) => { - return get(value, path) - } + return get(value, path); + }; - engine = engineFactory([], { pathResolver }) - const rule = factories.rule({ conditions, event }) - engine.addRule(rule) - engine.on('success', successSpy) - engine.on('failure', failureSpy) + engine = engineFactory([], { pathResolver }); + const rule = factories.rule({ conditions, event }); + engine.addRule(rule); + engine.on("success", successSpy); + engine.on("failure", failureSpy); - await engine.run(fact) + await engine.run(fact); - expect(successSpy).to.have.been.calledWith(event) - expect(failureSpy).to.not.have.been.called() - }) - }) - }) + expect(successSpy).to.have.been.calledWith(event); + expect(failureSpy).to.not.have.been.called(); + }); + }); + }); - describe('promises', () => { - it('works with asynchronous evaluations', async () => { - setup() + describe("promises", () => { + it("works with asynchronous evaluations", async () => { + setup(); const eligibilityField = function (params, engine) { return new Promise((resolve, reject) => { setImmediate(() => { - resolve(30) - }) - }) - } - engine.addFact('eligibilityField', eligibilityField) - await engine.run() - expect(successSpy).to.have.been.called() - }) - }) + resolve(30); + }); + }); + }; + engine.addFact("eligibilityField", eligibilityField); + await engine.run(); + expect(successSpy).to.have.been.called(); + }); + }); - describe('synchronous functions', () => { - it('works with synchronous, non-promise evaluations that are truthy', async () => { - setup() + describe("synchronous functions", () => { + it("works with synchronous, non-promise evaluations that are truthy", async () => { + setup(); const eligibilityField = function (params, engine) { - return 20 - } - engine.addFact('eligibilityField', eligibilityField) - await engine.run() - expect(successSpy).to.have.been.called() - }) + return 20; + }; + engine.addFact("eligibilityField", eligibilityField); + await engine.run(); + expect(successSpy).to.have.been.called(); + }); - it('works with synchronous, non-promise evaluations that are falsey', async () => { - setup() + it("works with synchronous, non-promise evaluations that are falsey", async () => { + setup(); const eligibilityField = function (params, engine) { - return 100 - } - engine.addFact('eligibilityField', eligibilityField) - await engine.run() - expect(successSpy).to.not.have.been.called() - }) - }) -}) + return 100; + }; + engine.addFact("eligibilityField", eligibilityField); + await engine.run(); + expect(successSpy).to.not.have.been.called(); + }); + }); +}); diff --git a/test/engine-facts-calling-facts.test.js b/test/engine-facts-calling-facts.test.js index 19b35554..7007e611 100644 --- a/test/engine-facts-calling-facts.test.js +++ b/test/engine-facts-calling-facts.test.js @@ -1,97 +1,105 @@ -'use strict' +"use strict"; -import engineFactory, { Fact } from '../src/index' -import sinon from 'sinon' +import engineFactory, { Fact } from "../src/index"; +import sinon from "sinon"; -describe('Engine: custom cache keys', () => { - let engine - let sandbox +describe("Engine: custom cache keys", () => { + let engine; + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); afterEach(() => { - sandbox.restore() - }) - const event = { type: 'early-twenties' } + sandbox.restore(); + }); + const event = { type: "early-twenties" }; const conditions = { - all: [{ - fact: 'demographics', - params: { - field: 'age' + all: [ + { + fact: "demographics", + params: { + field: "age", + }, + operator: "lessThanInclusive", + value: 25, }, - operator: 'lessThanInclusive', - value: 25 - }, { - fact: 'demographics', - params: { - field: 'zipCode' + { + fact: "demographics", + params: { + field: "zipCode", + }, + operator: "equal", + value: 80211, }, - operator: 'equal', - value: 80211 - }] - } + ], + }; - let eventSpy - let demographicDataSpy - let demographicSpy + let eventSpy; + let demographicDataSpy; + let demographicSpy; beforeEach(() => { - demographicSpy = sandbox.spy() - demographicDataSpy = sandbox.spy() - eventSpy = sandbox.spy() + demographicSpy = sandbox.spy(); + demographicDataSpy = sandbox.spy(); + eventSpy = sandbox.spy(); const demographicsDataDefinition = async (params, engine) => { - demographicDataSpy() + demographicDataSpy(); return { age: 20, - zipCode: 80211 - } - } + zipCode: 80211, + }; + }; const demographicsDefinition = async (params, engine) => { - demographicSpy() - const data = await engine.factValue('demographic-data') - return data[params.field] - } - const demographicsFact = new Fact('demographics', demographicsDefinition) - const demographicsDataFact = new Fact('demographic-data', demographicsDataDefinition) + demographicSpy(); + const data = await engine.factValue("demographic-data"); + return data[params.field]; + }; + const demographicsFact = new Fact("demographics", demographicsDefinition); + const demographicsDataFact = new Fact( + "demographic-data", + demographicsDataDefinition, + ); - engine = engineFactory() - const rule = factories.rule({ conditions, event }) - engine.addRule(rule) - engine.addFact(demographicsFact) - engine.addFact(demographicsDataFact) - engine.on('success', eventSpy) - }) + engine = engineFactory(); + const rule = factories.rule({ conditions, event }); + engine.addRule(rule); + engine.addFact(demographicsFact); + engine.addFact(demographicsDataFact); + engine.on("success", eventSpy); + }); - describe('1 rule', () => { - it('allows a fact to retrieve other fact values', async () => { - await engine.run() - expect(eventSpy).to.have.been.calledOnce() - expect(demographicDataSpy).to.have.been.calledOnce() - expect(demographicSpy).to.have.been.calledTwice() - }) - }) + describe("1 rule", () => { + it("allows a fact to retrieve other fact values", async () => { + await engine.run(); + expect(eventSpy).to.have.been.calledOnce(); + expect(demographicDataSpy).to.have.been.calledOnce(); + expect(demographicSpy).to.have.been.calledTwice(); + }); + }); - describe('2 rules with parallel conditions', () => { - it('calls the fact definition once', async () => { + describe("2 rules with parallel conditions", () => { + it("calls the fact definition once", async () => { const conditions = { - all: [{ - fact: 'demographics', - params: { - field: 'age' + all: [ + { + fact: "demographics", + params: { + field: "age", + }, + operator: "greaterThanInclusive", + value: 20, }, - operator: 'greaterThanInclusive', - value: 20 - }] - } - const rule = factories.rule({ conditions, event }) - engine.addRule(rule) + ], + }; + const rule = factories.rule({ conditions, event }); + engine.addRule(rule); - await engine.run() - expect(eventSpy).to.have.been.calledTwice() - expect(demographicDataSpy).to.have.been.calledOnce() - expect(demographicSpy).to.have.been.calledTwice() - expect(demographicDataSpy).to.have.been.calledOnce() - }) - }) -}) + await engine.run(); + expect(eventSpy).to.have.been.calledTwice(); + expect(demographicDataSpy).to.have.been.calledOnce(); + expect(demographicSpy).to.have.been.calledTwice(); + expect(demographicDataSpy).to.have.been.calledOnce(); + }); + }); +}); diff --git a/test/engine-failure.test.js b/test/engine-failure.test.js index 86b3f236..28ee69be 100644 --- a/test/engine-failure.test.js +++ b/test/engine-failure.test.js @@ -1,45 +1,47 @@ -'use strict' +"use strict"; -import engineFactory from '../src/index' -import sinon from 'sinon' +import engineFactory from "../src/index"; +import sinon from "sinon"; -describe('Engine: failure', () => { - let engine - let sandbox +describe("Engine: failure", () => { + let engine; + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); afterEach(() => { - sandbox.restore() - }) + sandbox.restore(); + }); - const event = { type: 'generic' } + const event = { type: "generic" }; const conditions = { - any: [{ - fact: 'age', - operator: 'greaterThanInclusive', - value: 21 - }] - } + any: [ + { + fact: "age", + operator: "greaterThanInclusive", + value: 21, + }, + ], + }; beforeEach(() => { - engine = engineFactory() - const determineDrinkingAgeRule = factories.rule({ conditions, event }) - engine.addRule(determineDrinkingAgeRule) - engine.addFact('age', 10) - }) + engine = engineFactory(); + const determineDrinkingAgeRule = factories.rule({ conditions, event }); + engine.addRule(determineDrinkingAgeRule); + engine.addFact("age", 10); + }); - it('emits an event on a rule failing', async () => { - const failureSpy = sandbox.spy() - engine.on('failure', failureSpy) - await engine.run() - expect(failureSpy).to.have.been.calledWith(engine.rules[0].ruleEvent) - }) + it("emits an event on a rule failing", async () => { + const failureSpy = sandbox.spy(); + engine.on("failure", failureSpy); + await engine.run(); + expect(failureSpy).to.have.been.calledWith(engine.rules[0].ruleEvent); + }); - it('does not emit when a rule passes', async () => { - const failureSpy = sandbox.spy() - engine.on('failure', failureSpy) - engine.addFact('age', 50) - await engine.run() - expect(failureSpy).to.not.have.been.calledOnce() - }) -}) + it("does not emit when a rule passes", async () => { + const failureSpy = sandbox.spy(); + engine.on("failure", failureSpy); + engine.addFact("age", 50); + await engine.run(); + expect(failureSpy).to.not.have.been.calledOnce(); + }); +}); diff --git a/test/engine-not.test.js b/test/engine-not.test.js index f83bed8a..6719b5ea 100644 --- a/test/engine-not.test.js +++ b/test/engine-not.test.js @@ -1,54 +1,54 @@ -'use strict' +"use strict"; -import sinon from 'sinon' -import engineFactory from '../src/index' +import sinon from "sinon"; +import engineFactory from "../src/index"; describe('Engine: "not" conditions', () => { - let engine - let sandbox + let engine; + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); afterEach(() => { - sandbox.restore() - }) + sandbox.restore(); + }); describe('supports a single "not" condition', () => { const event = { - type: 'ageTrigger', + type: "ageTrigger", params: { - demographic: 'under50' - } - } + demographic: "under50", + }, + }; const conditions = { not: { - fact: 'age', - operator: 'greaterThanInclusive', - value: 50 - } - } - let eventSpy - let ageSpy + fact: "age", + operator: "greaterThanInclusive", + value: 50, + }, + }; + let eventSpy; + let ageSpy; beforeEach(() => { - eventSpy = sandbox.spy() - ageSpy = sandbox.stub() - const rule = factories.rule({ conditions, event }) - engine = engineFactory() - engine.addRule(rule) - engine.addFact('age', ageSpy) - engine.on('success', eventSpy) - }) + eventSpy = sandbox.spy(); + ageSpy = sandbox.stub(); + const rule = factories.rule({ conditions, event }); + engine = engineFactory(); + engine.addRule(rule); + engine.addFact("age", ageSpy); + engine.on("success", eventSpy); + }); - it('emits when the condition is met', async () => { - ageSpy.returns(10) - await engine.run() - expect(eventSpy).to.have.been.calledWith(event) - }) + it("emits when the condition is met", async () => { + ageSpy.returns(10); + await engine.run(); + expect(eventSpy).to.have.been.calledWith(event); + }); - it('does not emit when the condition fails', () => { - ageSpy.returns(75) - engine.run() - expect(eventSpy).to.not.have.been.calledWith(event) - }) - }) -}) + it("does not emit when the condition fails", () => { + ageSpy.returns(75); + engine.run(); + expect(eventSpy).to.not.have.been.calledWith(event); + }); + }); +}); diff --git a/test/engine-operator-map.test.js b/test/engine-operator-map.test.js index b5ae9672..3ae06d96 100644 --- a/test/engine-operator-map.test.js +++ b/test/engine-operator-map.test.js @@ -1,86 +1,91 @@ -'use strict' +"use strict"; -import { expect } from 'chai' -import engineFactory, { Operator, OperatorDecorator } from '../src/index' +import { expect } from "chai"; +import engineFactory, { Operator, OperatorDecorator } from "../src/index"; -const startsWithLetter = new Operator('startsWithLetter', (factValue, jsonValue) => { - return factValue[0] === jsonValue -}) +const startsWithLetter = new Operator( + "startsWithLetter", + (factValue, jsonValue) => { + return factValue[0] === jsonValue; + }, +); -const never = new OperatorDecorator('never', () => false) +const never = new OperatorDecorator("never", () => false); -describe('Engine Operator Map', () => { - let engine +describe("Engine Operator Map", () => { + let engine; beforeEach(() => { - engine = engineFactory() - engine.addOperator(startsWithLetter) - engine.addOperatorDecorator(never) - }) + engine = engineFactory(); + engine.addOperator(startsWithLetter); + engine.addOperatorDecorator(never); + }); - describe('undecorated operator', () => { - let op + describe("undecorated operator", () => { + let op; beforeEach(() => { - op = engine.operators.get('startsWithLetter') - }) - - it('has the operator', () => { - expect(op).not.to.be.null() - }) - - it('the operator evaluates correctly', () => { - expect(op.evaluate('test', 't')).to.be.true() - }) - - it('after being removed the operator is null', () => { - engine.operators.removeOperator(startsWithLetter) - op = engine.operators.get('startsWithLetter') - expect(op).to.be.null() - }) - }) - - describe('decorated operator', () => { - let op + op = engine.operators.get("startsWithLetter"); + }); + + it("has the operator", () => { + expect(op).not.to.be.null(); + }); + + it("the operator evaluates correctly", () => { + expect(op.evaluate("test", "t")).to.be.true(); + }); + + it("after being removed the operator is null", () => { + engine.operators.removeOperator(startsWithLetter); + op = engine.operators.get("startsWithLetter"); + expect(op).to.be.null(); + }); + }); + + describe("decorated operator", () => { + let op; beforeEach(() => { - op = engine.operators.get('never:startsWithLetter') - }) - - it('has the operator', () => { - expect(op).not.to.be.null() - }) - - it('the operator evaluates correctly', () => { - expect(op.evaluate('test', 't')).to.be.false() - }) - - it('removing the base operator removes the decorated version', () => { - engine.operators.removeOperator(startsWithLetter) - op = engine.operators.get('never:startsWithLetter') - expect(op).to.be.null() - }) - - it('removing the decorator removes the decorated operator', () => { - engine.operators.removeOperatorDecorator(never) - op = engine.operators.get('never:startsWithLetter') - expect(op).to.be.null() - }) - }) - - describe('combinatorics with default operators', () => { - it('combines every, some, not, and greaterThanInclusive operators', () => { - const odds = [1, 3, 5, 7] - const evens = [2, 4, 6, 8] + op = engine.operators.get("never:startsWithLetter"); + }); + + it("has the operator", () => { + expect(op).not.to.be.null(); + }); + + it("the operator evaluates correctly", () => { + expect(op.evaluate("test", "t")).to.be.false(); + }); + + it("removing the base operator removes the decorated version", () => { + engine.operators.removeOperator(startsWithLetter); + op = engine.operators.get("never:startsWithLetter"); + expect(op).to.be.null(); + }); + + it("removing the decorator removes the decorated operator", () => { + engine.operators.removeOperatorDecorator(never); + op = engine.operators.get("never:startsWithLetter"); + expect(op).to.be.null(); + }); + }); + + describe("combinatorics with default operators", () => { + it("combines every, some, not, and greaterThanInclusive operators", () => { + const odds = [1, 3, 5, 7]; + const evens = [2, 4, 6, 8]; // technically not:greaterThanInclusive is the same as lessThan - const op = engine.operators.get('everyFact:someValue:not:greaterThanInclusive') - expect(op.evaluate(odds, evens)).to.be.true() - }) - }) - - it('the swap decorator', () => { - const factValue = 1 - const jsonValue = [1, 2, 3] - - const op = engine.operators.get('swap:contains') - expect(op.evaluate(factValue, jsonValue)).to.be.true() - }) -}) + const op = engine.operators.get( + "everyFact:someValue:not:greaterThanInclusive", + ); + expect(op.evaluate(odds, evens)).to.be.true(); + }); + }); + + it("the swap decorator", () => { + const factValue = 1; + const jsonValue = [1, 2, 3]; + + const op = engine.operators.get("swap:contains"); + expect(op.evaluate(factValue, jsonValue)).to.be.true(); + }); +}); diff --git a/test/engine-operator.test.js b/test/engine-operator.test.js index 03d04bcd..31a647ed 100644 --- a/test/engine-operator.test.js +++ b/test/engine-operator.test.js @@ -1,71 +1,75 @@ -'use strict' +"use strict"; -import sinon from 'sinon' -import engineFactory from '../src/index' +import sinon from "sinon"; +import engineFactory from "../src/index"; -async function dictionary (params, engine) { - const words = ['coffee', 'Aardvark', 'moose', 'ladder', 'antelope'] - return words[params.wordIndex] +async function dictionary(params, engine) { + const words = ["coffee", "Aardvark", "moose", "ladder", "antelope"]; + return words[params.wordIndex]; } -describe('Engine: operator', () => { - let sandbox +describe("Engine: operator", () => { + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); afterEach(() => { - sandbox.restore() - }) + sandbox.restore(); + }); const event = { - type: 'operatorTrigger' - } + type: "operatorTrigger", + }; const baseConditions = { - any: [{ - fact: 'dictionary', - operator: 'startsWithLetter', - value: 'a', - params: { - wordIndex: null - } - }] - } - let eventSpy - function setup (conditions = baseConditions) { - eventSpy = sandbox.spy() - const engine = engineFactory() - const rule = factories.rule({ conditions, event }) - engine.addRule(rule) - engine.addOperator('startsWithLetter', (factValue, jsonValue) => { - if (!factValue.length) return false - return factValue[0].toLowerCase() === jsonValue.toLowerCase() - }) - engine.addFact('dictionary', dictionary) - engine.on('success', eventSpy) - return engine + any: [ + { + fact: "dictionary", + operator: "startsWithLetter", + value: "a", + params: { + wordIndex: null, + }, + }, + ], + }; + let eventSpy; + function setup(conditions = baseConditions) { + eventSpy = sandbox.spy(); + const engine = engineFactory(); + const rule = factories.rule({ conditions, event }); + engine.addRule(rule); + engine.addOperator("startsWithLetter", (factValue, jsonValue) => { + if (!factValue.length) return false; + return factValue[0].toLowerCase() === jsonValue.toLowerCase(); + }); + engine.addFact("dictionary", dictionary); + engine.on("success", eventSpy); + return engine; } - describe('evaluation', () => { - it('emits when the condition is met', async () => { - const conditions = Object.assign({}, baseConditions) - conditions.any[0].params.wordIndex = 1 - const engine = setup() - await engine.run() - expect(eventSpy).to.have.been.calledWith(event) - }) + describe("evaluation", () => { + it("emits when the condition is met", async () => { + const conditions = Object.assign({}, baseConditions); + conditions.any[0].params.wordIndex = 1; + const engine = setup(); + await engine.run(); + expect(eventSpy).to.have.been.calledWith(event); + }); - it('does not emit when the condition fails', async () => { - const conditions = Object.assign({}, baseConditions) - conditions.any[0].params.wordIndex = 0 - const engine = setup() - await engine.run() - expect(eventSpy).to.not.have.been.calledWith(event) - }) + it("does not emit when the condition fails", async () => { + const conditions = Object.assign({}, baseConditions); + conditions.any[0].params.wordIndex = 0; + const engine = setup(); + await engine.run(); + expect(eventSpy).to.not.have.been.calledWith(event); + }); - it('throws when it encounters an unregistered operator', async () => { - const conditions = Object.assign({}, baseConditions) - conditions.any[0].operator = 'unknown-operator' - const engine = setup() - return expect(engine.run()).to.eventually.be.rejectedWith('Unknown operator: unknown-operator') - }) - }) -}) + it("throws when it encounters an unregistered operator", async () => { + const conditions = Object.assign({}, baseConditions); + conditions.any[0].operator = "unknown-operator"; + const engine = setup(); + return expect(engine.run()).to.eventually.be.rejectedWith( + "Unknown operator: unknown-operator", + ); + }); + }); +}); diff --git a/test/engine-parallel-condition-cache.test.js b/test/engine-parallel-condition-cache.test.js index 5bb86e8a..cb96a09a 100644 --- a/test/engine-parallel-condition-cache.test.js +++ b/test/engine-parallel-condition-cache.test.js @@ -1,84 +1,90 @@ -'use strict' +"use strict"; -import engineFactory from '../src/index' -import sinon from 'sinon' +import engineFactory from "../src/index"; +import sinon from "sinon"; -describe('Engine', () => { - let engine - let sandbox +describe("Engine", () => { + let engine; + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); afterEach(() => { - sandbox.restore() - }) - const event = { type: 'early-twenties' } + sandbox.restore(); + }); + const event = { type: "early-twenties" }; const conditions = { - all: [{ - fact: 'age', - operator: 'lessThanInclusive', - value: 25 - }, { - fact: 'age', - operator: 'greaterThanInclusive', - value: 20 - }, { - fact: 'age', - operator: 'notIn', - value: [21, 22] - }] - } + all: [ + { + fact: "age", + operator: "lessThanInclusive", + value: 25, + }, + { + fact: "age", + operator: "greaterThanInclusive", + value: 20, + }, + { + fact: "age", + operator: "notIn", + value: [21, 22], + }, + ], + }; - let eventSpy - let factSpy - function setup (factOptions) { - factSpy = sandbox.spy() - eventSpy = sandbox.spy() + let eventSpy; + let factSpy; + function setup(factOptions) { + factSpy = sandbox.spy(); + eventSpy = sandbox.spy(); const factDefinition = () => { - factSpy() - return 24 - } + factSpy(); + return 24; + }; - engine = engineFactory() - const rule = factories.rule({ conditions, event }) - engine.addRule(rule) - engine.addFact('age', factDefinition, factOptions) - engine.on('success', eventSpy) + engine = engineFactory(); + const rule = factories.rule({ conditions, event }); + engine.addRule(rule); + engine.addFact("age", factDefinition, factOptions); + engine.on("success", eventSpy); } - describe('1 rule with parallel conditions', () => { - it('calls the fact definition once for each condition if caching is off', async () => { - setup({ cache: false }) - await engine.run() - expect(eventSpy).to.have.been.calledOnce() - expect(factSpy).to.have.been.calledThrice() - }) + describe("1 rule with parallel conditions", () => { + it("calls the fact definition once for each condition if caching is off", async () => { + setup({ cache: false }); + await engine.run(); + expect(eventSpy).to.have.been.calledOnce(); + expect(factSpy).to.have.been.calledThrice(); + }); - it('calls the fact definition once', async () => { - setup() - await engine.run() - expect(eventSpy).to.have.been.calledOnce() - expect(factSpy).to.have.been.calledOnce() - }) - }) + it("calls the fact definition once", async () => { + setup(); + await engine.run(); + expect(eventSpy).to.have.been.calledOnce(); + expect(factSpy).to.have.been.calledOnce(); + }); + }); - describe('2 rules with parallel conditions', () => { - it('calls the fact definition once', async () => { - setup() + describe("2 rules with parallel conditions", () => { + it("calls the fact definition once", async () => { + setup(); const conditions = { - all: [{ - fact: 'age', - operator: 'notIn', - value: [21, 22] - }] - } - const rule = factories.rule({ conditions, event }) - engine.addRule(rule) + all: [ + { + fact: "age", + operator: "notIn", + value: [21, 22], + }, + ], + }; + const rule = factories.rule({ conditions, event }); + engine.addRule(rule); - await engine.run() - expect(eventSpy).to.have.been.calledTwice() - expect(factSpy).to.have.been.calledOnce() - }) - }) -}) + await engine.run(); + expect(eventSpy).to.have.been.calledTwice(); + expect(factSpy).to.have.been.calledOnce(); + }); + }); +}); diff --git a/test/engine-recusive-rules.test.js b/test/engine-recusive-rules.test.js index 48935998..86c2892b 100644 --- a/test/engine-recusive-rules.test.js +++ b/test/engine-recusive-rules.test.js @@ -1,126 +1,126 @@ -'use strict' +"use strict"; -import engineFactory from '../src/index' -import sinon from 'sinon' +import engineFactory from "../src/index"; +import sinon from "sinon"; -describe('Engine: recursive rules', () => { - let engine - const event = { type: 'middle-income-adult' } +describe("Engine: recursive rules", () => { + let engine; + const event = { type: "middle-income-adult" }; const nestedAnyCondition = { all: [ { - fact: 'age', - operator: 'lessThan', - value: 65 + fact: "age", + operator: "lessThan", + value: 65, }, { - fact: 'age', - operator: 'greaterThan', - value: 21 + fact: "age", + operator: "greaterThan", + value: 21, }, { any: [ { - fact: 'income', - operator: 'lessThanInclusive', - value: 100 + fact: "income", + operator: "lessThanInclusive", + value: 100, }, { - fact: 'family-size', - operator: 'lessThanInclusive', - value: 3 - } - ] - } - ] - } + fact: "family-size", + operator: "lessThanInclusive", + value: 3, + }, + ], + }, + ], + }; - let sandbox + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); afterEach(() => { - sandbox.restore() - }) + sandbox.restore(); + }); - let eventSpy - function setup (conditions = nestedAnyCondition) { - eventSpy = sandbox.spy() + let eventSpy; + function setup(conditions = nestedAnyCondition) { + eventSpy = sandbox.spy(); - engine = engineFactory() - const rule = factories.rule({ conditions, event }) - engine.addRule(rule) - engine.on('success', eventSpy) + engine = engineFactory(); + const rule = factories.rule({ conditions, event }); + engine.addRule(rule); + engine.on("success", eventSpy); } describe('"all" with nested "any"', () => { - it('evaluates true when facts pass rules', async () => { - setup() - engine.addFact('age', 30) - engine.addFact('income', 30) - engine.addFact('family-size', 2) - await engine.run() - expect(eventSpy).to.have.been.calledOnce() - }) - - it('evaluates false when facts do not pass rules', async () => { - setup() - engine.addFact('age', 30) - engine.addFact('income', 200) - engine.addFact('family-size', 8) - await engine.run() - expect(eventSpy).to.not.have.been.calledOnce() - }) - }) + it("evaluates true when facts pass rules", async () => { + setup(); + engine.addFact("age", 30); + engine.addFact("income", 30); + engine.addFact("family-size", 2); + await engine.run(); + expect(eventSpy).to.have.been.calledOnce(); + }); + + it("evaluates false when facts do not pass rules", async () => { + setup(); + engine.addFact("age", 30); + engine.addFact("income", 200); + engine.addFact("family-size", 8); + await engine.run(); + expect(eventSpy).to.not.have.been.calledOnce(); + }); + }); const nestedAllCondition = { any: [ { - fact: 'age', - operator: 'lessThan', - value: 65 + fact: "age", + operator: "lessThan", + value: 65, }, { - fact: 'age', - operator: 'equal', - value: 70 + fact: "age", + operator: "equal", + value: 70, }, { all: [ { - fact: 'income', - operator: 'lessThanInclusive', - value: 100 + fact: "income", + operator: "lessThanInclusive", + value: 100, }, { - fact: 'family-size', - operator: 'lessThanInclusive', - value: 3 - } - ] - } - ] - } + fact: "family-size", + operator: "lessThanInclusive", + value: 3, + }, + ], + }, + ], + }; describe('"any" with nested "all"', () => { - it('evaluates true when facts pass rules', async () => { - setup(nestedAllCondition) - engine.addFact('age', 90) - engine.addFact('income', 30) - engine.addFact('family-size', 2) - await engine.run() - expect(eventSpy).to.have.been.calledOnce() - }) - - it('evaluates false when facts do not pass rules', async () => { - setup(nestedAllCondition) - engine.addFact('age', 90) - engine.addFact('income', 200) - engine.addFact('family-size', 2) - await engine.run() - expect(eventSpy).to.not.have.been.calledOnce() - }) - }) + it("evaluates true when facts pass rules", async () => { + setup(nestedAllCondition); + engine.addFact("age", 90); + engine.addFact("income", 30); + engine.addFact("family-size", 2); + await engine.run(); + expect(eventSpy).to.have.been.calledOnce(); + }); + + it("evaluates false when facts do not pass rules", async () => { + setup(nestedAllCondition); + engine.addFact("age", 90); + engine.addFact("income", 200); + engine.addFact("family-size", 2); + await engine.run(); + expect(eventSpy).to.not.have.been.calledOnce(); + }); + }); const thriceNestedCondition = { any: [ @@ -129,105 +129,105 @@ describe('Engine: recursive rules', () => { { any: [ { - fact: 'income', - operator: 'lessThanInclusive', - value: 100 - } - ] + fact: "income", + operator: "lessThanInclusive", + value: 100, + }, + ], }, { - fact: 'family-size', - operator: 'lessThanInclusive', - value: 3 - } - ] - } - ] - } + fact: "family-size", + operator: "lessThanInclusive", + value: 3, + }, + ], + }, + ], + }; describe('"any" with "all" within "any"', () => { - it('evaluates true when facts pass rules', async () => { - setup(thriceNestedCondition) - engine.addFact('income', 30) - engine.addFact('family-size', 1) - await engine.run() - expect(eventSpy).to.have.been.calledOnce() - }) - - it('evaluates false when facts do not pass rules', async () => { - setup(thriceNestedCondition) - engine.addFact('income', 30) - engine.addFact('family-size', 5) - await engine.run() - expect(eventSpy).to.not.have.been.calledOnce() - }) - }) + it("evaluates true when facts pass rules", async () => { + setup(thriceNestedCondition); + engine.addFact("income", 30); + engine.addFact("family-size", 1); + await engine.run(); + expect(eventSpy).to.have.been.calledOnce(); + }); + + it("evaluates false when facts do not pass rules", async () => { + setup(thriceNestedCondition); + engine.addFact("income", 30); + engine.addFact("family-size", 5); + await engine.run(); + expect(eventSpy).to.not.have.been.calledOnce(); + }); + }); const notNotCondition = { not: { not: { - fact: 'age', - operator: 'lessThan', - value: 65 - } - } - } + fact: "age", + operator: "lessThan", + value: 65, + }, + }, + }; describe('"not" nested directly within a "not"', () => { - it('evaluates true when facts pass rules', async () => { - setup(notNotCondition) - engine.addFact('age', 30) - await engine.run() - expect(eventSpy).to.have.been.calledOnce() - }) - - it('evaluates false when facts do not pass rules', async () => { - setup(notNotCondition) - engine.addFact('age', 65) - await engine.run() - expect(eventSpy).to.not.have.been.calledOnce() - }) - }) + it("evaluates true when facts pass rules", async () => { + setup(notNotCondition); + engine.addFact("age", 30); + await engine.run(); + expect(eventSpy).to.have.been.calledOnce(); + }); + + it("evaluates false when facts do not pass rules", async () => { + setup(notNotCondition); + engine.addFact("age", 65); + await engine.run(); + expect(eventSpy).to.not.have.been.calledOnce(); + }); + }); const nestedNotCondition = { not: { all: [ { - fact: 'age', - operator: 'lessThan', - value: 65 + fact: "age", + operator: "lessThan", + value: 65, }, { - fact: 'age', - operator: 'greaterThan', - value: 21 + fact: "age", + operator: "greaterThan", + value: 21, }, { not: { - fact: 'income', - operator: 'lessThanInclusive', - value: 100 - } - } - ] - } - } + fact: "income", + operator: "lessThanInclusive", + value: 100, + }, + }, + ], + }, + }; describe('outer "not" with nested "all" and nested "not" condition', () => { - it('evaluates true when facts pass rules', async () => { - setup(nestedNotCondition) - engine.addFact('age', 30) - engine.addFact('income', 100) - await engine.run() - expect(eventSpy).to.have.been.calledOnce() - }) - - it('evaluates false when facts do not pass rules', async () => { - setup(nestedNotCondition) - engine.addFact('age', 30) - engine.addFact('income', 101) - await engine.run() - expect(eventSpy).to.not.have.been.calledOnce() - }) - }) -}) + it("evaluates true when facts pass rules", async () => { + setup(nestedNotCondition); + engine.addFact("age", 30); + engine.addFact("income", 100); + await engine.run(); + expect(eventSpy).to.have.been.calledOnce(); + }); + + it("evaluates false when facts do not pass rules", async () => { + setup(nestedNotCondition); + engine.addFact("age", 30); + engine.addFact("income", 101); + await engine.run(); + expect(eventSpy).to.not.have.been.calledOnce(); + }); + }); +}); diff --git a/test/engine-rule-priority.js b/test/engine-rule-priority.js index df96f995..c55ca2f8 100644 --- a/test/engine-rule-priority.js +++ b/test/engine-rule-priority.js @@ -1,96 +1,110 @@ -'use strict' +"use strict"; -import engineFactory from '../src/index' -import sinon from 'sinon' +import engineFactory from "../src/index"; +import sinon from "sinon"; -describe('Engine: rule priorities', () => { - let engine +describe("Engine: rule priorities", () => { + let engine; - const highPriorityEvent = { type: 'highPriorityEvent' } - const midPriorityEvent = { type: 'midPriorityEvent' } - const lowestPriorityEvent = { type: 'lowestPriorityEvent' } + const highPriorityEvent = { type: "highPriorityEvent" }; + const midPriorityEvent = { type: "midPriorityEvent" }; + const lowestPriorityEvent = { type: "lowestPriorityEvent" }; const conditions = { - any: [{ - fact: 'age', - operator: 'greaterThanInclusive', - value: 21 - }] - } - - let sandbox + any: [ + { + fact: "age", + operator: "greaterThanInclusive", + value: 21, + }, + ], + }; + + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); afterEach(() => { - sandbox.restore() - }) - - function setup () { - const factSpy = sandbox.stub().returns(22) - const eventSpy = sandbox.spy() - engine = engineFactory() - - const highPriorityRule = factories.rule({ conditions, event: midPriorityEvent, priority: 50 }) - engine.addRule(highPriorityRule) - - const midPriorityRule = factories.rule({ conditions, event: highPriorityEvent, priority: 100 }) - engine.addRule(midPriorityRule) - - const lowPriorityRule = factories.rule({ conditions, event: lowestPriorityEvent, priority: 1 }) - engine.addRule(lowPriorityRule) - - engine.addFact('age', factSpy) - engine.on('success', eventSpy) + sandbox.restore(); + }); + + function setup() { + const factSpy = sandbox.stub().returns(22); + const eventSpy = sandbox.spy(); + engine = engineFactory(); + + const highPriorityRule = factories.rule({ + conditions, + event: midPriorityEvent, + priority: 50, + }); + engine.addRule(highPriorityRule); + + const midPriorityRule = factories.rule({ + conditions, + event: highPriorityEvent, + priority: 100, + }); + engine.addRule(midPriorityRule); + + const lowPriorityRule = factories.rule({ + conditions, + event: lowestPriorityEvent, + priority: 1, + }); + engine.addRule(lowPriorityRule); + + engine.addFact("age", factSpy); + engine.on("success", eventSpy); } - it('runs the rules in order of priority', () => { - setup() - expect(engine.prioritizedRules).to.be.null() - engine.prioritizeRules() - expect(engine.prioritizedRules.length).to.equal(3) - expect(engine.prioritizedRules[0][0].priority).to.equal(100) - expect(engine.prioritizedRules[1][0].priority).to.equal(50) - expect(engine.prioritizedRules[2][0].priority).to.equal(1) - }) - - it('clears re-propriorizes the rules when a new Rule is added', () => { - engine.prioritizeRules() - expect(engine.prioritizedRules.length).to.equal(3) - engine.addRule(factories.rule()) - expect(engine.prioritizedRules).to.be.null() - }) - - it('resolves all events returning promises before executing the next rule', async () => { - setup() - - const highPrioritySpy = sandbox.spy() - const midPrioritySpy = sandbox.spy() - const lowPrioritySpy = sandbox.spy() + it("runs the rules in order of priority", () => { + setup(); + expect(engine.prioritizedRules).to.be.null(); + engine.prioritizeRules(); + expect(engine.prioritizedRules.length).to.equal(3); + expect(engine.prioritizedRules[0][0].priority).to.equal(100); + expect(engine.prioritizedRules[1][0].priority).to.equal(50); + expect(engine.prioritizedRules[2][0].priority).to.equal(1); + }); + + it("clears re-propriorizes the rules when a new Rule is added", () => { + engine.prioritizeRules(); + expect(engine.prioritizedRules.length).to.equal(3); + engine.addRule(factories.rule()); + expect(engine.prioritizedRules).to.be.null(); + }); + + it("resolves all events returning promises before executing the next rule", async () => { + setup(); + + const highPrioritySpy = sandbox.spy(); + const midPrioritySpy = sandbox.spy(); + const lowPrioritySpy = sandbox.spy(); engine.on(highPriorityEvent.type, () => { return new Promise(function (resolve) { setTimeout(function () { - highPrioritySpy() - resolve() - }, 10) // wait longest - }) - }) + highPrioritySpy(); + resolve(); + }, 10); // wait longest + }); + }); engine.on(midPriorityEvent.type, () => { return new Promise(function (resolve) { setTimeout(function () { - midPrioritySpy() - resolve() - }, 5) // wait half as much - }) - }) + midPrioritySpy(); + resolve(); + }, 5); // wait half as much + }); + }); engine.on(lowestPriorityEvent.type, () => { - lowPrioritySpy() // emit immediately. this event should still be triggered last - }) + lowPrioritySpy(); // emit immediately. this event should still be triggered last + }); - await engine.run() + await engine.run(); - expect(highPrioritySpy).to.be.calledBefore(midPrioritySpy) - expect(midPrioritySpy).to.be.calledBefore(lowPrioritySpy) - }) -}) + expect(highPrioritySpy).to.be.calledBefore(midPrioritySpy); + expect(midPrioritySpy).to.be.calledBefore(lowPrioritySpy); + }); +}); diff --git a/test/engine-run.test.js b/test/engine-run.test.js index a96d950a..c8b57999 100644 --- a/test/engine-run.test.js +++ b/test/engine-run.test.js @@ -1,136 +1,159 @@ -'use strict' +"use strict"; -import engineFactory from '../src/index' -import Almanac from '../src/almanac' -import sinon from 'sinon' +import engineFactory from "../src/index"; +import Almanac from "../src/almanac"; +import sinon from "sinon"; -describe('Engine: run', () => { - let engine, rule, rule2 - let sandbox +describe("Engine: run", () => { + let engine, rule, rule2; + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); afterEach(() => { - sandbox.restore() - }) + sandbox.restore(); + }); const condition21 = { - any: [{ - fact: 'age', - operator: 'greaterThanInclusive', - value: 21 - }] - } + any: [ + { + fact: "age", + operator: "greaterThanInclusive", + value: 21, + }, + ], + }; const condition75 = { - any: [{ - fact: 'age', - operator: 'greaterThanInclusive', - value: 75 - }] - } - let eventSpy + any: [ + { + fact: "age", + operator: "greaterThanInclusive", + value: 75, + }, + ], + }; + let eventSpy; beforeEach(() => { - eventSpy = sandbox.spy() - engine = engineFactory() - rule = factories.rule({ conditions: condition21, event: { type: 'generic1' } }) - engine.addRule(rule) - rule2 = factories.rule({ conditions: condition75, event: { type: 'generic2' } }) - engine.addRule(rule2) - engine.on('success', eventSpy) - }) - - describe('independent runs', () => { - it('treats each run() independently', async () => { - await Promise.all([50, 10, 12, 30, 14, 15, 25].map((age) => engine.run({ age }))) - expect(eventSpy).to.have.been.calledThrice() - }) - - it('allows runtime facts to override engine facts for a single run()', async () => { - engine.addFact('age', 30) - - await engine.run({ age: 85 }) // override 'age' with runtime fact - expect(eventSpy).to.have.been.calledTwice() - - sandbox.reset() - await engine.run() // no runtime fact; revert to age: 30 - expect(eventSpy).to.have.been.calledOnce() - - sandbox.reset() - await engine.run({ age: 2 }) // override 'age' with runtime fact - expect(eventSpy.callCount).to.equal(0) - }) - }) - - describe('returns', () => { - it('activated events', async () => { - const { events, failureEvents } = await engine.run({ age: 30 }) - expect(events.length).to.equal(1) - expect(events).to.deep.include(rule.event) - expect(failureEvents.length).to.equal(1) - expect(failureEvents).to.deep.include(rule2.event) - }) - - it('multiple activated events', () => { - return engine.run({ age: 90 }).then(results => { - expect(results.events.length).to.equal(2) - expect(results.events).to.deep.include(rule.event) - expect(results.events).to.deep.include(rule2.event) - }) - }) - - it('does not include unactived triggers', () => { - return engine.run({ age: 10 }).then(results => { - expect(results.events.length).to.equal(0) - }) - }) - - it('includes the almanac', () => { - return engine.run({ age: 10 }).then(results => { - expect(results.almanac).to.be.an.instanceOf(Almanac) - return results.almanac.factValue('age') - }).then(ageFact => expect(ageFact).to.equal(10)) - }) - }) - - describe('facts updated during run', () => { + eventSpy = sandbox.spy(); + engine = engineFactory(); + rule = factories.rule({ + conditions: condition21, + event: { type: "generic1" }, + }); + engine.addRule(rule); + rule2 = factories.rule({ + conditions: condition75, + event: { type: "generic2" }, + }); + engine.addRule(rule2); + engine.on("success", eventSpy); + }); + + describe("independent runs", () => { + it("treats each run() independently", async () => { + await Promise.all( + [50, 10, 12, 30, 14, 15, 25].map((age) => engine.run({ age })), + ); + expect(eventSpy).to.have.been.calledThrice(); + }); + + it("allows runtime facts to override engine facts for a single run()", async () => { + engine.addFact("age", 30); + + await engine.run({ age: 85 }); // override 'age' with runtime fact + expect(eventSpy).to.have.been.calledTwice(); + + sandbox.reset(); + await engine.run(); // no runtime fact; revert to age: 30 + expect(eventSpy).to.have.been.calledOnce(); + + sandbox.reset(); + await engine.run({ age: 2 }); // override 'age' with runtime fact + expect(eventSpy.callCount).to.equal(0); + }); + }); + + describe("returns", () => { + it("activated events", async () => { + const { events, failureEvents } = await engine.run({ age: 30 }); + expect(events.length).to.equal(1); + expect(events).to.deep.include(rule.event); + expect(failureEvents.length).to.equal(1); + expect(failureEvents).to.deep.include(rule2.event); + }); + + it("multiple activated events", () => { + return engine.run({ age: 90 }).then((results) => { + expect(results.events.length).to.equal(2); + expect(results.events).to.deep.include(rule.event); + expect(results.events).to.deep.include(rule2.event); + }); + }); + + it("does not include unactived triggers", () => { + return engine.run({ age: 10 }).then((results) => { + expect(results.events.length).to.equal(0); + }); + }); + + it("includes the almanac", () => { + return engine + .run({ age: 10 }) + .then((results) => { + expect(results.almanac).to.be.an.instanceOf(Almanac); + return results.almanac.factValue("age"); + }) + .then((ageFact) => expect(ageFact).to.equal(10)); + }); + }); + + describe("facts updated during run", () => { beforeEach(() => { - engine.on('success', (event, almanac, ruleResult) => { + engine.on("success", (event, almanac, ruleResult) => { // Assign unique runtime facts per event - almanac.addRuntimeFact(`runtime-fact-${event.type}`, ruleResult.conditions.any[0].value) - }) - }) - - it('returns an almanac with runtime facts added', () => { - return engine.run({ age: 90 }).then(results => { - return Promise.all([ - results.almanac.factValue('runtime-fact-generic1'), - results.almanac.factValue('runtime-fact-generic2') - ]) - }).then(promiseValues => { - expect(promiseValues[0]).to.equal(21) - expect(promiseValues[1]).to.equal(75) - }) - }) - }) - - describe('custom alamanc', () => { + almanac.addRuntimeFact( + `runtime-fact-${event.type}`, + ruleResult.conditions.any[0].value, + ); + }); + }); + + it("returns an almanac with runtime facts added", () => { + return engine + .run({ age: 90 }) + .then((results) => { + return Promise.all([ + results.almanac.factValue("runtime-fact-generic1"), + results.almanac.factValue("runtime-fact-generic2"), + ]); + }) + .then((promiseValues) => { + expect(promiseValues[0]).to.equal(21); + expect(promiseValues[1]).to.equal(75); + }); + }); + }); + + describe("custom alamanc", () => { class CapitalAlmanac extends Almanac { - factValue (factId, params, path) { - return super.factValue(factId, params, path).then(value => { - if (typeof value === 'string') { - return value.toUpperCase() + factValue(factId, params, path) { + return super.factValue(factId, params, path).then((value) => { + if (typeof value === "string") { + return value.toUpperCase(); } - return value - }) + return value; + }); } } - it('returns the capitalized value when using the CapitalAlamanc', () => { - return engine.run({ greeting: 'hello', age: 30 }, { almanac: new CapitalAlmanac() }).then((results) => { - const fact = results.almanac.factValue('greeting') - return expect(fact).to.eventually.equal('HELLO') - }) - }) - }) -}) + it("returns the capitalized value when using the CapitalAlamanc", () => { + return engine + .run({ greeting: "hello", age: 30 }, { almanac: new CapitalAlmanac() }) + .then((results) => { + const fact = results.almanac.factValue("greeting"); + return expect(fact).to.eventually.equal("HELLO"); + }); + }); + }); +}); diff --git a/test/engine.test.js b/test/engine.test.js index 8ad3ca74..8cf817cb 100644 --- a/test/engine.test.js +++ b/test/engine.test.js @@ -1,330 +1,338 @@ -'use strict' +"use strict"; -import sinon from 'sinon' -import engineFactory, { Fact, Rule, Operator } from '../src/index' -import defaultOperators from '../src/engine-default-operators' +import sinon from "sinon"; +import engineFactory, { Fact, Rule, Operator } from "../src/index"; +import defaultOperators from "../src/engine-default-operators"; -describe('Engine', () => { - let engine - let sandbox +describe("Engine", () => { + let engine; + let sandbox; before(() => { - sandbox = sinon.createSandbox() - }) + sandbox = sinon.createSandbox(); + }); afterEach(() => { - sandbox.restore() - }) + sandbox.restore(); + }); beforeEach(() => { - engine = engineFactory() - }) - - it('has methods for managing facts and rules, and running itself', () => { - expect(engine).to.have.property('addRule') - expect(engine).to.have.property('removeRule') - expect(engine).to.have.property('addOperator') - expect(engine).to.have.property('removeOperator') - expect(engine).to.have.property('addFact') - expect(engine).to.have.property('removeFact') - expect(engine).to.have.property('run') - expect(engine).to.have.property('stop') - }) - - describe('constructor', () => { - it('initializes with the default state', () => { - expect(engine.status).to.equal('READY') - expect(engine.rules.length).to.equal(0) - defaultOperators.forEach(op => { - expect(engine.operators.get(op.name)).to.be.an.instanceof(Operator) - }) - }) - - it('can be initialized with rules', () => { - const rules = [ - factories.rule(), - factories.rule(), - factories.rule() - ] - engine = engineFactory(rules) - expect(engine.rules.length).to.equal(rules.length) - }) - }) - - describe('stop()', () => { + engine = engineFactory(); + }); + + it("has methods for managing facts and rules, and running itself", () => { + expect(engine).to.have.property("addRule"); + expect(engine).to.have.property("removeRule"); + expect(engine).to.have.property("addOperator"); + expect(engine).to.have.property("removeOperator"); + expect(engine).to.have.property("addFact"); + expect(engine).to.have.property("removeFact"); + expect(engine).to.have.property("run"); + expect(engine).to.have.property("stop"); + }); + + describe("constructor", () => { + it("initializes with the default state", () => { + expect(engine.status).to.equal("READY"); + expect(engine.rules.length).to.equal(0); + defaultOperators.forEach((op) => { + expect(engine.operators.get(op.name)).to.be.an.instanceof(Operator); + }); + }); + + it("can be initialized with rules", () => { + const rules = [factories.rule(), factories.rule(), factories.rule()]; + engine = engineFactory(rules); + expect(engine.rules.length).to.equal(rules.length); + }); + }); + + describe("stop()", () => { it('changes the status to "FINISHED"', () => { - expect(engine.stop().status).to.equal('FINISHED') - }) - }) - - describe('addRule()', () => { - describe('rule instance', () => { - it('adds the rule', () => { - const rule = new Rule(factories.rule()) - expect(engine.rules.length).to.equal(0) - engine.addRule(rule) - expect(engine.rules.length).to.equal(1) - expect(engine.rules).to.include(rule) - }) - }) - - describe('required fields', () => { - it('.conditions', () => { - const rule = factories.rule() - delete rule.conditions + expect(engine.stop().status).to.equal("FINISHED"); + }); + }); + + describe("addRule()", () => { + describe("rule instance", () => { + it("adds the rule", () => { + const rule = new Rule(factories.rule()); + expect(engine.rules.length).to.equal(0); + engine.addRule(rule); + expect(engine.rules.length).to.equal(1); + expect(engine.rules).to.include(rule); + }); + }); + + describe("required fields", () => { + it(".conditions", () => { + const rule = factories.rule(); + delete rule.conditions; expect(() => { - engine.addRule(rule) - }).to.throw(/Engine: addRule\(\) argument requires "conditions" property/) - }) - - it('.event', () => { - const rule = factories.rule() - delete rule.event + engine.addRule(rule); + }).to.throw( + /Engine: addRule\(\) argument requires "conditions" property/, + ); + }); + + it(".event", () => { + const rule = factories.rule(); + delete rule.event; expect(() => { - engine.addRule(rule) - }).to.throw(/Engine: addRule\(\) argument requires "event" property/) - }) - }) - }) - - describe('updateRule()', () => { - it('updates rule', () => { - let rule1 = new Rule(factories.rule({ name: 'rule1' })) - let rule2 = new Rule(factories.rule({ name: 'rule2' })) - engine.addRule(rule1) - engine.addRule(rule2) - expect(engine.rules[0].conditions.all.length).to.equal(2) - expect(engine.rules[1].conditions.all.length).to.equal(2) - - rule1.conditions = { all: [] } - engine.updateRule(rule1) - - rule1 = engine.rules.find(rule => rule.name === 'rule1') - rule2 = engine.rules.find(rule => rule.name === 'rule2') - expect(rule1.conditions.all.length).to.equal(0) - expect(rule2.conditions.all.length).to.equal(2) - }) - - it('should throw error if rule not found', () => { - const rule1 = new Rule(factories.rule({ name: 'rule1' })) - engine.addRule(rule1) - const rule2 = new Rule(factories.rule({ name: 'rule2' })) + engine.addRule(rule); + }).to.throw(/Engine: addRule\(\) argument requires "event" property/); + }); + }); + }); + + describe("updateRule()", () => { + it("updates rule", () => { + let rule1 = new Rule(factories.rule({ name: "rule1" })); + let rule2 = new Rule(factories.rule({ name: "rule2" })); + engine.addRule(rule1); + engine.addRule(rule2); + expect(engine.rules[0].conditions.all.length).to.equal(2); + expect(engine.rules[1].conditions.all.length).to.equal(2); + + rule1.conditions = { all: [] }; + engine.updateRule(rule1); + + rule1 = engine.rules.find((rule) => rule.name === "rule1"); + rule2 = engine.rules.find((rule) => rule.name === "rule2"); + expect(rule1.conditions.all.length).to.equal(0); + expect(rule2.conditions.all.length).to.equal(2); + }); + + it("should throw error if rule not found", () => { + const rule1 = new Rule(factories.rule({ name: "rule1" })); + engine.addRule(rule1); + const rule2 = new Rule(factories.rule({ name: "rule2" })); expect(() => { - engine.updateRule(rule2) - }).to.throw(/Engine: updateRule\(\) rule not found/) - }) - }) - - describe('removeRule()', () => { - function setup () { - const rule1 = new Rule(factories.rule({ name: 'rule1' })) - const rule2 = new Rule(factories.rule({ name: 'rule2' })) - engine.addRule(rule1) - engine.addRule(rule2) - engine.prioritizeRules() - - return [rule1, rule2] + engine.updateRule(rule2); + }).to.throw(/Engine: updateRule\(\) rule not found/); + }); + }); + + describe("removeRule()", () => { + function setup() { + const rule1 = new Rule(factories.rule({ name: "rule1" })); + const rule2 = new Rule(factories.rule({ name: "rule2" })); + engine.addRule(rule1); + engine.addRule(rule2); + engine.prioritizeRules(); + + return [rule1, rule2]; } - context('remove by rule.name', () => { - it('removes a single rule', () => { - const [rule1] = setup() - expect(engine.rules.length).to.equal(2) - - const isRemoved = engine.removeRule(rule1.name) - - expect(isRemoved).to.be.true() - expect(engine.rules.length).to.equal(1) - expect(engine.prioritizedRules).to.equal(null) - }) - - it('removes multiple rules with the same name', () => { - const [rule1] = setup() - const rule3 = new Rule(factories.rule({ name: rule1.name })) - engine.addRule(rule3) - expect(engine.rules.length).to.equal(3) - - const isRemoved = engine.removeRule(rule1.name) - - expect(isRemoved).to.be.true() - expect(engine.rules.length).to.equal(1) - expect(engine.prioritizedRules).to.equal(null) - }) - - it('returns false when rule cannot be found', () => { - setup() - expect(engine.rules.length).to.equal(2) - - const isRemoved = engine.removeRule('not-found-name') - - expect(isRemoved).to.be.false() - expect(engine.rules.length).to.equal(2) - expect(engine.prioritizedRules).to.not.equal(null) - }) - }) - context('remove by rule object', () => { - it('removes a single rule', () => { - const [rule1] = setup() - expect(engine.rules.length).to.equal(2) - - const isRemoved = engine.removeRule(rule1) - - expect(isRemoved).to.be.true() - expect(engine.rules.length).to.equal(1) - expect(engine.prioritizedRules).to.equal(null) - }) - - it('removes a single rule, even if two have the same name', () => { - const [rule1] = setup() - const rule3 = new Rule(factories.rule({ name: rule1.name })) - engine.addRule(rule3) - expect(engine.rules.length).to.equal(3) - - const isRemoved = engine.removeRule(rule1) - - expect(isRemoved).to.be.true() - expect(engine.rules.length).to.equal(2) - expect(engine.prioritizedRules).to.equal(null) - }) - - it('returns false when rule cannot be found', () => { - setup() - expect(engine.rules.length).to.equal(2) - - const rule3 = new Rule(factories.rule({ name: 'rule3' })) - const isRemoved = engine.removeRule(rule3) - - expect(isRemoved).to.be.false() - expect(engine.rules.length).to.equal(2) - expect(engine.prioritizedRules).to.not.equal(null) - }) - }) - }) - - describe('addOperator()', () => { - it('adds the operator', () => { - engine.addOperator('startsWithLetter', (factValue, jsonValue) => { - return factValue[0] === jsonValue - }) - expect(engine.operators.get('startsWithLetter')).to.exist() - expect(engine.operators.get('startsWithLetter')).to.be.an.instanceof(Operator) - }) - - it('accepts an operator instance', () => { - const op = new Operator('my-operator', _ => true) - engine.addOperator(op) - expect(engine.operators.get('my-operator')).to.equal(op) - }) - }) - - describe('removeOperator()', () => { - it('removes the operator', () => { - engine.addOperator('startsWithLetter', (factValue, jsonValue) => { - return factValue[0] === jsonValue - }) - expect(engine.operators.get('startsWithLetter')).to.be.an.instanceof(Operator) - engine.removeOperator('startsWithLetter') - expect(engine.operators.get('startsWithLetter')).to.be.null() - }) - - it('can only remove added operators', () => { - const isRemoved = engine.removeOperator('nonExisting') - expect(isRemoved).to.equal(false) - }) - }) - - describe('addFact()', () => { - const FACT_NAME = 'FACT_NAME' - const FACT_VALUE = 'FACT_VALUE' - - function assertFact (engine) { - expect(engine.facts.size).to.equal(1) - expect(engine.facts.has(FACT_NAME)).to.be.true() + context("remove by rule.name", () => { + it("removes a single rule", () => { + const [rule1] = setup(); + expect(engine.rules.length).to.equal(2); + + const isRemoved = engine.removeRule(rule1.name); + + expect(isRemoved).to.be.true(); + expect(engine.rules.length).to.equal(1); + expect(engine.prioritizedRules).to.equal(null); + }); + + it("removes multiple rules with the same name", () => { + const [rule1] = setup(); + const rule3 = new Rule(factories.rule({ name: rule1.name })); + engine.addRule(rule3); + expect(engine.rules.length).to.equal(3); + + const isRemoved = engine.removeRule(rule1.name); + + expect(isRemoved).to.be.true(); + expect(engine.rules.length).to.equal(1); + expect(engine.prioritizedRules).to.equal(null); + }); + + it("returns false when rule cannot be found", () => { + setup(); + expect(engine.rules.length).to.equal(2); + + const isRemoved = engine.removeRule("not-found-name"); + + expect(isRemoved).to.be.false(); + expect(engine.rules.length).to.equal(2); + expect(engine.prioritizedRules).to.not.equal(null); + }); + }); + context("remove by rule object", () => { + it("removes a single rule", () => { + const [rule1] = setup(); + expect(engine.rules.length).to.equal(2); + + const isRemoved = engine.removeRule(rule1); + + expect(isRemoved).to.be.true(); + expect(engine.rules.length).to.equal(1); + expect(engine.prioritizedRules).to.equal(null); + }); + + it("removes a single rule, even if two have the same name", () => { + const [rule1] = setup(); + const rule3 = new Rule(factories.rule({ name: rule1.name })); + engine.addRule(rule3); + expect(engine.rules.length).to.equal(3); + + const isRemoved = engine.removeRule(rule1); + + expect(isRemoved).to.be.true(); + expect(engine.rules.length).to.equal(2); + expect(engine.prioritizedRules).to.equal(null); + }); + + it("returns false when rule cannot be found", () => { + setup(); + expect(engine.rules.length).to.equal(2); + + const rule3 = new Rule(factories.rule({ name: "rule3" })); + const isRemoved = engine.removeRule(rule3); + + expect(isRemoved).to.be.false(); + expect(engine.rules.length).to.equal(2); + expect(engine.prioritizedRules).to.not.equal(null); + }); + }); + }); + + describe("addOperator()", () => { + it("adds the operator", () => { + engine.addOperator("startsWithLetter", (factValue, jsonValue) => { + return factValue[0] === jsonValue; + }); + expect(engine.operators.get("startsWithLetter")).to.exist(); + expect(engine.operators.get("startsWithLetter")).to.be.an.instanceof( + Operator, + ); + }); + + it("accepts an operator instance", () => { + const op = new Operator("my-operator", (_) => true); + engine.addOperator(op); + expect(engine.operators.get("my-operator")).to.equal(op); + }); + }); + + describe("removeOperator()", () => { + it("removes the operator", () => { + engine.addOperator("startsWithLetter", (factValue, jsonValue) => { + return factValue[0] === jsonValue; + }); + expect(engine.operators.get("startsWithLetter")).to.be.an.instanceof( + Operator, + ); + engine.removeOperator("startsWithLetter"); + expect(engine.operators.get("startsWithLetter")).to.be.null(); + }); + + it("can only remove added operators", () => { + const isRemoved = engine.removeOperator("nonExisting"); + expect(isRemoved).to.equal(false); + }); + }); + + describe("addFact()", () => { + const FACT_NAME = "FACT_NAME"; + const FACT_VALUE = "FACT_VALUE"; + + function assertFact(engine) { + expect(engine.facts.size).to.equal(1); + expect(engine.facts.has(FACT_NAME)).to.be.true(); } - it('allows a constant fact', () => { - engine.addFact(FACT_NAME, FACT_VALUE) - assertFact(engine) - expect(engine.facts.get(FACT_NAME).value).to.equal(FACT_VALUE) - }) - - it('allows options to be passed', () => { - const options = { cache: false } - engine.addFact(FACT_NAME, FACT_VALUE, options) - assertFact(engine) - expect(engine.facts.get(FACT_NAME).value).to.equal(FACT_VALUE) - expect(engine.facts.get(FACT_NAME).options).to.eql(options) - }) - - it('allows a lamba fact with no options', () => { - engine.addFact(FACT_NAME, async (params, engine) => { - return FACT_VALUE - }) - assertFact(engine) - expect(engine.facts.get(FACT_NAME).value).to.be.undefined() - }) - - it('allows a lamba fact with options', () => { - const options = { cache: false } + it("allows a constant fact", () => { + engine.addFact(FACT_NAME, FACT_VALUE); + assertFact(engine); + expect(engine.facts.get(FACT_NAME).value).to.equal(FACT_VALUE); + }); + + it("allows options to be passed", () => { + const options = { cache: false }; + engine.addFact(FACT_NAME, FACT_VALUE, options); + assertFact(engine); + expect(engine.facts.get(FACT_NAME).value).to.equal(FACT_VALUE); + expect(engine.facts.get(FACT_NAME).options).to.eql(options); + }); + + it("allows a lamba fact with no options", () => { engine.addFact(FACT_NAME, async (params, engine) => { - return FACT_VALUE - }, options) - assertFact(engine) - expect(engine.facts.get(FACT_NAME).options).to.eql(options) - expect(engine.facts.get(FACT_NAME).value).to.be.undefined() - }) - - it('allows a fact instance', () => { - const options = { cache: false } - const fact = new Fact(FACT_NAME, 50, options) - engine.addFact(fact) - assertFact(engine) - expect(engine.facts.get(FACT_NAME)).to.exist() - expect(engine.facts.get(FACT_NAME).options).to.eql(options) - }) - }) - - describe('removeFact()', () => { - it('removes a Fact', () => { - expect(engine.facts.size).to.equal(0) - const fact = new Fact('newFact', 50, { cache: false }) - engine.addFact(fact) - expect(engine.facts.size).to.equal(1) - engine.removeFact('newFact') - expect(engine.facts.size).to.equal(0) - }) - - it('can only remove added facts', () => { - expect(engine.facts.size).to.equal(0) - const isRemoved = engine.removeFact('newFact') - expect(isRemoved).to.equal(false) - }) - }) - - describe('run()', () => { + return FACT_VALUE; + }); + assertFact(engine); + expect(engine.facts.get(FACT_NAME).value).to.be.undefined(); + }); + + it("allows a lamba fact with options", () => { + const options = { cache: false }; + engine.addFact( + FACT_NAME, + async (params, engine) => { + return FACT_VALUE; + }, + options, + ); + assertFact(engine); + expect(engine.facts.get(FACT_NAME).options).to.eql(options); + expect(engine.facts.get(FACT_NAME).value).to.be.undefined(); + }); + + it("allows a fact instance", () => { + const options = { cache: false }; + const fact = new Fact(FACT_NAME, 50, options); + engine.addFact(fact); + assertFact(engine); + expect(engine.facts.get(FACT_NAME)).to.exist(); + expect(engine.facts.get(FACT_NAME).options).to.eql(options); + }); + }); + + describe("removeFact()", () => { + it("removes a Fact", () => { + expect(engine.facts.size).to.equal(0); + const fact = new Fact("newFact", 50, { cache: false }); + engine.addFact(fact); + expect(engine.facts.size).to.equal(1); + engine.removeFact("newFact"); + expect(engine.facts.size).to.equal(0); + }); + + it("can only remove added facts", () => { + expect(engine.facts.size).to.equal(0); + const isRemoved = engine.removeFact("newFact"); + expect(isRemoved).to.equal(false); + }); + }); + + describe("run()", () => { beforeEach(() => { const conditions = { - all: [{ - fact: 'age', - operator: 'greaterThanInclusive', - value: 18 - }] - } - const event = { type: 'generic' } - const rule = factories.rule({ conditions, event }) - engine.addRule(rule) - engine.addFact('age', 20) - }) + all: [ + { + fact: "age", + operator: "greaterThanInclusive", + value: 18, + }, + ], + }; + const event = { type: "generic" }; + const rule = factories.rule({ conditions, event }); + engine.addRule(rule); + engine.addFact("age", 20); + }); it('changes the status to "RUNNING"', () => { - const eventSpy = sandbox.spy() - engine.on('success', (event, almanac) => { - eventSpy() - expect(engine.status).to.equal('RUNNING') - }) - return engine.run() - }) - - it('changes status to FINISHED once complete', async () => { - expect(engine.status).to.equal('READY') - await engine.run() - expect(engine.status).to.equal('FINISHED') - }) - }) -}) + const eventSpy = sandbox.spy(); + engine.on("success", (event, almanac) => { + eventSpy(); + expect(engine.status).to.equal("RUNNING"); + }); + return engine.run(); + }); + + it("changes status to FINISHED once complete", async () => { + expect(engine.status).to.equal("READY"); + await engine.run(); + expect(engine.status).to.equal("FINISHED"); + }); + }); +}); diff --git a/test/fact.test.js b/test/fact.test.js index 4504fa8c..8cea1d1f 100644 --- a/test/fact.test.js +++ b/test/fact.test.js @@ -1,48 +1,48 @@ -'use strict' +"use strict"; -import { Fact } from '../src/index' +import { Fact } from "../src/index"; -describe('Fact', () => { - function subject (id, definition, options) { - return new Fact(id, definition, options) +describe("Fact", () => { + function subject(id, definition, options) { + return new Fact(id, definition, options); } - describe('Fact::constructor', () => { - it('works for constant facts', () => { - const fact = subject('factId', 10) - expect(fact.id).to.equal('factId') - expect(fact.value).to.equal(10) - }) + describe("Fact::constructor", () => { + it("works for constant facts", () => { + const fact = subject("factId", 10); + expect(fact.id).to.equal("factId"); + expect(fact.value).to.equal(10); + }); - it('works for dynamic facts', () => { - const fact = subject('factId', () => 10) - expect(fact.id).to.equal('factId') - expect(fact.calculate()).to.equal(10) - }) + it("works for dynamic facts", () => { + const fact = subject("factId", () => 10); + expect(fact.id).to.equal("factId"); + expect(fact.calculate()).to.equal(10); + }); - it('allows options to be passed', () => { - const opts = { test: true, cache: false } - const fact = subject('factId', 10, opts) - expect(fact.options).to.eql(opts) - }) + it("allows options to be passed", () => { + const opts = { test: true, cache: false }; + const fact = subject("factId", 10, opts); + expect(fact.options).to.eql(opts); + }); - describe('validations', () => { - it('throws if no id provided', () => { - expect(subject).to.throw(/factId required/) - }) - }) - }) + describe("validations", () => { + it("throws if no id provided", () => { + expect(subject).to.throw(/factId required/); + }); + }); + }); - describe('Fact::types', () => { - it('initializes facts with method values as dynamic', () => { - const fact = subject('factId', () => {}) - expect(fact.type).to.equal(Fact.DYNAMIC) - expect(fact.isDynamic()).to.be.true() - }) + describe("Fact::types", () => { + it("initializes facts with method values as dynamic", () => { + const fact = subject("factId", () => {}); + expect(fact.type).to.equal(Fact.DYNAMIC); + expect(fact.isDynamic()).to.be.true(); + }); - it('initializes facts with non-methods as constant', () => { - const fact = subject('factId', 2) - expect(fact.type).to.equal(Fact.CONSTANT) - expect(fact.isConstant()).to.be.true() - }) - }) -}) + it("initializes facts with non-methods as constant", () => { + const fact = subject("factId", 2); + expect(fact.type).to.equal(Fact.CONSTANT); + expect(fact.isConstant()).to.be.true(); + }); + }); +}); diff --git a/test/index.test.js b/test/index.test.js index 350c3ef7..5355759b 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,14 +1,14 @@ -'use strict' +"use strict"; -import subject from '../src/index' +import subject from "../src/index"; -describe('json-business-subject', () => { - it('treats each rule engine independently', () => { - const engine1 = subject() - const engine2 = subject() - engine1.addRule(factories.rule()) - engine2.addRule(factories.rule()) - expect(engine1.rules.length).to.equal(1) - expect(engine2.rules.length).to.equal(1) - }) -}) +describe("json-business-subject", () => { + it("treats each rule engine independently", () => { + const engine1 = subject(); + const engine2 = subject(); + engine1.addRule(factories.rule()); + engine2.addRule(factories.rule()); + expect(engine1.rules.length).to.equal(1); + expect(engine2.rules.length).to.equal(1); + }); +}); diff --git a/test/operator-decorator.test.js b/test/operator-decorator.test.js index 3c10ff4d..b519b061 100644 --- a/test/operator-decorator.test.js +++ b/test/operator-decorator.test.js @@ -1,40 +1,45 @@ -'use strict' +"use strict"; -import { OperatorDecorator, Operator } from '../src/index' +import { OperatorDecorator, Operator } from "../src/index"; -const startsWithLetter = new Operator('startsWithLetter', (factValue, jsonValue) => { - return factValue[0] === jsonValue -}) +const startsWithLetter = new Operator( + "startsWithLetter", + (factValue, jsonValue) => { + return factValue[0] === jsonValue; + }, +); -describe('OperatorDecorator', () => { - describe('constructor()', () => { - function subject (...args) { - return new OperatorDecorator(...args) +describe("OperatorDecorator", () => { + describe("constructor()", () => { + function subject(...args) { + return new OperatorDecorator(...args); } - it('adds the decorator', () => { - const decorator = subject('test', () => false) - expect(decorator.name).to.equal('test') - expect(decorator.cb).to.an.instanceof(Function) - }) + it("adds the decorator", () => { + const decorator = subject("test", () => false); + expect(decorator.name).to.equal("test"); + expect(decorator.cb).to.an.instanceof(Function); + }); - it('decorator name', () => { + it("decorator name", () => { expect(() => { - subject() - }).to.throw(/Missing decorator name/) - }) + subject(); + }).to.throw(/Missing decorator name/); + }); - it('decorator definition', () => { + it("decorator definition", () => { expect(() => { - subject('test') - }).to.throw(/Missing decorator callback/) - }) - }) + subject("test"); + }).to.throw(/Missing decorator callback/); + }); + }); - describe('decorating', () => { - const subject = new OperatorDecorator('test', () => false).decorate(startsWithLetter) - it('creates a new operator with the prefixed name', () => { - expect(subject.name).to.equal('test:startsWithLetter') - }) - }) -}) + describe("decorating", () => { + const subject = new OperatorDecorator("test", () => false).decorate( + startsWithLetter, + ); + it("creates a new operator with the prefixed name", () => { + expect(subject.name).to.equal("test:startsWithLetter"); + }); + }); +}); diff --git a/test/operator.test.js b/test/operator.test.js index 24cc89aa..bb00c307 100644 --- a/test/operator.test.js +++ b/test/operator.test.js @@ -1,31 +1,31 @@ -'use strict' +"use strict"; -import { Operator } from '../src/index' +import { Operator } from "../src/index"; -describe('Operator', () => { - describe('constructor()', () => { - function subject (...args) { - return new Operator(...args) +describe("Operator", () => { + describe("constructor()", () => { + function subject(...args) { + return new Operator(...args); } - it('adds the operator', () => { - const operator = subject('startsWithLetter', (factValue, jsonValue) => { - return factValue[0] === jsonValue - }) - expect(operator.name).to.equal('startsWithLetter') - expect(operator.cb).to.an.instanceof(Function) - }) + it("adds the operator", () => { + const operator = subject("startsWithLetter", (factValue, jsonValue) => { + return factValue[0] === jsonValue; + }); + expect(operator.name).to.equal("startsWithLetter"); + expect(operator.cb).to.an.instanceof(Function); + }); - it('operator name', () => { + it("operator name", () => { expect(() => { - subject() - }).to.throw(/Missing operator name/) - }) + subject(); + }).to.throw(/Missing operator name/); + }); - it('operator definition', () => { + it("operator definition", () => { expect(() => { - subject('startsWithLetter') - }).to.throw(/Missing operator callback/) - }) - }) -}) + subject("startsWithLetter"); + }).to.throw(/Missing operator callback/); + }); + }); +}); diff --git a/test/performance.test.js b/test/performance.test.js index 7a0ce6d4..08b676d8 100644 --- a/test/performance.test.js +++ b/test/performance.test.js @@ -1,65 +1,67 @@ -'use strict' +"use strict"; -import engineFactory from '../src/index' -import perfy from 'perfy' -import deepClone from 'clone' +import engineFactory from "../src/index"; +import perfy from "perfy"; +import deepClone from "clone"; -describe('Performance', () => { +describe("Performance", () => { const baseConditions = { - any: [{ - fact: 'age', - operator: 'lessThan', - value: 50 - }, - { - fact: 'segment', - operator: 'equal', - value: 'european' - }] - } + any: [ + { + fact: "age", + operator: "lessThan", + value: 50, + }, + { + fact: "segment", + operator: "equal", + value: "european", + }, + ], + }; const event = { - type: 'ageTrigger', + type: "ageTrigger", params: { - demographic: 'under50' - } - } + demographic: "under50", + }, + }; /* - * Generates an array of integers of length 'num' - */ - function range (num) { - return Array.from(Array(num).keys()) + * Generates an array of integers of length 'num' + */ + function range(num) { + return Array.from(Array(num).keys()); } - function setup (conditions) { - const engine = engineFactory() - const config = deepClone({ conditions, event }) + function setup(conditions) { + const engine = engineFactory(); + const config = deepClone({ conditions, event }); range(1000).forEach(() => { - const rule = factories.rule(config) - engine.addRule(rule) - }) - engine.addFact('segment', 'european', { cache: true }) - engine.addFact('age', 15, { cache: true }) - return engine + const rule = factories.rule(config); + engine.addRule(rule); + }); + engine.addFact("segment", "european", { cache: true }); + engine.addFact("age", 15, { cache: true }); + return engine; } it('performs "any" quickly', async () => { - const engine = setup(baseConditions) - perfy.start('any') - await engine.run() - const result = perfy.end('any') - expect(result.time).to.be.greaterThan(0.001) - expect(result.time).to.be.lessThan(0.5) - }) + const engine = setup(baseConditions); + perfy.start("any"); + await engine.run(); + const result = perfy.end("any"); + expect(result.time).to.be.greaterThan(0.001); + expect(result.time).to.be.lessThan(0.5); + }); it('performs "all" quickly', async () => { - const conditions = deepClone(baseConditions) - conditions.all = conditions.any - delete conditions.any - const engine = setup(conditions) - perfy.start('all') - await engine.run() - const result = perfy.end('all') - expect(result.time).to.be.greaterThan(0.001) // assert lower value - expect(result.time).to.be.lessThan(0.5) - }) -}) + const conditions = deepClone(baseConditions); + conditions.all = conditions.any; + delete conditions.any; + const engine = setup(conditions); + perfy.start("all"); + await engine.run(); + const result = perfy.end("all"); + expect(result.time).to.be.greaterThan(0.001); // assert lower value + expect(result.time).to.be.lessThan(0.5); + }); +}); diff --git a/test/rule.test.js b/test/rule.test.js index fc3357b8..76fdc557 100644 --- a/test/rule.test.js +++ b/test/rule.test.js @@ -1,339 +1,358 @@ -'use strict' +"use strict"; -import Engine from '../src/index' -import Rule from '../src/rule' -import sinon from 'sinon' +import Engine from "../src/index"; +import Rule from "../src/rule"; +import sinon from "sinon"; -describe('Rule', () => { - const rule = new Rule() +describe("Rule", () => { + const rule = new Rule(); const conditionBase = factories.condition({ - fact: 'age', - value: 50 - }) + fact: "age", + value: 50, + }); - describe('constructor()', () => { - it('can be initialized with priority, conditions, event, and name', () => { + describe("constructor()", () => { + it("can be initialized with priority, conditions, event, and name", () => { const condition = { - all: [Object.assign({}, conditionBase)] - } - condition.operator = 'all' - condition.priority = 25 + all: [Object.assign({}, conditionBase)], + }; + condition.operator = "all"; + condition.priority = 25; const opts = { priority: 50, conditions: condition, event: { - type: 'awesome' + type: "awesome", }, - name: 'testName' - } - const rule = new Rule(opts) - expect(rule.priority).to.eql(opts.priority) - expect(rule.conditions).to.eql(opts.conditions) - expect(rule.ruleEvent).to.eql(opts.event) - expect(rule.name).to.eql(opts.name) - }) - - it('it can be initialized with a json string', () => { + name: "testName", + }; + const rule = new Rule(opts); + expect(rule.priority).to.eql(opts.priority); + expect(rule.conditions).to.eql(opts.conditions); + expect(rule.ruleEvent).to.eql(opts.event); + expect(rule.name).to.eql(opts.name); + }); + + it("it can be initialized with a json string", () => { const condition = { - all: [Object.assign({}, conditionBase)] - } - condition.operator = 'all' - condition.priority = 25 + all: [Object.assign({}, conditionBase)], + }; + condition.operator = "all"; + condition.priority = 25; const opts = { priority: 50, conditions: condition, event: { - type: 'awesome' + type: "awesome", }, - name: 'testName' - } - const json = JSON.stringify(opts) - const rule = new Rule(json) - expect(rule.priority).to.eql(opts.priority) - expect(rule.conditions).to.eql(opts.conditions) - expect(rule.ruleEvent).to.eql(opts.event) - expect(rule.name).to.eql(opts.name) - }) - }) - - describe('event emissions', () => { - it('can emit', () => { - const rule = new Rule() - const successSpy = sinon.spy() - rule.on('test', successSpy) - rule.emit('test') - expect(successSpy.callCount).to.equal(1) - }) - - it('can be initialized with an onSuccess option', (done) => { - const event = { type: 'test' } + name: "testName", + }; + const json = JSON.stringify(opts); + const rule = new Rule(json); + expect(rule.priority).to.eql(opts.priority); + expect(rule.conditions).to.eql(opts.conditions); + expect(rule.ruleEvent).to.eql(opts.event); + expect(rule.name).to.eql(opts.name); + }); + }); + + describe("event emissions", () => { + it("can emit", () => { + const rule = new Rule(); + const successSpy = sinon.spy(); + rule.on("test", successSpy); + rule.emit("test"); + expect(successSpy.callCount).to.equal(1); + }); + + it("can be initialized with an onSuccess option", (done) => { + const event = { type: "test" }; const onSuccess = function (e) { - expect(e).to.equal(event) - done() - } - const rule = new Rule({ onSuccess }) - rule.emit('success', event) - }) - - it('can be initialized with an onFailure option', (done) => { - const event = { type: 'test' } + expect(e).to.equal(event); + done(); + }; + const rule = new Rule({ onSuccess }); + rule.emit("success", event); + }); + + it("can be initialized with an onFailure option", (done) => { + const event = { type: "test" }; const onFailure = function (e) { - expect(e).to.equal(event) - done() - } - const rule = new Rule({ onFailure }) - rule.emit('failure', event) - }) - }) - - describe('setEvent()', () => { - it('throws if no argument provided', () => { - expect(() => rule.setEvent()).to.throw(/Rule: setEvent\(\) requires event object/) - }) + expect(e).to.equal(event); + done(); + }; + const rule = new Rule({ onFailure }); + rule.emit("failure", event); + }); + }); + + describe("setEvent()", () => { + it("throws if no argument provided", () => { + expect(() => rule.setEvent()).to.throw( + /Rule: setEvent\(\) requires event object/, + ); + }); it('throws if argument is missing "type" property', () => { - expect(() => rule.setEvent({})).to.throw(/Rule: setEvent\(\) requires event object with "type" property/) - }) - }) - - describe('setEvent()', () => { - it('throws if no argument provided', () => { - expect(() => rule.setEvent()).to.throw(/Rule: setEvent\(\) requires event object/) - }) + expect(() => rule.setEvent({})).to.throw( + /Rule: setEvent\(\) requires event object with "type" property/, + ); + }); + }); + + describe("setEvent()", () => { + it("throws if no argument provided", () => { + expect(() => rule.setEvent()).to.throw( + /Rule: setEvent\(\) requires event object/, + ); + }); it('throws if argument is missing "type" property', () => { - expect(() => rule.setEvent({})).to.throw(/Rule: setEvent\(\) requires event object with "type" property/) - }) - }) - - describe('setConditions()', () => { - describe('validations', () => { - it('throws an exception for invalid root conditions', () => { - expect(rule.setConditions.bind(rule, { foo: true })).to.throw(/"conditions" root must contain a single instance of "all", "any", "not", or "condition"/) - }) - }) - }) - - describe('setPriority', () => { - it('defaults to a priority of 1', () => { - expect(rule.priority).to.equal(1) - }) - - it('allows a priority to be set', () => { - rule.setPriority(10) - expect(rule.priority).to.equal(10) - }) - - it('errors if priority is less than 0', () => { - expect(rule.setPriority.bind(null, 0)).to.throw(/greater than zero/) - }) - }) - - describe('accessors', () => { - it('retrieves event', () => { - const event = { type: 'e', params: { a: 'b' } } - rule.setEvent(event) - expect(rule.getEvent()).to.deep.equal(event) - }) - - it('retrieves priority', () => { - const priority = 100 - rule.setPriority(priority) - expect(rule.getPriority()).to.equal(priority) - }) - - it('retrieves conditions', () => { - const condition = { all: [] } - rule.setConditions(condition) + expect(() => rule.setEvent({})).to.throw( + /Rule: setEvent\(\) requires event object with "type" property/, + ); + }); + }); + + describe("setConditions()", () => { + describe("validations", () => { + it("throws an exception for invalid root conditions", () => { + expect(rule.setConditions.bind(rule, { foo: true })).to.throw( + /"conditions" root must contain a single instance of "all", "any", "not", or "condition"/, + ); + }); + }); + }); + + describe("setPriority", () => { + it("defaults to a priority of 1", () => { + expect(rule.priority).to.equal(1); + }); + + it("allows a priority to be set", () => { + rule.setPriority(10); + expect(rule.priority).to.equal(10); + }); + + it("errors if priority is less than 0", () => { + expect(rule.setPriority.bind(null, 0)).to.throw(/greater than zero/); + }); + }); + + describe("accessors", () => { + it("retrieves event", () => { + const event = { type: "e", params: { a: "b" } }; + rule.setEvent(event); + expect(rule.getEvent()).to.deep.equal(event); + }); + + it("retrieves priority", () => { + const priority = 100; + rule.setPriority(priority); + expect(rule.getPriority()).to.equal(priority); + }); + + it("retrieves conditions", () => { + const condition = { all: [] }; + rule.setConditions(condition); expect(rule.getConditions()).to.deep.equal({ all: [], - operator: 'all', - priority: 1 - }) - }) - }) - - describe('setName', () => { - it('defaults to undefined', () => { - expect(rule.name).to.equal(undefined) - }) - - it('allows the name to be set', () => { - rule.setName('Test Name') - expect(rule.name).to.equal('Test Name') - }) - - it('allows input of the number 0', () => { - rule.setName(0) - expect(rule.name).to.equal(0) - }) - - it('allows input of an object', () => { + operator: "all", + priority: 1, + }); + }); + }); + + describe("setName", () => { + it("defaults to undefined", () => { + expect(rule.name).to.equal(undefined); + }); + + it("allows the name to be set", () => { + rule.setName("Test Name"); + expect(rule.name).to.equal("Test Name"); + }); + + it("allows input of the number 0", () => { + rule.setName(0); + expect(rule.name).to.equal(0); + }); + + it("allows input of an object", () => { rule.setName({ id: 123, - name: 'myRule' - }) + name: "myRule", + }); expect(rule.name).to.eql({ id: 123, - name: 'myRule' - }) - }) - - it('errors if name is an empty string', () => { - expect(rule.setName.bind(null, '')).to.throw(/Rule "name" must be defined/) - }) - }) - - describe('priotizeConditions()', () => { - const conditions = [{ - fact: 'age', - operator: 'greaterThanInclusive', - value: 18 - }, { - fact: 'segment', - operator: 'equal', - value: 'human' - }, { - fact: 'accountType', - operator: 'equal', - value: 'admin' - }, { - fact: 'state', - operator: 'equal', - value: 'admin' - }] - - it('orders based on priority', async () => { - const engine = new Engine() - engine.addFact('state', async () => {}, { priority: 500 }) - engine.addFact('segment', async () => {}, { priority: 50 }) - engine.addFact('accountType', async () => {}, { priority: 25 }) - engine.addFact('age', async () => {}, { priority: 100 }) - const rule = new Rule() - rule.setEngine(engine) - - const prioritizedConditions = rule.prioritizeConditions(conditions) - expect(prioritizedConditions.length).to.equal(4) - expect(prioritizedConditions[0][0].fact).to.equal('state') - expect(prioritizedConditions[1][0].fact).to.equal('age') - expect(prioritizedConditions[2][0].fact).to.equal('segment') - expect(prioritizedConditions[3][0].fact).to.equal('accountType') - }) - }) - - describe('evaluate()', () => { - function setup () { - const engine = new Engine() - const rule = new Rule() + name: "myRule", + }); + }); + + it("errors if name is an empty string", () => { + expect(rule.setName.bind(null, "")).to.throw( + /Rule "name" must be defined/, + ); + }); + }); + + describe("priotizeConditions()", () => { + const conditions = [ + { + fact: "age", + operator: "greaterThanInclusive", + value: 18, + }, + { + fact: "segment", + operator: "equal", + value: "human", + }, + { + fact: "accountType", + operator: "equal", + value: "admin", + }, + { + fact: "state", + operator: "equal", + value: "admin", + }, + ]; + + it("orders based on priority", async () => { + const engine = new Engine(); + engine.addFact("state", async () => {}, { priority: 500 }); + engine.addFact("segment", async () => {}, { priority: 50 }); + engine.addFact("accountType", async () => {}, { priority: 25 }); + engine.addFact("age", async () => {}, { priority: 100 }); + const rule = new Rule(); + rule.setEngine(engine); + + const prioritizedConditions = rule.prioritizeConditions(conditions); + expect(prioritizedConditions.length).to.equal(4); + expect(prioritizedConditions[0][0].fact).to.equal("state"); + expect(prioritizedConditions[1][0].fact).to.equal("age"); + expect(prioritizedConditions[2][0].fact).to.equal("segment"); + expect(prioritizedConditions[3][0].fact).to.equal("accountType"); + }); + }); + + describe("evaluate()", () => { + function setup() { + const engine = new Engine(); + const rule = new Rule(); rule.setConditions({ - all: [] - }) - engine.addRule(rule) + all: [], + }); + engine.addRule(rule); - return { engine, rule } + return { engine, rule }; } - it('evalutes truthy when there are no conditions', async () => { - const engineSuccessSpy = sinon.spy() - const { engine } = setup() + it("evalutes truthy when there are no conditions", async () => { + const engineSuccessSpy = sinon.spy(); + const { engine } = setup(); - engine.on('success', engineSuccessSpy) + engine.on("success", engineSuccessSpy); - await engine.run() + await engine.run(); - expect(engineSuccessSpy).to.have.been.calledOnce() - }) + expect(engineSuccessSpy).to.have.been.calledOnce(); + }); it('waits for all on("success") event promises to be resolved', async () => { - const engineSuccessSpy = sinon.spy() - const ruleSuccessSpy = sinon.spy() - const engineRunSpy = sinon.spy() - const { engine, rule } = setup() - rule.on('success', () => { + const engineSuccessSpy = sinon.spy(); + const ruleSuccessSpy = sinon.spy(); + const engineRunSpy = sinon.spy(); + const { engine, rule } = setup(); + rule.on("success", () => { return new Promise(function (resolve) { setTimeout(function () { - ruleSuccessSpy() - resolve() - }, 5) - }) - }) - engine.on('success', engineSuccessSpy) - - await engine.run().then(() => engineRunSpy()) - - expect(ruleSuccessSpy).to.have.been.calledOnce() - expect(engineSuccessSpy).to.have.been.calledOnce() - expect(ruleSuccessSpy).to.have.been.calledBefore(engineRunSpy) - expect(ruleSuccessSpy).to.have.been.calledBefore(engineSuccessSpy) - }) - }) - - describe('toJSON() and fromJSON()', () => { - const priority = 50 + ruleSuccessSpy(); + resolve(); + }, 5); + }); + }); + engine.on("success", engineSuccessSpy); + + await engine.run().then(() => engineRunSpy()); + + expect(ruleSuccessSpy).to.have.been.calledOnce(); + expect(engineSuccessSpy).to.have.been.calledOnce(); + expect(ruleSuccessSpy).to.have.been.calledBefore(engineRunSpy); + expect(ruleSuccessSpy).to.have.been.calledBefore(engineSuccessSpy); + }); + }); + + describe("toJSON() and fromJSON()", () => { + const priority = 50; const event = { - type: 'to-json!', - params: { id: 1 } - } + type: "to-json!", + params: { id: 1 }, + }; const conditions = { priority: 1, - all: [{ - value: 10, - operator: 'equals', - fact: 'user', - params: { - foo: true + all: [ + { + value: 10, + operator: "equals", + fact: "user", + params: { + foo: true, + }, + path: "$.id", }, - path: '$.id' - }] - } - const name = 'testName' - let rule + ], + }; + const name = "testName"; + let rule; beforeEach(() => { - rule = new Rule() - rule.setConditions(conditions) - rule.setPriority(priority) - rule.setEvent(event) - rule.setName(name) - }) - - it('serializes itself', () => { - const json = rule.toJSON(false) - expect(Object.keys(json).length).to.equal(4) - expect(json.conditions).to.eql(conditions) - expect(json.priority).to.eql(priority) - expect(json.event).to.eql(event) - expect(json.name).to.eql(name) - }) - - it('serializes itself as json', () => { - const jsonString = rule.toJSON() - expect(jsonString).to.be.a('string') - const json = JSON.parse(jsonString) - expect(Object.keys(json).length).to.equal(4) - expect(json.conditions).to.eql(conditions) - expect(json.priority).to.eql(priority) - expect(json.event).to.eql(event) - expect(json.name).to.eql(name) - }) - - it('rehydrates itself using a JSON string', () => { - const jsonString = rule.toJSON() - expect(jsonString).to.be.a('string') - const hydratedRule = new Rule(jsonString) - expect(hydratedRule.conditions).to.eql(rule.conditions) - expect(hydratedRule.priority).to.eql(rule.priority) - expect(hydratedRule.ruleEvent).to.eql(rule.ruleEvent) - expect(hydratedRule.name).to.eql(rule.name) - }) - - it('rehydrates itself using an object from JSON.parse()', () => { - const jsonString = rule.toJSON() - expect(jsonString).to.be.a('string') - const json = JSON.parse(jsonString) - const hydratedRule = new Rule(json) - expect(hydratedRule.conditions).to.eql(rule.conditions) - expect(hydratedRule.priority).to.eql(rule.priority) - expect(hydratedRule.ruleEvent).to.eql(rule.ruleEvent) - expect(hydratedRule.name).to.eql(rule.name) - }) - }) -}) + rule = new Rule(); + rule.setConditions(conditions); + rule.setPriority(priority); + rule.setEvent(event); + rule.setName(name); + }); + + it("serializes itself", () => { + const json = rule.toJSON(false); + expect(Object.keys(json).length).to.equal(4); + expect(json.conditions).to.eql(conditions); + expect(json.priority).to.eql(priority); + expect(json.event).to.eql(event); + expect(json.name).to.eql(name); + }); + + it("serializes itself as json", () => { + const jsonString = rule.toJSON(); + expect(jsonString).to.be.a("string"); + const json = JSON.parse(jsonString); + expect(Object.keys(json).length).to.equal(4); + expect(json.conditions).to.eql(conditions); + expect(json.priority).to.eql(priority); + expect(json.event).to.eql(event); + expect(json.name).to.eql(name); + }); + + it("rehydrates itself using a JSON string", () => { + const jsonString = rule.toJSON(); + expect(jsonString).to.be.a("string"); + const hydratedRule = new Rule(jsonString); + expect(hydratedRule.conditions).to.eql(rule.conditions); + expect(hydratedRule.priority).to.eql(rule.priority); + expect(hydratedRule.ruleEvent).to.eql(rule.ruleEvent); + expect(hydratedRule.name).to.eql(rule.name); + }); + + it("rehydrates itself using an object from JSON.parse()", () => { + const jsonString = rule.toJSON(); + expect(jsonString).to.be.a("string"); + const json = JSON.parse(jsonString); + const hydratedRule = new Rule(json); + expect(hydratedRule.conditions).to.eql(rule.conditions); + expect(hydratedRule.priority).to.eql(rule.priority); + expect(hydratedRule.ruleEvent).to.eql(rule.ruleEvent); + expect(hydratedRule.name).to.eql(rule.name); + }); + }); +}); diff --git a/test/support/bootstrap.js b/test/support/bootstrap.js index a80b80af..8a702efb 100644 --- a/test/support/bootstrap.js +++ b/test/support/bootstrap.js @@ -1,14 +1,14 @@ -'use strict' +"use strict"; -const chai = require('chai') -const sinonChai = require('sinon-chai') -const chaiAsPromised = require('chai-as-promised') -const dirtyChai = require('dirty-chai') -chai.use(chaiAsPromised) -chai.use(sinonChai) -chai.use(dirtyChai) -global.expect = chai.expect +const chai = require("chai"); +const sinonChai = require("sinon-chai"); +const chaiAsPromised = require("chai-as-promised"); +const dirtyChai = require("dirty-chai"); +chai.use(chaiAsPromised); +chai.use(sinonChai); +chai.use(dirtyChai); +global.expect = chai.expect; global.factories = { - rule: require('./rule-factory'), - condition: require('./condition-factory') -} + rule: require("./rule-factory"), + condition: require("./condition-factory"), +}; diff --git a/test/support/condition-factory.js b/test/support/condition-factory.js index 3546ed13..ad89f90c 100644 --- a/test/support/condition-factory.js +++ b/test/support/condition-factory.js @@ -1,9 +1,9 @@ -'use strict' +"use strict"; module.exports = function (options) { return { fact: options.fact || null, value: options.value || null, - operator: options.operator || 'equal' - } -} + operator: options.operator || "equal", + }; +}; diff --git a/test/support/rule-factory.js b/test/support/rule-factory.js index f9467805..cbee1ce7 100644 --- a/test/support/rule-factory.js +++ b/test/support/rule-factory.js @@ -1,28 +1,30 @@ -'use strict' +"use strict"; module.exports = (options) => { - options = options || {} + options = options || {}; return { name: options.name, priority: options.priority || 1, conditions: options.conditions || { - all: [{ - fact: 'age', - operator: 'lessThan', - value: 45 - }, - { - fact: 'pointBalance', - operator: 'greaterThanInclusive', - value: 1000 - }] + all: [ + { + fact: "age", + operator: "lessThan", + value: 45, + }, + { + fact: "pointBalance", + operator: "greaterThanInclusive", + value: 1000, + }, + ], }, event: options.event || { - type: 'pointCapReached', + type: "pointCapReached", params: { - currency: 'points', - pointCap: 1000 - } - } - } -} + currency: "points", + pointCap: 1000, + }, + }, + }; +}; diff --git a/types/index.d.ts b/types/index.d.ts index 81f08dcb..bf3fd2c6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + export interface AlmanacOptions { allowUndefinedFacts?: boolean; pathResolver?: PathResolver; @@ -22,7 +24,7 @@ export interface EngineResult { export default function engineFactory( rules: Array, - options?: EngineOptions + options?: EngineOptions, ): Engine; export class Engine { @@ -38,26 +40,32 @@ export class Engine { addOperator(operator: Operator): void; addOperator( operatorName: string, - callback: OperatorEvaluator + callback: OperatorEvaluator, ): void; removeOperator(operator: Operator | string): boolean; addOperatorDecorator(decorator: OperatorDecorator): void; - addOperatorDecorator(decoratorName: string, callback: OperatorDecoratorEvaluator): void; + addOperatorDecorator( + decoratorName: string, + callback: OperatorDecoratorEvaluator, + ): void; removeOperatorDecorator(decorator: OperatorDecorator | string): boolean; addFact(fact: Fact): this; addFact( id: string, valueCallback: DynamicFactCallback | T, - options?: FactOptions + options?: FactOptions, ): this; removeFact(factOrId: string | Fact): boolean; getFact(factId: string): Fact; on(eventName: string, handler: EventHandler): this; - run(facts?: Record, runOptions?: RunOptions): Promise; + run( + facts?: Record, + runOptions?: RunOptions, + ): Promise; stop(): this; } @@ -70,21 +78,30 @@ export class Operator { constructor( name: string, evaluator: OperatorEvaluator, - validator?: (factValue: A) => boolean + validator?: (factValue: A) => boolean, ); } export interface OperatorDecoratorEvaluator { - (factValue: A, compareToValue: B, next: OperatorEvaluator): boolean -} - -export class OperatorDecorator { + ( + factValue: A, + compareToValue: B, + next: OperatorEvaluator, + ): boolean; +} + +export class OperatorDecorator< + A = unknown, + B = unknown, + NextA = unknown, + NextB = unknown, +> { public name: string; constructor( name: string, evaluator: OperatorDecoratorEvaluator, - validator?: (factValue: A) => boolean - ) + validator?: (factValue: A) => boolean, + ); } export class Almanac { @@ -92,13 +109,13 @@ export class Almanac { factValue( factId: string, params?: Record, - path?: string + path?: string, ): Promise; addFact(fact: Fact): this; addFact( id: string, valueCallback: DynamicFactCallback | T, - options?: FactOptions + options?: FactOptions, ): this; addRuntimeFact(factId: string, value: any): void; } @@ -110,7 +127,7 @@ export type FactOptions = { export type DynamicFactCallback = ( params: Record, - almanac: Almanac + almanac: Almanac, ) => T; export class Fact { @@ -123,7 +140,7 @@ export class Fact { constructor( id: string, value: T | DynamicFactCallback, - options?: FactOptions + options?: FactOptions, ); } @@ -137,7 +154,7 @@ export type PathResolver = (value: object, path: string) => any; export type EventHandler = ( event: T, almanac: Almanac, - ruleResult: RuleResult + ruleResult: RuleResult, ) => void; export interface RuleProperties { @@ -172,7 +189,7 @@ export class Rule implements RuleProperties { setPriority(priority: number): this; toJSON(): string; toJSON( - stringify: T + stringify: T, ): T extends true ? string : RuleSerializable; } diff --git a/types/index.test-d.ts b/types/index.test-d.ts index 6d5b37b1..f39ee9d4 100644 --- a/types/index.test-d.ts +++ b/types/index.test-d.ts @@ -14,17 +14,17 @@ import rulesEngine, { Rule, RuleProperties, RuleResult, - RuleSerializable + RuleSerializable, } from "../"; // setup basic fixture data const ruleProps: RuleProperties = { conditions: { - all: [] + all: [], }, event: { - type: "message" - } + type: "message", + }, }; const complexRuleProps: RuleProperties = { @@ -33,25 +33,25 @@ const complexRuleProps: RuleProperties = { { any: [ { - all: [] + all: [], }, { fact: "foo", operator: "equal", - value: "bar" - } - ] - } - ] + value: "bar", + }, + ], + }, + ], }, event: { - type: "message" - } + type: "message", + }, }; // path resolver -const pathResolver = function(value: object, path: string): any {} -expectType(pathResolver) +const pathResolver = function (value: object, path: string): any {}; +expectType(pathResolver); // default export test expectType(rulesEngine([ruleProps])); @@ -74,32 +74,32 @@ expectType(rule.toJSON(false)); // Operator tests const operatorEvaluator: OperatorEvaluator = ( a: number, - b: number + b: number, ) => a === b; -expectType( - engine.addOperator("test", operatorEvaluator) -); +expectType(engine.addOperator("test", operatorEvaluator)); const operator: Operator = new Operator( "test", operatorEvaluator, - (num: number) => num > 0 + (num: number) => num > 0, ); expectType(engine.addOperator(operator)); expectType(engine.removeOperator(operator)); // Operator Decorator tests -const operatorDecoratorEvaluator: OperatorDecoratorEvaluator = ( - a: number[], - b: number, - next: OperatorEvaluator -) => next(a[0], b); +const operatorDecoratorEvaluator: OperatorDecoratorEvaluator< + number[], + number, + number, + number +> = (a: number[], b: number, next: OperatorEvaluator) => + next(a[0], b); expectType( - engine.addOperatorDecorator("first", operatorDecoratorEvaluator) + engine.addOperatorDecorator("first", operatorDecoratorEvaluator), ); const operatorDecorator: OperatorDecorator = new OperatorDecorator( "first", operatorDecoratorEvaluator, - (a: number[]) => a.length > 0 + (a: number[]) => a.length > 0, ); expectType(engine.addOperatorDecorator(operatorDecorator)); expectType(engine.removeOperatorDecorator(operatorDecorator)); @@ -108,22 +108,22 @@ expectType(engine.removeOperatorDecorator(operatorDecorator)); const fact = new Fact("test-fact", 3); const dynamicFact = new Fact("test-fact", () => [42]); expectType( - engine.addFact("test-fact", "value", { priority: 10 }) + engine.addFact("test-fact", "value", { priority: 10 }), ); expectType(engine.addFact(fact)); expectType(engine.addFact(dynamicFact)); expectType(engine.removeFact(fact)); expectType>(engine.getFact("test")); -engine.on('success', (event, almanac, ruleResult) => { - expectType(event) - expectType(almanac) - expectType(ruleResult) -}) -engine.on<{ foo: Array }>('foo', (event, almanac, ruleResult) => { - expectType<{ foo: Array }>(event) - expectType(almanac) - expectType(ruleResult) -}) +engine.on("success", (event, almanac, ruleResult) => { + expectType(event); + expectType(almanac); + expectType(ruleResult); +}); +engine.on<{ foo: Array }>("foo", (event, almanac, ruleResult) => { + expectType<{ foo: Array }>(event); + expectType(almanac); + expectType(ruleResult); +}); // Run the Engine expectType>(engine.run({ displayMessage: true })); From 454f679aec190ff3546d62d9bde7a8467485c25e Mon Sep 17 00:00:00 2001 From: Chris Pardy Date: Wed, 23 Oct 2024 16:39:05 -0400 Subject: [PATCH 2/4] Move to Vitest for testing Vitest can test all the files, including testing the types. --- package.json | 25 +- .../{acceptance.js => acceptance.test.mjs} | 97 +++-- test/{almanac.test.js => almanac.test.mjs} | 85 ++-- .../{condition.test.js => condition.test.mjs} | 222 ++++------ ...engine-all.test.js => engine-all.test.mjs} | 58 +-- ...engine-any.test.js => engine-any.test.mjs} | 74 ++-- ...ne-cache.test.js => engine-cache.test.mjs} | 32 +- ...tion.test.js => engine-condition.test.mjs} | 166 +++++--- ...trols.test.js => engine-controls.test.mjs} | 34 +- ...t.js => engine-custom-properties.test.mjs} | 22 +- ...test.js => engine-error-handling.test.mjs} | 12 +- ...ne-event.test.js => engine-event.test.mjs} | 383 ++++++++++-------- ...est.js => engine-fact-comparison.test.mjs} | 42 +- ....test.js => engine-fact-priority.test.mjs} | 106 +++-- ...gine-fact.test.js => engine-fact.test.mjs} | 180 ++++---- ...js => engine-facts-calling-facts.test.mjs} | 40 +- ...ailure.test.js => engine-failure.test.mjs} | 27 +- ...engine-not.test.js => engine-not.test.mjs} | 36 +- ...p.test.js => engine-operator-map.test.mjs} | 24 +- ...rator.test.js => engine-operator.test.mjs} | 30 +- ... engine-parallel-condition-cache.test.mjs} | 34 +- ...test.js => engine-recusive-rules.test.mjs} | 38 +- ...ority.js => engine-rule-priority.test.mjs} | 54 ++- ...engine-run.test.js => engine-run.test.mjs} | 59 ++- test/{engine.test.js => engine.test.mjs} | 205 +++++----- test/{fact.test.js => fact.test.mjs} | 25 +- test/index.test.js | 14 - test/index.test.mjs | 14 + ...or.test.js => operator-decorator.test.mjs} | 15 +- test/{operator.test.js => operator.test.mjs} | 13 +- ...rformance.test.js => performance.test.mjs} | 16 +- test/{rule.test.js => rule.test.mjs} | 152 +++---- test/support/bootstrap.js | 14 - ...ition-factory.js => condition-factory.mjs} | 6 +- .../{rule-factory.js => rule-factory.mjs} | 4 +- test/types.test-d.mts | 217 ++++++++++ tsconfig.json | 113 ++++++ types/index.test-d.ts | 135 ------ 38 files changed, 1495 insertions(+), 1328 deletions(-) rename test/acceptance/{acceptance.js => acceptance.test.mjs} (73%) rename test/{almanac.test.js => almanac.test.mjs} (64%) rename test/{condition.test.js => condition.test.mjs} (79%) rename test/{engine-all.test.js => engine-all.test.mjs} (64%) rename test/{engine-any.test.js => engine-any.test.mjs} (55%) rename test/{engine-cache.test.js => engine-cache.test.mjs} (65%) rename test/{engine-condition.test.js => engine-condition.test.mjs} (65%) rename test/{engine-controls.test.js => engine-controls.test.mjs} (57%) rename test/{engine-custom-properties.test.js => engine-custom-properties.test.mjs} (70%) rename test/{engine-error-handling.test.js => engine-error-handling.test.mjs} (55%) rename test/{engine-event.test.js => engine-event.test.mjs} (58%) rename test/{engine-fact-comparison.test.js => engine-fact-comparison.test.mjs} (75%) rename test/{engine-fact-priority.test.js => engine-fact-priority.test.mjs} (58%) rename test/{engine-fact.test.js => engine-fact.test.mjs} (67%) rename test/{engine-facts-calling-facts.test.js => engine-facts-calling-facts.test.mjs} (66%) rename test/{engine-failure.test.js => engine-failure.test.mjs} (57%) rename test/{engine-not.test.js => engine-not.test.mjs} (56%) rename test/{engine-operator-map.test.js => engine-operator-map.test.mjs} (82%) rename test/{engine-operator.test.js => engine-operator.test.mjs} (75%) rename test/{engine-parallel-condition-cache.test.js => engine-parallel-condition-cache.test.mjs} (67%) rename test/{engine-recusive-rules.test.js => engine-recusive-rules.test.mjs} (85%) rename test/{engine-rule-priority.js => engine-rule-priority.test.mjs} (60%) rename test/{engine-run.test.js => engine-run.test.mjs} (71%) rename test/{engine.test.js => engine.test.mjs} (50%) rename test/{fact.test.js => fact.test.mjs} (63%) delete mode 100644 test/index.test.js create mode 100644 test/index.test.mjs rename test/{operator-decorator.test.js => operator-decorator.test.mjs} (69%) rename test/{operator.test.js => operator.test.mjs} (64%) rename test/{performance.test.js => performance.test.mjs} (77%) rename test/{rule.test.js => rule.test.mjs} (63%) delete mode 100644 test/support/bootstrap.js rename test/support/{condition-factory.js => condition-factory.mjs} (69%) rename test/support/{rule-factory.js => rule-factory.mjs} (91%) create mode 100644 test/types.test-d.mts create mode 100644 tsconfig.json delete mode 100644 types/index.test-d.ts diff --git a/package.json b/package.json index 4e078dd0..ef46f969 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,12 @@ "description": "Rules Engine expressed in simple json", "main": "dist/index.js", "types": "types/index.d.ts", + "type": "module", "engines": { "node": ">=18.0.0" }, "scripts": { - "test": "mocha && npm run lint --silent && npm run test:types", - "test:types": "tsd", + "test": "vitest --typecheck", "lint": "eslint", "format": "prettier -w .", "prepublishOnly": "npm run build", @@ -29,18 +29,6 @@ "publishConfig": { "tag": "next" }, - "mocha": { - "require": [ - "babel-core/register", - "babel-polyfill" - ], - "file": "./test/support/bootstrap.js", - "checkLeaks": true, - "recursive": true, - "globals": [ - "expect" - ] - }, "author": "Cache Hamm ", "contributors": [ "Chris Pardy " @@ -60,20 +48,17 @@ "babel-preset-es2015": "~6.24.1", "babel-preset-stage-0": "~6.24.1", "babel-register": "6.26.0", - "chai": "^4.3.4", - "chai-as-promised": "^7.1.1", "colors": "~1.4.0", "dirty-chai": "2.0.1", "eslint": "^9.13.0", "globals": "^15.11.0", "lodash": "4.17.21", - "mocha": "^8.4.0", "perfy": "^1.1.5", "prettier": "^3.3.3", - "sinon": "^11.1.1", - "sinon-chai": "^3.7.0", "tsd": "^0.17.0", - "typescript-eslint": "^8.11.0" + "typescript": "^5.6.3", + "typescript-eslint": "^8.11.0", + "vitest": "^2.1.3" }, "dependencies": { "clone": "^2.1.2", diff --git a/test/acceptance/acceptance.js b/test/acceptance/acceptance.test.mjs similarity index 73% rename from test/acceptance/acceptance.js rename to test/acceptance/acceptance.test.mjs index e2c5efbb..db703895 100644 --- a/test/acceptance/acceptance.js +++ b/test/acceptance/acceptance.test.mjs @@ -1,21 +1,10 @@ -"use strict"; - -import sinon from "sinon"; -import { expect } from "chai"; -import { Engine } from "../../src/index"; - +import { Engine } from "../../src/index.mjs"; +import { describe, it, expect, vi } from "vitest"; /** * acceptance tests are intended to use features that, when used in combination, * could cause integration bugs not caught by the rest of the test suite */ describe("Acceptance", () => { - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); const factParam = 1; const event1 = { type: "event-1", @@ -61,8 +50,8 @@ describe("Acceptance", () => { function setup(options = {}) { const engine = new Engine(); - highPrioritySpy = sandbox.spy(); - lowPrioritySpy = sandbox.spy(); + highPrioritySpy = vi.fn(); + lowPrioritySpy = vi.fn(); engine.addRule({ name: "first", @@ -87,10 +76,10 @@ describe("Acceptance", () => { }, event: event1, onSuccess: async (event, almanac, ruleResults) => { - expect(ruleResults.name).to.equal("first"); - expect(ruleResults.event).to.deep.equal(event1); - expect(ruleResults.priority).to.equal(10); - expect(ruleResults.conditions).to.deep.equal(expectedFirstRuleResult); + expect(ruleResults.name).toBe("first"); + expect(ruleResults.event).toEqual(event1); + expect(ruleResults.priority).toBe(10); + expect(ruleResults.conditions).toEqual(expectedFirstRuleResult); return delay( almanac.addRuntimeFact("rule-created-fact", { @@ -150,8 +139,8 @@ describe("Acceptance", () => { const baseIndex = await almanac.factValue("baseIndex"); return delay(baseIndex); }); - successSpy = sandbox.spy(); - failureSpy = sandbox.spy(); + successSpy = vi.fn(); + failureSpy = vi.fn(); engine.on("success", successSpy); engine.on("failure", failureSpy); @@ -169,8 +158,8 @@ describe("Acceptance", () => { ); // results - expect(results.length).to.equal(2); - expect(results[0]).to.deep.equal({ + expect(results.length).toBe(2); + expect(results[0]).toEqual({ conditions: { all: [ { @@ -205,7 +194,7 @@ describe("Acceptance", () => { priority: 10, result: true, }); - expect(results[1]).to.deep.equal({ + expect(results[1]).toEqual({ conditions: { all: [ { @@ -233,20 +222,30 @@ describe("Acceptance", () => { priority: 1, result: true, }); - expect(failureResults).to.be.empty(); + expect(failureResults).toHaveLength(0); // events - expect(failureEvents.length).to.equal(0); - expect(events.length).to.equal(2); - expect(events[0]).to.deep.equal(event1); - expect(events[1]).to.deep.equal(event2); + expect(failureEvents.length).toBe(0); + expect(events.length).toBe(2); + expect(events[0]).toEqual(event1); + expect(events[1]).toEqual(event2); // callbacks - expect(successSpy).to.have.been.calledTwice(); - expect(successSpy).to.have.been.calledWith(event1); - expect(successSpy).to.have.been.calledWith(event2); - expect(highPrioritySpy).to.have.been.calledBefore(lowPrioritySpy); - expect(failureSpy).to.not.have.been.called(); + expect(successSpy).toHaveBeenCalledTimes(2); + expect(successSpy).toHaveBeenCalledWith( + event1, + expect.anything(), + expect.anything(), + ); + expect(successSpy).toHaveBeenCalledWith( + event2, + expect.anything(), + expect.anything(), + ); + expect(Math.min(...highPrioritySpy.mock.invocationCallOrder)).toBeLessThan( + Math.min(...lowPrioritySpy.mock.invocationCallOrder), + ); + expect(failureSpy).not.toHaveBeenCalled(); }); it("fails", async () => { @@ -259,16 +258,26 @@ describe("Acceptance", () => { { baseIndex: 1, "rule-created-fact": "" }, ); - expect(results.length).to.equal(0); - expect(failureResults.length).to.equal(2); - expect(failureResults.every((rr) => rr.result === false)).to.be.true(); + expect(results.length).toBe(0); + expect(failureResults.length).toBe(2); + expect(failureResults.every((rr) => rr.result === false)).toBe(true); - expect(events.length).to.equal(0); - expect(failureEvents.length).to.equal(2); - expect(failureSpy).to.have.been.calledTwice(); - expect(failureSpy).to.have.been.calledWith(event1); - expect(failureSpy).to.have.been.calledWith(event2); - expect(highPrioritySpy).to.have.been.calledBefore(lowPrioritySpy); - expect(successSpy).to.not.have.been.called(); + expect(events.length).toBe(0); + expect(failureEvents.length).toBe(2); + expect(failureSpy).toHaveBeenCalledTimes(2); + expect(failureSpy).toHaveBeenCalledWith( + event1, + expect.anything(), + expect.anything(), + ); + expect(failureSpy).toHaveBeenCalledWith( + event2, + expect.anything(), + expect.anything(), + ); + expect(Math.min(...highPrioritySpy.mock.invocationCallOrder)).toBeLessThan( + Math.min(...lowPrioritySpy.mock.invocationCallOrder), + ); + expect(successSpy).not.toHaveBeenCalled(); }); }); diff --git a/test/almanac.test.js b/test/almanac.test.mjs similarity index 64% rename from test/almanac.test.js rename to test/almanac.test.mjs index e8b16506..6a64cff9 100644 --- a/test/almanac.test.js +++ b/test/almanac.test.mjs @@ -1,31 +1,25 @@ -import { Fact } from "../src/index"; -import Almanac from "../src/almanac"; -import sinon from "sinon"; +import { Fact } from "../src/index.mjs"; +import Almanac from "../src/almanac.mjs"; +import { describe, it, beforeEach, expect, vi } from "vitest"; describe("Almanac", () => { let almanac; let factSpy; - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); + beforeEach(() => { - factSpy = sandbox.spy(); - }); - afterEach(() => { - sandbox.restore(); + factSpy = vi.fn(); }); describe("properties", () => { it("has methods for managing facts", () => { almanac = new Almanac(); - expect(almanac).to.have.property("factValue"); + expect(almanac).toHaveProperty("factValue"); }); it("adds runtime facts", () => { almanac = new Almanac(); almanac.addFact("modelId", "XYZ"); - expect(almanac.factMap.get("modelId").value).to.equal("XYZ"); + expect(almanac.factMap.get("modelId").value).toBe("XYZ"); }); }); @@ -33,7 +27,7 @@ describe("Almanac", () => { it("supports runtime facts as key => values", () => { almanac = new Almanac(); almanac.addFact("fact1", 3); - return expect(almanac.factValue("fact1")).to.eventually.equal(3); + return expect(almanac.factValue("fact1")).resolves.toBe(3); }); it("supporrts runtime facts as dynamic callbacks", async () => { @@ -42,15 +36,15 @@ describe("Almanac", () => { factSpy(); return Promise.resolve(3); }); - await expect(almanac.factValue("fact1")).to.eventually.equal(3); - await expect(factSpy).to.have.been.calledOnce(); + await expect(almanac.factValue("fact1")).resolves.toBe(3); + await expect(factSpy).toHaveBeenCalledOnce(); }); it("supports runtime fact instances", () => { const fact = new Fact("fact1", 3); almanac = new Almanac(); almanac.addFact(fact); - return expect(almanac.factValue("fact1")).to.eventually.equal(fact.value); + return expect(almanac.factValue("fact1")).resolves.toBe(fact.value); }); }); @@ -59,26 +53,26 @@ describe("Almanac", () => { ["success", "failure"].forEach((outcome) => { it(`manages ${outcome} events`, () => { almanac = new Almanac(); - expect(almanac.getEvents(outcome)).to.be.empty(); + expect(almanac.getEvents(outcome)).toHaveLength(0); almanac.addEvent(event, outcome); - expect(almanac.getEvents(outcome)).to.have.a.lengthOf(1); - expect(almanac.getEvents(outcome)[0]).to.equal(event); + expect(almanac.getEvents(outcome)).toHaveLength(1); + expect(almanac.getEvents(outcome)[0]).toEqual(event); }); it("getEvent() filters when outcome provided, or returns all events", () => { almanac = new Almanac(); almanac.addEvent(event, "success"); almanac.addEvent(event, "failure"); - expect(almanac.getEvents("success")).to.have.a.lengthOf(1); - expect(almanac.getEvents("failure")).to.have.a.lengthOf(1); - expect(almanac.getEvents()).to.have.a.lengthOf(2); + expect(almanac.getEvents("success")).toHaveLength(1); + expect(almanac.getEvents("failure")).toHaveLength(1); + expect(almanac.getEvents()).toHaveLength(2); }); }); }); describe("arguments", () => { beforeEach(() => { - const fact = new Fact("foo", async (params, facts) => { + const fact = new Fact("foo", async (params) => { if (params.userId) return params.userId; return "unknown"; }); @@ -87,17 +81,15 @@ describe("Almanac", () => { }); it("allows parameters to be passed to the fact", async () => { - return expect(almanac.factValue("foo")).to.eventually.equal("unknown"); + return expect(almanac.factValue("foo")).resolves.toBe("unknown"); }); it("allows parameters to be passed to the fact", async () => { - return expect( - almanac.factValue("foo", { userId: 1 }), - ).to.eventually.equal(1); + return expect(almanac.factValue("foo", { userId: 1 })).resolves.toBe(1); }); it("throws an exception if it encounters an undefined fact", () => { - return expect(almanac.factValue("bar")).to.be.rejectedWith( + return expect(almanac.factValue("bar")).rejects.toThrow( /Undefined fact: bar/, ); }); @@ -107,7 +99,7 @@ describe("Almanac", () => { it("adds a key/value pair to the factMap as a fact instance", () => { almanac = new Almanac(); almanac.addRuntimeFact("factId", "factValue"); - expect(almanac.factMap.get("factId").value).to.equal("factValue"); + expect(almanac.factMap.get("factId").value).toBe("factValue"); }); }); @@ -116,24 +108,24 @@ describe("Almanac", () => { const fact = new Fact("factId", "factValue"); almanac = new Almanac(); almanac._addConstantFact(fact); - expect(almanac.factMap.get(fact.id).value).to.equal(fact.value); + expect(almanac.factMap.get(fact.id).value).toBe(fact.value); }); }); - describe("_getFact", (_) => { + describe("_getFact", () => { it("retrieves the fact object", () => { const fact = new Fact("id", 1); almanac = new Almanac(); almanac.addFact(fact); - expect(almanac._getFact("id")).to.equal(fact); + expect(almanac._getFact("id")).toEqual(fact); }); }); describe("_setFactValue()", () => { function expectFactResultsCache(expected) { const promise = almanac.factResultsCache.values().next().value; - expect(promise).to.be.instanceof(Promise); - promise.then((value) => expect(value).to.equal(expected)); + expect(promise).toBeInstanceOf(Promise); + promise.then((value) => expect(value).toEqual(expected)); return promise; } @@ -145,22 +137,17 @@ describe("Almanac", () => { let fact; const FACT_VALUE = 2; - it("updates the fact results and returns a promise", (done) => { + it("updates the fact results and returns a promise", async () => { setup(); almanac._setFactValue(fact, {}, FACT_VALUE); - expectFactResultsCache(FACT_VALUE) - .then((_) => done()) - .catch(done); + await expectFactResultsCache(FACT_VALUE); }); - it("honors facts with caching disabled", (done) => { + it("honors facts with caching disabled", async () => { setup(new Fact("id", 1, { cache: false })); const promise = almanac._setFactValue(fact, {}, FACT_VALUE); - expect(almanac.factResultsCache.values().next().value).to.be.undefined(); - promise - .then((value) => expect(value).to.equal(FACT_VALUE)) - .then((_) => done()) - .catch(done); + expect(almanac.factResultsCache.values().next().value).toBeUndefined(); + await expect(promise).resolves.toBe(FACT_VALUE); }); }); @@ -179,14 +166,14 @@ describe("Almanac", () => { almanac = new Almanac(); almanac.addFact(fact); const result = await almanac.factValue("foo", null, "$..name"); - expect(result).to.deep.equal(["George", "Thomas"]); + expect(result).toEqual(["George", "Thomas"]); }); describe("caching", () => { function setup(factOptions) { const fact = new Fact( "foo", - async (params, facts) => { + async () => { factSpy(); return "unknown"; }, @@ -201,7 +188,7 @@ describe("Almanac", () => { almanac.factValue("foo"); almanac.factValue("foo"); almanac.factValue("foo"); - expect(factSpy).to.have.been.calledThrice(); + expect(factSpy).toHaveBeenCalledTimes(3); }); it("evaluates the fact once when fact caching is on", () => { @@ -209,7 +196,7 @@ describe("Almanac", () => { almanac.factValue("foo"); almanac.factValue("foo"); almanac.factValue("foo"); - expect(factSpy).to.have.been.calledOnce(); + expect(factSpy).toHaveBeenCalledOnce(); }); }); }); diff --git a/test/condition.test.js b/test/condition.test.mjs similarity index 79% rename from test/condition.test.js rename to test/condition.test.mjs index 891b0270..2e1d4298 100644 --- a/test/condition.test.js +++ b/test/condition.test.mjs @@ -1,9 +1,9 @@ -"use strict"; - -import Condition from "../src/condition"; -import defaultOperators from "../src/engine-default-operators"; -import Almanac from "../src/almanac"; -import Fact from "../src/fact"; +import Condition from "../src/condition.mjs"; +import defaultOperators from "../src/engine-default-operators.mjs"; +import Almanac from "../src/almanac.mjs"; +import Fact from "../src/fact.mjs"; +import { describe, it, beforeEach, expect } from "vitest"; +import conditionFactory from "./support/condition-factory.mjs"; const operators = new Map(); defaultOperators.forEach((o) => operators.set(o.name, o)); @@ -28,25 +28,25 @@ describe("Condition", () => { it("fact conditions have properties", () => { const properties = condition(); const subject = new Condition(properties.all[0]); - expect(subject).to.have.property("fact"); - expect(subject).to.have.property("operator"); - expect(subject).to.have.property("value"); - expect(subject).to.have.property("path"); - expect(subject).to.have.property("name"); + expect(subject).toHaveProperty("fact"); + expect(subject).toHaveProperty("operator"); + expect(subject).toHaveProperty("value"); + expect(subject).toHaveProperty("path"); + expect(subject).toHaveProperty("name"); }); it("boolean conditions have properties", () => { const properties = condition(); const subject = new Condition(properties); - expect(subject).to.have.property("operator"); - expect(subject).to.have.property("priority"); - expect(subject.priority).to.equal(1); + expect(subject).toHaveProperty("operator"); + expect(subject).toHaveProperty("priority"); + expect(subject.priority).toBe(1); }); }); describe("toJSON", () => { it("converts the condition into a json string", () => { - const properties = factories.condition({ + const properties = conditionFactory({ fact: "age", value: { fact: "weight", @@ -58,7 +58,7 @@ describe("Condition", () => { }); const condition = new Condition(properties); const json = condition.toJSON(); - expect(json).to.equal( + expect(json).toBe( '{"operator":"equal","value":{"fact":"weight","params":{"unit":"lbs"},"path":".value"},"fact":"age"}', ); }); @@ -66,7 +66,7 @@ describe("Condition", () => { it('converts "not" conditions', () => { const properties = { not: { - ...factories.condition({ + ...conditionFactory({ fact: "age", value: { fact: "weight", @@ -80,14 +80,14 @@ describe("Condition", () => { }; const condition = new Condition(properties); const json = condition.toJSON(); - expect(json).to.equal( + expect(json).toBe( '{"priority":1,"not":{"operator":"equal","value":{"fact":"weight","params":{"unit":"lbs"},"path":".value"},"fact":"age"}}', ); }); }); describe("evaluate", () => { - const conditionBase = factories.condition({ + const conditionBase = conditionFactory({ fact: "age", value: 50, }); @@ -101,33 +101,33 @@ describe("Condition", () => { almanac.addFact(fact); } - context("validations", () => { + describe("validations", () => { beforeEach(() => setup({}, 1)); it("throws when missing an almanac", () => { - return expect( - condition.evaluate(undefined, operators), - ).to.be.rejectedWith("almanac required"); + return expect(condition.evaluate(undefined, operators)).rejects.toThrow( + "almanac required", + ); }); it("throws when missing operators", () => { - return expect( - condition.evaluate(almanac, undefined), - ).to.be.rejectedWith("operatorMap required"); + return expect(condition.evaluate(almanac, undefined)).rejects.toThrow( + "operatorMap required", + ); }); it("throws when run against a boolean operator", () => { condition.all = []; - return expect( - condition.evaluate(almanac, operators), - ).to.be.rejectedWith("Cannot evaluate() a boolean condition"); + return expect(condition.evaluate(almanac, operators)).rejects.toThrow( + "Cannot evaluate() a boolean condition", + ); }); }); it('evaluates "equal"', async () => { setup({ operator: "equal" }, 50); - expect( - (await condition.evaluate(almanac, operators, 50)).result, - ).to.equal(true); + expect((await condition.evaluate(almanac, operators, 50)).result).toBe( + true, + ); setup({ operator: "equal" }, 5); - expect((await condition.evaluate(almanac, operators, 5)).result).to.equal( + expect((await condition.evaluate(almanac, operators, 5)).result).toBe( false, ); }); @@ -142,179 +142,131 @@ describe("Condition", () => { almanac = new Almanac(); almanac.addFact(fact); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - true, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(true); fact = new Fact("age", 1); almanac = new Almanac(); almanac.addFact(fact); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - false, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(false); }); it('evaluates "notEqual"', async () => { setup({ operator: "notEqual" }, 50); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - false, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(false); setup({ operator: "notEqual" }, 5); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - true, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(true); }); it('evaluates "in"', async () => { setup({ operator: "in", value: [5, 10, 15, 20] }, 15); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - true, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(true); setup({ operator: "in", value: [5, 10, 15, 20] }, 99); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - false, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(false); }); it('evaluates "contains"', async () => { setup({ operator: "contains", value: 10 }, [5, 10, 15]); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - true, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(true); setup({ operator: "contains", value: 10 }, [1, 2, 3]); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - false, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(false); }); it('evaluates "doesNotContain"', async () => { setup({ operator: "doesNotContain", value: 10 }, [5, 10, 15]); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - false, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(false); setup({ operator: "doesNotContain", value: 10 }, [1, 2, 3]); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - true, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(true); }); it('evaluates "notIn"', async () => { setup({ operator: "notIn", value: [5, 10, 15, 20] }, 15); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - false, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(false); setup({ operator: "notIn", value: [5, 10, 15, 20] }, 99); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - true, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(true); }); it('evaluates "lessThan"', async () => { setup({ operator: "lessThan" }, 49); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - true, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(true); setup({ operator: "lessThan" }, 50); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - false, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(false); setup({ operator: "lessThan" }, 51); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - false, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(false); }); it('evaluates "lessThanInclusive"', async () => { setup({ operator: "lessThanInclusive" }, 49); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - true, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(true); setup({ operator: "lessThanInclusive" }, 50); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - true, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(true); setup({ operator: "lessThanInclusive" }, 51); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - false, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(false); }); it('evaluates "greaterThan"', async () => { setup({ operator: "greaterThan" }, 51); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - true, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(true); setup({ operator: "greaterThan" }, 49); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - false, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(false); setup({ operator: "greaterThan" }, 50); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - false, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(false); }); it('evaluates "greaterThanInclusive"', async () => { setup({ operator: "greaterThanInclusive" }, 51); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - true, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(true); setup({ operator: "greaterThanInclusive" }, 50); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - true, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(true); setup({ operator: "greaterThanInclusive" }, 49); - expect((await condition.evaluate(almanac, operators)).result).to.equal( - false, - ); + expect((await condition.evaluate(almanac, operators)).result).toBe(false); }); describe("invalid comparisonValues", () => { it("returns false when using contains or doesNotContain with a non-array", async () => { setup({ operator: "contains" }, null); - expect((await condition.evaluate(almanac, operators)).result).to.equal( + expect((await condition.evaluate(almanac, operators)).result).toBe( false, ); setup({ operator: "doesNotContain" }, null); - expect((await condition.evaluate(almanac, operators)).result).to.equal( + expect((await condition.evaluate(almanac, operators)).result).toBe( false, ); }); it("returns false when using comparison operators with null", async () => { setup({ operator: "lessThan" }, null); - expect((await condition.evaluate(almanac, operators)).result).to.equal( + expect((await condition.evaluate(almanac, operators)).result).toBe( false, ); setup({ operator: "lessThanInclusive" }, null); - expect((await condition.evaluate(almanac, operators)).result).to.equal( + expect((await condition.evaluate(almanac, operators)).result).toBe( false, ); setup({ operator: "greaterThan" }, null); - expect((await condition.evaluate(almanac, operators)).result).to.equal( + expect((await condition.evaluate(almanac, operators)).result).toBe( false, ); setup({ operator: "greaterThanInclusive" }, null); - expect((await condition.evaluate(almanac, operators)).result).to.equal( + expect((await condition.evaluate(almanac, operators)).result).toBe( false, ); }); it("returns false when using comparison operators with non-numbers", async () => { setup({ operator: "lessThan" }, "non-number"); - expect((await condition.evaluate(almanac, operators)).result).to.equal( + expect((await condition.evaluate(almanac, operators)).result).toBe( false, ); setup({ operator: "lessThan" }, null); - expect((await condition.evaluate(almanac, operators)).result).to.equal( + expect((await condition.evaluate(almanac, operators)).result).toBe( false, ); setup({ operator: "lessThan" }, []); - expect((await condition.evaluate(almanac, operators)).result).to.equal( + expect((await condition.evaluate(almanac, operators)).result).toBe( false, ); setup({ operator: "lessThan" }, {}); - expect((await condition.evaluate(almanac, operators)).result).to.equal( + expect((await condition.evaluate(almanac, operators)).result).toBe( false, ); }); @@ -333,12 +285,12 @@ describe("Condition", () => { const ageFact = new Fact("age", [{ id: 50 }, { id: 60 }]); const almanac = new Almanac(); almanac.addFact(ageFact); - expect((await condition.evaluate(almanac, operators)).result).to.equal( + expect((await condition.evaluate(almanac, operators)).result).toBe( true, ); condition.value = 100; // negative case - expect((await condition.evaluate(almanac, operators)).result).to.equal( + expect((await condition.evaluate(almanac, operators)).result).toBe( false, ); }); @@ -354,14 +306,14 @@ describe("Condition", () => { fact: "age", value: 50, }); - expect( - (await condition.evaluate(almanac, operators, 50)).result, - ).to.equal(true); + expect((await condition.evaluate(almanac, operators, 50)).result).toBe( + true, + ); condition.value = 100; // negative case - expect( - (await condition.evaluate(almanac, operators, 50)).result, - ).to.equal(false); + expect((await condition.evaluate(almanac, operators, 50)).result).toBe( + false, + ); }); }); @@ -389,12 +341,12 @@ describe("Condition", () => { const usersFact = new Fact("users", userData); const almanac = new Almanac(); almanac.addFact(usersFact); - expect((await condition.evaluate(almanac, operators)).result).to.equal( + expect((await condition.evaluate(almanac, operators)).result).toBe( true, ); condition.value = "work"; // negative case - expect((await condition.evaluate(almanac, operators)).result).to.equal( + expect((await condition.evaluate(almanac, operators)).result).toBe( false, ); }); @@ -405,16 +357,14 @@ describe("Condition", () => { it("throws if not not an array", () => { const conditions = condition(); conditions.all = { foo: true }; - expect(() => new Condition(conditions)).to.throw( - /"all" must be an array/, - ); + expect(() => new Condition(conditions)).toThrow(/"all" must be an array/); }); it('throws if is an array and condition is "not"', () => { const conditions = { not: [{ foo: true }], }; - expect(() => new Condition(conditions)).to.throw( + expect(() => new Condition(conditions)).toThrow( /"not" cannot be an array/, ); }); @@ -427,13 +377,13 @@ describe("Condition", () => { value: "bar", }, }; - expect(() => new Condition(conditions)).to.not.throw(); + expect(() => new Condition(conditions)).not.toThrow(); }); }); describe("atomic facts", () => { it("throws if no options are provided", () => { - expect(() => new Condition()).to.throw( + expect(() => new Condition()).toThrow( /Condition: constructor options required/, ); }); @@ -441,7 +391,7 @@ describe("Condition", () => { it('throws for a missing "operator"', () => { const conditions = condition(); delete conditions.all[0].operator; - expect(() => new Condition(conditions)).to.throw( + expect(() => new Condition(conditions)).toThrow( /Condition: constructor "operator" property required/, ); }); @@ -449,7 +399,7 @@ describe("Condition", () => { it('throws for a missing "fact"', () => { const conditions = condition(); delete conditions.all[0].fact; - expect(() => new Condition(conditions)).to.throw( + expect(() => new Condition(conditions)).toThrow( /Condition: constructor "fact" property required/, ); }); @@ -457,7 +407,7 @@ describe("Condition", () => { it('throws for a missing "value"', () => { const conditions = condition(); delete conditions.all[0].value; - expect(() => new Condition(conditions)).to.throw( + expect(() => new Condition(conditions)).toThrow( /Condition: constructor "value" property required/, ); }); @@ -495,13 +445,13 @@ describe("Condition", () => { }; } it("recursively parses nested conditions", () => { - expect(() => new Condition(complexCondition())).to.not.throw(); + expect(() => new Condition(complexCondition())).not.toThrow(); }); it("throws if a nested condition is invalid", () => { const conditions = complexCondition(); delete conditions.all[2].any[0].fact; - expect(() => new Condition(conditions)).to.throw( + expect(() => new Condition(conditions)).toThrow( /Condition: constructor "fact" property required/, ); }); diff --git a/test/engine-all.test.js b/test/engine-all.test.mjs similarity index 64% rename from test/engine-all.test.js rename to test/engine-all.test.mjs index 3e3c3127..ea4e9de6 100644 --- a/test/engine-all.test.js +++ b/test/engine-all.test.mjs @@ -1,29 +1,21 @@ -"use strict"; +import engineFactory from "../src/index.mjs"; +import { describe, it, beforeEach, expect, vi } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; -import sinon from "sinon"; -import engineFactory from "../src/index"; - -async function factSenior(params, engine) { +async function factSenior() { return 65; } -async function factChild(params, engine) { +async function factChild() { return 10; } -async function factAdult(params, engine) { +async function factAdult() { return 30; } describe('Engine: "all" conditions', () => { let engine; - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); describe('supports a single "all" condition', () => { const event = { @@ -43,8 +35,8 @@ describe('Engine: "all" conditions', () => { }; let eventSpy; beforeEach(() => { - eventSpy = sandbox.spy(); - const rule = factories.rule({ conditions, event }); + eventSpy = vi.fn(); + const rule = ruleFactory({ conditions, event }); engine = engineFactory(); engine.addRule(rule); engine.on("success", eventSpy); @@ -53,13 +45,21 @@ describe('Engine: "all" conditions', () => { it("emits when the condition is met", async () => { engine.addFact("age", factChild); await engine.run(); - expect(eventSpy).to.have.been.calledWith(event); + expect(eventSpy).toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); it("does not emit when the condition fails", () => { engine.addFact("age", factSenior); engine.run(); - expect(eventSpy).to.not.have.been.calledWith(event); + expect(eventSpy).not.toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); }); @@ -86,8 +86,8 @@ describe('Engine: "all" conditions', () => { }; let eventSpy; beforeEach(() => { - eventSpy = sandbox.spy(); - const rule = factories.rule({ conditions, event }); + eventSpy = vi.fn(); + const rule = ruleFactory({ conditions, event }); engine = engineFactory(); engine.addRule(rule); engine.on("success", eventSpy); @@ -96,20 +96,32 @@ describe('Engine: "all" conditions', () => { it("emits an event when every condition is met", async () => { engine.addFact("age", factAdult); await engine.run(); - expect(eventSpy).to.have.been.calledWith(event); + expect(eventSpy).toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); describe("a condition fails", () => { it("does not emit when the first condition fails", async () => { engine.addFact("age", factChild); await engine.run(); - expect(eventSpy).to.not.have.been.calledWith(event); + expect(eventSpy).not.toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); it("does not emit when the second condition", async () => { engine.addFact("age", factSenior); await engine.run(); - expect(eventSpy).to.not.have.been.calledWith(event); + expect(eventSpy).not.toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); }); }); diff --git a/test/engine-any.test.js b/test/engine-any.test.mjs similarity index 55% rename from test/engine-any.test.js rename to test/engine-any.test.mjs index b915e6f9..440e95c2 100644 --- a/test/engine-any.test.js +++ b/test/engine-any.test.mjs @@ -1,17 +1,9 @@ -"use strict"; - -import sinon from "sinon"; -import engineFactory from "../src/index"; +import engineFactory from "../src/index.mjs"; +import { describe, it, beforeEach, expect, vi } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; describe('Engine: "any" conditions', () => { let engine; - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); describe('supports a single "any" condition', () => { const event = { @@ -32,9 +24,9 @@ describe('Engine: "any" conditions', () => { let eventSpy; let ageSpy; beforeEach(() => { - eventSpy = sandbox.spy(); - ageSpy = sandbox.stub(); - const rule = factories.rule({ conditions, event }); + eventSpy = vi.fn(); + ageSpy = vi.fn(); + const rule = ruleFactory({ conditions, event }); engine = engineFactory(); engine.addRule(rule); engine.addFact("age", ageSpy); @@ -42,15 +34,23 @@ describe('Engine: "any" conditions', () => { }); it("emits when the condition is met", async () => { - ageSpy.returns(10); + ageSpy.mockReturnValue(10); await engine.run(); - expect(eventSpy).to.have.been.calledWith(event); + expect(eventSpy).toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); it("does not emit when the condition fails", () => { - ageSpy.returns(75); + ageSpy.mockReturnValue(75); engine.run(); - expect(eventSpy).to.not.have.been.calledWith(event); + expect(eventSpy).not.toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); }); @@ -79,10 +79,10 @@ describe('Engine: "any" conditions', () => { let ageSpy; let segmentSpy; beforeEach(() => { - eventSpy = sandbox.spy(); - ageSpy = sandbox.stub(); - segmentSpy = sandbox.stub(); - const rule = factories.rule({ conditions, event }); + eventSpy = vi.fn(); + ageSpy = vi.fn(); + segmentSpy = vi.fn(); + const rule = ruleFactory({ conditions, event }); engine = engineFactory(); engine.addRule(rule); engine.addFact("segment", segmentSpy); @@ -91,22 +91,34 @@ describe('Engine: "any" conditions', () => { }); it("emits an event when any condition is met", async () => { - segmentSpy.returns("north-american"); - ageSpy.returns(25); + segmentSpy.mockReturnValue("north-american"); + ageSpy.mockReturnValue(25); await engine.run(); - expect(eventSpy).to.have.been.calledWith(event); + expect(eventSpy).toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); - segmentSpy.returns("european"); - ageSpy.returns(100); + segmentSpy.mockReturnValue("european"); + ageSpy.mockReturnValue(100); await engine.run(); - expect(eventSpy).to.have.been.calledWith(event); + expect(eventSpy).toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); it("does not emit when all conditions fail", async () => { - segmentSpy.returns("north-american"); - ageSpy.returns(100); + segmentSpy.mockReturnValue("north-american"); + ageSpy.mockReturnValue(100); await engine.run(); - expect(eventSpy).to.not.have.been.calledWith(event); + expect(eventSpy).not.toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); }); }); diff --git a/test/engine-cache.test.js b/test/engine-cache.test.mjs similarity index 65% rename from test/engine-cache.test.js rename to test/engine-cache.test.mjs index 56dd1840..dcd1a687 100644 --- a/test/engine-cache.test.js +++ b/test/engine-cache.test.mjs @@ -1,17 +1,9 @@ -"use strict"; - -import sinon from "sinon"; -import engineFactory from "../src/index"; +import engineFactory from "../src/index.mjs"; +import { describe, it, expect, vi } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; describe("Engine: cache", () => { let engine; - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); const event = { type: "setDrinkingFlag" }; const collegeSeniorEvent = { type: "isCollegeSenior" }; @@ -32,22 +24,22 @@ describe("Engine: cache", () => { return 22; }; function setup(factOptions) { - factSpy = sandbox.spy(); - eventSpy = sandbox.spy(); + factSpy = vi.fn(); + eventSpy = vi.fn(); engine = engineFactory(); - const determineDrinkingAge = factories.rule({ + const determineDrinkingAge = ruleFactory({ conditions, event, priority: 100, }); engine.addRule(determineDrinkingAge); - const determineCollegeSenior = factories.rule({ + const determineCollegeSenior = ruleFactory({ conditions, event: collegeSeniorEvent, priority: 1, }); engine.addRule(determineCollegeSenior); - const over20 = factories.rule({ + const over20 = ruleFactory({ conditions, event: collegeSeniorEvent, priority: 50, @@ -60,14 +52,14 @@ describe("Engine: cache", () => { it("loads facts once and caches the results for future use", async () => { setup({ cache: true }); await engine.run(); - expect(eventSpy).to.have.been.calledThrice(); - expect(factSpy).to.have.been.calledOnce(); + expect(eventSpy).toHaveBeenCalledTimes(3); + expect(factSpy).toHaveBeenCalledOnce(); }); it("allows caching to be turned off", async () => { setup({ cache: false }); await engine.run(); - expect(eventSpy).to.have.been.calledThrice(); - expect(factSpy).to.have.been.calledThrice(); + expect(eventSpy).toHaveBeenCalledTimes(3); + expect(factSpy).toHaveBeenCalledTimes(3); }); }); diff --git a/test/engine-condition.test.js b/test/engine-condition.test.mjs similarity index 65% rename from test/engine-condition.test.js rename to test/engine-condition.test.mjs index fc49f7d9..0662a307 100644 --- a/test/engine-condition.test.js +++ b/test/engine-condition.test.mjs @@ -1,17 +1,9 @@ -"use strict"; - -import sinon from "sinon"; -import engineFactory from "../src/index"; +import engineFactory from "../src/index.mjs"; +import { describe, it, beforeEach, expect, vi } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; describe("Engine: condition", () => { let engine; - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); describe("setCondition()", () => { describe("validations", () => { @@ -19,9 +11,7 @@ describe("Engine: condition", () => { engine = engineFactory(); }); it("throws an exception for invalid root conditions", () => { - expect( - engine.setCondition.bind(engine, "test", { foo: true }), - ).to.throw( + expect(engine.setCondition.bind(engine, "test", { foo: true })).toThrow( /"conditions" root must contain a single instance of "all", "any", "not", or "condition"/, ); }); @@ -50,8 +40,8 @@ describe("Engine: condition", () => { describe("allowUndefinedConditions: true", () => { let eventSpy; beforeEach(() => { - eventSpy = sandbox.spy(); - const sendRule = factories.rule({ + eventSpy = vi.fn(); + const sendRule = ruleFactory({ conditions: sendConditions, event: sendEvent, }); @@ -63,13 +53,13 @@ describe("Engine: condition", () => { it("evaluates undefined conditions as false", async () => { await engine.run(); - expect(eventSpy).to.have.been.called(); + expect(eventSpy).toHaveBeenCalled(); }); }); describe("allowUndefinedConditions: false", () => { beforeEach(() => { - const sendRule = factories.rule({ + const sendRule = ruleFactory({ conditions: sendConditions, event: sendEvent, }); @@ -82,7 +72,7 @@ describe("Engine: condition", () => { try { await engine.run(); } catch (error) { - expect(error.message).to.equal("No condition over60 exists"); + expect(error.message).toBe("No condition over60 exists"); } }); }); @@ -138,19 +128,19 @@ describe("Engine: condition", () => { let isRetiredSpy; let requestedOutreachSpy; beforeEach(() => { - eventSpy = sandbox.spy(); - ageSpy = sandbox.stub(); - isRetiredSpy = sandbox.stub(); - requestedOutreachSpy = sandbox.stub(); + eventSpy = vi.fn(); + ageSpy = vi.fn(); + isRetiredSpy = vi.fn(); + requestedOutreachSpy = vi.fn(); engine = engineFactory(); - const sendRule = factories.rule({ + const sendRule = ruleFactory({ conditions: sendConditions, event: sendEvent, }); engine.addRule(sendRule); - const outreachRule = factories.rule({ + const outreachRule = ruleFactory({ conditions: outreachConditions, event: outreachEvent, }); @@ -165,27 +155,54 @@ describe("Engine: condition", () => { }); it("emits all events when all conditions are met", async () => { - ageSpy.returns(65); - isRetiredSpy.returns(true); - requestedOutreachSpy.returns(true); + ageSpy.mockReturnValue(65); + isRetiredSpy.mockReturnValue(true); + requestedOutreachSpy.mockReturnValue(true); await engine.run(); expect(eventSpy) - .to.have.been.calledWith(sendEvent) - .and.to.have.been.calledWith(outreachEvent); + .toHaveBeenCalledWith(sendEvent, expect.anything(), expect.anything()) + .and.toHaveBeenCalledWith( + outreachEvent, + expect.anything(), + expect.anything(), + ); }); it("expands condition in rule results", async () => { - ageSpy.returns(65); - isRetiredSpy.returns(true); - requestedOutreachSpy.returns(true); + ageSpy.mockReturnValue(65); + isRetiredSpy.mockReturnValue(true); + requestedOutreachSpy.mockReturnValue(true); const { results } = await engine.run(); - const nestedCondition = { - "conditions.all[0].all[0].fact": "age", - "conditions.all[0].all[0].operator": "greaterThanInclusive", - "conditions.all[0].all[0].value": 60, - }; - expect(results[0]).to.nested.include(nestedCondition); - expect(results[1]).to.nested.include(nestedCondition); + expect(results[0]).toMatchObject({ + conditions: { + all: { + 0: { + all: [ + { + fact: "age", + operator: "greaterThanInclusive", + value: 60, + }, + ], + }, + }, + }, + }); + expect(results[1]).toMatchObject({ + conditions: { + all: { + 0: { + all: [ + { + fact: "age", + operator: "greaterThanInclusive", + value: 60, + }, + ], + }, + }, + }, + }); }); }); @@ -233,13 +250,13 @@ describe("Engine: condition", () => { let isRetiredSpy; let requestedOutreachSpy; beforeEach(() => { - eventSpy = sandbox.spy(); - ageSpy = sandbox.stub(); - isRetiredSpy = sandbox.stub(); - requestedOutreachSpy = sandbox.stub(); + eventSpy = vi.fn(); + ageSpy = vi.fn(); + isRetiredSpy = vi.fn(); + requestedOutreachSpy = vi.fn(); engine = engineFactory(); - const outreachRule = factories.rule({ + const outreachRule = ruleFactory({ conditions: outreachConditions, event: outreachEvent, }); @@ -256,27 +273,48 @@ describe("Engine: condition", () => { }); it("emits all events when all conditions are met", async () => { - ageSpy.returns(55); - isRetiredSpy.returns(true); - requestedOutreachSpy.returns(true); + ageSpy.mockReturnValue(55); + isRetiredSpy.mockReturnValue(true); + requestedOutreachSpy.mockReturnValue(true); await engine.run(); - expect(eventSpy).to.have.been.calledWith(outreachEvent); + expect(eventSpy).toHaveBeenCalledWith( + outreachEvent, + expect.anything(), + expect.anything(), + ); }); it("expands condition in rule results", async () => { - ageSpy.returns(55); - isRetiredSpy.returns(true); - requestedOutreachSpy.returns(true); + ageSpy.mockReturnValue(55); + isRetiredSpy.mockReturnValue(true); + requestedOutreachSpy.mockReturnValue(true); const { results } = await engine.run(); - const nestedCondition = { - "conditions.all[0].all[0].not.all[0].fact": "age", - "conditions.all[0].all[0].not.all[0].operator": "greaterThanInclusive", - "conditions.all[0].all[0].not.all[0].value": 60, - "conditions.all[0].all[1].fact": "isRetired", - "conditions.all[0].all[1].operator": "equal", - "conditions.all[0].all[1].value": true, - }; - expect(results[0]).to.nested.include(nestedCondition); + expect(results[0]).toMatchObject({ + conditions: { + all: { + 0: { + all: [ + { + not: { + all: [ + { + fact: "age", + operator: "greaterThanInclusive", + value: 60, + }, + ], + }, + }, + { + fact: "isRetired", + operator: "equal", + value: true, + }, + ], + }, + }, + }, + }); }); }); @@ -299,8 +337,8 @@ describe("Engine: condition", () => { let eventSpy; beforeEach(() => { - eventSpy = sandbox.spy(); - const sendRule = factories.rule({ + eventSpy = vi.fn(); + const sendRule = ruleFactory({ conditions: sendConditions, event: sendEvent, }); @@ -315,7 +353,7 @@ describe("Engine: condition", () => { it("evaluates top level conditions correctly", async () => { await engine.run(); - expect(eventSpy).to.have.been.called(); + expect(eventSpy).toHaveBeenCalled(); }); }); }); diff --git a/test/engine-controls.test.js b/test/engine-controls.test.mjs similarity index 57% rename from test/engine-controls.test.js rename to test/engine-controls.test.mjs index c450fe62..3ef59443 100644 --- a/test/engine-controls.test.js +++ b/test/engine-controls.test.mjs @@ -1,17 +1,11 @@ -"use strict"; +import engineFactory from "../src/index.mjs"; -import engineFactory from "../src/index"; -import sinon from "sinon"; +import { describe, it, expect, vi } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; describe("Engine: fact priority", () => { let engine; - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); + const event = { type: "adult-human-admins" }; let eventSpy; @@ -19,9 +13,9 @@ describe("Engine: fact priority", () => { let segmentStub; function setup() { - ageStub = sandbox.stub(); - segmentStub = sandbox.stub(); - eventSpy = sandbox.stub(); + ageStub = vi.fn(); + segmentStub = vi.fn(); + eventSpy = vi.fn(); engine = engineFactory(); let conditions = { @@ -33,7 +27,7 @@ describe("Engine: fact priority", () => { }, ], }; - let rule = factories.rule({ conditions, event, priority: 100 }); + let rule = ruleFactory({ conditions, event, priority: 100 }); engine.addRule(rule); conditions = { @@ -45,7 +39,7 @@ describe("Engine: fact priority", () => { }, ], }; - rule = factories.rule({ conditions, event }); + rule = ruleFactory({ conditions, event }); engine.addRule(rule); engine.addFact("age", ageStub, { priority: 100 }); @@ -55,15 +49,15 @@ describe("Engine: fact priority", () => { describe("stop()", () => { it("stops the rules from executing", async () => { setup(); - ageStub.returns(20); // success - engine.on("success", (event) => { + ageStub.mockReturnValue(20); // success + engine.on("success", () => { eventSpy(); engine.stop(); }); await engine.run(); - expect(eventSpy).to.have.been.calledOnce(); - expect(ageStub).to.have.been.calledOnce(); - expect(segmentStub).to.not.have.been.called(); + expect(eventSpy).toHaveBeenCalledOnce(); + expect(ageStub).toHaveBeenCalledOnce(); + expect(segmentStub).not.toHaveBeenCalled(); }); }); }); diff --git a/test/engine-custom-properties.test.js b/test/engine-custom-properties.test.mjs similarity index 70% rename from test/engine-custom-properties.test.js rename to test/engine-custom-properties.test.mjs index 9753ab85..58d3ac07 100644 --- a/test/engine-custom-properties.test.js +++ b/test/engine-custom-properties.test.mjs @@ -1,6 +1,6 @@ -"use strict"; - -import engineFactory, { Fact, Rule } from "../src/index"; +import engineFactory, { Fact, Rule } from "../src/index.mjs"; +import { describe, it, expect } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; describe("Engine: custom properties", () => { let engine; @@ -12,8 +12,8 @@ describe("Engine: custom properties", () => { const fact = new Fact("age", 12); fact.customId = "uuid"; engine.addFact(fact); - expect(engine.facts.get("age")).to.have.property("customId"); - expect(engine.facts.get("age").customId).to.equal(fact.customId); + expect(engine.facts.get("age")).toHaveProperty("customId"); + expect(engine.facts.get("age").customId).toBe(fact.customId); }); describe("conditions", () => { @@ -29,9 +29,9 @@ describe("Engine: custom properties", () => { }, ], }; - const rule = factories.rule({ conditions, event }); + const rule = ruleFactory({ conditions, event }); engine.addRule(rule); - expect(engine.rules[0].conditions).to.have.property("customId"); + expect(engine.rules[0].conditions).toHaveProperty("customId"); }); it("preserves custom properties set on regular conditions", () => { @@ -46,9 +46,9 @@ describe("Engine: custom properties", () => { }, ], }; - const rule = factories.rule({ conditions, event }); + const rule = ruleFactory({ conditions, event }); engine.addRule(rule); - expect(engine.rules[0].conditions.all[0]).to.have.property("customId"); + expect(engine.rules[0].conditions.all[0]).toHaveProperty("customId"); expect(engine.rules[0].conditions.all[0].customId).equal("uuid"); }); }); @@ -56,14 +56,14 @@ describe("Engine: custom properties", () => { it("preserves custom properties set on regular conditions", () => { engine = engineFactory(); const rule = new Rule(); - const ruleProperties = factories.rule(); + const ruleProperties = ruleFactory(); rule .setPriority(ruleProperties.priority) .setConditions(ruleProperties.conditions) .setEvent(ruleProperties.event); rule.customId = "uuid"; engine.addRule(rule); - expect(engine.rules[0]).to.have.property("customId"); + expect(engine.rules[0]).toHaveProperty("customId"); expect(engine.rules[0].customId).equal("uuid"); }); }); diff --git a/test/engine-error-handling.test.js b/test/engine-error-handling.test.mjs similarity index 55% rename from test/engine-error-handling.test.js rename to test/engine-error-handling.test.mjs index 871d4933..ccb1ac64 100644 --- a/test/engine-error-handling.test.js +++ b/test/engine-error-handling.test.mjs @@ -1,6 +1,6 @@ -"use strict"; - -import engineFactory from "../src/index"; +import engineFactory from "../src/index.mjs"; +import { describe, it, beforeEach, expect } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; describe("Engine: failure", () => { let engine; @@ -17,14 +17,14 @@ describe("Engine: failure", () => { }; beforeEach(() => { engine = engineFactory(); - const determineDrinkingAgeRule = factories.rule({ conditions, event }); + const determineDrinkingAgeRule = ruleFactory({ conditions, event }); engine.addRule(determineDrinkingAgeRule); - engine.addFact("age", function (params, engine) { + engine.addFact("age", function () { throw new Error("problem occurred"); }); }); it("surfaces errors", () => { - return expect(engine.run()).to.eventually.rejectedWith(/problem occurred/); + return expect(engine.run()).rejects.toThrow(/problem occurred/); }); }); diff --git a/test/engine-event.test.js b/test/engine-event.test.mjs similarity index 58% rename from test/engine-event.test.js rename to test/engine-event.test.mjs index 2ebd7df8..11bae3eb 100644 --- a/test/engine-event.test.js +++ b/test/engine-event.test.mjs @@ -1,18 +1,11 @@ -"use strict"; +import Almanac from "../src/almanac.mjs"; +import engineFactory, { Fact } from "../src/index.mjs"; -import engineFactory, { Fact } from "../src/index"; -import Almanac from "../src/almanac"; -import sinon from "sinon"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; describe("Engine: event", () => { let engine; - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); const event = { type: "setDrinkingFlag", @@ -41,7 +34,7 @@ describe("Engine: event", () => { }; engine = engineFactory(); const ruleOptions = { conditions, event, priority: 100 }; - const determineDrinkingAgeRule = factories.rule(ruleOptions); + const determineDrinkingAgeRule = ruleFactory(ruleOptions); engine.addRule(determineDrinkingAgeRule); // age will succeed because 21 >= 21 engine.addFact("age", 21); @@ -83,7 +76,7 @@ describe("Engine: event", () => { }; engine = engineFactory(); const ruleOptions = { conditions, event, priority: 100 }; - const determineDrinkingAgeRule = factories.rule(ruleOptions); + const determineDrinkingAgeRule = ruleFactory(ruleOptions); engine.addRule(determineDrinkingAgeRule); // rule will succeed because of 'any' engine.addFact("age", 10); // age fails @@ -92,23 +85,23 @@ describe("Engine: event", () => { engine.addFact("gender", "male"); // gender succeeds } - context("engine events: simple", () => { + describe("engine events: simple", () => { beforeEach(() => simpleSetup()); it('"success" passes the event, almanac, and results', async () => { - const failureSpy = sandbox.spy(); - const successSpy = sandbox.spy(); + const failureSpy = vi.fn(); + const successSpy = vi.fn(); function assertResult(ruleResult) { - expect(ruleResult.result).to.be.true(); - expect(ruleResult.conditions.any[0].result).to.be.true(); - expect(ruleResult.conditions.any[0].factResult).to.equal(21); - expect(ruleResult.conditions.any[0].name).to.equal("over 21"); - expect(ruleResult.conditions.any[1].result).to.be.false(); - expect(ruleResult.conditions.any[1].factResult).to.equal(false); + expect(ruleResult.result).toBe(true); + expect(ruleResult.conditions.any[0].result).toBe(true); + expect(ruleResult.conditions.any[0].factResult).toBe(21); + expect(ruleResult.conditions.any[0].name).toBe("over 21"); + expect(ruleResult.conditions.any[1].result).toBe(false); + expect(ruleResult.conditions.any[1].factResult).toBe(false); } engine.on("success", function (e, almanac, ruleResult) { - expect(e).to.eql(event); - expect(almanac).to.be.an.instanceof(Almanac); + expect(e).toEqual(event); + expect(almanac).toBeInstanceOf(Almanac); assertResult(ruleResult); successSpy(); }); @@ -116,26 +109,26 @@ describe("Engine: event", () => { const { results, failureResults } = await engine.run(); - expect(failureResults).to.have.lengthOf(0); - expect(results).to.have.lengthOf(1); + expect(failureResults).toHaveLength(0); + expect(results).toHaveLength(1); assertResult(results[0]); - expect(failureSpy.callCount).to.equal(0); - expect(successSpy.callCount).to.equal(1); + expect(failureSpy).not.toHaveBeenCalled(); + expect(successSpy).toHaveBeenCalledOnce(); }); it('"event.type" passes the event parameters, almanac, and results', async () => { - const failureSpy = sandbox.spy(); - const successSpy = sandbox.spy(); + const failureSpy = vi.fn(); + const successSpy = vi.fn(); function assertResult(ruleResult) { - expect(ruleResult.result).to.be.true(); - expect(ruleResult.conditions.any[0].result).to.be.true(); - expect(ruleResult.conditions.any[0].factResult).to.equal(21); - expect(ruleResult.conditions.any[1].result).to.be.false(); - expect(ruleResult.conditions.any[1].factResult).to.equal(false); + expect(ruleResult.result).toBe(true); + expect(ruleResult.conditions.any[0].result).toBe(true); + expect(ruleResult.conditions.any[0].factResult).toBe(21); + expect(ruleResult.conditions.any[1].result).toBe(false); + expect(ruleResult.conditions.any[1].factResult).toBe(false); } engine.on(event.type, function (params, almanac, ruleResult) { - expect(params).to.eql(event.params); - expect(almanac).to.be.an.instanceof(Almanac); + expect(params).toEqual(event.params); + expect(almanac).toBeInstanceOf(Almanac); assertResult(ruleResult); successSpy(); }); @@ -143,29 +136,29 @@ describe("Engine: event", () => { const { results, failureResults } = await engine.run(); - expect(failureResults).to.have.lengthOf(0); - expect(results).to.have.lengthOf(1); + expect(failureResults).toHaveLength(0); + expect(results).toHaveLength(1); assertResult(results[0]); - expect(failureSpy.callCount).to.equal(0); - expect(successSpy.callCount).to.equal(1); + expect(failureSpy).not.toHaveBeenCalled(); + expect(successSpy).toHaveBeenCalledOnce(); }); it('"failure" passes the event, almanac, and results', async () => { const AGE = 10; - const failureSpy = sandbox.spy(); - const successSpy = sandbox.spy(); + const failureSpy = vi.fn(); + const successSpy = vi.fn(); function assertResult(ruleResult) { - expect(ruleResult.result).to.be.false(); - expect(ruleResult.conditions.any[0].result).to.be.false(); - expect(ruleResult.conditions.any[0].factResult).to.equal(AGE); - expect(ruleResult.conditions.any[1].result).to.be.false(); - expect(ruleResult.conditions.any[1].factResult).to.equal(false); + expect(ruleResult.result).toBe(false); + expect(ruleResult.conditions.any[0].result).toBe(false); + expect(ruleResult.conditions.any[0].factResult).toBe(AGE); + expect(ruleResult.conditions.any[1].result).toBe(false); + expect(ruleResult.conditions.any[1].factResult).toBe(false); } engine.on("failure", function (e, almanac, ruleResult) { - expect(e).to.eql(event); - expect(almanac).to.be.an.instanceof(Almanac); + expect(e).toEqual(event); + expect(almanac).toBeInstanceOf(Almanac); assertResult(ruleResult); failureSpy(); }); @@ -174,12 +167,12 @@ describe("Engine: event", () => { const { results, failureResults } = await engine.run(); - expect(failureResults).to.have.lengthOf(1); - expect(results).to.have.lengthOf(0); + expect(failureResults).toHaveLength(1); + expect(results).toHaveLength(0); assertResult(failureResults[0]); - expect(failureSpy.callCount).to.equal(1); - expect(successSpy.callCount).to.equal(0); + expect(failureSpy).toHaveBeenCalledOnce(); + expect(successSpy).not.toHaveBeenCalled(); }); it("allows facts to be added by the event handler, affecting subsequent rules", () => { @@ -197,14 +190,14 @@ describe("Engine: event", () => { }, ], }; - const drinkOrderRule = factories.rule({ + const drinkOrderRule = ruleFactory({ conditions: drinkOrderConditions, event: drinkOrderEvent, priority: 1, }); engine.addRule(drinkOrderRule); return new Promise((resolve, reject) => { - engine.on("success", function (event, almanac, ruleResult) { + engine.on("success", function (event, almanac) { switch (event.type) { case "setDrinkingFlag": almanac.addRuntimeFact( @@ -213,7 +206,7 @@ describe("Engine: event", () => { ); break; case "offerDrink": - expect(event.params).to.eql(drinkOrderParams); + expect(event.params).toEqual(drinkOrderParams); break; default: reject(new Error("default case not expected")); @@ -224,29 +217,29 @@ describe("Engine: event", () => { }); }); - context("engine events: advanced", () => { + describe("engine events: advanced", () => { beforeEach(() => advancedSetup()); it('"success" passes the event, almanac, and results', async () => { - const failureSpy = sandbox.spy(); - const successSpy = sandbox.spy(); + const failureSpy = vi.fn(); + const successSpy = vi.fn(); function assertResult(ruleResult) { - expect(ruleResult.result).to.be.true(); - expect(ruleResult.conditions.any[0].result).to.be.false(); - expect(ruleResult.conditions.any[0].factResult).to.equal(10); - expect(ruleResult.conditions.any[1].result).to.be.false(); - expect(ruleResult.conditions.any[1].factResult).to.equal(false); - expect(ruleResult.conditions.any[2].result).to.be.true(); - expect(ruleResult.conditions.any[2].all[0].result).to.be.true(); - expect(ruleResult.conditions.any[2].all[0].factResult).to.equal(80403); - expect(ruleResult.conditions.any[2].all[1].result).to.be.true(); - expect(ruleResult.conditions.any[2].all[1].factResult).to.equal("male"); + expect(ruleResult.result).toBe(true); + expect(ruleResult.conditions.any[0].result).toBe(false); + expect(ruleResult.conditions.any[0].factResult).toBe(10); + expect(ruleResult.conditions.any[1].result).toBe(false); + expect(ruleResult.conditions.any[1].factResult).toBe(false); + expect(ruleResult.conditions.any[2].result).toBe(true); + expect(ruleResult.conditions.any[2].all[0].result).toBe(true); + expect(ruleResult.conditions.any[2].all[0].factResult).toBe(80403); + expect(ruleResult.conditions.any[2].all[1].result).toBe(true); + expect(ruleResult.conditions.any[2].all[1].factResult).toBe("male"); } engine.on("success", function (e, almanac, ruleResult) { - expect(e).to.eql(event); - expect(almanac).to.be.an.instanceof(Almanac); + expect(e).toEqual(event); + expect(almanac).toBeInstanceOf(Almanac); assertResult(ruleResult); successSpy(); }); @@ -255,34 +248,32 @@ describe("Engine: event", () => { const { results, failureResults } = await engine.run(); assertResult(results[0]); - expect(failureResults).to.have.lengthOf(0); - expect(results).to.have.lengthOf(1); - expect(failureSpy.callCount).to.equal(0); - expect(successSpy.callCount).to.equal(1); + expect(failureResults).toHaveLength(0); + expect(results).toHaveLength(1); + expect(failureSpy).not.toHaveBeenCalled(); + expect(successSpy).toHaveBeenCalledOnce(); }); it('"failure" passes the event, almanac, and results', async () => { const ZIP_CODE = 99992; const GENDER = "female"; - const failureSpy = sandbox.spy(); - const successSpy = sandbox.spy(); + const failureSpy = vi.fn(); + const successSpy = vi.fn(); function assertResult(ruleResult) { - expect(ruleResult.result).to.be.false(); - expect(ruleResult.conditions.any[0].result).to.be.false(); - expect(ruleResult.conditions.any[0].factResult).to.equal(10); - expect(ruleResult.conditions.any[1].result).to.be.false(); - expect(ruleResult.conditions.any[1].factResult).to.equal(false); - expect(ruleResult.conditions.any[2].result).to.be.false(); - expect(ruleResult.conditions.any[2].all[0].result).to.be.false(); - expect(ruleResult.conditions.any[2].all[0].factResult).to.equal( - ZIP_CODE, - ); - expect(ruleResult.conditions.any[2].all[1].result).to.be.false(); - expect(ruleResult.conditions.any[2].all[1].factResult).to.equal(GENDER); + expect(ruleResult.result).toBe(false); + expect(ruleResult.conditions.any[0].result).toBe(false); + expect(ruleResult.conditions.any[0].factResult).toBe(10); + expect(ruleResult.conditions.any[1].result).toBe(false); + expect(ruleResult.conditions.any[1].factResult).toBe(false); + expect(ruleResult.conditions.any[2].result).toBe(false); + expect(ruleResult.conditions.any[2].all[0].result).toBe(false); + expect(ruleResult.conditions.any[2].all[0].factResult).toBe(ZIP_CODE); + expect(ruleResult.conditions.any[2].all[1].result).toBe(false); + expect(ruleResult.conditions.any[2].all[1].factResult).toBe(GENDER); } engine.on("failure", function (e, almanac, ruleResult) { - expect(e).to.eql(event); - expect(almanac).to.be.an.instanceof(Almanac); + expect(e).toEqual(event); + expect(almanac).toBeInstanceOf(Almanac); assertResult(ruleResult); failureSpy(); }); @@ -293,15 +284,15 @@ describe("Engine: event", () => { const { results, failureResults } = await engine.run(); assertResult(failureResults[0]); - expect(failureResults).to.have.lengthOf(1); - expect(results).to.have.lengthOf(0); + expect(failureResults).toHaveLength(1); + expect(results).toHaveLength(0); - expect(failureSpy.callCount).to.equal(1); - expect(successSpy.callCount).to.equal(0); + expect(failureSpy).toHaveBeenCalledOnce(); + expect(successSpy).not.toHaveBeenCalled(); }); }); - context("engine events: with facts", () => { + describe("engine events: with facts", () => { const eventWithFact = { type: "countedEnough", params: { @@ -323,53 +314,69 @@ describe("Engine: event", () => { }; const ruleOptions = { conditions, event, priority: 100 }; - const countedEnoughRule = factories.rule(ruleOptions); + const countedEnoughRule = ruleFactory(ruleOptions); engine = engineFactory([countedEnoughRule], { replaceFactsInEventParams, }); } - context("without flag", () => { + describe("without flag", () => { beforeEach(() => setup(false)); it('"success" passes the event without resolved facts', async () => { - const successSpy = sandbox.spy(); + const successSpy = vi.fn(); engine.on("success", successSpy); const { results } = await engine.run({ success: true, count: 5 }); - expect(results[0].event).to.deep.equal(eventWithFact); - expect(successSpy.firstCall.args[0]).to.deep.equal(eventWithFact); + expect(results[0].event).toEqual(eventWithFact); + expect(successSpy).toHaveBeenCalledWith( + eventWithFact, + expect.anything(), + expect.anything(), + ); }); it("failure passes the event without resolved facts", async () => { - const failureSpy = sandbox.spy(); + const failureSpy = vi.fn(); engine.on("failure", failureSpy); const { failureResults } = await engine.run({ success: false, count: 5, }); - expect(failureResults[0].event).to.deep.equal(eventWithFact); - expect(failureSpy.firstCall.args[0]).to.deep.equal(eventWithFact); + expect(failureResults[0].event).toEqual(eventWithFact); + expect(failureSpy).toHaveBeenCalledWith( + eventWithFact, + expect.anything(), + expect.anything(), + ); }); }); - context("with flag", () => { + describe("with flag", () => { beforeEach(() => setup(true)); it('"success" passes the event with resolved facts', async () => { - const successSpy = sandbox.spy(); + const successSpy = vi.fn(); engine.on("success", successSpy); const { results } = await engine.run({ success: true, count: 5 }); - expect(results[0].event).to.deep.equal(expectedEvent); - expect(successSpy.firstCall.args[0]).to.deep.equal(expectedEvent); + expect(results[0].event).toEqual(expectedEvent); + expect(successSpy).toHaveBeenCalledWith( + expectedEvent, + expect.anything(), + expect.anything(), + ); }); it("failure passes the event with resolved facts", async () => { - const failureSpy = sandbox.spy(); + const failureSpy = vi.fn(); engine.on("failure", failureSpy); const { failureResults } = await engine.run({ success: false, count: 5, }); - expect(failureResults[0].event).to.deep.equal(expectedEvent); - expect(failureSpy.firstCall.args[0]).to.deep.equal(expectedEvent); + expect(failureResults[0].event).toEqual(expectedEvent); + expect(failureSpy).toHaveBeenCalledWith( + expectedEvent, + expect.anything(), + expect.anything(), + ); }); - context("using fact params and path", () => { + describe("using fact params and path", () => { const eventWithFactWithParamsAndPath = { type: "countedEnough", params: { @@ -393,25 +400,33 @@ describe("Engine: event", () => { ); }); it('"success" passes the event with resolved facts', async () => { - const successSpy = sandbox.spy(); + const successSpy = vi.fn(); engine.on("success", successSpy); const { results } = await engine.run({ success: true }); - expect(results[0].event).to.deep.equal(expectedEvent); - expect(successSpy.firstCall.args[0]).to.deep.equal(expectedEvent); + expect(results[0].event).toEqual(expectedEvent); + expect(successSpy).toHaveBeenCalledWith( + expectedEvent, + expect.anything(), + expect.anything(), + ); }); it("failure passes the event with resolved facts", async () => { - const failureSpy = sandbox.spy(); + const failureSpy = vi.fn(); engine.on("failure", failureSpy); const { failureResults } = await engine.run({ success: false }); - expect(failureResults[0].event).to.deep.equal(expectedEvent); - expect(failureSpy.firstCall.args[0]).to.deep.equal(expectedEvent); + expect(failureResults[0].event).toEqual(expectedEvent); + expect(failureSpy).toHaveBeenCalledWith( + expectedEvent, + expect.anything(), + expect.anything(), + ); }); }); }); }); - context("rule events: simple", () => { + describe("rule events: simple", () => { beforeEach(() => simpleSetup()); it("the rule result is a _copy_ of the rule`s conditions, and unaffected by mutation", async () => { @@ -430,25 +445,25 @@ describe("Engine: event", () => { }); await engine.run(); - expect(firstPass).to.deep.equal(secondPass); // second pass was unaffected by first pass + expect(firstPass).toEqual(secondPass); // second pass was unaffected by first pass }); it("on-success, it passes the event type and params", async () => { - const failureSpy = sandbox.spy(); - const successSpy = sandbox.spy(); + const failureSpy = vi.fn(); + const successSpy = vi.fn(); const rule = engine.rules[0]; function assertResult(ruleResult) { - expect(ruleResult.result).to.be.true(); - expect(ruleResult.conditions.any[0].result).to.be.true(); - expect(ruleResult.conditions.any[0].factResult).to.equal(21); - expect(ruleResult.conditions.any[1].result).to.be.false(); - expect(ruleResult.conditions.any[1].factResult).to.equal(false); + expect(ruleResult.result).toBe(true); + expect(ruleResult.conditions.any[0].result).toBe(true); + expect(ruleResult.conditions.any[0].factResult).toBe(21); + expect(ruleResult.conditions.any[1].result).toBe(false); + expect(ruleResult.conditions.any[1].factResult).toBe(false); } rule.on("success", function (e, almanac, ruleResult) { - expect(e).to.eql(event); - expect(almanac).to.be.an.instanceof(Almanac); - expect(failureSpy.callCount).to.equal(0); + expect(e).toEqual(event); + expect(almanac).toBeInstanceOf(Almanac); + expect(failureSpy).not.toHaveBeenCalled(); assertResult(ruleResult); successSpy(); }); @@ -457,29 +472,29 @@ describe("Engine: event", () => { const { results, failureResults } = await engine.run(); assertResult(results[0]); - expect(failureResults).to.have.lengthOf(0); - expect(results).to.have.lengthOf(1); + expect(failureResults).toHaveLength(0); + expect(results).toHaveLength(1); - expect(successSpy.callCount).to.equal(1); - expect(failureSpy.callCount).to.equal(0); + expect(successSpy).toHaveBeenCalledOnce(); + expect(failureSpy).not.toHaveBeenCalled(); }); it("on-failure, it passes the event type and params", async () => { const AGE = 10; - const successSpy = sandbox.spy(); - const failureSpy = sandbox.spy(); + const successSpy = vi.fn(); + const failureSpy = vi.fn(); const rule = engine.rules[0]; function assertResult(ruleResult) { - expect(ruleResult.result).to.be.false(); - expect(ruleResult.conditions.any[0].result).to.be.false(); - expect(ruleResult.conditions.any[0].factResult).to.equal(AGE); - expect(ruleResult.conditions.any[1].result).to.be.false(); - expect(ruleResult.conditions.any[1].factResult).to.equal(false); + expect(ruleResult.result).toBe(false); + expect(ruleResult.conditions.any[0].result).toBe(false); + expect(ruleResult.conditions.any[0].factResult).toBe(AGE); + expect(ruleResult.conditions.any[1].result).toBe(false); + expect(ruleResult.conditions.any[1].factResult).toBe(false); } rule.on("failure", function (e, almanac, ruleResult) { - expect(e).to.eql(event); - expect(almanac).to.be.an.instanceof(Almanac); - expect(successSpy.callCount).to.equal(0); + expect(e).toEqual(event); + expect(almanac).toBeInstanceOf(Almanac); + expect(successSpy).not.toHaveBeenCalled(); assertResult(ruleResult); failureSpy(); }); @@ -489,14 +504,14 @@ describe("Engine: event", () => { const { results, failureResults } = await engine.run(); assertResult(failureResults[0]); - expect(failureResults).to.have.lengthOf(1); - expect(results).to.have.lengthOf(0); - expect(failureSpy.callCount).to.equal(1); - expect(successSpy.callCount).to.equal(0); + expect(failureResults).toHaveLength(1); + expect(results).toHaveLength(0); + expect(failureSpy).toHaveBeenCalledOnce(); + expect(successSpy).not.toHaveBeenCalled(); }); }); - context("rule events: with facts", () => { + describe("rule events: with facts", () => { const expectedEvent = { type: "countedEnough", params: { count: 5 } }; const eventWithFact = { type: "countedEnough", @@ -517,43 +532,59 @@ describe("Engine: event", () => { }; const ruleOptions = { conditions, event, priority: 100 }; - const countedEnoughRule = factories.rule(ruleOptions); + const countedEnoughRule = ruleFactory(ruleOptions); engine = engineFactory([countedEnoughRule], { replaceFactsInEventParams, }); } - context("without flag", () => { + describe("without flag", () => { beforeEach(() => setup(false)); it('"success" passes the event without resolved facts', async () => { - const successSpy = sandbox.spy(); + const successSpy = vi.fn(); engine.rules[0].on("success", successSpy); await engine.run({ success: true, count: 5 }); - expect(successSpy.firstCall.args[0]).to.deep.equal(eventWithFact); + expect(successSpy).toHaveBeenCalledWith( + eventWithFact, + expect.anything(), + expect.anything(), + ); }); it("failure passes the event without resolved facts", async () => { - const failureSpy = sandbox.spy(); + const failureSpy = vi.fn(); engine.rules[0].on("failure", failureSpy); await engine.run({ success: false, count: 5 }); - expect(failureSpy.firstCall.args[0]).to.deep.equal(eventWithFact); + expect(failureSpy).toHaveBeenCalledWith( + eventWithFact, + expect.anything(), + expect.anything(), + ); }); }); - context("with flag", () => { + describe("with flag", () => { beforeEach(() => setup(true)); it('"success" passes the event with resolved facts', async () => { - const successSpy = sandbox.spy(); + const successSpy = vi.fn(); engine.rules[0].on("success", successSpy); await engine.run({ success: true, count: 5 }); - expect(successSpy.firstCall.args[0]).to.deep.equal(expectedEvent); + expect(successSpy).toHaveBeenCalledWith( + expectedEvent, + expect.anything(), + expect.anything(), + ); }); it("failure passes the event with resolved facts", async () => { - const failureSpy = sandbox.spy(); + const failureSpy = vi.fn(); engine.rules[0].on("failure", failureSpy); await engine.run({ success: false, count: 5 }); - expect(failureSpy.firstCall.args[0]).to.deep.equal(expectedEvent); + expect(failureSpy).toHaveBeenCalledWith( + expectedEvent, + expect.anything(), + expect.anything(), + ); }); - context("using fact params and path", () => { + describe("using fact params and path", () => { const eventWithFactWithParamsAndPath = { type: "countedEnough", params: { @@ -577,35 +608,43 @@ describe("Engine: event", () => { ); }); it('"success" passes the event with resolved facts', async () => { - const successSpy = sandbox.spy(); + const successSpy = vi.fn(); engine.on("success", successSpy); const { results } = await engine.run({ success: true }); - expect(results[0].event).to.deep.equal(expectedEvent); - expect(successSpy.firstCall.args[0]).to.deep.equal(expectedEvent); + expect(results[0].event).toEqual(expectedEvent); + expect(successSpy).toHaveBeenCalledWith( + expectedEvent, + expect.anything(), + expect.anything(), + ); }); it("failure passes the event with resolved facts", async () => { - const failureSpy = sandbox.spy(); + const failureSpy = vi.fn(); engine.on("failure", failureSpy); const { failureResults } = await engine.run({ success: false }); - expect(failureResults[0].event).to.deep.equal(expectedEvent); - expect(failureSpy.firstCall.args[0]).to.deep.equal(expectedEvent); + expect(failureResults[0].event).toEqual(expectedEvent); + expect(failureSpy).toHaveBeenCalledWith( + expectedEvent, + expect.anything(), + expect.anything(), + ); }); }); }); }); - context("rule events: json serializing", () => { + describe("rule events: json serializing", () => { beforeEach(() => simpleSetup()); it("serializes properties", async () => { - const successSpy = sandbox.spy(); + const successSpy = vi.fn(); const rule = engine.rules[0]; rule.on("success", successSpy); await engine.run(); - const ruleResult = successSpy.getCall(0).args[2]; + const ruleResult = successSpy.mock.calls[0][2]; const expected = '{"conditions":{"priority":1,"any":[{"name":"over 21","operator":"greaterThanInclusive","value":21,"fact":"age","factResult":21,"result":true},{"operator":"equal","value":true,"fact":"qualified","factResult":false,"result":false}]},"event":{"type":"setDrinkingFlag","params":{"canOrderDrinks":true}},"priority":100,"result":true}'; - expect(JSON.stringify(ruleResult)).to.equal(expected); + expect(JSON.stringify(ruleResult)).toBe(expected); }); }); }); diff --git a/test/engine-fact-comparison.test.js b/test/engine-fact-comparison.test.mjs similarity index 75% rename from test/engine-fact-comparison.test.js rename to test/engine-fact-comparison.test.mjs index 92fe5457..585f33c4 100644 --- a/test/engine-fact-comparison.test.js +++ b/test/engine-fact-comparison.test.mjs @@ -1,29 +1,23 @@ -"use strict"; +import engineFactory from "../src/index.mjs"; -import engineFactory from "../src/index"; -import sinon from "sinon"; +import { describe, it, expect, vi } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; describe("Engine: fact to fact comparison", () => { let engine; - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); + let eventSpy; function setup(conditions) { const event = { type: "success-event" }; - eventSpy = sandbox.spy(); + eventSpy = vi.fn(); engine = engineFactory(); - const rule = factories.rule({ conditions, event }); + const rule = ruleFactory({ conditions, event }); engine.addRule(rule); engine.on("success", eventSpy); } - context("constant facts", () => { + describe("constant facts", () => { const constantCondition = { all: [ { @@ -38,16 +32,16 @@ describe("Engine: fact to fact comparison", () => { it("allows a fact to retrieve other fact values", async () => { setup(constantCondition); await engine.run({ height: 1, width: 2 }); - expect(eventSpy).to.have.been.calledOnce(); + expect(eventSpy).toHaveBeenCalledOnce(); - sandbox.reset(); + eventSpy.mockReset(); await engine.run({ height: 2, width: 1 }); // negative case - expect(eventSpy.callCount).to.equal(0); + expect(eventSpy).not.toHaveBeenCalled(); }); }); - context("rules with parameterized conditions", () => { + describe("rules with parameterized conditions", () => { const paramsCondition = { all: [ { @@ -76,16 +70,16 @@ describe("Engine: fact to fact comparison", () => { return params.multiplier * width; }); await engine.run({ height: 5, width: 10 }); - expect(eventSpy).to.have.been.calledOnce(); + expect(eventSpy).toHaveBeenCalledOnce(); - sandbox.reset(); + eventSpy.mockReset(); await engine.run({ height: 5, width: 9 }); // negative case - expect(eventSpy.callCount).to.equal(0); + expect(eventSpy).not.toHaveBeenCalled(); }); }); - context("rules with parameterized conditions and path values", () => { + describe("rules with parameterized conditions and path values", () => { const pathCondition = { all: [ { @@ -116,12 +110,12 @@ describe("Engine: fact to fact comparison", () => { return { feet: params.multiplier * width }; }); await engine.run({ height: 5, width: 10 }); - expect(eventSpy).to.have.been.calledOnce(); + expect(eventSpy).toHaveBeenCalledOnce(); - sandbox.reset(); + eventSpy.mockReset(); await engine.run({ height: 5, width: 9 }); // negative case - expect(eventSpy.callCount).to.equal(0); + expect(eventSpy).not.toHaveBeenCalled(); }); }); }); diff --git a/test/engine-fact-priority.test.js b/test/engine-fact-priority.test.mjs similarity index 58% rename from test/engine-fact-priority.test.js rename to test/engine-fact-priority.test.mjs index a8dfe60f..ffb2e692 100644 --- a/test/engine-fact-priority.test.js +++ b/test/engine-fact-priority.test.mjs @@ -1,17 +1,11 @@ -"use strict"; +import engineFactory from "../src/index.mjs"; -import engineFactory from "../src/index"; -import sinon from "sinon"; +import { describe, it, expect, vi } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; describe("Engine: fact priority", () => { let engine; - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); + const event = { type: "adult-human-admins" }; let eventSpy; @@ -21,14 +15,14 @@ describe("Engine: fact priority", () => { let accountTypeStub; function setup(conditions) { - ageStub = sandbox.stub(); - segmentStub = sandbox.stub(); - accountTypeStub = sandbox.stub(); - eventSpy = sandbox.stub(); - failureSpy = sandbox.stub(); + ageStub = vi.fn(); + segmentStub = vi.fn(); + accountTypeStub = vi.fn(); + eventSpy = vi.fn(); + failureSpy = vi.fn(); engine = engineFactory(); - const rule = factories.rule({ conditions, event }); + const rule = ruleFactory({ conditions, event }); engine.addRule(rule); engine.addFact("age", ageStub, { priority: 100 }); engine.addFact("segment", segmentStub, { priority: 50 }); @@ -60,25 +54,25 @@ describe("Engine: fact priority", () => { it("stops on the first fact to fail, part 1", async () => { setup(allCondition); - ageStub.returns(10); // fail + ageStub.mockReturnValue(10); // fail await engine.run(); - expect(failureSpy).to.have.been.called(); - expect(eventSpy).to.not.have.been.called(); - expect(ageStub).to.have.been.calledOnce(); - expect(segmentStub).to.not.have.been.called(); - expect(accountTypeStub).to.not.have.been.called(); + expect(failureSpy).toHaveBeenCalled(); + expect(eventSpy).not.toHaveBeenCalled(); + expect(ageStub).toHaveBeenCalledOnce(); + expect(segmentStub).not.toHaveBeenCalled(); + expect(accountTypeStub).not.toHaveBeenCalled(); }); it("stops on the first fact to fail, part 2", async () => { setup(allCondition); - ageStub.returns(20); // pass - segmentStub.returns("android"); // fail + ageStub.mockReturnValue(20); // pass + segmentStub.mockReturnValue("android"); // fail await engine.run(); - expect(failureSpy).to.have.been.called(); - expect(eventSpy).to.not.have.been.called(); - expect(ageStub).to.have.been.calledOnce(); - expect(segmentStub).to.have.been.calledOnce(); - expect(accountTypeStub).to.not.have.been.called(); + expect(failureSpy).toHaveBeenCalled(); + expect(eventSpy).not.toHaveBeenCalled(); + expect(ageStub).toHaveBeenCalledOnce(); + expect(segmentStub).toHaveBeenCalledOnce(); + expect(accountTypeStub).not.toHaveBeenCalled(); }); describe("sub-conditions", () => { @@ -108,14 +102,14 @@ describe("Engine: fact priority", () => { it("stops after the first sub-condition fact fails", async () => { setup(allSubCondition); - ageStub.returns(20); // pass - segmentStub.returns("android"); // fail + ageStub.mockReturnValue(20); // pass + segmentStub.mockReturnValue("android"); // fail await engine.run(); - expect(failureSpy).to.have.been.called(); - expect(eventSpy).to.not.have.been.called(); - expect(ageStub).to.have.been.calledOnce(); - expect(segmentStub).to.have.been.calledOnce(); - expect(accountTypeStub).to.not.have.been.called(); + expect(failureSpy).toHaveBeenCalled(); + expect(eventSpy).not.toHaveBeenCalled(); + expect(ageStub).toHaveBeenCalledOnce(); + expect(segmentStub).toHaveBeenCalledOnce(); + expect(accountTypeStub).not.toHaveBeenCalled(); }); }); }); @@ -142,25 +136,25 @@ describe("Engine: fact priority", () => { }; it("complete on the first fact to succeed, part 1", async () => { setup(anyCondition); - ageStub.returns(20); // succeed + ageStub.mockReturnValue(20); // succeed await engine.run(); - expect(eventSpy).to.have.been.calledOnce(); - expect(failureSpy).to.not.have.been.called(); - expect(ageStub).to.have.been.calledOnce(); - expect(segmentStub).to.not.have.been.called(); - expect(accountTypeStub).to.not.have.been.called(); + expect(eventSpy).toHaveBeenCalledOnce(); + expect(failureSpy).not.toHaveBeenCalled(); + expect(ageStub).toHaveBeenCalledOnce(); + expect(segmentStub).not.toHaveBeenCalled(); + expect(accountTypeStub).not.toHaveBeenCalled(); }); it("short circuits on the first fact to fail, part 2", async () => { setup(anyCondition); - ageStub.returns(10); // fail - segmentStub.returns("human"); // pass + ageStub.mockReturnValue(10); // fail + segmentStub.mockReturnValue("human"); // pass await engine.run(); - expect(eventSpy).to.have.been.calledOnce(); - expect(failureSpy).to.not.have.been.called(); - expect(ageStub).to.have.been.calledOnce(); - expect(segmentStub).to.have.been.calledOnce(); - expect(accountTypeStub).to.not.have.been.called(); + expect(eventSpy).toHaveBeenCalledOnce(); + expect(failureSpy).not.toHaveBeenCalled(); + expect(ageStub).toHaveBeenCalledOnce(); + expect(segmentStub).toHaveBeenCalledOnce(); + expect(accountTypeStub).not.toHaveBeenCalled(); }); describe("sub-conditions", () => { @@ -190,14 +184,14 @@ describe("Engine: fact priority", () => { it("stops after the first sub-condition fact succeeds", async () => { setup(anySubCondition); - ageStub.returns(20); // success - segmentStub.returns("human"); // success + ageStub.mockReturnValue(20); // success + segmentStub.mockReturnValue("human"); // success await engine.run(); - expect(failureSpy).to.not.have.been.called(); - expect(eventSpy).to.have.been.called(); - expect(ageStub).to.have.been.calledOnce(); - expect(segmentStub).to.have.been.calledOnce(); - expect(accountTypeStub).to.not.have.been.called(); + expect(failureSpy).not.toHaveBeenCalled(); + expect(eventSpy).toHaveBeenCalled(); + expect(ageStub).toHaveBeenCalledOnce(); + expect(segmentStub).toHaveBeenCalledOnce(); + expect(accountTypeStub).not.toHaveBeenCalled(); }); }); }); diff --git a/test/engine-fact.test.js b/test/engine-fact.test.mjs similarity index 67% rename from test/engine-fact.test.js rename to test/engine-fact.test.mjs index 61c78699..22e94d00 100644 --- a/test/engine-fact.test.js +++ b/test/engine-fact.test.mjs @@ -1,13 +1,12 @@ -"use strict"; - -import sinon from "sinon"; import { get } from "lodash"; -import engineFactory from "../src/index"; +import engineFactory from "../src/index.mjs"; +import { describe, it, beforeEach, expect, vi } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; const CHILD = 14; const ADULT = 75; -async function eligibilityField(params, engine) { +async function eligibilityField(params) { if (params.field === "age") { if (params.eligibilityId === 1) { return CHILD; @@ -16,7 +15,7 @@ async function eligibilityField(params, engine) { } } -async function eligibilityData(params, engine) { +async function eligibilityData(params) { const address = { street: "123 Fake Street", state: { @@ -39,13 +38,7 @@ async function eligibilityData(params, engine) { describe("Engine: fact evaluation", () => { let engine; - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); + const event = { type: "ageTrigger", params: { @@ -70,13 +63,13 @@ describe("Engine: fact evaluation", () => { let successSpy; let failureSpy; beforeEach(() => { - successSpy = sandbox.spy(); - failureSpy = sandbox.spy(); + successSpy = vi.fn(); + failureSpy = vi.fn(); }); function setup(conditions = baseConditions(), engineOptions = {}) { engine = engineFactory([], engineOptions); - const rule = factories.rule({ conditions, event }); + const rule = ruleFactory({ conditions, event }); engine.addRule(rule); engine.addFact("eligibilityField", eligibilityField); engine.addFact("eligibilityData", eligibilityData); @@ -94,42 +87,39 @@ describe("Engine: fact evaluation", () => { value: true, }); setup(conditions); - return expect(engine.run()).to.be.rejectedWith( + return expect(engine.run()).rejects.toThrow( /Undefined fact: undefined-fact/, ); }); - context( - "treats undefined facts as falsey when allowUndefinedFacts is set", - () => { - it('emits "success" when the condition succeeds', async () => { - const conditions = Object.assign({}, baseConditions()); - conditions.any.push({ - fact: "undefined-fact", - operator: "equal", - value: true, - }); - setup(conditions, { allowUndefinedFacts: true }); - await engine.run(); - expect(successSpy).to.have.been.called(); - expect(failureSpy).to.not.have.been.called(); + describe("treats undefined facts as falsey when allowUndefinedFacts is set", () => { + it('emits "success" when the condition succeeds', async () => { + const conditions = Object.assign({}, baseConditions()); + conditions.any.push({ + fact: "undefined-fact", + operator: "equal", + value: true, }); + setup(conditions, { allowUndefinedFacts: true }); + await engine.run(); + expect(successSpy).toHaveBeenCalled(); + expect(failureSpy).not.toHaveBeenCalled(); + }); - it('emits "failure" when the condition fails', async () => { - const conditions = Object.assign({}, baseConditions()); - conditions.any.push({ - fact: "undefined-fact", - operator: "equal", - value: true, - }); - conditions.any[0].params.eligibilityId = 2; - setup(conditions, { allowUndefinedFacts: true }); - await engine.run(); - expect(successSpy).to.not.have.been.called(); - expect(failureSpy).to.have.been.called(); + it('emits "failure" when the condition fails', async () => { + const conditions = Object.assign({}, baseConditions()); + conditions.any.push({ + fact: "undefined-fact", + operator: "equal", + value: true, }); - }, - ); + conditions.any[0].params.eligibilityId = 2; + setup(conditions, { allowUndefinedFacts: true }); + await engine.run(); + expect(successSpy).not.toHaveBeenCalled(); + expect(failureSpy).toHaveBeenCalled(); + }); + }); }); }); @@ -137,7 +127,11 @@ describe("Engine: fact evaluation", () => { it("emits when the condition is met", async () => { setup(); await engine.run(); - expect(successSpy).to.have.been.calledWith(event); + expect(successSpy).toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); it("does not emit when the condition fails", async () => { @@ -145,7 +139,7 @@ describe("Engine: fact evaluation", () => { conditions.any[0].params.eligibilityId = 2; setup(conditions); await engine.run(); - expect(successSpy).to.not.have.been.called(); + expect(successSpy).not.toHaveBeenCalled(); }); }); @@ -168,7 +162,11 @@ describe("Engine: fact evaluation", () => { it("emits when the condition is met", async () => { setup(conditions()); await engine.run(); - expect(successSpy).to.have.been.calledWith(event); + expect(successSpy).toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); it("does not emit when the condition fails", async () => { @@ -176,7 +174,7 @@ describe("Engine: fact evaluation", () => { failureCondition.any[0].params.eligibilityId = 2; setup(failureCondition); await engine.run(); - expect(successSpy).to.not.have.been.called(); + expect(successSpy).not.toHaveBeenCalled(); }); describe("arrays", () => { @@ -187,7 +185,11 @@ describe("Engine: fact evaluation", () => { complexCondition.any[0].operator = "contains"; setup(complexCondition); await engine.run(); - expect(successSpy).to.have.been.calledWith(event); + expect(successSpy).toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); it("can extract an array with a single element", async () => { @@ -197,11 +199,15 @@ describe("Engine: fact evaluation", () => { complexCondition.any[0].operator = "contains"; setup(complexCondition); await engine.run(); - expect(successSpy).to.have.been.calledWith(event); + expect(successSpy).toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); }); - context("complex paths", () => { + describe("complex paths", () => { it('correctly interprets "path" when dynamic facts return objects', async () => { const complexCondition = conditions(); complexCondition.any[0].path = "$.address.occupantHistory[0].year"; @@ -209,7 +215,11 @@ describe("Engine: fact evaluation", () => { complexCondition.any[0].operator = "equal"; setup(complexCondition); await engine.run(); - expect(successSpy).to.have.been.calledWith(event); + expect(successSpy).toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); it('correctly interprets "path" when target object properties have dots', async () => { @@ -219,7 +229,11 @@ describe("Engine: fact evaluation", () => { complexCondition.any[0].operator = "equal"; setup(complexCondition); await engine.run(); - expect(successSpy).to.have.been.calledWith(event); + expect(successSpy).toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); it('correctly interprets "path" with runtime fact objects', async () => { @@ -236,13 +250,21 @@ describe("Engine: fact evaluation", () => { }; engine = engineFactory([]); - const rule = factories.rule({ conditions, event }); + const rule = ruleFactory({ conditions, event }); engine.addRule(rule); engine.on("success", successSpy); engine.on("failure", failureSpy); await engine.run(fact); - expect(successSpy).to.have.been.calledWith(event); - expect(failureSpy).to.not.have.been.calledWith(event); + expect(successSpy).toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); + expect(failureSpy).not.toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); }); @@ -253,7 +275,11 @@ describe("Engine: fact evaluation", () => { complexCondition.any[0].operator = "equal"; setup(complexCondition); await engine.run(); - expect(successSpy).to.not.have.been.calledWith(event); + expect(successSpy).not.toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); it("treats invalid object paths as undefined", async () => { @@ -263,17 +289,25 @@ describe("Engine: fact evaluation", () => { complexCondition.any[0].operator = "equal"; setup(complexCondition); await engine.run(); - expect(successSpy).to.have.been.calledWith(event); + expect(successSpy).toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); it('ignores "path" when facts return non-objects', async () => { setup(conditions()); - const eligibilityData = async (params, engine) => { + const eligibilityData = async () => { return CHILD; }; engine.addFact("eligibilityData", eligibilityData); await engine.run(); - expect(successSpy).to.have.been.calledWith(event); + expect(successSpy).toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); describe("pathResolver", () => { @@ -294,15 +328,19 @@ describe("Engine: fact evaluation", () => { }; engine = engineFactory([], { pathResolver }); - const rule = factories.rule({ conditions, event }); + const rule = ruleFactory({ conditions, event }); engine.addRule(rule); engine.on("success", successSpy); engine.on("failure", failureSpy); await engine.run(fact); - expect(successSpy).to.have.been.calledWith(event); - expect(failureSpy).to.not.have.been.called(); + expect(successSpy).toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); + expect(failureSpy).not.toHaveBeenCalled(); }); }); }); @@ -310,8 +348,8 @@ describe("Engine: fact evaluation", () => { describe("promises", () => { it("works with asynchronous evaluations", async () => { setup(); - const eligibilityField = function (params, engine) { - return new Promise((resolve, reject) => { + const eligibilityField = function () { + return new Promise((resolve) => { setImmediate(() => { resolve(30); }); @@ -319,29 +357,29 @@ describe("Engine: fact evaluation", () => { }; engine.addFact("eligibilityField", eligibilityField); await engine.run(); - expect(successSpy).to.have.been.called(); + expect(successSpy).toHaveBeenCalled(); }); }); describe("synchronous functions", () => { it("works with synchronous, non-promise evaluations that are truthy", async () => { setup(); - const eligibilityField = function (params, engine) { + const eligibilityField = function () { return 20; }; engine.addFact("eligibilityField", eligibilityField); await engine.run(); - expect(successSpy).to.have.been.called(); + expect(successSpy).toHaveBeenCalled(); }); it("works with synchronous, non-promise evaluations that are falsey", async () => { setup(); - const eligibilityField = function (params, engine) { + const eligibilityField = function () { return 100; }; engine.addFact("eligibilityField", eligibilityField); await engine.run(); - expect(successSpy).to.not.have.been.called(); + expect(successSpy).not.toHaveBeenCalled(); }); }); }); diff --git a/test/engine-facts-calling-facts.test.js b/test/engine-facts-calling-facts.test.mjs similarity index 66% rename from test/engine-facts-calling-facts.test.js rename to test/engine-facts-calling-facts.test.mjs index 7007e611..631fdf8e 100644 --- a/test/engine-facts-calling-facts.test.js +++ b/test/engine-facts-calling-facts.test.mjs @@ -1,17 +1,11 @@ -"use strict"; +import engineFactory, { Fact } from "../src/index.mjs"; -import engineFactory, { Fact } from "../src/index"; -import sinon from "sinon"; +import { describe, it, beforeEach, expect, vi } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; describe("Engine: custom cache keys", () => { let engine; - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); + const event = { type: "early-twenties" }; const conditions = { all: [ @@ -38,11 +32,11 @@ describe("Engine: custom cache keys", () => { let demographicDataSpy; let demographicSpy; beforeEach(() => { - demographicSpy = sandbox.spy(); - demographicDataSpy = sandbox.spy(); - eventSpy = sandbox.spy(); + demographicSpy = vi.fn(); + demographicDataSpy = vi.fn(); + eventSpy = vi.fn(); - const demographicsDataDefinition = async (params, engine) => { + const demographicsDataDefinition = async () => { demographicDataSpy(); return { age: 20, @@ -62,7 +56,7 @@ describe("Engine: custom cache keys", () => { ); engine = engineFactory(); - const rule = factories.rule({ conditions, event }); + const rule = ruleFactory({ conditions, event }); engine.addRule(rule); engine.addFact(demographicsFact); engine.addFact(demographicsDataFact); @@ -72,9 +66,9 @@ describe("Engine: custom cache keys", () => { describe("1 rule", () => { it("allows a fact to retrieve other fact values", async () => { await engine.run(); - expect(eventSpy).to.have.been.calledOnce(); - expect(demographicDataSpy).to.have.been.calledOnce(); - expect(demographicSpy).to.have.been.calledTwice(); + expect(eventSpy).toHaveBeenCalledOnce(); + expect(demographicDataSpy).toHaveBeenCalledOnce(); + expect(demographicSpy).toHaveBeenCalledTimes(2); }); }); @@ -92,14 +86,14 @@ describe("Engine: custom cache keys", () => { }, ], }; - const rule = factories.rule({ conditions, event }); + const rule = ruleFactory({ conditions, event }); engine.addRule(rule); await engine.run(); - expect(eventSpy).to.have.been.calledTwice(); - expect(demographicDataSpy).to.have.been.calledOnce(); - expect(demographicSpy).to.have.been.calledTwice(); - expect(demographicDataSpy).to.have.been.calledOnce(); + expect(eventSpy).toHaveBeenCalledTimes(2); + expect(demographicDataSpy).toHaveBeenCalledOnce(); + expect(demographicSpy).toHaveBeenCalledTimes(2); + expect(demographicDataSpy).toHaveBeenCalledOnce(); }); }); }); diff --git a/test/engine-failure.test.js b/test/engine-failure.test.mjs similarity index 57% rename from test/engine-failure.test.js rename to test/engine-failure.test.mjs index 28ee69be..934d4198 100644 --- a/test/engine-failure.test.js +++ b/test/engine-failure.test.mjs @@ -1,17 +1,10 @@ -"use strict"; +import engineFactory from "../src/index.mjs"; -import engineFactory from "../src/index"; -import sinon from "sinon"; +import { describe, it, beforeEach, expect, vi } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; describe("Engine: failure", () => { let engine; - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); const event = { type: "generic" }; const conditions = { @@ -25,23 +18,27 @@ describe("Engine: failure", () => { }; beforeEach(() => { engine = engineFactory(); - const determineDrinkingAgeRule = factories.rule({ conditions, event }); + const determineDrinkingAgeRule = ruleFactory({ conditions, event }); engine.addRule(determineDrinkingAgeRule); engine.addFact("age", 10); }); it("emits an event on a rule failing", async () => { - const failureSpy = sandbox.spy(); + const failureSpy = vi.fn(); engine.on("failure", failureSpy); await engine.run(); - expect(failureSpy).to.have.been.calledWith(engine.rules[0].ruleEvent); + expect(failureSpy).toHaveBeenCalledWith( + engine.rules[0].ruleEvent, + expect.anything(), + expect.anything(), + ); }); it("does not emit when a rule passes", async () => { - const failureSpy = sandbox.spy(); + const failureSpy = vi.fn(); engine.on("failure", failureSpy); engine.addFact("age", 50); await engine.run(); - expect(failureSpy).to.not.have.been.calledOnce(); + expect(failureSpy).not.toHaveBeenCalledOnce(); }); }); diff --git a/test/engine-not.test.js b/test/engine-not.test.mjs similarity index 56% rename from test/engine-not.test.js rename to test/engine-not.test.mjs index 6719b5ea..06ee39a1 100644 --- a/test/engine-not.test.js +++ b/test/engine-not.test.mjs @@ -1,17 +1,9 @@ -"use strict"; - -import sinon from "sinon"; -import engineFactory from "../src/index"; +import engineFactory from "../src/index.mjs"; +import { describe, it, beforeEach, expect, vi } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; describe('Engine: "not" conditions', () => { let engine; - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); describe('supports a single "not" condition', () => { const event = { @@ -30,9 +22,9 @@ describe('Engine: "not" conditions', () => { let eventSpy; let ageSpy; beforeEach(() => { - eventSpy = sandbox.spy(); - ageSpy = sandbox.stub(); - const rule = factories.rule({ conditions, event }); + eventSpy = vi.fn(); + ageSpy = vi.fn(); + const rule = ruleFactory({ conditions, event }); engine = engineFactory(); engine.addRule(rule); engine.addFact("age", ageSpy); @@ -40,15 +32,23 @@ describe('Engine: "not" conditions', () => { }); it("emits when the condition is met", async () => { - ageSpy.returns(10); + ageSpy.mockReturnValue(10); await engine.run(); - expect(eventSpy).to.have.been.calledWith(event); + expect(eventSpy).toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); it("does not emit when the condition fails", () => { - ageSpy.returns(75); + ageSpy.mockReturnValue(75); engine.run(); - expect(eventSpy).to.not.have.been.calledWith(event); + expect(eventSpy).not.toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); }); }); diff --git a/test/engine-operator-map.test.js b/test/engine-operator-map.test.mjs similarity index 82% rename from test/engine-operator-map.test.js rename to test/engine-operator-map.test.mjs index 3ae06d96..c4aeb2a5 100644 --- a/test/engine-operator-map.test.js +++ b/test/engine-operator-map.test.mjs @@ -1,7 +1,5 @@ -"use strict"; - -import { expect } from "chai"; -import engineFactory, { Operator, OperatorDecorator } from "../src/index"; +import engineFactory, { Operator, OperatorDecorator } from "../src/index.mjs"; +import { describe, it, beforeEach, expect } from "vitest"; const startsWithLetter = new Operator( "startsWithLetter", @@ -27,17 +25,17 @@ describe("Engine Operator Map", () => { }); it("has the operator", () => { - expect(op).not.to.be.null(); + expect(op).not.toBeNull(); }); it("the operator evaluates correctly", () => { - expect(op.evaluate("test", "t")).to.be.true(); + expect(op.evaluate("test", "t")).toBe(true); }); it("after being removed the operator is null", () => { engine.operators.removeOperator(startsWithLetter); op = engine.operators.get("startsWithLetter"); - expect(op).to.be.null(); + expect(op).toBeNull(); }); }); @@ -48,23 +46,23 @@ describe("Engine Operator Map", () => { }); it("has the operator", () => { - expect(op).not.to.be.null(); + expect(op).not.toBeNull(); }); it("the operator evaluates correctly", () => { - expect(op.evaluate("test", "t")).to.be.false(); + expect(op.evaluate("test", "t")).toBe(false); }); it("removing the base operator removes the decorated version", () => { engine.operators.removeOperator(startsWithLetter); op = engine.operators.get("never:startsWithLetter"); - expect(op).to.be.null(); + expect(op).toBeNull(); }); it("removing the decorator removes the decorated operator", () => { engine.operators.removeOperatorDecorator(never); op = engine.operators.get("never:startsWithLetter"); - expect(op).to.be.null(); + expect(op).toBeNull(); }); }); @@ -77,7 +75,7 @@ describe("Engine Operator Map", () => { const op = engine.operators.get( "everyFact:someValue:not:greaterThanInclusive", ); - expect(op.evaluate(odds, evens)).to.be.true(); + expect(op.evaluate(odds, evens)).toBe(true); }); }); @@ -86,6 +84,6 @@ describe("Engine Operator Map", () => { const jsonValue = [1, 2, 3]; const op = engine.operators.get("swap:contains"); - expect(op.evaluate(factValue, jsonValue)).to.be.true(); + expect(op.evaluate(factValue, jsonValue)).toBe(true); }); }); diff --git a/test/engine-operator.test.js b/test/engine-operator.test.mjs similarity index 75% rename from test/engine-operator.test.js rename to test/engine-operator.test.mjs index 31a647ed..1e200a49 100644 --- a/test/engine-operator.test.js +++ b/test/engine-operator.test.mjs @@ -1,21 +1,13 @@ -"use strict"; +import engineFactory from "../src/index.mjs"; +import { describe, it, expect, vi } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; -import sinon from "sinon"; -import engineFactory from "../src/index"; - -async function dictionary(params, engine) { +async function dictionary(params) { const words = ["coffee", "Aardvark", "moose", "ladder", "antelope"]; return words[params.wordIndex]; } describe("Engine: operator", () => { - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); const event = { type: "operatorTrigger", }; @@ -33,9 +25,9 @@ describe("Engine: operator", () => { }; let eventSpy; function setup(conditions = baseConditions) { - eventSpy = sandbox.spy(); + eventSpy = vi.fn(); const engine = engineFactory(); - const rule = factories.rule({ conditions, event }); + const rule = ruleFactory({ conditions, event }); engine.addRule(rule); engine.addOperator("startsWithLetter", (factValue, jsonValue) => { if (!factValue.length) return false; @@ -52,7 +44,11 @@ describe("Engine: operator", () => { conditions.any[0].params.wordIndex = 1; const engine = setup(); await engine.run(); - expect(eventSpy).to.have.been.calledWith(event); + expect(eventSpy).toHaveBeenCalledWith( + event, + expect.anything(), + expect.anything(), + ); }); it("does not emit when the condition fails", async () => { @@ -60,14 +56,14 @@ describe("Engine: operator", () => { conditions.any[0].params.wordIndex = 0; const engine = setup(); await engine.run(); - expect(eventSpy).to.not.have.been.calledWith(event); + expect(eventSpy).not.toHaveBeenCalled(); }); it("throws when it encounters an unregistered operator", async () => { const conditions = Object.assign({}, baseConditions); conditions.any[0].operator = "unknown-operator"; const engine = setup(); - return expect(engine.run()).to.eventually.be.rejectedWith( + return expect(engine.run()).rejects.toThrow( "Unknown operator: unknown-operator", ); }); diff --git a/test/engine-parallel-condition-cache.test.js b/test/engine-parallel-condition-cache.test.mjs similarity index 67% rename from test/engine-parallel-condition-cache.test.js rename to test/engine-parallel-condition-cache.test.mjs index cb96a09a..0c9ae8f7 100644 --- a/test/engine-parallel-condition-cache.test.js +++ b/test/engine-parallel-condition-cache.test.mjs @@ -1,17 +1,11 @@ -"use strict"; +import engineFactory from "../src/index.mjs"; -import engineFactory from "../src/index"; -import sinon from "sinon"; +import { describe, it, expect, vi } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; describe("Engine", () => { let engine; - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); + const event = { type: "early-twenties" }; const conditions = { all: [ @@ -36,8 +30,8 @@ describe("Engine", () => { let eventSpy; let factSpy; function setup(factOptions) { - factSpy = sandbox.spy(); - eventSpy = sandbox.spy(); + factSpy = vi.fn(); + eventSpy = vi.fn(); const factDefinition = () => { factSpy(); @@ -45,7 +39,7 @@ describe("Engine", () => { }; engine = engineFactory(); - const rule = factories.rule({ conditions, event }); + const rule = ruleFactory({ conditions, event }); engine.addRule(rule); engine.addFact("age", factDefinition, factOptions); engine.on("success", eventSpy); @@ -55,15 +49,15 @@ describe("Engine", () => { it("calls the fact definition once for each condition if caching is off", async () => { setup({ cache: false }); await engine.run(); - expect(eventSpy).to.have.been.calledOnce(); - expect(factSpy).to.have.been.calledThrice(); + expect(eventSpy).toHaveBeenCalledOnce(); + expect(factSpy).toHaveBeenCalledTimes(3); }); it("calls the fact definition once", async () => { setup(); await engine.run(); - expect(eventSpy).to.have.been.calledOnce(); - expect(factSpy).to.have.been.calledOnce(); + expect(eventSpy).toHaveBeenCalledOnce(); + expect(factSpy).toHaveBeenCalledOnce(); }); }); @@ -79,12 +73,12 @@ describe("Engine", () => { }, ], }; - const rule = factories.rule({ conditions, event }); + const rule = ruleFactory({ conditions, event }); engine.addRule(rule); await engine.run(); - expect(eventSpy).to.have.been.calledTwice(); - expect(factSpy).to.have.been.calledOnce(); + expect(eventSpy).toHaveBeenCalledTimes(2); + expect(factSpy).toHaveBeenCalledOnce(); }); }); }); diff --git a/test/engine-recusive-rules.test.js b/test/engine-recusive-rules.test.mjs similarity index 85% rename from test/engine-recusive-rules.test.js rename to test/engine-recusive-rules.test.mjs index 86c2892b..cb744c32 100644 --- a/test/engine-recusive-rules.test.js +++ b/test/engine-recusive-rules.test.mjs @@ -1,7 +1,7 @@ -"use strict"; +import engineFactory from "../src/index.mjs"; -import engineFactory from "../src/index"; -import sinon from "sinon"; +import { describe, it, expect, vi } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; describe("Engine: recursive rules", () => { let engine; @@ -35,20 +35,12 @@ describe("Engine: recursive rules", () => { ], }; - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); - let eventSpy; function setup(conditions = nestedAnyCondition) { - eventSpy = sandbox.spy(); + eventSpy = vi.fn(); engine = engineFactory(); - const rule = factories.rule({ conditions, event }); + const rule = ruleFactory({ conditions, event }); engine.addRule(rule); engine.on("success", eventSpy); } @@ -60,7 +52,7 @@ describe("Engine: recursive rules", () => { engine.addFact("income", 30); engine.addFact("family-size", 2); await engine.run(); - expect(eventSpy).to.have.been.calledOnce(); + expect(eventSpy).toHaveBeenCalledOnce(); }); it("evaluates false when facts do not pass rules", async () => { @@ -69,7 +61,7 @@ describe("Engine: recursive rules", () => { engine.addFact("income", 200); engine.addFact("family-size", 8); await engine.run(); - expect(eventSpy).to.not.have.been.calledOnce(); + expect(eventSpy).not.toHaveBeenCalledOnce(); }); }); @@ -109,7 +101,7 @@ describe("Engine: recursive rules", () => { engine.addFact("income", 30); engine.addFact("family-size", 2); await engine.run(); - expect(eventSpy).to.have.been.calledOnce(); + expect(eventSpy).toHaveBeenCalledOnce(); }); it("evaluates false when facts do not pass rules", async () => { @@ -118,7 +110,7 @@ describe("Engine: recursive rules", () => { engine.addFact("income", 200); engine.addFact("family-size", 2); await engine.run(); - expect(eventSpy).to.not.have.been.calledOnce(); + expect(eventSpy).not.toHaveBeenCalledOnce(); }); }); @@ -151,7 +143,7 @@ describe("Engine: recursive rules", () => { engine.addFact("income", 30); engine.addFact("family-size", 1); await engine.run(); - expect(eventSpy).to.have.been.calledOnce(); + expect(eventSpy).toHaveBeenCalledOnce(); }); it("evaluates false when facts do not pass rules", async () => { @@ -159,7 +151,7 @@ describe("Engine: recursive rules", () => { engine.addFact("income", 30); engine.addFact("family-size", 5); await engine.run(); - expect(eventSpy).to.not.have.been.calledOnce(); + expect(eventSpy).not.toHaveBeenCalledOnce(); }); }); @@ -178,14 +170,14 @@ describe("Engine: recursive rules", () => { setup(notNotCondition); engine.addFact("age", 30); await engine.run(); - expect(eventSpy).to.have.been.calledOnce(); + expect(eventSpy).toHaveBeenCalledOnce(); }); it("evaluates false when facts do not pass rules", async () => { setup(notNotCondition); engine.addFact("age", 65); await engine.run(); - expect(eventSpy).to.not.have.been.calledOnce(); + expect(eventSpy).not.toHaveBeenCalledOnce(); }); }); @@ -219,7 +211,7 @@ describe("Engine: recursive rules", () => { engine.addFact("age", 30); engine.addFact("income", 100); await engine.run(); - expect(eventSpy).to.have.been.calledOnce(); + expect(eventSpy).toHaveBeenCalledOnce(); }); it("evaluates false when facts do not pass rules", async () => { @@ -227,7 +219,7 @@ describe("Engine: recursive rules", () => { engine.addFact("age", 30); engine.addFact("income", 101); await engine.run(); - expect(eventSpy).to.not.have.been.calledOnce(); + expect(eventSpy).not.toHaveBeenCalledOnce(); }); }); }); diff --git a/test/engine-rule-priority.js b/test/engine-rule-priority.test.mjs similarity index 60% rename from test/engine-rule-priority.js rename to test/engine-rule-priority.test.mjs index c55ca2f8..c8372c91 100644 --- a/test/engine-rule-priority.js +++ b/test/engine-rule-priority.test.mjs @@ -1,7 +1,7 @@ -"use strict"; +import engineFactory from "../src/index.mjs"; -import engineFactory from "../src/index"; -import sinon from "sinon"; +import { describe, it, expect, vi } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; describe("Engine: rule priorities", () => { let engine; @@ -19,34 +19,26 @@ describe("Engine: rule priorities", () => { ], }; - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); - function setup() { - const factSpy = sandbox.stub().returns(22); - const eventSpy = sandbox.spy(); + const factSpy = vi.fn().mockReturnValue(22); + const eventSpy = vi.fn(); engine = engineFactory(); - const highPriorityRule = factories.rule({ + const highPriorityRule = ruleFactory({ conditions, event: midPriorityEvent, priority: 50, }); engine.addRule(highPriorityRule); - const midPriorityRule = factories.rule({ + const midPriorityRule = ruleFactory({ conditions, event: highPriorityEvent, priority: 100, }); engine.addRule(midPriorityRule); - const lowPriorityRule = factories.rule({ + const lowPriorityRule = ruleFactory({ conditions, event: lowestPriorityEvent, priority: 1, @@ -59,27 +51,27 @@ describe("Engine: rule priorities", () => { it("runs the rules in order of priority", () => { setup(); - expect(engine.prioritizedRules).to.be.null(); + expect(engine.prioritizedRules).toBeNull(); engine.prioritizeRules(); - expect(engine.prioritizedRules.length).to.equal(3); - expect(engine.prioritizedRules[0][0].priority).to.equal(100); - expect(engine.prioritizedRules[1][0].priority).to.equal(50); - expect(engine.prioritizedRules[2][0].priority).to.equal(1); + expect(engine.prioritizedRules.length).toBe(3); + expect(engine.prioritizedRules[0][0].priority).toBe(100); + expect(engine.prioritizedRules[1][0].priority).toBe(50); + expect(engine.prioritizedRules[2][0].priority).toBe(1); }); it("clears re-propriorizes the rules when a new Rule is added", () => { engine.prioritizeRules(); - expect(engine.prioritizedRules.length).to.equal(3); - engine.addRule(factories.rule()); - expect(engine.prioritizedRules).to.be.null(); + expect(engine.prioritizedRules.length).toBe(3); + engine.addRule(ruleFactory()); + expect(engine.prioritizedRules).toBeNull(); }); it("resolves all events returning promises before executing the next rule", async () => { setup(); - const highPrioritySpy = sandbox.spy(); - const midPrioritySpy = sandbox.spy(); - const lowPrioritySpy = sandbox.spy(); + const highPrioritySpy = vi.fn(); + const midPrioritySpy = vi.fn(); + const lowPrioritySpy = vi.fn(); engine.on(highPriorityEvent.type, () => { return new Promise(function (resolve) { @@ -104,7 +96,11 @@ describe("Engine: rule priorities", () => { await engine.run(); - expect(highPrioritySpy).to.be.calledBefore(midPrioritySpy); - expect(midPrioritySpy).to.be.calledBefore(lowPrioritySpy); + expect(Math.min(...highPrioritySpy.mock.invocationCallOrder)).toBeLessThan( + Math.min(...midPrioritySpy.mock.invocationCallOrder), + ); + expect(Math.min(...midPrioritySpy.mock.invocationCallOrder)).toBeLessThan( + Math.min(...lowPrioritySpy.mock.invocationCallOrder), + ); }); }); diff --git a/test/engine-run.test.js b/test/engine-run.test.mjs similarity index 71% rename from test/engine-run.test.js rename to test/engine-run.test.mjs index c8b57999..b2f5064c 100644 --- a/test/engine-run.test.js +++ b/test/engine-run.test.mjs @@ -1,18 +1,11 @@ -"use strict"; +import engineFactory from "../src/index.mjs"; +import Almanac from "../src/almanac.mjs"; -import engineFactory from "../src/index"; -import Almanac from "../src/almanac"; -import sinon from "sinon"; +import { describe, it, beforeEach, expect, vi } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; describe("Engine: run", () => { let engine, rule, rule2; - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); const condition21 = { any: [ @@ -35,14 +28,14 @@ describe("Engine: run", () => { let eventSpy; beforeEach(() => { - eventSpy = sandbox.spy(); + eventSpy = vi.fn(); engine = engineFactory(); - rule = factories.rule({ + rule = ruleFactory({ conditions: condition21, event: { type: "generic1" }, }); engine.addRule(rule); - rule2 = factories.rule({ + rule2 = ruleFactory({ conditions: condition75, event: { type: "generic2" }, }); @@ -55,45 +48,45 @@ describe("Engine: run", () => { await Promise.all( [50, 10, 12, 30, 14, 15, 25].map((age) => engine.run({ age })), ); - expect(eventSpy).to.have.been.calledThrice(); + expect(eventSpy).toHaveBeenCalledTimes(3); }); it("allows runtime facts to override engine facts for a single run()", async () => { engine.addFact("age", 30); await engine.run({ age: 85 }); // override 'age' with runtime fact - expect(eventSpy).to.have.been.calledTwice(); + expect(eventSpy).toHaveBeenCalledTimes(2); - sandbox.reset(); + eventSpy.mockReset(); await engine.run(); // no runtime fact; revert to age: 30 - expect(eventSpy).to.have.been.calledOnce(); + expect(eventSpy).toHaveBeenCalledOnce(); - sandbox.reset(); + eventSpy.mockReset(); await engine.run({ age: 2 }); // override 'age' with runtime fact - expect(eventSpy.callCount).to.equal(0); + expect(eventSpy).not.toHaveBeenCalled(); }); }); describe("returns", () => { it("activated events", async () => { const { events, failureEvents } = await engine.run({ age: 30 }); - expect(events.length).to.equal(1); - expect(events).to.deep.include(rule.event); - expect(failureEvents.length).to.equal(1); - expect(failureEvents).to.deep.include(rule2.event); + expect(events.length).toBe(1); + expect(events).toContainEqual(rule.event); + expect(failureEvents.length).toBe(1); + expect(failureEvents).toContainEqual(rule2.event); }); it("multiple activated events", () => { return engine.run({ age: 90 }).then((results) => { - expect(results.events.length).to.equal(2); - expect(results.events).to.deep.include(rule.event); - expect(results.events).to.deep.include(rule2.event); + expect(results.events.length).toBe(2); + expect(results.events).toContainEqual(rule.event); + expect(results.events).toContainEqual(rule2.event); }); }); it("does not include unactived triggers", () => { return engine.run({ age: 10 }).then((results) => { - expect(results.events.length).to.equal(0); + expect(results.events.length).toBe(0); }); }); @@ -101,10 +94,10 @@ describe("Engine: run", () => { return engine .run({ age: 10 }) .then((results) => { - expect(results.almanac).to.be.an.instanceOf(Almanac); + expect(results.almanac).toBeInstanceOf(Almanac); return results.almanac.factValue("age"); }) - .then((ageFact) => expect(ageFact).to.equal(10)); + .then((ageFact) => expect(ageFact).toBe(10)); }); }); @@ -129,8 +122,8 @@ describe("Engine: run", () => { ]); }) .then((promiseValues) => { - expect(promiseValues[0]).to.equal(21); - expect(promiseValues[1]).to.equal(75); + expect(promiseValues[0]).toBe(21); + expect(promiseValues[1]).toBe(75); }); }); }); @@ -152,7 +145,7 @@ describe("Engine: run", () => { .run({ greeting: "hello", age: 30 }, { almanac: new CapitalAlmanac() }) .then((results) => { const fact = results.almanac.factValue("greeting"); - return expect(fact).to.eventually.equal("HELLO"); + return expect(fact).resolves.toBe("HELLO"); }); }); }); diff --git a/test/engine.test.js b/test/engine.test.mjs similarity index 50% rename from test/engine.test.js rename to test/engine.test.mjs index 8cf817cb..6dca0219 100644 --- a/test/engine.test.js +++ b/test/engine.test.mjs @@ -1,196 +1,189 @@ -"use strict"; - -import sinon from "sinon"; -import engineFactory, { Fact, Rule, Operator } from "../src/index"; -import defaultOperators from "../src/engine-default-operators"; +import engineFactory, { Fact, Rule, Operator } from "../src/index.mjs"; +import defaultOperators from "../src/engine-default-operators.mjs"; +import { describe, it, beforeEach, expect, vi } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; describe("Engine", () => { let engine; - let sandbox; - before(() => { - sandbox = sinon.createSandbox(); - }); - afterEach(() => { - sandbox.restore(); - }); + beforeEach(() => { engine = engineFactory(); }); it("has methods for managing facts and rules, and running itself", () => { - expect(engine).to.have.property("addRule"); - expect(engine).to.have.property("removeRule"); - expect(engine).to.have.property("addOperator"); - expect(engine).to.have.property("removeOperator"); - expect(engine).to.have.property("addFact"); - expect(engine).to.have.property("removeFact"); - expect(engine).to.have.property("run"); - expect(engine).to.have.property("stop"); + expect(engine).toHaveProperty("addRule"); + expect(engine).toHaveProperty("removeRule"); + expect(engine).toHaveProperty("addOperator"); + expect(engine).toHaveProperty("removeOperator"); + expect(engine).toHaveProperty("addFact"); + expect(engine).toHaveProperty("removeFact"); + expect(engine).toHaveProperty("run"); + expect(engine).toHaveProperty("stop"); }); describe("constructor", () => { it("initializes with the default state", () => { - expect(engine.status).to.equal("READY"); - expect(engine.rules.length).to.equal(0); + expect(engine.status).toBe("READY"); + expect(engine.rules.length).toBe(0); defaultOperators.forEach((op) => { - expect(engine.operators.get(op.name)).to.be.an.instanceof(Operator); + expect(engine.operators.get(op.name)).toBeInstanceOf(Operator); }); }); it("can be initialized with rules", () => { - const rules = [factories.rule(), factories.rule(), factories.rule()]; + const rules = [ruleFactory(), ruleFactory(), ruleFactory()]; engine = engineFactory(rules); - expect(engine.rules.length).to.equal(rules.length); + expect(engine.rules.length).toBe(rules.length); }); }); describe("stop()", () => { it('changes the status to "FINISHED"', () => { - expect(engine.stop().status).to.equal("FINISHED"); + expect(engine.stop().status).toBe("FINISHED"); }); }); describe("addRule()", () => { describe("rule instance", () => { it("adds the rule", () => { - const rule = new Rule(factories.rule()); - expect(engine.rules.length).to.equal(0); + const rule = new Rule(ruleFactory()); + expect(engine.rules.length).toBe(0); engine.addRule(rule); - expect(engine.rules.length).to.equal(1); - expect(engine.rules).to.include(rule); + expect(engine.rules.length).toBe(1); + expect(engine.rules).toContain(rule); }); }); describe("required fields", () => { it(".conditions", () => { - const rule = factories.rule(); + const rule = ruleFactory(); delete rule.conditions; expect(() => { engine.addRule(rule); - }).to.throw( + }).toThrow( /Engine: addRule\(\) argument requires "conditions" property/, ); }); it(".event", () => { - const rule = factories.rule(); + const rule = ruleFactory(); delete rule.event; expect(() => { engine.addRule(rule); - }).to.throw(/Engine: addRule\(\) argument requires "event" property/); + }).toThrow(/Engine: addRule\(\) argument requires "event" property/); }); }); }); describe("updateRule()", () => { it("updates rule", () => { - let rule1 = new Rule(factories.rule({ name: "rule1" })); - let rule2 = new Rule(factories.rule({ name: "rule2" })); + let rule1 = new Rule(ruleFactory({ name: "rule1" })); + let rule2 = new Rule(ruleFactory({ name: "rule2" })); engine.addRule(rule1); engine.addRule(rule2); - expect(engine.rules[0].conditions.all.length).to.equal(2); - expect(engine.rules[1].conditions.all.length).to.equal(2); + expect(engine.rules[0].conditions.all.length).toBe(2); + expect(engine.rules[1].conditions.all.length).toBe(2); rule1.conditions = { all: [] }; engine.updateRule(rule1); rule1 = engine.rules.find((rule) => rule.name === "rule1"); rule2 = engine.rules.find((rule) => rule.name === "rule2"); - expect(rule1.conditions.all.length).to.equal(0); - expect(rule2.conditions.all.length).to.equal(2); + expect(rule1.conditions.all.length).toBe(0); + expect(rule2.conditions.all.length).toBe(2); }); it("should throw error if rule not found", () => { - const rule1 = new Rule(factories.rule({ name: "rule1" })); + const rule1 = new Rule(ruleFactory({ name: "rule1" })); engine.addRule(rule1); - const rule2 = new Rule(factories.rule({ name: "rule2" })); + const rule2 = new Rule(ruleFactory({ name: "rule2" })); expect(() => { engine.updateRule(rule2); - }).to.throw(/Engine: updateRule\(\) rule not found/); + }).toThrow(/Engine: updateRule\(\) rule not found/); }); }); describe("removeRule()", () => { function setup() { - const rule1 = new Rule(factories.rule({ name: "rule1" })); - const rule2 = new Rule(factories.rule({ name: "rule2" })); + const rule1 = new Rule(ruleFactory({ name: "rule1" })); + const rule2 = new Rule(ruleFactory({ name: "rule2" })); engine.addRule(rule1); engine.addRule(rule2); engine.prioritizeRules(); return [rule1, rule2]; } - context("remove by rule.name", () => { + describe("remove by rule.name", () => { it("removes a single rule", () => { const [rule1] = setup(); - expect(engine.rules.length).to.equal(2); + expect(engine.rules.length).toBe(2); const isRemoved = engine.removeRule(rule1.name); - expect(isRemoved).to.be.true(); - expect(engine.rules.length).to.equal(1); - expect(engine.prioritizedRules).to.equal(null); + expect(isRemoved).toBe(true); + expect(engine.rules.length).toBe(1); + expect(engine.prioritizedRules).toBeNull(); }); it("removes multiple rules with the same name", () => { const [rule1] = setup(); - const rule3 = new Rule(factories.rule({ name: rule1.name })); + const rule3 = new Rule(ruleFactory({ name: rule1.name })); engine.addRule(rule3); - expect(engine.rules.length).to.equal(3); + expect(engine.rules.length).toBe(3); const isRemoved = engine.removeRule(rule1.name); - expect(isRemoved).to.be.true(); - expect(engine.rules.length).to.equal(1); - expect(engine.prioritizedRules).to.equal(null); + expect(isRemoved).toBe(true); + expect(engine.rules.length).toBe(1); + expect(engine.prioritizedRules).toBeNull(); }); it("returns false when rule cannot be found", () => { setup(); - expect(engine.rules.length).to.equal(2); + expect(engine.rules.length).toBe(2); const isRemoved = engine.removeRule("not-found-name"); - expect(isRemoved).to.be.false(); - expect(engine.rules.length).to.equal(2); - expect(engine.prioritizedRules).to.not.equal(null); + expect(isRemoved).toBe(false); + expect(engine.rules.length).toBe(2); + expect(engine.prioritizedRules).not.toBeNull(); }); }); - context("remove by rule object", () => { + describe("remove by rule object", () => { it("removes a single rule", () => { const [rule1] = setup(); - expect(engine.rules.length).to.equal(2); + expect(engine.rules.length).toBe(2); const isRemoved = engine.removeRule(rule1); - expect(isRemoved).to.be.true(); - expect(engine.rules.length).to.equal(1); - expect(engine.prioritizedRules).to.equal(null); + expect(isRemoved).toBe(true); + expect(engine.rules.length).toBe(1); + expect(engine.prioritizedRules).toBeNull(); }); it("removes a single rule, even if two have the same name", () => { const [rule1] = setup(); - const rule3 = new Rule(factories.rule({ name: rule1.name })); + const rule3 = new Rule(ruleFactory({ name: rule1.name })); engine.addRule(rule3); - expect(engine.rules.length).to.equal(3); + expect(engine.rules.length).toBe(3); const isRemoved = engine.removeRule(rule1); - expect(isRemoved).to.be.true(); - expect(engine.rules.length).to.equal(2); - expect(engine.prioritizedRules).to.equal(null); + expect(isRemoved).toBe(true); + expect(engine.rules.length).toBe(2); + expect(engine.prioritizedRules).toBeNull(); }); it("returns false when rule cannot be found", () => { setup(); - expect(engine.rules.length).to.equal(2); + expect(engine.rules.length).toBe(2); - const rule3 = new Rule(factories.rule({ name: "rule3" })); + const rule3 = new Rule(ruleFactory({ name: "rule3" })); const isRemoved = engine.removeRule(rule3); - expect(isRemoved).to.be.false(); - expect(engine.rules.length).to.equal(2); - expect(engine.prioritizedRules).to.not.equal(null); + expect(isRemoved).toBe(false); + expect(engine.rules.length).toBe(2); + expect(engine.prioritizedRules).not.toBeNull(); }); }); }); @@ -200,16 +193,14 @@ describe("Engine", () => { engine.addOperator("startsWithLetter", (factValue, jsonValue) => { return factValue[0] === jsonValue; }); - expect(engine.operators.get("startsWithLetter")).to.exist(); - expect(engine.operators.get("startsWithLetter")).to.be.an.instanceof( - Operator, - ); + expect(engine.operators.get("startsWithLetter")).toBeDefined(); + expect(engine.operators.get("startsWithLetter")).toBeInstanceOf(Operator); }); it("accepts an operator instance", () => { - const op = new Operator("my-operator", (_) => true); + const op = new Operator("my-operator", () => true); engine.addOperator(op); - expect(engine.operators.get("my-operator")).to.equal(op); + expect(engine.operators.get("my-operator")).toEqual(op); }); }); @@ -218,16 +209,14 @@ describe("Engine", () => { engine.addOperator("startsWithLetter", (factValue, jsonValue) => { return factValue[0] === jsonValue; }); - expect(engine.operators.get("startsWithLetter")).to.be.an.instanceof( - Operator, - ); + expect(engine.operators.get("startsWithLetter")).toBeInstanceOf(Operator); engine.removeOperator("startsWithLetter"); - expect(engine.operators.get("startsWithLetter")).to.be.null(); + expect(engine.operators.get("startsWithLetter")).toBeNull(); }); it("can only remove added operators", () => { const isRemoved = engine.removeOperator("nonExisting"); - expect(isRemoved).to.equal(false); + expect(isRemoved).toBe(false); }); }); @@ -236,44 +225,44 @@ describe("Engine", () => { const FACT_VALUE = "FACT_VALUE"; function assertFact(engine) { - expect(engine.facts.size).to.equal(1); - expect(engine.facts.has(FACT_NAME)).to.be.true(); + expect(engine.facts.size).toBe(1); + expect(engine.facts.has(FACT_NAME)).toBe(true); } it("allows a constant fact", () => { engine.addFact(FACT_NAME, FACT_VALUE); assertFact(engine); - expect(engine.facts.get(FACT_NAME).value).to.equal(FACT_VALUE); + expect(engine.facts.get(FACT_NAME).value).toBe(FACT_VALUE); }); it("allows options to be passed", () => { const options = { cache: false }; engine.addFact(FACT_NAME, FACT_VALUE, options); assertFact(engine); - expect(engine.facts.get(FACT_NAME).value).to.equal(FACT_VALUE); - expect(engine.facts.get(FACT_NAME).options).to.eql(options); + expect(engine.facts.get(FACT_NAME).value).toBe(FACT_VALUE); + expect(engine.facts.get(FACT_NAME).options).toEqual(options); }); it("allows a lamba fact with no options", () => { - engine.addFact(FACT_NAME, async (params, engine) => { + engine.addFact(FACT_NAME, async () => { return FACT_VALUE; }); assertFact(engine); - expect(engine.facts.get(FACT_NAME).value).to.be.undefined(); + expect(engine.facts.get(FACT_NAME).value).toBeUndefined(); }); it("allows a lamba fact with options", () => { const options = { cache: false }; engine.addFact( FACT_NAME, - async (params, engine) => { + async () => { return FACT_VALUE; }, options, ); assertFact(engine); - expect(engine.facts.get(FACT_NAME).options).to.eql(options); - expect(engine.facts.get(FACT_NAME).value).to.be.undefined(); + expect(engine.facts.get(FACT_NAME).options).toEqual(options); + expect(engine.facts.get(FACT_NAME).value).toBeUndefined(); }); it("allows a fact instance", () => { @@ -281,25 +270,25 @@ describe("Engine", () => { const fact = new Fact(FACT_NAME, 50, options); engine.addFact(fact); assertFact(engine); - expect(engine.facts.get(FACT_NAME)).to.exist(); - expect(engine.facts.get(FACT_NAME).options).to.eql(options); + expect(engine.facts.get(FACT_NAME)).toBeDefined(); + expect(engine.facts.get(FACT_NAME).options).toEqual(options); }); }); describe("removeFact()", () => { it("removes a Fact", () => { - expect(engine.facts.size).to.equal(0); + expect(engine.facts.size).toBe(0); const fact = new Fact("newFact", 50, { cache: false }); engine.addFact(fact); - expect(engine.facts.size).to.equal(1); + expect(engine.facts.size).toBe(1); engine.removeFact("newFact"); - expect(engine.facts.size).to.equal(0); + expect(engine.facts.size).toBe(0); }); it("can only remove added facts", () => { - expect(engine.facts.size).to.equal(0); + expect(engine.facts.size).toBe(0); const isRemoved = engine.removeFact("newFact"); - expect(isRemoved).to.equal(false); + expect(isRemoved).toBe(false); }); }); @@ -315,24 +304,24 @@ describe("Engine", () => { ], }; const event = { type: "generic" }; - const rule = factories.rule({ conditions, event }); + const rule = ruleFactory({ conditions, event }); engine.addRule(rule); engine.addFact("age", 20); }); it('changes the status to "RUNNING"', () => { - const eventSpy = sandbox.spy(); - engine.on("success", (event, almanac) => { + const eventSpy = vi.fn(); + engine.on("success", () => { eventSpy(); - expect(engine.status).to.equal("RUNNING"); + expect(engine.status).toBe("RUNNING"); }); return engine.run(); }); it("changes status to FINISHED once complete", async () => { - expect(engine.status).to.equal("READY"); + expect(engine.status).toBe("READY"); await engine.run(); - expect(engine.status).to.equal("FINISHED"); + expect(engine.status).toBe("FINISHED"); }); }); }); diff --git a/test/fact.test.js b/test/fact.test.mjs similarity index 63% rename from test/fact.test.js rename to test/fact.test.mjs index 8cea1d1f..9602c13a 100644 --- a/test/fact.test.js +++ b/test/fact.test.mjs @@ -1,6 +1,5 @@ -"use strict"; - -import { Fact } from "../src/index"; +import { Fact } from "../src/index.mjs"; +import { describe, it, expect } from "vitest"; describe("Fact", () => { function subject(id, definition, options) { @@ -9,25 +8,25 @@ describe("Fact", () => { describe("Fact::constructor", () => { it("works for constant facts", () => { const fact = subject("factId", 10); - expect(fact.id).to.equal("factId"); - expect(fact.value).to.equal(10); + expect(fact.id).toBe("factId"); + expect(fact.value).toBe(10); }); it("works for dynamic facts", () => { const fact = subject("factId", () => 10); - expect(fact.id).to.equal("factId"); - expect(fact.calculate()).to.equal(10); + expect(fact.id).toBe("factId"); + expect(fact.calculate()).toBe(10); }); it("allows options to be passed", () => { const opts = { test: true, cache: false }; const fact = subject("factId", 10, opts); - expect(fact.options).to.eql(opts); + expect(fact.options).toEqual(opts); }); describe("validations", () => { it("throws if no id provided", () => { - expect(subject).to.throw(/factId required/); + expect(subject).toThrow(/factId required/); }); }); }); @@ -35,14 +34,14 @@ describe("Fact", () => { describe("Fact::types", () => { it("initializes facts with method values as dynamic", () => { const fact = subject("factId", () => {}); - expect(fact.type).to.equal(Fact.DYNAMIC); - expect(fact.isDynamic()).to.be.true(); + expect(fact.type).toBe(Fact.DYNAMIC); + expect(fact.isDynamic()).toBe(true); }); it("initializes facts with non-methods as constant", () => { const fact = subject("factId", 2); - expect(fact.type).to.equal(Fact.CONSTANT); - expect(fact.isConstant()).to.be.true(); + expect(fact.type).toBe(Fact.CONSTANT); + expect(fact.isConstant()).toBe(true); }); }); }); diff --git a/test/index.test.js b/test/index.test.js deleted file mode 100644 index 5355759b..00000000 --- a/test/index.test.js +++ /dev/null @@ -1,14 +0,0 @@ -"use strict"; - -import subject from "../src/index"; - -describe("json-business-subject", () => { - it("treats each rule engine independently", () => { - const engine1 = subject(); - const engine2 = subject(); - engine1.addRule(factories.rule()); - engine2.addRule(factories.rule()); - expect(engine1.rules.length).to.equal(1); - expect(engine2.rules.length).to.equal(1); - }); -}); diff --git a/test/index.test.mjs b/test/index.test.mjs new file mode 100644 index 00000000..e45c60b3 --- /dev/null +++ b/test/index.test.mjs @@ -0,0 +1,14 @@ +import subject from "../src/index.mjs"; +import { describe, it, expect } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; + +describe("json-business-subject", () => { + it("treats each rule engine independently", () => { + const engine1 = subject(); + const engine2 = subject(); + engine1.addRule(ruleFactory()); + engine2.addRule(ruleFactory()); + expect(engine1.rules.length).toBe(1); + expect(engine2.rules.length).toBe(1); + }); +}); diff --git a/test/operator-decorator.test.js b/test/operator-decorator.test.mjs similarity index 69% rename from test/operator-decorator.test.js rename to test/operator-decorator.test.mjs index b519b061..28df63f3 100644 --- a/test/operator-decorator.test.js +++ b/test/operator-decorator.test.mjs @@ -1,6 +1,5 @@ -"use strict"; - -import { OperatorDecorator, Operator } from "../src/index"; +import { OperatorDecorator, Operator } from "../src/index.mjs"; +import { describe, it, expect } from "vitest"; const startsWithLetter = new Operator( "startsWithLetter", @@ -17,20 +16,20 @@ describe("OperatorDecorator", () => { it("adds the decorator", () => { const decorator = subject("test", () => false); - expect(decorator.name).to.equal("test"); - expect(decorator.cb).to.an.instanceof(Function); + expect(decorator.name).toBe("test"); + expect(decorator.cb).toBeInstanceOf(Function); }); it("decorator name", () => { expect(() => { subject(); - }).to.throw(/Missing decorator name/); + }).toThrow(/Missing decorator name/); }); it("decorator definition", () => { expect(() => { subject("test"); - }).to.throw(/Missing decorator callback/); + }).toThrow(/Missing decorator callback/); }); }); @@ -39,7 +38,7 @@ describe("OperatorDecorator", () => { startsWithLetter, ); it("creates a new operator with the prefixed name", () => { - expect(subject.name).to.equal("test:startsWithLetter"); + expect(subject.name).toBe("test:startsWithLetter"); }); }); }); diff --git a/test/operator.test.js b/test/operator.test.mjs similarity index 64% rename from test/operator.test.js rename to test/operator.test.mjs index bb00c307..78687324 100644 --- a/test/operator.test.js +++ b/test/operator.test.mjs @@ -1,6 +1,5 @@ -"use strict"; - -import { Operator } from "../src/index"; +import { Operator } from "../src/index.mjs"; +import { describe, it, expect } from "vitest"; describe("Operator", () => { describe("constructor()", () => { @@ -12,20 +11,20 @@ describe("Operator", () => { const operator = subject("startsWithLetter", (factValue, jsonValue) => { return factValue[0] === jsonValue; }); - expect(operator.name).to.equal("startsWithLetter"); - expect(operator.cb).to.an.instanceof(Function); + expect(operator.name).toBe("startsWithLetter"); + expect(operator.cb).toBeInstanceOf(Function); }); it("operator name", () => { expect(() => { subject(); - }).to.throw(/Missing operator name/); + }).toThrow(/Missing operator name/); }); it("operator definition", () => { expect(() => { subject("startsWithLetter"); - }).to.throw(/Missing operator callback/); + }).toThrow(/Missing operator callback/); }); }); }); diff --git a/test/performance.test.js b/test/performance.test.mjs similarity index 77% rename from test/performance.test.js rename to test/performance.test.mjs index 08b676d8..83a0d91f 100644 --- a/test/performance.test.js +++ b/test/performance.test.mjs @@ -1,8 +1,8 @@ -"use strict"; - -import engineFactory from "../src/index"; +import engineFactory from "../src/index.mjs"; import perfy from "perfy"; import deepClone from "clone"; +import { describe, it, expect } from "vitest"; +import ruleFactory from "./support/rule-factory.mjs"; describe("Performance", () => { const baseConditions = { @@ -36,7 +36,7 @@ describe("Performance", () => { const engine = engineFactory(); const config = deepClone({ conditions, event }); range(1000).forEach(() => { - const rule = factories.rule(config); + const rule = ruleFactory(config); engine.addRule(rule); }); engine.addFact("segment", "european", { cache: true }); @@ -49,8 +49,8 @@ describe("Performance", () => { perfy.start("any"); await engine.run(); const result = perfy.end("any"); - expect(result.time).to.be.greaterThan(0.001); - expect(result.time).to.be.lessThan(0.5); + expect(result.time).toBeGreaterThan(0.001); + expect(result.time).toBeLessThan(0.5); }); it('performs "all" quickly', async () => { @@ -61,7 +61,7 @@ describe("Performance", () => { perfy.start("all"); await engine.run(); const result = perfy.end("all"); - expect(result.time).to.be.greaterThan(0.001); // assert lower value - expect(result.time).to.be.lessThan(0.5); + expect(result.time).toBeGreaterThan(0.001); // assert lower value + expect(result.time).toBeLessThan(0.5); }); }); diff --git a/test/rule.test.js b/test/rule.test.mjs similarity index 63% rename from test/rule.test.js rename to test/rule.test.mjs index 76fdc557..05c26aef 100644 --- a/test/rule.test.js +++ b/test/rule.test.mjs @@ -1,12 +1,12 @@ -"use strict"; +import Engine from "../src/index.mjs"; +import Rule from "../src/rule.mjs"; -import Engine from "../src/index"; -import Rule from "../src/rule"; -import sinon from "sinon"; +import { describe, it, beforeEach, expect, vi } from "vitest"; +import conditionFactory from "./support/condition-factory.mjs"; describe("Rule", () => { const rule = new Rule(); - const conditionBase = factories.condition({ + const conditionBase = conditionFactory({ fact: "age", value: 50, }); @@ -27,10 +27,10 @@ describe("Rule", () => { name: "testName", }; const rule = new Rule(opts); - expect(rule.priority).to.eql(opts.priority); - expect(rule.conditions).to.eql(opts.conditions); - expect(rule.ruleEvent).to.eql(opts.event); - expect(rule.name).to.eql(opts.name); + expect(rule.priority).toEqual(opts.priority); + expect(rule.conditions).toEqual(opts.conditions); + expect(rule.ruleEvent).toEqual(opts.event); + expect(rule.name).toBe(opts.name); }); it("it can be initialized with a json string", () => { @@ -49,52 +49,48 @@ describe("Rule", () => { }; const json = JSON.stringify(opts); const rule = new Rule(json); - expect(rule.priority).to.eql(opts.priority); - expect(rule.conditions).to.eql(opts.conditions); - expect(rule.ruleEvent).to.eql(opts.event); - expect(rule.name).to.eql(opts.name); + expect(rule.priority).toEqual(opts.priority); + expect(rule.conditions).toEqual(opts.conditions); + expect(rule.ruleEvent).toEqual(opts.event); + expect(rule.name).toEqual(opts.name); }); }); describe("event emissions", () => { it("can emit", () => { const rule = new Rule(); - const successSpy = sinon.spy(); + const successSpy = vi.fn(); rule.on("test", successSpy); rule.emit("test"); - expect(successSpy.callCount).to.equal(1); + expect(successSpy).toHaveBeenCalledOnce(); }); - it("can be initialized with an onSuccess option", (done) => { + it("can be initialized with an onSuccess option", () => { const event = { type: "test" }; - const onSuccess = function (e) { - expect(e).to.equal(event); - done(); - }; + const onSuccess = vi.fn(); const rule = new Rule({ onSuccess }); rule.emit("success", event); + expect(onSuccess).toHaveBeenCalledWith(event); }); - it("can be initialized with an onFailure option", (done) => { + it("can be initialized with an onFailure option", () => { const event = { type: "test" }; - const onFailure = function (e) { - expect(e).to.equal(event); - done(); - }; + const onFailure = vi.fn(); const rule = new Rule({ onFailure }); rule.emit("failure", event); + expect(onFailure).toHaveBeenCalledWith(event); }); }); describe("setEvent()", () => { it("throws if no argument provided", () => { - expect(() => rule.setEvent()).to.throw( + expect(() => rule.setEvent()).toThrow( /Rule: setEvent\(\) requires event object/, ); }); it('throws if argument is missing "type" property', () => { - expect(() => rule.setEvent({})).to.throw( + expect(() => rule.setEvent({})).toThrow( /Rule: setEvent\(\) requires event object with "type" property/, ); }); @@ -102,13 +98,13 @@ describe("Rule", () => { describe("setEvent()", () => { it("throws if no argument provided", () => { - expect(() => rule.setEvent()).to.throw( + expect(() => rule.setEvent()).toThrow( /Rule: setEvent\(\) requires event object/, ); }); it('throws if argument is missing "type" property', () => { - expect(() => rule.setEvent({})).to.throw( + expect(() => rule.setEvent({})).toThrow( /Rule: setEvent\(\) requires event object with "type" property/, ); }); @@ -117,7 +113,7 @@ describe("Rule", () => { describe("setConditions()", () => { describe("validations", () => { it("throws an exception for invalid root conditions", () => { - expect(rule.setConditions.bind(rule, { foo: true })).to.throw( + expect(rule.setConditions.bind(rule, { foo: true })).toThrow( /"conditions" root must contain a single instance of "all", "any", "not", or "condition"/, ); }); @@ -126,16 +122,16 @@ describe("Rule", () => { describe("setPriority", () => { it("defaults to a priority of 1", () => { - expect(rule.priority).to.equal(1); + expect(rule.priority).toBe(1); }); it("allows a priority to be set", () => { rule.setPriority(10); - expect(rule.priority).to.equal(10); + expect(rule.priority).toBe(10); }); it("errors if priority is less than 0", () => { - expect(rule.setPriority.bind(null, 0)).to.throw(/greater than zero/); + expect(rule.setPriority.bind(null, 0)).toThrow(/greater than zero/); }); }); @@ -143,19 +139,19 @@ describe("Rule", () => { it("retrieves event", () => { const event = { type: "e", params: { a: "b" } }; rule.setEvent(event); - expect(rule.getEvent()).to.deep.equal(event); + expect(rule.getEvent()).toEqual(event); }); it("retrieves priority", () => { const priority = 100; rule.setPriority(priority); - expect(rule.getPriority()).to.equal(priority); + expect(rule.getPriority()).toBe(priority); }); it("retrieves conditions", () => { const condition = { all: [] }; rule.setConditions(condition); - expect(rule.getConditions()).to.deep.equal({ + expect(rule.getConditions()).toEqual({ all: [], operator: "all", priority: 1, @@ -165,17 +161,17 @@ describe("Rule", () => { describe("setName", () => { it("defaults to undefined", () => { - expect(rule.name).to.equal(undefined); + expect(rule.name).toBeUndefined(); }); it("allows the name to be set", () => { rule.setName("Test Name"); - expect(rule.name).to.equal("Test Name"); + expect(rule.name).toBe("Test Name"); }); it("allows input of the number 0", () => { rule.setName(0); - expect(rule.name).to.equal(0); + expect(rule.name).toBe(0); }); it("allows input of an object", () => { @@ -183,14 +179,14 @@ describe("Rule", () => { id: 123, name: "myRule", }); - expect(rule.name).to.eql({ + expect(rule.name).toEqual({ id: 123, name: "myRule", }); }); it("errors if name is an empty string", () => { - expect(rule.setName.bind(null, "")).to.throw( + expect(rule.setName.bind(null, "")).toThrow( /Rule "name" must be defined/, ); }); @@ -230,11 +226,11 @@ describe("Rule", () => { rule.setEngine(engine); const prioritizedConditions = rule.prioritizeConditions(conditions); - expect(prioritizedConditions.length).to.equal(4); - expect(prioritizedConditions[0][0].fact).to.equal("state"); - expect(prioritizedConditions[1][0].fact).to.equal("age"); - expect(prioritizedConditions[2][0].fact).to.equal("segment"); - expect(prioritizedConditions[3][0].fact).to.equal("accountType"); + expect(prioritizedConditions.length).toBe(4); + expect(prioritizedConditions[0][0].fact).toBe("state"); + expect(prioritizedConditions[1][0].fact).toBe("age"); + expect(prioritizedConditions[2][0].fact).toBe("segment"); + expect(prioritizedConditions[3][0].fact).toBe("accountType"); }); }); @@ -250,20 +246,20 @@ describe("Rule", () => { return { engine, rule }; } it("evalutes truthy when there are no conditions", async () => { - const engineSuccessSpy = sinon.spy(); + const engineSuccessSpy = vi.fn(); const { engine } = setup(); engine.on("success", engineSuccessSpy); await engine.run(); - expect(engineSuccessSpy).to.have.been.calledOnce(); + expect(engineSuccessSpy).toHaveBeenCalledOnce(); }); it('waits for all on("success") event promises to be resolved', async () => { - const engineSuccessSpy = sinon.spy(); - const ruleSuccessSpy = sinon.spy(); - const engineRunSpy = sinon.spy(); + const engineSuccessSpy = vi.fn(); + const ruleSuccessSpy = vi.fn(); + const engineRunSpy = vi.fn(); const { engine, rule } = setup(); rule.on("success", () => { return new Promise(function (resolve) { @@ -277,10 +273,14 @@ describe("Rule", () => { await engine.run().then(() => engineRunSpy()); - expect(ruleSuccessSpy).to.have.been.calledOnce(); - expect(engineSuccessSpy).to.have.been.calledOnce(); - expect(ruleSuccessSpy).to.have.been.calledBefore(engineRunSpy); - expect(ruleSuccessSpy).to.have.been.calledBefore(engineSuccessSpy); + expect(ruleSuccessSpy).toHaveBeenCalledOnce(); + expect(engineSuccessSpy).toHaveBeenCalledOnce(); + expect(Math.min(...ruleSuccessSpy.mock.invocationCallOrder)).toBeLessThan( + Math.min(...engineRunSpy.mock.invocationCallOrder), + ); + expect(Math.min(...ruleSuccessSpy.mock.invocationCallOrder)).toBeLessThan( + Math.min(...engineSuccessSpy.mock.invocationCallOrder), + ); }); }); @@ -316,43 +316,43 @@ describe("Rule", () => { it("serializes itself", () => { const json = rule.toJSON(false); - expect(Object.keys(json).length).to.equal(4); - expect(json.conditions).to.eql(conditions); - expect(json.priority).to.eql(priority); - expect(json.event).to.eql(event); - expect(json.name).to.eql(name); + expect(Object.keys(json).length).toBe(4); + expect(json.conditions).toEqual(conditions); + expect(json.priority).toBe(priority); + expect(json.event).toEqual(event); + expect(json.name).toBe(name); }); it("serializes itself as json", () => { const jsonString = rule.toJSON(); - expect(jsonString).to.be.a("string"); + expect(jsonString).toBeTypeOf("string"); const json = JSON.parse(jsonString); - expect(Object.keys(json).length).to.equal(4); - expect(json.conditions).to.eql(conditions); - expect(json.priority).to.eql(priority); - expect(json.event).to.eql(event); - expect(json.name).to.eql(name); + expect(Object.keys(json).length).toBe(4); + expect(json.conditions).toEqual(conditions); + expect(json.priority).toBe(priority); + expect(json.event).toEqual(event); + expect(json.name).toBe(name); }); it("rehydrates itself using a JSON string", () => { const jsonString = rule.toJSON(); - expect(jsonString).to.be.a("string"); + expect(jsonString).toBeTypeOf("string"); const hydratedRule = new Rule(jsonString); - expect(hydratedRule.conditions).to.eql(rule.conditions); - expect(hydratedRule.priority).to.eql(rule.priority); - expect(hydratedRule.ruleEvent).to.eql(rule.ruleEvent); - expect(hydratedRule.name).to.eql(rule.name); + expect(hydratedRule.conditions).toEqual(rule.conditions); + expect(hydratedRule.priority).toBe(rule.priority); + expect(hydratedRule.ruleEvent).toEqual(rule.ruleEvent); + expect(hydratedRule.name).toBe(rule.name); }); it("rehydrates itself using an object from JSON.parse()", () => { const jsonString = rule.toJSON(); - expect(jsonString).to.be.a("string"); + expect(jsonString).toBeTypeOf("string"); const json = JSON.parse(jsonString); const hydratedRule = new Rule(json); - expect(hydratedRule.conditions).to.eql(rule.conditions); - expect(hydratedRule.priority).to.eql(rule.priority); - expect(hydratedRule.ruleEvent).to.eql(rule.ruleEvent); - expect(hydratedRule.name).to.eql(rule.name); + expect(hydratedRule.conditions).toEqual(rule.conditions); + expect(hydratedRule.priority).toBe(rule.priority); + expect(hydratedRule.ruleEvent).toEqual(rule.ruleEvent); + expect(hydratedRule.name).toBe(rule.name); }); }); }); diff --git a/test/support/bootstrap.js b/test/support/bootstrap.js deleted file mode 100644 index 8a702efb..00000000 --- a/test/support/bootstrap.js +++ /dev/null @@ -1,14 +0,0 @@ -"use strict"; - -const chai = require("chai"); -const sinonChai = require("sinon-chai"); -const chaiAsPromised = require("chai-as-promised"); -const dirtyChai = require("dirty-chai"); -chai.use(chaiAsPromised); -chai.use(sinonChai); -chai.use(dirtyChai); -global.expect = chai.expect; -global.factories = { - rule: require("./rule-factory"), - condition: require("./condition-factory"), -}; diff --git a/test/support/condition-factory.js b/test/support/condition-factory.mjs similarity index 69% rename from test/support/condition-factory.js rename to test/support/condition-factory.mjs index ad89f90c..d3bff432 100644 --- a/test/support/condition-factory.js +++ b/test/support/condition-factory.mjs @@ -1,9 +1,7 @@ -"use strict"; - -module.exports = function (options) { +export default function (options) { return { fact: options.fact || null, value: options.value || null, operator: options.operator || "equal", }; -}; +} diff --git a/test/support/rule-factory.js b/test/support/rule-factory.mjs similarity index 91% rename from test/support/rule-factory.js rename to test/support/rule-factory.mjs index cbee1ce7..a63b12c5 100644 --- a/test/support/rule-factory.js +++ b/test/support/rule-factory.mjs @@ -1,6 +1,4 @@ -"use strict"; - -module.exports = (options) => { +export default (options) => { options = options || {}; return { name: options.name, diff --git a/test/types.test-d.mts b/test/types.test-d.mts new file mode 100644 index 00000000..86c3f5a1 --- /dev/null +++ b/test/types.test-d.mts @@ -0,0 +1,217 @@ +import { describe, it, expectTypeOf } from "vitest"; + +import rulesEngine, { + Almanac, + EngineResult, + Engine, + Event, + Fact, + Operator, + OperatorEvaluator, + OperatorDecorator, + OperatorDecoratorEvaluator, + PathResolver, + Rule, + RuleProperties, + RuleResult, + RuleSerializable, +} from "../types/index.js"; + +// setup basic fixture data +const ruleProps: RuleProperties = { + conditions: { + all: [], + }, + event: { + type: "message", + }, +}; + +const complexRuleProps: RuleProperties = { + conditions: { + all: [ + { + any: [ + { + all: [], + }, + { + fact: "foo", + operator: "equal", + value: "bar", + }, + ], + }, + ], + }, + event: { + type: "message", + }, +}; + +describe("type tests", () => { + it("path resolver type", () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pathResolver = function (_value: object, _path: string): any {}; + expectTypeOf(pathResolver); + }); + + it("default export", () => { + expectTypeOf(rulesEngine([ruleProps])); + }); + + const engine = rulesEngine([complexRuleProps]); + + it("engine run returns a promise of the result", () => { + expectTypeOf>(engine.run({ displayMessage: true })); + }); + + describe("rule tests", () => { + const rule = new Rule(ruleProps); + const ruleFromString: Rule = new Rule(JSON.stringify(ruleProps)); + + it("returns the engine when adding a rule", () => { + expectTypeOf(engine.addRule(rule)); + }); + + it("returns boolean when removing a rule", () => { + expectTypeOf(engine.removeRule(ruleFromString)); + }); + + it("returns void when updating a rule", () => { + expectTypeOf(engine.updateRule(ruleFromString)); + }); + + it("returns rule when setting conditions", () => { + expectTypeOf(rule.setConditions({ any: [] })); + }); + + it("returns rule when setting event", () => { + expectTypeOf(rule.setEvent({ type: "test" })); + }); + + it("returns rule when setting priority", () => { + expectTypeOf(rule.setPriority(1)); + }); + + it("returns string when json stringifying", () => { + expectTypeOf(rule.toJSON()); + expectTypeOf(rule.toJSON(true)); + }); + + it("returns serializable props when converting to json", () => { + expectTypeOf(rule.toJSON(false)); + }); + }); + + describe("operator tests", () => { + const operatorEvaluator: OperatorEvaluator = ( + a: number, + b: number, + ) => a === b; + + const operator: Operator = new Operator( + "test", + operatorEvaluator, + (num: number) => num > 0, + ); + + it("returns void when adding an operatorEvaluator", () => { + expectTypeOf(engine.addOperator("test", operatorEvaluator)); + }); + + it("returns void when adding an operator object", () => { + expectTypeOf(engine.addOperator(operator)); + }); + + it("returns a boolean when removing an operator", () => { + expectTypeOf(engine.removeOperator(operator)); + }); + }); + + describe("operator decorator tests", () => { + const operatorDecoratorEvaluator: OperatorDecoratorEvaluator< + number[], + number, + number, + number + > = (a: number[], b: number, next: OperatorEvaluator) => + next(a[0], b); + const operatorDecorator: OperatorDecorator = new OperatorDecorator( + "first", + operatorDecoratorEvaluator, + (a: number[]) => a.length > 0, + ); + + it("returns void when adding a decorator evaluator", () => { + expectTypeOf( + engine.addOperatorDecorator("first", operatorDecoratorEvaluator), + ); + }); + + it("returns void when adding a decorator object", () => { + expectTypeOf(engine.addOperatorDecorator(operatorDecorator)); + }); + + it("returns a boolean when removing a decorator", () => { + expectTypeOf(engine.removeOperatorDecorator(operatorDecorator)); + }); + }); + + describe("fact tests", () => { + const fact = new Fact("test-fact", 3); + const dynamicFact = new Fact("test-fact", () => [42]); + + it("returns engine when adding a fact value", () => { + expectTypeOf( + engine.addFact("test-fact", "value", { priority: 10 }), + ); + }); + + it("returns engine when adding a constant fact object", () => { + expectTypeOf(engine.addFact(fact)); + }); + + it("returns engine when adding a dynamic fact object", () => { + expectTypeOf(engine.addFact(dynamicFact)); + }); + + it("returns boolean when removing a fact", () => { + expectTypeOf(engine.removeFact(fact)); + }); + + it("returns fact when getting a fact", () => { + expectTypeOf>(engine.getFact("test")); + }); + }); + + describe("almanac tests", () => { + const almanac: Almanac = new Almanac(); + + it("factValue returns promise of value", () => { + expectTypeOf>(almanac.factValue("test-fact")); + }); + + it("addRuntimeFact returns void", () => { + expectTypeOf(almanac.addRuntimeFact("test-fact", "some-value")); + }); + }); + + describe("event tests", () => { + it("standard event has event, almanac, and ruleResult", () => { + engine.on("success", (event, almanac, ruleResult) => { + expectTypeOf(event); + expectTypeOf(almanac); + expectTypeOf(ruleResult); + }); + }); + + it("custom event type has custom type, almanac, and ruleResult", () => { + engine.on<{ foo: Array }>("foo", (event, almanac, ruleResult) => { + expectTypeOf<{ foo: Array }>(event); + expectTypeOf(almanac); + expectTypeOf(ruleResult); + }); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..9e58401c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,113 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "ESNext", + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "Node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "exclude": [ + "tsup.config.ts" + ] +} diff --git a/types/index.test-d.ts b/types/index.test-d.ts deleted file mode 100644 index f39ee9d4..00000000 --- a/types/index.test-d.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { expectType } from "tsd"; - -import rulesEngine, { - Almanac, - EngineResult, - Engine, - Event, - Fact, - Operator, - OperatorEvaluator, - OperatorDecorator, - OperatorDecoratorEvaluator, - PathResolver, - Rule, - RuleProperties, - RuleResult, - RuleSerializable, -} from "../"; - -// setup basic fixture data -const ruleProps: RuleProperties = { - conditions: { - all: [], - }, - event: { - type: "message", - }, -}; - -const complexRuleProps: RuleProperties = { - conditions: { - all: [ - { - any: [ - { - all: [], - }, - { - fact: "foo", - operator: "equal", - value: "bar", - }, - ], - }, - ], - }, - event: { - type: "message", - }, -}; - -// path resolver -const pathResolver = function (value: object, path: string): any {}; -expectType(pathResolver); - -// default export test -expectType(rulesEngine([ruleProps])); -const engine = rulesEngine([complexRuleProps]); - -// Rule tests -const rule: Rule = new Rule(ruleProps); -const ruleFromString: Rule = new Rule(JSON.stringify(ruleProps)); -expectType(engine.addRule(rule)); -expectType(engine.removeRule(ruleFromString)); -expectType(engine.updateRule(ruleFromString)); - -expectType(rule.setConditions({ any: [] })); -expectType(rule.setEvent({ type: "test" })); -expectType(rule.setPriority(1)); -expectType(rule.toJSON()); -expectType(rule.toJSON(true)); -expectType(rule.toJSON(false)); - -// Operator tests -const operatorEvaluator: OperatorEvaluator = ( - a: number, - b: number, -) => a === b; -expectType(engine.addOperator("test", operatorEvaluator)); -const operator: Operator = new Operator( - "test", - operatorEvaluator, - (num: number) => num > 0, -); -expectType(engine.addOperator(operator)); -expectType(engine.removeOperator(operator)); - -// Operator Decorator tests -const operatorDecoratorEvaluator: OperatorDecoratorEvaluator< - number[], - number, - number, - number -> = (a: number[], b: number, next: OperatorEvaluator) => - next(a[0], b); -expectType( - engine.addOperatorDecorator("first", operatorDecoratorEvaluator), -); -const operatorDecorator: OperatorDecorator = new OperatorDecorator( - "first", - operatorDecoratorEvaluator, - (a: number[]) => a.length > 0, -); -expectType(engine.addOperatorDecorator(operatorDecorator)); -expectType(engine.removeOperatorDecorator(operatorDecorator)); - -// Fact tests -const fact = new Fact("test-fact", 3); -const dynamicFact = new Fact("test-fact", () => [42]); -expectType( - engine.addFact("test-fact", "value", { priority: 10 }), -); -expectType(engine.addFact(fact)); -expectType(engine.addFact(dynamicFact)); -expectType(engine.removeFact(fact)); -expectType>(engine.getFact("test")); -engine.on("success", (event, almanac, ruleResult) => { - expectType(event); - expectType(almanac); - expectType(ruleResult); -}); -engine.on<{ foo: Array }>("foo", (event, almanac, ruleResult) => { - expectType<{ foo: Array }>(event); - expectType(almanac); - expectType(ruleResult); -}); - -// Run the Engine -expectType>(engine.run({ displayMessage: true })); - -// Alamanac tests -const almanac: Almanac = (await engine.run()).almanac; - -expectType>(almanac.factValue("test-fact")); -expectType(almanac.addRuntimeFact("test-fact", "some-value")); From b7039c588ca542d20d5f5a8b76a28d45ed92042e Mon Sep 17 00:00:00 2001 From: Chris Pardy Date: Wed, 23 Oct 2024 17:54:03 -0400 Subject: [PATCH 3/4] Migrate examples to typescript --- .../{01-hello-world.js => 01-hello-world.mts} | 7 ++- ...n-logic.js => 02-nested-boolean-logic.mts} | 7 ++- ...-dynamic-facts.js => 03-dynamic-facts.mts} | 16 +++--- ...t-dependency.js => 04-fact-dependency.mts} | 49 ++++++++++--------- ...timizing-runtime-with-fact-priorities.mts} | 16 +++--- ...m-operators.js => 06-custom-operators.mts} | 11 ++--- ...-rule-chaining.js => 07-rule-chaining.mts} | 21 ++++---- ...t-comparison.js => 08-fact-comparison.mts} | 44 ++++++++--------- ...09-rule-results.js => 09-rule-results.mts} | 25 +++++----- ...on-sharing.js => 10-condition-sharing.mts} | 7 ++- ...events.js => 11-using-facts-in-events.mts} | 22 ++++++--- ...almanac.js => 12-using-custom-almanac.mts} | 42 ++++++++-------- ...rs.js => 13-using-operator-decorators.mts} | 9 ++-- examples/package.json | 6 ++- ...t-api-client.js => account-api-client.mts} | 12 ++--- 15 files changed, 145 insertions(+), 149 deletions(-) rename examples/{01-hello-world.js => 01-hello-world.mts} (88%) rename examples/{02-nested-boolean-logic.js => 02-nested-boolean-logic.mts} (92%) rename examples/{03-dynamic-facts.js => 03-dynamic-facts.mts} (88%) rename examples/{04-fact-dependency.js => 04-fact-dependency.mts} (80%) rename examples/{05-optimizing-runtime-with-fact-priorities.js => 05-optimizing-runtime-with-fact-priorities.mts} (89%) rename examples/{06-custom-operators.js => 06-custom-operators.mts} (91%) rename examples/{07-rule-chaining.js => 07-rule-chaining.mts} (87%) rename examples/{08-fact-comparison.js => 08-fact-comparison.mts} (73%) rename examples/{09-rule-results.js => 09-rule-results.mts} (77%) rename examples/{10-condition-sharing.js => 10-condition-sharing.mts} (96%) rename examples/{11-using-facts-in-events.js => 11-using-facts-in-events.mts} (85%) rename examples/{12-using-custom-almanac.js => 12-using-custom-almanac.mts} (72%) rename examples/{13-using-operator-decorators.js => 13-using-operator-decorators.mts} (94%) rename examples/support/{account-api-client.js => account-api-client.mts} (80%) diff --git a/examples/01-hello-world.js b/examples/01-hello-world.mts similarity index 88% rename from examples/01-hello-world.js rename to examples/01-hello-world.mts index 4ec9b336..f0cc1f58 100644 --- a/examples/01-hello-world.js +++ b/examples/01-hello-world.mts @@ -1,4 +1,3 @@ -"use strict"; /* * This is the hello-world example from the README. * @@ -9,8 +8,8 @@ * DEBUG=json-rules-engine node ./examples/01-hello-world.js */ -require("colors"); -const { Engine } = require("json-rules-engine"); +import "colors"; +import { Engine } from "json-rules-engine"; async function start() { /** @@ -51,7 +50,7 @@ async function start() { // engine.run() evaluates the rule using the facts provided const { events } = await engine.run(facts); - events.map((event) => console.log(event.params.data.green)); + events.map((event) => console.log(event.params!.data.green)); } start(); diff --git a/examples/02-nested-boolean-logic.js b/examples/02-nested-boolean-logic.mts similarity index 92% rename from examples/02-nested-boolean-logic.js rename to examples/02-nested-boolean-logic.mts index 3bd0c169..c596e9c2 100644 --- a/examples/02-nested-boolean-logic.js +++ b/examples/02-nested-boolean-logic.mts @@ -1,4 +1,3 @@ -"use strict"; /* * This example demonstates nested boolean logic - e.g. (x OR y) AND (a OR b). * @@ -9,8 +8,8 @@ * DEBUG=json-rules-engine node ./examples/02-nested-boolean-logic.js */ -require("colors"); -const { Engine } = require("json-rules-engine"); +import "colors"; +import { Engine } from "json-rules-engine"; async function start() { /** @@ -77,7 +76,7 @@ async function start() { const { events } = await engine.run(facts); - events.map((event) => console.log(event.params.message.red)); + events.map((event) => console.log(event.params!.message.red)); } start(); /* diff --git a/examples/03-dynamic-facts.js b/examples/03-dynamic-facts.mts similarity index 88% rename from examples/03-dynamic-facts.js rename to examples/03-dynamic-facts.mts index 2a7e65cc..e241be02 100644 --- a/examples/03-dynamic-facts.js +++ b/examples/03-dynamic-facts.mts @@ -1,4 +1,3 @@ -"use strict"; /* * This example demonstrates computing fact values at runtime, and leveraging the 'path' feature * to select object properties returned by facts @@ -10,11 +9,11 @@ * DEBUG=json-rules-engine node ./examples/03-dynamic-facts.js */ -require("colors"); -const { Engine } = require("json-rules-engine"); +import "colors"; +import { Engine } from "json-rules-engine"; // example client for making asynchronous requests to an api, database, etc -const apiClient = require("./support/account-api-client"); +import apiClient from "./support/account-api-client.mjs"; async function start() { /** @@ -65,10 +64,9 @@ async function start() { * into the engine. The major advantage of this technique is that although there are THREE conditions * requiring this data, only ONE api call is made. This results in much more efficient runtime performance. */ - engine.addFact("account-information", function (params, almanac) { - return almanac.factValue("accountId").then((accountId) => { - return apiClient.getAccountInformation(accountId); - }); + engine.addFact("account-information", async function (_params, almanac) { + const accountId = await almanac.factValue("accountId"); + return apiClient.getAccountInformation(accountId); }); // define fact(s) known at runtime @@ -76,7 +74,7 @@ async function start() { const { events } = await engine.run(facts); console.log( - facts.accountId + " is a " + events.map((event) => event.params.message), + facts.accountId + " is a " + events.map((event) => event.params!.message), ); } start(); diff --git a/examples/04-fact-dependency.js b/examples/04-fact-dependency.mts similarity index 80% rename from examples/04-fact-dependency.js rename to examples/04-fact-dependency.mts index 55e9b78d..3315528f 100644 --- a/examples/04-fact-dependency.js +++ b/examples/04-fact-dependency.mts @@ -1,4 +1,3 @@ -"use strict"; /* * This is an advanced example that demonstrates facts with dependencies * on other facts. In addition, it demonstrates facts that load data asynchronously @@ -11,9 +10,9 @@ * DEBUG=json-rules-engine node ./examples/04-fact-dependency.js */ -require("colors"); -const { Engine } = require("json-rules-engine"); -const accountClient = require("./support/account-api-client"); +import "colors"; +import { Engine } from "json-rules-engine"; +import accountClient from "./support/account-api-client.mjs"; async function start() { /** @@ -72,7 +71,7 @@ async function start() { /** * Register listeners with the engine for rule success and failure */ - let facts; + let facts: Record; engine .on("success", (event) => { console.log( @@ -98,31 +97,33 @@ async function start() { * 'account-information' fact executes an api call and retrieves account data * - Demonstrates facts called only by other facts and never mentioned directly in a rule */ - engine.addFact("account-information", (params, almanac) => { - return almanac.factValue("accountId").then((accountId) => { - return accountClient.getAccountInformation(accountId); - }); + engine.addFact("account-information", async (_params, almanac) => { + const accountId = await almanac.factValue("accountId"); + return accountClient.getAccountInformation(accountId); }); /** * 'employee-tenure' fact retrieves account-information, and computes the duration of employment * since the account was created using 'accountInformation.createdAt' */ - engine.addFact("employee-tenure", (params, almanac) => { - return almanac - .factValue("account-information") - .then((accountInformation) => { - const created = new Date(accountInformation.createdAt); - const now = new Date(); - switch (params.unit) { - case "years": - return now.getFullYear() - created.getFullYear(); - case "milliseconds": - default: - return now.getTime() - created.getTime(); - } - }) - .catch(console.log); + engine.addFact("employee-tenure", async (params, almanac) => { + try { + const accountInformation = await almanac.factValue<{ createdAt: string }>( + "account-information", + ); + const created = new Date(accountInformation.createdAt); + const now = new Date(); + switch (params.unit) { + case "years": + return now.getFullYear() - created.getFullYear(); + case "milliseconds": + default: + return now.getTime() - created.getTime(); + } + } catch (err) { + console.log(err); + return undefined; + } }); // first run, using washington's facts diff --git a/examples/05-optimizing-runtime-with-fact-priorities.js b/examples/05-optimizing-runtime-with-fact-priorities.mts similarity index 89% rename from examples/05-optimizing-runtime-with-fact-priorities.js rename to examples/05-optimizing-runtime-with-fact-priorities.mts index d39ce1a6..20753623 100644 --- a/examples/05-optimizing-runtime-with-fact-priorities.js +++ b/examples/05-optimizing-runtime-with-fact-priorities.mts @@ -1,4 +1,3 @@ -"use strict"; /* * This is an advanced example that demonstrates using fact priorities to optimize the rules engine. * @@ -9,9 +8,9 @@ * DEBUG=json-rules-engine node ./examples/05-optimizing-runtime-with-fact-priorities.js */ -require("colors"); -const { Engine } = require("json-rules-engine"); -const accountClient = require("./support/account-api-client"); +import "colors"; +import { Engine } from "json-rules-engine"; +import accountClient from "./support/account-api-client.mjs"; async function start() { /** @@ -81,12 +80,11 @@ async function start() { */ engine.addFact( "account-information", - (params, almanac) => { + async (_params, almanac) => { // this fact will not be evaluated, because the "date" fact will fail first console.log('Checking the "account-information" fact...'); // this message will not appear - return almanac.factValue("accountId").then((accountId) => { - return accountClient.getAccountInformation(accountId); - }); + const accountId = await almanac.factValue("accountId"); + return accountClient.getAccountInformation(accountId); }, { priority: LOW }, ); @@ -97,7 +95,7 @@ async function start() { */ engine.addFact( "date", - (params, almanac) => { + () => { console.log('Checking the "date" fact...'); return Date.now(); }, diff --git a/examples/06-custom-operators.js b/examples/06-custom-operators.mts similarity index 91% rename from examples/06-custom-operators.js rename to examples/06-custom-operators.mts index 36511de8..e318201a 100644 --- a/examples/06-custom-operators.js +++ b/examples/06-custom-operators.mts @@ -1,4 +1,3 @@ -"use strict"; /* * This example demonstrates using custom operators. * @@ -15,8 +14,8 @@ * DEBUG=json-rules-engine node ./examples/06-custom-operators.js */ -require("colors"); -const { Engine } = require("json-rules-engine"); +import "colors"; +import { Engine } from "json-rules-engine"; async function start() { /** @@ -27,7 +26,7 @@ async function start() { /** * Define a 'startsWith' custom operator, for use in later rules */ - engine.addOperator("startsWith", (factValue, jsonValue) => { + engine.addOperator("startsWith", (factValue: string, jsonValue: string) => { if (!factValue.length) return false; return factValue[0].toLowerCase() === jsonValue.toLowerCase(); }); @@ -71,7 +70,7 @@ async function start() { engine.addRule(ruleB); // utility for printing output - const printEventType = { + const printEventType: Record = { "start-with-a": 'start with "a"', "start-with-b": 'start with "b"', }; @@ -79,7 +78,7 @@ async function start() { /** * Register listeners with the engine for rule success and failure */ - let facts; + let facts: Record; engine .on("success", (event) => { console.log(facts.word + " DID ".green + printEventType[event.type]); diff --git a/examples/07-rule-chaining.js b/examples/07-rule-chaining.mts similarity index 87% rename from examples/07-rule-chaining.js rename to examples/07-rule-chaining.mts index 873b08a8..cece64da 100644 --- a/examples/07-rule-chaining.js +++ b/examples/07-rule-chaining.mts @@ -1,4 +1,3 @@ -"use strict"; /* * This is an advanced example demonstrating rules that passed based off the * results of other rules by adding runtime facts. It also demonstrates @@ -11,9 +10,9 @@ * DEBUG=json-rules-engine node ./examples/07-rule-chaining.js */ -require("colors"); -const { Engine } = require("json-rules-engine"); -const { getAccountInformation } = require("./support/account-api-client"); +import "colors"; +import { Almanac, Engine } from "json-rules-engine"; +import apiClient from "./support/account-api-client.mjs"; async function start() { /** @@ -41,16 +40,16 @@ async function start() { }, event: { type: "drinks-screwdrivers" }, priority: 10, // IMPORTANT! Set a higher priority for the drinkRule, so it runs first - onSuccess: async function (event, almanac) { + onSuccess: async function (_event: unknown, almanac: Almanac) { almanac.addFact("screwdriverAficionado", true); // asychronous operations can be performed within callbacks // engine execution will not proceed until the returned promises is resolved - const accountId = await almanac.factValue("accountId"); - const accountInfo = await getAccountInformation(accountId); + const accountId = await almanac.factValue("accountId"); + const accountInfo = await apiClient.getAccountInformation(accountId); almanac.addFact("accountInfo", accountInfo); }, - onFailure: function (event, almanac) { + onFailure: function (_event: unknown, almanac: Almanac) { almanac.addFact("screwdriverAficionado", false); }, }; @@ -92,7 +91,9 @@ async function start() { */ engine .on("success", async (event, almanac) => { - const accountInfo = await almanac.factValue("accountInfo"); + const accountInfo = await almanac.factValue<{ company: string }>( + "accountInfo", + ); const accountId = await almanac.factValue("accountId"); console.log( `${accountId}(${accountInfo.company}) ` + @@ -122,7 +123,7 @@ async function start() { let results = await engine.run(facts); // isScrewdriverAficionado was a fact set by engine.run() - let isScrewdriverAficionado = results.almanac.factValue( + let isScrewdriverAficionado = await results.almanac.factValue( "screwdriverAficionado", ); console.log( diff --git a/examples/08-fact-comparison.js b/examples/08-fact-comparison.mts similarity index 73% rename from examples/08-fact-comparison.js rename to examples/08-fact-comparison.mts index d6bb635a..7c38d42c 100644 --- a/examples/08-fact-comparison.js +++ b/examples/08-fact-comparison.mts @@ -1,4 +1,3 @@ -"use strict"; /* * This is a basic example demonstrating a condition that compares two facts * @@ -9,8 +8,8 @@ * DEBUG=json-rules-engine node ./examples/08-fact-comparison.js */ -require("colors"); -const { Engine } = require("json-rules-engine"); +import "colors"; +import { Engine } from "json-rules-engine"; async function start() { /** @@ -52,36 +51,35 @@ async function start() { }; engine.addRule(rule); - engine.addFact("account", (params, almanac) => { + engine.addFact("account", async (params, almanac) => { // get account list - return almanac.factValue("accounts").then((accounts) => { - // use "params" to filter down to the type specified, in this case the "customer" account - const customerAccount = accounts.filter( - (account) => account.type === params.accountType, - ); - // return the customerAccount object, which "path" will use to pull the "balance" property - return customerAccount[0]; - }); + const accounts = await almanac.factValue<{ type: string }[]>("accounts"); + // use "params" to filter down to the type specified, in this case the "customer" account + const customerAccount = accounts.filter( + (account) => account.type === params.accountType, + ); + // return the customerAccount object, which "path" will use to pull the "balance" property + return customerAccount[0]; }); - engine.addFact("product", (params, almanac) => { + engine.addFact("product", async (params, almanac) => { // get product list - return almanac.factValue("products").then((products) => { - // use "params" to filter down to the product specified, in this case the "giftCard" product - const product = products.filter( - (product) => product.productId === params.productId, - ); - // return the product object, which "path" will use to pull the "price" property - return product[0]; - }); + const products = + await almanac.factValue<{ productId: string }[]>("products"); + // use "params" to filter down to the product specified, in this case the "giftCard" product + const product = products.filter( + (product) => product.productId === params.productId, + ); + // return the product object, which "path" will use to pull the "price" property + return product[0]; }); /** * Register listeners with the engine for rule success and failure */ - let facts; + let facts: Record; engine - .on("success", (event, almanac) => { + .on("success", (event) => { console.log( facts.userId + " DID ".green + diff --git a/examples/09-rule-results.js b/examples/09-rule-results.mts similarity index 77% rename from examples/09-rule-results.js rename to examples/09-rule-results.mts index 608ccf03..48a56930 100644 --- a/examples/09-rule-results.js +++ b/examples/09-rule-results.mts @@ -1,4 +1,3 @@ -"use strict"; /* * This is a basic example demonstrating how to leverage the metadata supplied by rule results * @@ -8,8 +7,8 @@ * For detailed output: * DEBUG=json-rules-engine node ./examples/09-rule-results.js */ -require("colors"); -const { Engine } = require("json-rules-engine"); +import "colors"; +import { Engine, NestedCondition, RuleResult } from "json-rules-engine"; async function start() { /** @@ -43,20 +42,20 @@ async function start() { name: "Athlete GPA Rule", }); - function render(message, ruleResult) { + function render(message: string, ruleResult: RuleResult) { // if rule succeeded, render success message if (ruleResult.result) { return console.log(`${message}`.green); } // if rule failed, iterate over each failed condition to determine why the student didn't qualify for athletics honor roll - const detail = ruleResult.conditions.all - .filter((condition) => !condition.result) + const detail = (ruleResult.conditions as { all: NestedCondition[] }).all + .filter((condition) => !(condition as { result?: boolean }).result) .map((condition) => { - switch (condition.operator) { + switch ((condition as { operator?: string }).operator) { case "equal": - return `was not an ${condition.fact}`; + return `was not an ${(condition as { fact?: string }).fact}`; case "greaterThanInclusive": - return `${condition.fact} of ${condition.factResult} was too low`; + return `${(condition as { fact: string }).fact} of ${(condition as { factResult?: unknown }).factResult} was too low`; default: return ""; } @@ -69,9 +68,9 @@ async function start() { * On success, retrieve the student's username and print rule name for display purposes, and render */ engine.on("success", (event, almanac, ruleResult) => { - almanac.factValue("username").then((username) => { + almanac.factValue("username").then((username) => { render( - `${username.bold} succeeded ${ruleResult.name}! ${event.params.message}`, + `${username.bold} succeeded ${ruleResult.name}! ${event.params!.message}`, ruleResult, ); }); @@ -80,8 +79,8 @@ async function start() { /** * On failure, retrieve the student's username and print rule name for display purposes, and render */ - engine.on("failure", (event, almanac, ruleResult) => { - almanac.factValue("username").then((username) => { + engine.on("failure", (_event, almanac, ruleResult) => { + almanac.factValue("username").then((username) => { render(`${username.bold} failed ${ruleResult.name} - `, ruleResult); }); }); diff --git a/examples/10-condition-sharing.js b/examples/10-condition-sharing.mts similarity index 96% rename from examples/10-condition-sharing.js rename to examples/10-condition-sharing.mts index 09eee8cd..836bafe1 100644 --- a/examples/10-condition-sharing.js +++ b/examples/10-condition-sharing.mts @@ -1,4 +1,3 @@ -"use strict"; /* * This is an advanced example demonstrating rules that re-use a condition defined * in the engine. @@ -10,8 +9,8 @@ * DEBUG=json-rules-engine node ./examples/10-condition-sharing.js */ -require("colors"); -const { Engine } = require("json-rules-engine"); +import "colors"; +import { Engine } from "json-rules-engine"; async function start() { /** @@ -105,7 +104,7 @@ async function start() { }); // define fact(s) known at runtime - let facts = { + let facts: Record = { accountId: "washington", drinksOrangeJuice: true, enjoysVodka: true, diff --git a/examples/11-using-facts-in-events.js b/examples/11-using-facts-in-events.mts similarity index 85% rename from examples/11-using-facts-in-events.js rename to examples/11-using-facts-in-events.mts index 21e323fa..6a8f3c41 100644 --- a/examples/11-using-facts-in-events.js +++ b/examples/11-using-facts-in-events.mts @@ -1,4 +1,3 @@ -"use strict"; /* * This is an advanced example demonstrating an event that emits the value * of a fact in it's parameters. @@ -10,8 +9,8 @@ * DEBUG=json-rules-engine node ./examples/11-using-facts-in-events.js */ -require("colors"); -const { Engine, Fact } = require("json-rules-engine"); +import "colors"; +import { Engine, Fact } from "json-rules-engine"; async function start() { /** @@ -20,7 +19,7 @@ async function start() { const engine = new Engine([], { replaceFactsInEventParams: true }); // in-memory "database" - let currentHighScore = null; + let currentHighScore: { initials: string; score: number } | null = null; const currentHighScoreFact = new Fact( "currentHighScore", () => currentHighScore, @@ -96,12 +95,19 @@ async function start() { * Register listeners with the engine for rule success */ engine - .on("success", async ({ params: { initials, score } }) => { - console.log(`HIGH SCORE\n${initials} - ${score}`); - }) + .on( + "success", + async ({ + params: { initials, score }, + }: { + params: { initials: string; score: number }; + }) => { + console.log(`HIGH SCORE\n${initials} - ${score}`); + }, + ) .on("success", ({ type, params }) => { if (type === "highscore") { - currentHighScore = params; + currentHighScore = params as { initials: string; score: number }; } }); diff --git a/examples/12-using-custom-almanac.js b/examples/12-using-custom-almanac.mts similarity index 72% rename from examples/12-using-custom-almanac.js rename to examples/12-using-custom-almanac.mts index e494c7f5..b1743c0b 100644 --- a/examples/12-using-custom-almanac.js +++ b/examples/12-using-custom-almanac.mts @@ -1,36 +1,36 @@ -"use strict"; +import "colors"; +import { Almanac, Engine } from "json-rules-engine"; -require("colors"); -const { Almanac, Engine } = require("json-rules-engine"); +type Pipe = (value: T) => unknown; /** * Almanac that support piping values through named functions */ class PipedAlmanac extends Almanac { - constructor(options) { - super(options); - this.pipes = new Map(); - } + pipes = new Map>(); - addPipe(name, pipe) { - this.pipes.set(name, pipe); + addPipe(name: string, pipe: Pipe) { + this.pipes.set(name, pipe as Pipe); } - factValue(factId, params, path) { - let pipes = []; + async factValue( + factId: string, + params?: Record, + path?: string, + ) { + let pipes: string[] = []; if (params && "pipes" in params && Array.isArray(params.pipes)) { pipes = params.pipes; delete params.pipes; } - return super.factValue(factId, params, path).then((value) => { - return pipes.reduce((value, pipeName) => { - const pipe = this.pipes.get(pipeName); - if (pipe) { - return pipe(value); - } - return value; - }, value); - }); + const value = await super.factValue(factId, params, path); + return pipes.reduce((value, pipeName) => { + const pipe = this.pipes.get(pipeName); + if (pipe) { + return pipe(value); + } + return value; + }, value) as T; } } @@ -70,7 +70,7 @@ async function start() { const createAlmanacWithPipes = () => { const almanac = new PipedAlmanac(); - almanac.addPipe("addOne", (v) => v + 1); + almanac.addPipe("addOne", (v: number) => v + 1); return almanac; }; diff --git a/examples/13-using-operator-decorators.js b/examples/13-using-operator-decorators.mts similarity index 94% rename from examples/13-using-operator-decorators.js rename to examples/13-using-operator-decorators.mts index ffc787d7..60b34f6b 100644 --- a/examples/13-using-operator-decorators.js +++ b/examples/13-using-operator-decorators.mts @@ -1,4 +1,3 @@ -"use strict"; /* * This example demonstrates using operator decorators. * @@ -11,8 +10,8 @@ * DEBUG=json-rules-engine node ./examples/12-using-operator-decorators.js */ -require("colors"); -const { Engine } = require("json-rules-engine"); +import "colors"; +import { Engine } from "json-rules-engine"; async function start() { /** @@ -43,7 +42,7 @@ async function start() { engine.addFact("validTags", ["dev", "staging", "load", "prod"]); - let facts; + let facts: { tags: string[] }; engine .on("success", (event) => { @@ -66,7 +65,7 @@ async function start() { // add a new decorator to allow for a case-insensitive match engine.addOperatorDecorator( "caseInsensitive", - (factValue, jsonValue, next) => { + (factValue: string, jsonValue: string, next) => { return next(factValue.toLowerCase(), jsonValue.toLowerCase()); }, ); diff --git a/examples/package.json b/examples/package.json index 1758ee5e..231cd7da 100644 --- a/examples/package.json +++ b/examples/package.json @@ -5,11 +5,13 @@ "main": "", "private": true, "scripts": { - "all": "for i in *.js; do node $i; done;" + "all": "for i in *.mts; do tsx $i; done;" }, "author": "Cache Hamm ", "license": "ISC", "dependencies": { - "json-rules-engine": "../" + "colors": "^1.4.0", + "json-rules-engine": "../", + "tsx": "^4.19.1" } } diff --git a/examples/support/account-api-client.js b/examples/support/account-api-client.mts similarity index 80% rename from examples/support/account-api-client.js rename to examples/support/account-api-client.mts index d166b721..ea5fbe12 100644 --- a/examples/support/account-api-client.js +++ b/examples/support/account-api-client.mts @@ -1,8 +1,6 @@ -"use strict"; +import "colors"; -require("colors"); - -const accountData = { +const accountData: Record = { washington: { company: "microsoft", status: "terminated", @@ -26,11 +24,11 @@ const accountData = { /** * mock api client for retrieving account information */ -module.exports = { - getAccountInformation: (accountId) => { +export default { + getAccountInformation: (accountId: string) => { const message = 'loading account information for "' + accountId + '"'; console.log(message.dim); - return new Promise((resolve, reject) => { + return new Promise((resolve) => { setImmediate(() => { resolve(accountData[accountId]); }); From 35277910fb8fff9a3555d3d7960e49e606ec3e83 Mon Sep 17 00:00:00 2001 From: Chris Pardy Date: Wed, 23 Oct 2024 23:29:09 -0400 Subject: [PATCH 4/4] Move to TSUP for build Move to tsup to build and bundle --- .babelrc | 3 --- package.json | 19 +++++-------------- src/{almanac.js => almanac.mjs} | 8 +++----- src/{condition.js => condition.mjs} | 4 +--- src/{debug.js => debug.mjs} | 2 +- ...=> engine-default-operator-decorators.mjs} | 4 +--- ...rators.js => engine-default-operators.mjs} | 4 +--- src/{engine.js => engine.mjs} | 18 ++++++++---------- src/{errors.js => errors.mjs} | 2 -- src/{fact.js => fact.mjs} | 2 -- src/index.js | 3 --- src/index.mjs | 11 +++++++++++ src/json-rules-engine.js | 11 ----------- ...or-decorator.js => operator-decorator.mjs} | 4 +--- src/{operator-map.js => operator-map.mjs} | 8 +++----- src/{operator.js => operator.mjs} | 2 -- src/{rule-result.js => rule-result.mjs} | 2 -- src/{rule.js => rule.mjs} | 8 +++----- tsup.config.ts | 9 +++++++++ 19 files changed, 47 insertions(+), 77 deletions(-) delete mode 100644 .babelrc rename src/{almanac.js => almanac.mjs} (98%) rename src/{condition.js => condition.mjs} (99%) rename src/{debug.js => debug.mjs} (95%) rename src/{engine-default-operator-decorators.js => engine-default-operator-decorators.mjs} (93%) rename src/{engine-default-operators.js => engine-default-operators.mjs} (94%) rename src/{engine.js => engine.mjs} (97%) rename src/{errors.js => errors.mjs} (90%) rename src/{fact.js => fact.mjs} (99%) delete mode 100644 src/index.js create mode 100644 src/index.mjs delete mode 100644 src/json-rules-engine.js rename src/{operator-decorator.js => operator-decorator.mjs} (96%) rename src/{operator-map.js => operator-map.mjs} (97%) rename src/{operator.js => operator.mjs} (98%) rename src/{rule-result.js => rule-result.mjs} (98%) rename src/{rule.js => rule.mjs} (99%) create mode 100644 tsup.config.ts diff --git a/.babelrc b/.babelrc deleted file mode 100644 index eaf32387..00000000 --- a/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015", "stage-0"] -} diff --git a/package.json b/package.json index ef46f969..ed2a94bf 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,8 @@ "name": "json-rules-engine", "version": "8.0.0-alpha.1", "description": "Rules Engine expressed in simple json", - "main": "dist/index.js", + "main": "dist/index.cjs", + "module": "dist/index.js", "types": "types/index.d.ts", "type": "module", "engines": { @@ -12,9 +13,8 @@ "test": "vitest --typecheck", "lint": "eslint", "format": "prettier -w .", - "prepublishOnly": "npm run build", - "build": "babel --stage 1 -d dist/ src/", - "watch": "babel --watch --stage 1 -d dist/ src", + "build": "tsup", + "watch": "tsup --watch", "examples": "./test/support/example_runner.sh" }, "repository": { @@ -40,22 +40,13 @@ "homepage": "https://github.com/cachecontrol/json-rules-engine", "devDependencies": { "@eslint/js": "^9.13.0", - "babel-cli": "6.26.0", - "babel-core": "6.26.3", - "babel-eslint": "10.1.0", - "babel-loader": "8.2.2", - "babel-polyfill": "6.26.0", - "babel-preset-es2015": "~6.24.1", - "babel-preset-stage-0": "~6.24.1", - "babel-register": "6.26.0", - "colors": "~1.4.0", - "dirty-chai": "2.0.1", "eslint": "^9.13.0", "globals": "^15.11.0", "lodash": "4.17.21", "perfy": "^1.1.5", "prettier": "^3.3.3", "tsd": "^0.17.0", + "tsup": "^8.3.0", "typescript": "^5.6.3", "typescript-eslint": "^8.11.0", "vitest": "^2.1.3" diff --git a/src/almanac.js b/src/almanac.mjs similarity index 98% rename from src/almanac.js rename to src/almanac.mjs index 37b0b3c0..9963406d 100644 --- a/src/almanac.js +++ b/src/almanac.mjs @@ -1,8 +1,6 @@ -"use strict"; - -import Fact from "./fact"; -import { UndefinedFactError } from "./errors"; -import debug from "./debug"; +import Fact from "./fact.mjs"; +import { UndefinedFactError } from "./errors.mjs"; +import debug from "./debug.mjs"; import { JSONPath } from "jsonpath-plus"; diff --git a/src/condition.js b/src/condition.mjs similarity index 99% rename from src/condition.js rename to src/condition.mjs index fec9344c..4f0ca65d 100644 --- a/src/condition.js +++ b/src/condition.mjs @@ -1,6 +1,4 @@ -"use strict"; - -import debug from "./debug"; +import debug from "./debug.mjs"; export default class Condition { constructor(properties) { diff --git a/src/debug.js b/src/debug.mjs similarity index 95% rename from src/debug.js rename to src/debug.mjs index 3e315012..5acfeaa3 100644 --- a/src/debug.js +++ b/src/debug.mjs @@ -12,7 +12,7 @@ function createDebug() { ) { return console.debug.bind(console); } - } catch (ex) { + } catch (_error) { // Do nothing } return () => {}; diff --git a/src/engine-default-operator-decorators.js b/src/engine-default-operator-decorators.mjs similarity index 93% rename from src/engine-default-operator-decorators.js rename to src/engine-default-operator-decorators.mjs index 5ab95345..6959c4dd 100644 --- a/src/engine-default-operator-decorators.js +++ b/src/engine-default-operator-decorators.mjs @@ -1,6 +1,4 @@ -"use strict"; - -import OperatorDecorator from "./operator-decorator"; +import OperatorDecorator from "./operator-decorator.mjs"; const OperatorDecorators = []; diff --git a/src/engine-default-operators.js b/src/engine-default-operators.mjs similarity index 94% rename from src/engine-default-operators.js rename to src/engine-default-operators.mjs index f22fd549..77872ee1 100644 --- a/src/engine-default-operators.js +++ b/src/engine-default-operators.mjs @@ -1,6 +1,4 @@ -"use strict"; - -import Operator from "./operator"; +import Operator from "./operator.mjs"; const Operators = []; Operators.push(new Operator("equal", (a, b) => a === b)); diff --git a/src/engine.js b/src/engine.mjs similarity index 97% rename from src/engine.js rename to src/engine.mjs index 45dffcb6..86b1dcd5 100644 --- a/src/engine.js +++ b/src/engine.mjs @@ -1,14 +1,12 @@ -"use strict"; - -import Fact from "./fact"; -import Rule from "./rule"; -import Almanac from "./almanac"; +import Fact from "./fact.mjs"; +import Rule from "./rule.mjs"; +import Almanac from "./almanac.mjs"; import EventEmitter from "eventemitter2"; -import defaultOperators from "./engine-default-operators"; -import defaultDecorators from "./engine-default-operator-decorators"; -import debug from "./debug"; -import Condition from "./condition"; -import OperatorMap from "./operator-map"; +import defaultOperators from "./engine-default-operators.mjs"; +import defaultDecorators from "./engine-default-operator-decorators.mjs"; +import debug from "./debug.mjs"; +import Condition from "./condition.mjs"; +import OperatorMap from "./operator-map.mjs"; export const READY = "READY"; export const RUNNING = "RUNNING"; diff --git a/src/errors.js b/src/errors.mjs similarity index 90% rename from src/errors.js rename to src/errors.mjs index df345f03..6a149906 100644 --- a/src/errors.js +++ b/src/errors.mjs @@ -1,5 +1,3 @@ -"use strict"; - export class UndefinedFactError extends Error { constructor(...props) { super(...props); diff --git a/src/fact.js b/src/fact.mjs similarity index 99% rename from src/fact.js rename to src/fact.mjs index c1e72aeb..2a8cf19d 100644 --- a/src/fact.js +++ b/src/fact.mjs @@ -1,5 +1,3 @@ -"use strict"; - import hash from "hash-it"; class Fact { diff --git a/src/index.js b/src/index.js deleted file mode 100644 index eb82cd8a..00000000 --- a/src/index.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; - -module.exports = require("./json-rules-engine"); diff --git a/src/index.mjs b/src/index.mjs new file mode 100644 index 00000000..c5eb2bf5 --- /dev/null +++ b/src/index.mjs @@ -0,0 +1,11 @@ +import Engine from "./engine.mjs"; +import Fact from "./fact.mjs"; +import Rule from "./rule.mjs"; +import Operator from "./operator.mjs"; +import Almanac from "./almanac.mjs"; +import OperatorDecorator from "./operator-decorator.mjs"; + +export { Fact, Rule, Operator, Engine, Almanac, OperatorDecorator }; +export default function (rules, options) { + return new Engine(rules, options); +} diff --git a/src/json-rules-engine.js b/src/json-rules-engine.js deleted file mode 100644 index 68962583..00000000 --- a/src/json-rules-engine.js +++ /dev/null @@ -1,11 +0,0 @@ -import Engine from "./engine"; -import Fact from "./fact"; -import Rule from "./rule"; -import Operator from "./operator"; -import Almanac from "./almanac"; -import OperatorDecorator from "./operator-decorator"; - -export { Fact, Rule, Operator, Engine, Almanac, OperatorDecorator }; -export default function (rules, options) { - return new Engine(rules, options); -} diff --git a/src/operator-decorator.js b/src/operator-decorator.mjs similarity index 96% rename from src/operator-decorator.js rename to src/operator-decorator.mjs index 27cd98d9..b66f8867 100644 --- a/src/operator-decorator.js +++ b/src/operator-decorator.mjs @@ -1,6 +1,4 @@ -"use strict"; - -import Operator from "./operator"; +import Operator from "./operator.mjs"; export default class OperatorDecorator { /** diff --git a/src/operator-map.js b/src/operator-map.mjs similarity index 97% rename from src/operator-map.js rename to src/operator-map.mjs index 8bc2a3e7..9fe55b64 100644 --- a/src/operator-map.js +++ b/src/operator-map.mjs @@ -1,8 +1,6 @@ -"use strict"; - -import Operator from "./operator"; -import OperatorDecorator from "./operator-decorator"; -import debug from "./debug"; +import Operator from "./operator.mjs"; +import OperatorDecorator from "./operator-decorator.mjs"; +import debug from "./debug.mjs"; export default class OperatorMap { constructor() { diff --git a/src/operator.js b/src/operator.mjs similarity index 98% rename from src/operator.js rename to src/operator.mjs index b01281a9..66664aaa 100644 --- a/src/operator.js +++ b/src/operator.mjs @@ -1,5 +1,3 @@ -"use strict"; - export default class Operator { /** * Constructor diff --git a/src/rule-result.js b/src/rule-result.mjs similarity index 98% rename from src/rule-result.js rename to src/rule-result.mjs index c67b8448..3ee5c83d 100644 --- a/src/rule-result.js +++ b/src/rule-result.mjs @@ -1,5 +1,3 @@ -"use strict"; - import deepClone from "clone"; export default class RuleResult { diff --git a/src/rule.js b/src/rule.mjs similarity index 99% rename from src/rule.js rename to src/rule.mjs index 4fc5e97d..922b5707 100644 --- a/src/rule.js +++ b/src/rule.mjs @@ -1,8 +1,6 @@ -"use strict"; - -import Condition from "./condition"; -import RuleResult from "./rule-result"; -import debug from "./debug"; +import Condition from "./condition.mjs"; +import RuleResult from "./rule-result.mjs"; +import debug from "./debug.mjs"; import deepClone from "clone"; import EventEmitter from "eventemitter2"; diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 00000000..4f77f3ce --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.mjs"], + sourcemap: true, + format: ["esm", "cjs"], + target: ["es2015"], + cjsInterop: true, +});