Skip to content

Commit

Permalink
Merge pull request #6 from andrewbrg/feat-introspection-v2
Browse files Browse the repository at this point in the history
feat(core): New sub-rule format & better introspection
  • Loading branch information
andrewbrg authored Nov 15, 2024
2 parents 61cc80a + 988e9ab commit 3e508ba
Show file tree
Hide file tree
Showing 25 changed files with 2,012 additions and 1,120 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ module.exports = {
},
},
"newlines-between": "always",
"max-line-length": 80,
"max-line-length": 140,
},
],
"perfectionist/sort-named-imports": [
Expand Down
21 changes: 15 additions & 6 deletions .github/workflows/jest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,19 @@ jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ArtiomTr/jest-coverage-report-action@v2
- name: Checkout repository
uses: actions/checkout@v2

- name: Set up Node.js
uses: actions/setup-node@v3
with:
test-script: yarn jest --testPathPattern=test --color --forceExit --coverage
- run: yarn
- run: yarn build
- run: yarn test
node-version: '20'

- name: Install dependencies
run: yarn install

- name: Install dependencies
run: yarn build

- name: Run Jest tests
run: yarn test
2 changes: 1 addition & 1 deletion .github/workflows/npm-publish-github-packages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20
- run: yarn
- run: yarn build
- run: yarn test
Expand Down
107 changes: 47 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,17 +428,15 @@ let result = await RulePilot.evaluate(rule, criteria); // result = [true, false]

### Sub Rules

Sub-rules can be used to create early exit points rules by assigning a `rule` property to a condition. This rule property
will contain a new rule in itself which will be evaluated if the constraints of the host condition are met.
Sub-rules can be used to create early exit points in a rule by assigning a `result` property to a condition.

Sub-rules do not need to evaluate to true for their parent condition to pass evaluation. They contain their own result
which will be returned if the following conditions are met:
This value of this result will be returned when the rule is evaluated if all the constraints from the root of the rule
to this result are met.

- The parent condition (all criteria and/or nested rules) are met
- The sub-rule is met
> Note: Sub-rules do not need to evaluate to true for their parent condition to pass evaluation.
They provide a convenient way to create complex rules with early exit points and avoid repeating the same constraints
in multiple places.
Sub-rules provide a convenient way to create complex rules with early exit points and avoid repeating the same
constraints in multiple places.

An example of a sub-rule can be seen below:

Expand All @@ -449,18 +447,8 @@ const rule: Rule = {
conditions: {
any: [
{ field: "profile.age", operator: ">=", value: 18 },
{
rule: {
conditions: { all: [{ field: "foo", operator: "==", value: 'A' }] },
result: 10
}
},
{
rule: {
conditions: { all: [{ field: "foo", operator: "==", value: 'B' }] },
result: 20
},
}
{ all: [{ field: "foo", operator: "==", value: 'A' }], result: 10 },
{ all: [{ field: "foo", operator: "==", value: 'B' }], result: 20 }
],
result: 5
}
Expand Down Expand Up @@ -597,11 +585,13 @@ process.env.DEBUG = "true";

Rule introspection is built into `RulePilot` and can be used to inspect a rule and get information about it.

when `RulePilot` introspects a rule it attempts to determine, for each distinct result set in the rule, the
distribution of inputs which will satisfy the rule in a manner which evaluates to said result.
When `RulePilot` introspects a rule it attempts to determine, for each distinct result in the rule, the
distribution of inputs which will satisfy the rule resolving to said result provided an input constraint.

For example, using introspection we can ask Rule Pilot the following question:

What this means is that given any rule, the introspection feature will return all the possible input combinations
which the rule can be evaluated against which will result in all the possible outputs the rule can have.
> Given rule A, If I evaluate the rule with a value X = 100, what are all the possible values of Y for which the rule will
> evaluate to a result, and what results would the rule evaluate to?
This is a useful feature when you want to know what inputs will result in a specific output, or what inputs will result
in a specific output distribution.
Expand Down Expand Up @@ -641,39 +631,44 @@ const rule: Rule = {
},
],
};
```

// Intropect the rule
const introspection = RulePilot.introspect(rule);
We can introspect the rule to determine what countries are available to get a discount if the user has a coupon, and what
the discount amount would be in each case.

```typescript
const subjects = ["country"];
const constraint = { field: "hasCoupon", value: true };
const introspection = RulePilot.introspect(rule, constraint, subjects);
```

The following will be returned in the `introspection` variable:
The following will be returned by the `introspection`:

```json
{
"results": [
{
"result": 5,
"options": [
{ "country": "SE" },
{ "country": ["GB", "FI"], "hasCoupon": true, "totalCheckoutPrice": { "operator": ">=", "value": 120 } }
]
},
{
"result": 10,
"options": [
{ "age": { "operator": ">=", "value": 18 }, "hasStudentCard": true }
]
}
]
"5": {
"country": [{ "value": "SE", "operator": "==" }, { "value": ["GB", "FI"], "operator": "in" }]
}
}
```

Each possible result that the rule can evaluate to is returned in the `results` array, along with the possible inputs
which will result in that result in the `options` array.
```typescript
const subjects = ["country"];
const constraint = { field: "totalCheckoutPrice", value: 100 };
const introspection = RulePilot.introspect(rule, constraint, subjects);
```

The following will be returned by the `introspection`:

```json
{
"5": {
"country": [{ "value": "SE", "operator": "==" }]
}
}
```

Each object in the `options` array is a set of criteria which must be met in order for the rule to evaluate to the
result, we can consider the list of objects in the `options` as an `OR` and the criteria inside each object as
an `AND`.
Each object in the `response` criteria which are possible inputs for the rule to evaluate to the result provided.

Although calculating such results might seem trivial, it can be in fact be quite a complex thing to do especially when
dealing with complex rules with multiple nested conditions comprised of many different operators.
Expand Down Expand Up @@ -738,18 +733,6 @@ import { RulePilot, Rule } from "rulepilot";

const builder = RulePilot.builder();

const subRule = builder.subRule();
subRule.add(
subRule.condition(
"all",
[
subRule.constraint("color", "==", "green"),
subRule.constraint("discount", ">", 20),
],
{ price: 10 }
)
);

const rule: Rule = builder
.add(
builder.condition(
Expand All @@ -758,11 +741,15 @@ const rule: Rule = builder
builder.condition("any", [
builder.constraint("size", "==", "medium"),
builder.constraint("weight", ">=", 2),
builder.condition(
"all",
[builder.constraint("color", "==", "green"), builder.constraint("discount", ">", 20)],
{ price: 10 }
)
]),
builder.constraint("category", "not in", ["free", "out of stock"]),
],
{ price: 20 },
subRule
)
)
.add(
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"name": "rulepilot",
"version": "1.3.1",
"version": "1.4.0",
"description": "Rule parsing engine for JSON rules",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"lint": "eslint \"{src,test}/**/*.ts\" --fix",
"badges": "jest-badges-readme",
"test": "jest --testPathPattern=test --detectOpenHandles --color --forceExit",
"test": "jest --testPathPattern=test --color --forceExit",
"build": "rm -rf dist && tsc",
"prettier": "prettier --write .",
"publish": "npm run build && npm publish --access public"
Expand Down
2 changes: 1 addition & 1 deletion prettier.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"printWidth": 80,
"printWidth": 140,
"singleQuote": true,
"trailingComma": "all"
}
13 changes: 1 addition & 12 deletions src/builder/builder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { RuleError } from "../errors";
import { Validator } from "../services";
import { SubRuleBuilder } from "./sub-rule-builder";
import { Rule, Operator, Condition, Constraint, ConditionType } from "../types";

export class Builder {
Expand Down Expand Up @@ -28,17 +27,14 @@ export class Builder {
* @param type The type of condition
* @param nodes Any child nodes of the condition
* @param result The result of the condition node (for granular rules)
* @param subRule A sub-rule to apply to the condition
*/
condition(
type: ConditionType,
nodes: Condition[ConditionType],
result?: Condition["result"],
subRule?: SubRuleBuilder
result?: Condition["result"]
): Condition {
return {
[type]: nodes,
...(subRule ? { rule: subRule.build() } : {}),
...(result ? { result } : {}),
};
}
Expand Down Expand Up @@ -83,11 +79,4 @@ export class Builder {

throw new RuleError(validationResult);
}

/**
* Creates a new sub-rule builder
*/
subRule(): SubRuleBuilder {
return new SubRuleBuilder();
}
}
1 change: 0 additions & 1 deletion src/builder/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from "./builder";
export * from "./sub-rule-builder";
75 changes: 0 additions & 75 deletions src/builder/sub-rule-builder.ts

This file was deleted.

Loading

0 comments on commit 3e508ba

Please sign in to comment.