From 1a55d50670b5a1f21bb05a58800f2f24147ff717 Mon Sep 17 00:00:00 2001 From: Matt Ryavec Date: Mon, 2 Oct 2023 14:24:30 -0500 Subject: [PATCH 1/2] New 'contains' and 'contains any' operators --- README.md | 19 +++++++++++++++++-- package.json | 4 ++-- src/services/evaluator.ts | 10 ++++++++++ src/services/validator.ts | 6 +++--- src/types/rule.ts | 2 +- test/engine.spec.ts | 23 +++++++++++++++++++++++ test/rulesets/valid5.json.ts | 18 ++++++++++++++++++ test/validator.spec.ts | 10 +++++++++- 8 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 test/rulesets/valid5.json.ts diff --git a/README.md b/README.md index c66dda3..79ac7c1 100644 --- a/README.md +++ b/README.md @@ -34,8 +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`, and `NotIn` operators - +- Supports `Equal`, `NotEqual`, `GreaterThan`, `GreaterThanOrEqual`, `LessThan`, `LessThanOrEqual`, `In`, `NotIn`, `Contains`, and `ContainsAny` operators +, ## Usage ### Installation @@ -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. diff --git a/package.json b/package.json index b8cce50..3fb2044 100644 --- a/package.json +++ b/package.json @@ -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 ." }, diff --git a/src/services/evaluator.ts b/src/services/evaluator.ts index 2bcbb37..b30913d 100644 --- a/src/services/evaluator.ts +++ b/src/services/evaluator.ts @@ -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; } diff --git a/src/services/validator.ts b/src/services/validator.ts index b25e9c6..60f5189 100644 --- a/src/services/validator.ts +++ b/src/services/validator.ts @@ -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, @@ -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, }, }; diff --git a/src/types/rule.ts b/src/types/rule.ts index 3045245..f4d703d 100644 --- a/src/types/rule.ts +++ b/src/types/rule.ts @@ -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; diff --git a/test/engine.spec.ts b/test/engine.spec.ts index a3c2cfe..bd86162 100644 --- a/test/engine.spec.ts +++ b/test/engine.spec.ts @@ -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"; @@ -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); + }); }); diff --git a/test/rulesets/valid5.json.ts b/test/rulesets/valid5.json.ts new file mode 100644 index 0000000..2051472 --- /dev/null +++ b/test/rulesets/valid5.json.ts @@ -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"] + }, + ], + }, +}; diff --git a/test/validator.spec.ts b/test/validator.spec.ts index 96bcbac..ef29364 100644 --- a/test/validator.spec.ts +++ b/test/validator.spec.ts @@ -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: [ @@ -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", () => { From 1ef36dc92cab989cf922314cc4ba331e997fd969 Mon Sep 17 00:00:00 2001 From: Matt Ryavec Date: Mon, 2 Oct 2023 14:27:24 -0500 Subject: [PATCH 2/2] typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 79ac7c1..5d84abe 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ each condition's evaluation. - Rule validation & debugging tools - Supports `Any`, `All`, and `None` type conditions - Supports `Equal`, `NotEqual`, `GreaterThan`, `GreaterThanOrEqual`, `LessThan`, `LessThanOrEqual`, `In`, `NotIn`, `Contains`, and `ContainsAny` operators -, + ## Usage ### Installation