diff --git a/README.md b/README.md index 6efe1e6..fa93d5c 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,9 @@ const rools = new Rools(); await rools.register([ruleMoodGreat, ruleGoWalking]); await rools.evaluate(facts); ``` + These are the resulting facts: + ```javascript { user: { name: 'frank', stars: 347, mood: 'great' }, weather: { temperature: 20, windy: true, rainy: false }, @@ -105,10 +107,11 @@ or through getters and setters if applicable, e.g., `facts.user.getSalery()`. ### Conflict resolution If there is more than one rule ready to fire, i.e., the conflict set is greater 1, the following conflict resolution strategies are applied (by default, in this order): - * Refraction -- Each rule will fire only once, at most, during any one match-resolve-act cycle. - * Priority -- Rules with higher priority will fire first. Set the rule's property `priority` to an integer value. Default priority is `0`. Negative values are supported. - * Specificity -- Rules which are more specific will fire first. For example, there is rule R1 with premises P1 and P2, and rule R2 with premises P1, P2 and P3. R2 is more specific than R1 and will fire first. R2 is more specific than R1 because it has *all* premises of R1 and additional ones. - * Order of rules -- The rules that were registered first will fire first. + +* Refraction -- Each rule will fire only once, at most, during any one match-resolve-act cycle. +* Priority -- Rules with higher priority will fire first. Set the rule's property `priority` to an integer value. Default priority is `0`. Negative values are supported. +* Specificity -- Rules which are more specific will fire first. For example, there is rule R1 with premises P1 and P2, and rule R2 with premises P1, P2 and P3. R2 is more specific than R1 and will fire first. R2 is more specific than R1 because it has *all* premises of R1 and additional ones. +* Order of rules -- The rules that were registered first will fire first. ### Final rules @@ -122,6 +125,7 @@ While premises (`when`) are always working synchronously on the facts, actions (`then`) can be synchronous or asynchronous. Example: asynchronous action using async/await + ```javascript const rule = new Rule({ name: 'check availability', @@ -133,6 +137,7 @@ const rule = new Rule({ ``` Example: asynchronous action using promises + ```javascript const rule = new Rule({ name: 'check availability', @@ -152,6 +157,7 @@ The extended rule simply inherits all the premises from its parents (and their p Use the rule's `extend` property to set its parents. Example: extended rule + ```javascript const baseRule = new Rule({ name: 'user lives in Germany', @@ -180,6 +186,7 @@ However, if that solves your needs, you can consecutively run different sets of Rules in different instances of Rools are perfectly isolated and can, of course, run against the same facts. Example: evaluate different sets of rules on the same facts + ```javascript const facts = {...}; const rools1 = new Rools(); @@ -207,6 +214,7 @@ You are free to use references or just to repeat the same premise. Both options are working fine. Example 1: by reference + ```javascript const isApplicable = (facts) => facts.user.salery >= 2000; const rule1 = new Rule({ @@ -226,6 +234,7 @@ const rule2 = new Rule({ ``` Example 2: repeat premise + ```javascript const rule1 = new Rule({ when: [ @@ -312,6 +321,7 @@ Calling `new Rools()` creates a new Rools instance, i.e., a new rule engine. You usually do this once for a given set of rules. Example: + ```javascript const { Rools } = require('rools'); const rools = new Rools(); @@ -344,6 +354,7 @@ New rules will become effective immediately. If this promise is rejected, the affected Rools instance is inconsistent and should no longer be used. Example: + ```javascript const { Rools, Rule } = require('rools'); const ruleMoodGreat = new Rule({ @@ -372,6 +383,7 @@ await rools.register([ruleMoodGreat, ruleGoWalking]); Facts are plain JavaScript or JSON objects or objects from ES6 classes with getters and setters. For example: + ```javascript const user = { name: 'frank', @@ -395,10 +407,12 @@ If a premise (`when`) fails, `evaluate()` will still *not* fail (for robustness If an action (`then`) fails, `evaluate()` will reject its promise. If there is more than one rule ready to fire, Rools applies a *conflict resolution strategy* to decide which rule/action to fire first. The default conflict resolution strategy is 'ps'. - * 'ps' -- (1) priority, (2) specificity, (3) order of registration - * 'sp' -- (1) specificity, (2) priority, (3) order of registration + +* 'ps' -- (1) priority, (2) specificity, (3) order of registration +* 'sp' -- (1) specificity, (2) priority, (3) order of registration If you don't like the default 'ps', you can change the conflict resolution strategy like this: + ```javascript await rools.evaluate(facts, { strategy: 'sp' }); ``` @@ -432,6 +446,28 @@ const rools = new Rools({ The error log reports failed actions or premises. The debug log reports the entire evaluation process for debugging purposes. +## TypeScript + +This package provides types for TypeScript. + +```typescript +import { Rools, Rule } from "rools"; + +// ... +``` + +For this module to work, your TypeScript compiler options must include: + +```json +{ + "compilerOptions": { + "target": "ES2015", // or later + "moduleResolution": "node", + "esModuleInterop": true + } +} +``` + ## Migration ### Version 1.x.x to Version 2.x.x diff --git a/package-lock.json b/package-lock.json index f4741c7..0fc594c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -391,12 +391,30 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "@types/chai": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.14.tgz", + "integrity": "sha512-G+ITQPXkwTrslfG5L/BksmbLUA0M1iybEsmCWPqzSxsRRhJZimBKJkoMi8fr/CPygPTj4zO5pJH7I2/cm9M7SQ==", + "dev": true + }, "@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/mocha": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.2.0.tgz", + "integrity": "sha512-/Sge3BymXo4lKc31C8OINJgXLaw+7vL1/L1pGiBNpGrBiT8FQiaFpSYV0uhTaG4y78vcMBTMFsWaHDvuD+xGzQ==", + "dev": true + }, + "@types/node": { + "version": "14.14.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.19.tgz", + "integrity": "sha512-4nhBPStMK04rruRVtVc6cDqhu7S9GZai0fpXgPXrFpcPX6Xul8xnrjSdGB4KPBVYG/R5+fXWdCM8qBoiULWGPQ==", + "dev": true + }, "@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", @@ -483,6 +501,12 @@ "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -612,6 +636,12 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, "caching-transform": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", @@ -894,6 +924,12 @@ "request": "^2.88.2" } }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2185,6 +2221,12 @@ } } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -3095,6 +3137,24 @@ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "spawn-wrap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", @@ -3279,6 +3339,20 @@ "punycode": "^2.1.1" } }, + "ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + } + }, "tsconfig-paths": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", @@ -3336,6 +3410,12 @@ "is-typedarray": "^1.0.0" } }, + "typescript": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", + "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==", + "dev": true + }, "uniqueid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/uniqueid/-/uniqueid-1.0.0.tgz", @@ -3670,6 +3750,12 @@ } } }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index a048dda..68d903f 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ ], "scripts": { "lint": "eslint . --ignore-path ./.eslintignore", - "test": "NODE_ENV=test nyc --reporter=lcov --reporter=text-summary mocha --exit --recursive test", + "test": "npm run test:unit && npm run test:typescript", + "test:unit": "nyc --reporter=lcov --reporter=text-summary mocha --exit --recursive test/**/*.spec.js", + "test:typescript": "mocha -r ts-node/register test/typescript/**/*.spec.ts", "coveralls": "nyc report --reporter=lcovonly && cat ./coverage/lcov.info | coveralls", "preversion": "npm run lint && npm test" }, @@ -28,6 +30,9 @@ "node": ">=10.x.x" }, "devDependencies": { + "@types/chai": "^4.2.14", + "@types/mocha": "^8.2.0", + "@types/node": "^14.14.19", "chai": "^4.2.0", "chai-as-promised": "^7.1.1", "coveralls": "^3.1.0", @@ -39,7 +44,9 @@ "mocha": "^8.2.1", "nyc": "^15.1.0", "sinon": "^9.2.2", - "sinon-chai": "^3.5.0" + "sinon-chai": "^3.5.0", + "ts-node": "^9.1.1", + "typescript": "^4.1.3" }, "dependencies": { "arrify": "^2.0.1", diff --git a/test/typescript/typescript.spec.ts b/test/typescript/typescript.spec.ts new file mode 100644 index 0000000..7ce2df4 --- /dev/null +++ b/test/typescript/typescript.spec.ts @@ -0,0 +1,25 @@ +import 'mocha'; +import { expect } from 'chai'; +import { Rools, Rule } from "../.."; + +describe('Integration TypeScript', () => { + it('should work', async () => { + const facts: any = { + foo: false, + bar: true, + }; + const rools = new Rools(); + const rule = new Rule({ + name: 'rule', + when: (facts: any) => { + return true + }, + then: (facts: any) => { + facts.foo = true; + }, + }); + await rools.register([rule]); + await rools.evaluate(facts); + expect(facts.foo).to.be.equal(true); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..089173b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "target": "ES2015", /* 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + } +}