@clipboard-health/rules-engine 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # @clipboard-health/rules-engine
2
+
3
+ A pure functional rules engine with two rule runners:
4
+
5
+ - `firstMatch`: Runs the first rule that matches the criteria
6
+ - `all`: Runs all the rules that matches the criteria
7
+
8
+ ## Table of Contents
9
+
10
+ - [Install](#install)
11
+ - [Usage](#usage)
12
+ - [Local development commands](#local-development-commands)
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install @clipboard-health/rules-engine
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```ts
23
+ // ./examples/rules.ts
24
+
25
+ import {
26
+ all,
27
+ appendOutput,
28
+ firstMatch,
29
+ type Rule,
30
+ type RuleContext,
31
+ } from "@clipboard-health/rules-engine";
32
+
33
+ interface Input {
34
+ number1: number;
35
+ number2: number;
36
+ }
37
+
38
+ interface Output {
39
+ result: number;
40
+ }
41
+
42
+ const exampleContext: RuleContext<Input, Output> = {
43
+ input: {
44
+ number1: 2,
45
+ number2: 5,
46
+ },
47
+ output: [],
48
+ };
49
+
50
+ const addNumbersIfPositiveRule: Rule<Input, Output> = {
51
+ runIf: (input) => input.number1 > 0 && input.number2 > 0,
52
+ run: (context) => {
53
+ const { number1, number2 } = context.input;
54
+ const sum = number1 + number2;
55
+ return appendOutput(context, { result: sum });
56
+ },
57
+ };
58
+
59
+ const multiplyNumbersIfPositiveRule: Rule<Input, Output> = {
60
+ runIf: (input) => input.number1 > 0 && input.number2 > 0,
61
+ run: (context) => {
62
+ const { number1, number2 } = context.input;
63
+ const sum = number1 * number2;
64
+ return appendOutput(context, { result: sum });
65
+ },
66
+ };
67
+
68
+ const divideNumbersIfNegative: Rule<Input, Output> = {
69
+ runIf: (input) => input.number1 < 0 && input.number2 < 0,
70
+ run: (context) => {
71
+ const { number1, number2 } = context.input;
72
+ const sum = number1 * number2;
73
+ return appendOutput(context, { result: sum });
74
+ },
75
+ };
76
+
77
+ // Using all() applies all the rules to the context
78
+ const allResult = all(
79
+ addNumbersIfPositiveRule,
80
+ divideNumbersIfNegative,
81
+ multiplyNumbersIfPositiveRule,
82
+ ).run(exampleContext);
83
+
84
+ console.log(allResult.output);
85
+ // => [{ result: 7 }, { result: 10 }]
86
+
87
+ // Using firstMatch() applies the first the rules to the context
88
+ const firstMatchResult = firstMatch(
89
+ divideNumbersIfNegative,
90
+ addNumbersIfPositiveRule,
91
+ multiplyNumbersIfPositiveRule,
92
+ ).run(exampleContext);
93
+
94
+ console.log(firstMatchResult.output);
95
+ // => [{ result: 7 }]
96
+ ```
97
+
98
+ ## Local development commands
99
+
100
+ See [`package.json`](./package.json) `scripts` for a list of commands.
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@clipboard-health/rules-engine",
3
+ "description": "A pure functional rules engine.",
4
+ "version": "0.0.0",
5
+ "bugs": "https://github.com/clipboardhealth/core-utils/issues",
6
+ "dependencies": {
7
+ "tslib": "2.6.3",
8
+ "type-fest": "4.25.0"
9
+ },
10
+ "keywords": [],
11
+ "license": "MIT",
12
+ "main": "./src/index.js",
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/clipboardhealth/core-utils.git",
19
+ "directory": "packages/rules-engine"
20
+ },
21
+ "scripts": {
22
+ "embed": "embedme README.md"
23
+ },
24
+ "type": "commonjs",
25
+ "typings": "./src/index.d.ts",
26
+ "types": "./src/index.d.ts"
27
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./lib/appendOutput";
2
+ export * from "./lib/rule";
3
+ export * from "./lib/runners";
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./lib/appendOutput"), exports);
5
+ tslib_1.__exportStar(require("./lib/rule"), exports);
6
+ tslib_1.__exportStar(require("./lib/runners"), exports);
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../packages/rules-engine/src/index.ts"],"names":[],"mappings":";;;AAAA,6DAAmC;AACnC,qDAA2B;AAC3B,wDAA8B"}
@@ -0,0 +1,6 @@
1
+ import { type ReadonlyDeep } from "type-fest";
2
+ import { type RuleContext } from "./rule";
3
+ /**
4
+ * Rule output is immutable, do not modify existing items, only append using this function.
5
+ */
6
+ export declare function appendOutput<TInput, TOutput>(context: RuleContext<TInput, TOutput>, output: ReadonlyDeep<TOutput>): RuleContext<TInput, TOutput>;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.appendOutput = appendOutput;
4
+ /**
5
+ * Rule output is immutable, do not modify existing items, only append using this function.
6
+ */
7
+ function appendOutput(context, output) {
8
+ return {
9
+ input: context.input,
10
+ output: [...context.output, output],
11
+ };
12
+ }
13
+ //# sourceMappingURL=appendOutput.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appendOutput.js","sourceRoot":"","sources":["../../../../../packages/rules-engine/src/lib/appendOutput.ts"],"names":[],"mappings":";;AAOA,oCAQC;AAXD;;GAEG;AACH,SAAgB,YAAY,CAC1B,OAAqC,EACrC,MAA6B;IAE7B,OAAO;QACL,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC;KACpC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { type ReadonlyDeep } from "type-fest";
2
+ export interface RuleContext<TInput, TOutput> {
3
+ /**
4
+ * Input is immutable, rules must not modify it.
5
+ */
6
+ input: ReadonlyDeep<TInput>;
7
+ /**
8
+ * Output is immutable, do not modify existing items, only append.
9
+ *
10
+ * @see {@link appendOutput}
11
+ */
12
+ output: ReadonlyArray<ReadonlyDeep<TOutput>>;
13
+ }
14
+ export interface Rule<TInput, TOutput, TContext extends RuleContext<TInput, TOutput> = RuleContext<TInput, TOutput>> {
15
+ /**
16
+ * Returns whether the rule should run or not.
17
+ *
18
+ * Only the `input` and not the full `context` is passed to `runIf`. This prevents rules from
19
+ * relying on `output`, which may be appended to by previous rules by the time `run` is called.
20
+ */
21
+ runIf: (input: ReadonlyDeep<TInput>) => boolean;
22
+ /**
23
+ * A pure function that runs rule logic and returns a new context by appending to the output
24
+ * array.
25
+ *
26
+ * @see {@link appendOutput}
27
+ */
28
+ run: (context: TContext) => TContext;
29
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=rule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule.js","sourceRoot":"","sources":["../../../../../packages/rules-engine/src/lib/rule.ts"],"names":[],"mappings":""}
@@ -0,0 +1,7 @@
1
+ import { type Rule, type RuleContext } from "../rule";
2
+ /**
3
+ * Run all rules that return true for `runIf`.
4
+ *
5
+ * @param rules The rules to run.
6
+ */
7
+ export declare function all<TInput, TOutput, TContext extends RuleContext<TInput, TOutput>>(...rules: Array<Rule<TInput, TOutput, TContext>>): Rule<TInput, TOutput, TContext>;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.all = all;
4
+ /**
5
+ * Run all rules that return true for `runIf`.
6
+ *
7
+ * @param rules The rules to run.
8
+ */
9
+ function all(...rules) {
10
+ return {
11
+ runIf: (input) => rules.some((rule) => rule.runIf(input)),
12
+ run: (context) => rules
13
+ .filter((rule) => rule.runIf(context.input))
14
+ .reduce((previousContext, rule) => rule.run(previousContext), context),
15
+ };
16
+ }
17
+ //# sourceMappingURL=all.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"all.js","sourceRoot":"","sources":["../../../../../../packages/rules-engine/src/lib/runners/all.ts"],"names":[],"mappings":";;AAOA,kBAUC;AAfD;;;;GAIG;AACH,SAAgB,GAAG,CACjB,GAAG,KAA6C;IAEhD,OAAO;QACL,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACzD,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE,CACf,KAAK;aACF,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;aAC3C,MAAM,CAAC,CAAC,eAAe,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;KAC3E,CAAC;AACJ,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { type Rule, type RuleContext } from "../rule";
2
+ /**
3
+ * Run the first rule that returns true for `runIf`.
4
+ *
5
+ * @param rules The rules to run.
6
+ */
7
+ export declare function firstMatch<TInput, TOutput, TContext extends RuleContext<TInput, TOutput>>(...rules: Array<Rule<TInput, TOutput, TContext>>): Rule<TInput, TOutput, TContext>;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.firstMatch = firstMatch;
4
+ /**
5
+ * Run the first rule that returns true for `runIf`.
6
+ *
7
+ * @param rules The rules to run.
8
+ */
9
+ function firstMatch(...rules) {
10
+ return {
11
+ runIf: (input) => rules.some((rule) => rule.runIf(input)),
12
+ run: (context) => {
13
+ const rule = rules.find((rule) => rule.runIf(context.input));
14
+ return rule ? rule.run(context) : context;
15
+ },
16
+ };
17
+ }
18
+ //# sourceMappingURL=firstMatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"firstMatch.js","sourceRoot":"","sources":["../../../../../../packages/rules-engine/src/lib/runners/firstMatch.ts"],"names":[],"mappings":";;AAOA,gCAUC;AAfD;;;;GAIG;AACH,SAAgB,UAAU,CACxB,GAAG,KAA6C;IAEhD,OAAO;QACL,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACzD,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE;YACf,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;YAC7D,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;QAC5C,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from "./all";
2
+ export * from "./firstMatch";
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./all"), exports);
5
+ tslib_1.__exportStar(require("./firstMatch"), exports);
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../packages/rules-engine/src/lib/runners/index.ts"],"names":[],"mappings":";;;AAAA,gDAAsB;AACtB,uDAA6B"}