Skip to content

Commit

Permalink
Merge pull request #1 from mattryavec/contains-and-contains-any
Browse files Browse the repository at this point in the history
New 'contains' and 'contains any' operators
  • Loading branch information
andrewbrg authored Oct 3, 2023
2 parents 517f473 + 1ef36dc commit 5509134
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 8 deletions.
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ 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`, and `NotIn` operators
- Supports `Equal`, `NotEqual`, `GreaterThan`, `GreaterThanOrEqual`, `LessThan`, `LessThanOrEqual`, `In`, `NotIn`, `Contains`, and `ContainsAny` operators

## Usage

Expand Down Expand Up @@ -501,6 +501,21 @@ There are three (3) types of conditions which can be used in a rule:

Condition types can be mixed and matched or nested to create complex rules.

#### Operators

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 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)

### Criteria With Nested Properties

In some cases, the criteria which is used to evaluate a rule might be more complex objects with nested properties.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "rulepilot",
"version": "1.1.12",
"version": "1.2.0",
"description": "Rule parsing engine for JSON rules",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"badges": "jest-badges-readme",
"test": "jest --testPathPattern=test --color --forceExit",
"test": "jest --testPathPattern=test --detectOpenHandles --color --forceExit",
"build": "rm -rf dist && tsc",
"prettier": "prettier --write ."
},
Expand Down
10 changes: 10 additions & 0 deletions src/services/evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,16 @@ export class Evaluator {
!Array.isArray(constraint.value) ||
!constraint.value.includes(criterion as never)
);
case "contains":
return (
Array.isArray(criterion) &&
criterion.includes(constraint.value)
);
case "contains any":
return (
Array.isArray(constraint.value) &&
constraint.value.some(x => criterion.includes(x))
);
default:
return false;
}
Expand Down
6 changes: 3 additions & 3 deletions src/services/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export class Validator {
};
}

const operators = ["==", "!=", ">", "<", ">=", "<=", "in", "not in"];
const operators: Operator[] = ["==", "!=", ">", "<", ">=", "<=", "in", "not in", "contains", "contains any"];
if (!operators.includes(constraint.operator as Operator)) {
return {
isValid: false,
Expand All @@ -164,14 +164,14 @@ export class Validator {

// We must check that the value is an array if the operator is 'in' or 'not in'.
if (
["in", "not in"].includes(constraint.operator) &&
["in", "not in", "contains any"].includes(constraint.operator) &&
!Array.isArray(constraint.value)
) {
return {
isValid: false,
error: {
message:
'Constraint "value" must be an array if the "operator" is "in" or "not in"',
'Constraint "value" must be an array if the "operator" is "in", "not in", or "contains any"',
element: constraint,
},
};
Expand Down
2 changes: 1 addition & 1 deletion src/types/rule.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type ConditionType = "any" | "all" | "none";
export type Operator = "==" | "!=" | ">" | "<" | ">=" | "<=" | "in" | "not in";
export type Operator = "==" | "!=" | ">" | "<" | ">=" | "<=" | "in" | "not in" | "contains" | "contains any";

export interface Constraint {
field: string;
Expand Down
23 changes: 23 additions & 0 deletions test/engine.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { valid1Json } from "./rulesets/valid1.json";
import { valid2Json } from "./rulesets/valid2.json";
import { valid3Json } from "./rulesets/valid3.json";
import { valid4Json } from "./rulesets/valid4.json";
import { valid5Json } from "./rulesets/valid5.json";

import { invalid1Json } from "./rulesets/invalid1.json";

Expand Down Expand Up @@ -188,4 +189,26 @@ describe("RulePilot engine correctly", () => {
})
).toEqual(true);
});

it("Evaluates a simple ruleset with a Contains and ContainsAny any condition", async () => {
expect(
await RulePilot.evaluate(valid5Json, { countries: ["US", "FR"] })
).toEqual(true);

expect(
await RulePilot.evaluate(valid5Json, { countries: ["GB", "DE"] })
).toEqual(false);

expect(
await RulePilot.evaluate(valid5Json, { states: ["CA", "TN"] })
).toEqual(true);

expect(
await RulePilot.evaluate(valid5Json, { states: ["NY", "WI"] })
).toEqual(false);

expect(
await RulePilot.evaluate(valid5Json, { states: "invalid criterion type" })
).toEqual(false);
});
});
18 changes: 18 additions & 0 deletions test/rulesets/valid5.json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Rule } from "../../src";

export const valid5Json: Rule = {
conditions: {
any: [
{
field: "countries",
operator: "contains",
value: "US"
},
{
field: "states",
operator: "contains any",
value: ["KY", "TN"]
},
],
},
};
10 changes: 9 additions & 1 deletion test/validator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ describe("RulePilot validator correctly", () => {
);
});

it("Identifies invalid values for In/Not In operators", () => {
it("Identifies invalid values for In/NotIn/ContainsAny operators", () => {
expect(
RulePilot.validate({
conditions: [
Expand All @@ -124,6 +124,14 @@ describe("RulePilot validator correctly", () => {
],
}).isValid
).toEqual(false);

expect(
RulePilot.validate({
conditions: [
{ all: [{ field: "name", operator: "contains any", value: "test" }] },
],
}).isValid
).toEqual(false);
});

it("Validates a correct rule", () => {
Expand Down

0 comments on commit 5509134

Please sign in to comment.