Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introspection #3

Merged
merged 13 commits into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 130 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<img src=".github/logo.png" width="190" alt="RulePilot" />

[![npm version](https://badge.fury.io/js/rulepilot.svg)](https://badge.fury.io/js/rulepilot?v1.1.2)
[![npm version](https://badge.fury.io/js/rulepilot.svg)](https://badge.fury.io/js/rulepilot?v1.2.1)

| Statements | Functions | Lines |
| --------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
Expand Down Expand Up @@ -34,7 +34,8 @@ each condition's evaluation.
- Supports Criteria objects with nested properties
- Rule validation & debugging tools
- Supports `Any`, `All`, and `None` type conditions
- Supports `Equal`, `NotEqual`, `GreaterThan`, `GreaterThanOrEqual`, `LessThan`, `LessThanOrEqual`, `In`, `NotIn`, `Contains`, and `ContainsAny` operators
- Supports `Equal`, `NotEqual`, `GreaterThan`, `GreaterThanOrEqual`, `LessThan`, `LessThanOrEqual`, `In`, `NotIn`,
`Contains`, `Not Contains`, `ContainsAny`, `Not ContainsAny`, `Matches` and `Not Matches` operators

## Usage

Expand Down Expand Up @@ -507,14 +508,18 @@ These are the operators available for a constraint and how they are used:

- `==`: Applies JavaScript equality (`==`) operator to criterion and constraint value
- `!=`: Applies JavaScript inequality (`!=`) operator to criterion and constraint value
- `>`: Applies JavaScript greather than (`>`) operator to criterion and constraint value
- `>`: Applies JavaScript greater than (`>`) operator to criterion and constraint value
- `<`: Applies JavaScript less than (`<`) operator to criterion and constraint value
- `>=`: Applies JavaScript greater than or equal (`>=`) operator to criterion and constraint value
- `<=`: Applies JavaScript less than or equal (`<=`) operator to criterion and constraint value
- `in`: Tests if the criterion is an element of the constraint value (value must be an array)
- `not in`: Tests if the criterion is not an element of the constraint value (value must be an array)
- `contains`: Tests if the constraint value is an element of the criterion (criterion must be an array)
- `contains any`: Tests if any element in the constraint value is an element of the criterion (criterion and constraint value must be an array)
- `not contains`: Tests if the constraint value is not an element of the criterion (criterion must be an array)
- `not contains any`: Tests if any element in the constraint value is bot an element of the criterion (criterion and constraint value must be an array)
- `matches`: Tests if the constraint value matches a regular expression (criterion must be a valid regex)
- `not matches`: Tests if the constraint value does not match a regular expression (criterion must be a valid regex)

### Criteria With Nested Properties

Expand Down Expand Up @@ -708,6 +713,128 @@ cause mutations to log debug information to the console.
process.env.DEBUG = "true";
```

## Introspection

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.

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.

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, for example if a `RulePilot` rule is being used to evaluate what type of discount a
user should get, you can use the introspection feature to tell the user what products, quantities, requirements, etc.
they must fulfill in order to obtain each possible discount.

Taking a simple granular rule as an example:

```typescript
import { RulePilot, Rule } from "rulepilot";

const rule: Rule = {
conditions: [
{
any: [
{
all: [
{
field: "country",
operator: "in",
value: ["GB", "FI"],
},
{
field: "hasCoupon",
operator: "==",
value: true,
},
{
field: "totalCheckoutPrice",
operator: ">=",
value: 120.0,
},
],
},
{
field: "country",
operator: "==",
value: "SE",
},
],
result: 5,
},
{
all: [
{
field: "age",
operator: ">=",
value: 18,
},
{
field: "hasStudentCard",
operator: "==",
value: true,
},
],
result: 10,
},
],
};

// Intropect the rule
const introspection = RulePilot.introspect(rule);
```

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

```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
}
]
}
]
}
```

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.

Similarly to mutations, it is possible to enable debug mode on the introspection feature by setting the environment
variable `DEBUG="true"`.

```typescript
process.env.DEBUG = "true";
```

**Note:** Introspection requires a [Granular](#granular-example) rule to be passed to it, otherwise `RulePilot` will
throw an `RuleTypeError`.


## Fluent Rule Builder

Although creating rules in plain JSON is very straightforward, `RulePilot` comes with a `Builder` class which can be
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rulepilot",
"version": "1.2.0",
"version": "1.2.1",
"description": "Rule parsing engine for JSON rules",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -32,7 +32,7 @@
"@olavoparno/jest-badges-readme": "^1.5.1",
"@types/jest": "^29.5.1",
"@types/node": "^18.16.1",
"axios": "^1.4.0",
"axios": "^1.6.0",
"jest": "^29.5.0",
"prettier": "^2.8.8",
"ts-jest": "^29.1.0",
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": 100,
"printWidth": 120,
"singleQuote": true,
"trailingComma": "all"
}
28 changes: 19 additions & 9 deletions src/services/evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ObjectDiscovery } from "./object-discovery";
import { Condition, Constraint, Rule } from "../types/rule";

export class Evaluator {
private _objectDiscovery: ObjectDiscovery = new ObjectDiscovery();
#objectDiscovery: ObjectDiscovery = new ObjectDiscovery();

/**
* Evaluates a rule against a set of criteria and returns the result.
Expand Down Expand Up @@ -63,7 +63,7 @@ export class Evaluator {
*/
private evaluateCondition(condition: Condition, criteria: object): boolean {
// The condition must have an 'any' or 'all' property.
const type = this._objectDiscovery.conditionType(condition);
const type = this.#objectDiscovery.conditionType(condition);
if (!type) {
return false;
}
Expand All @@ -74,11 +74,11 @@ export class Evaluator {

// Check each node in the condition.
for (const node of condition[type]) {
let fn;
if (this._objectDiscovery.isCondition(node)) {
let fn: string;
if (this.#objectDiscovery.isCondition(node)) {
fn = "evaluateCondition";
}
if (this._objectDiscovery.isConstraint(node)) {
if (this.#objectDiscovery.isConstraint(node)) {
fn = "checkConstraint";
}

Expand Down Expand Up @@ -106,7 +106,7 @@ export class Evaluator {
private checkConstraint(constraint: Constraint, criteria: object): boolean {
// If the value contains '.' we should assume it is a nested property
const criterion = constraint.field.includes(".")
? this._objectDiscovery.resolveNestedProperty(constraint.field, criteria)
? this.#objectDiscovery.resolveNestedProperty(constraint.field, criteria)
: criteria[constraint.field];

// If the criteria object does not have the field
Expand Down Expand Up @@ -139,15 +139,25 @@ export class Evaluator {
!constraint.value.includes(criterion as never)
);
case "contains":
return Array.isArray(criterion) && criterion.includes(constraint.value);
case "not contains":
return (
Array.isArray(criterion) &&
criterion.includes(constraint.value)
!Array.isArray(criterion) || !criterion.includes(constraint.value)
);
case "contains any":
return (
Array.isArray(constraint.value) &&
constraint.value.some(x => criterion.includes(x))
constraint.value.some((x) => criterion.includes(x))
);
case "not contains any":
return (
!Array.isArray(constraint.value) ||
!constraint.value.some((x) => criterion.includes(x))
);
case "matches":
return new RegExp(criterion).test(`${constraint.value}`);
case "not matches":
return !new RegExp(criterion).test(`${constraint.value}`);
default:
return false;
}
Expand Down
Loading
Loading