Skip to content

Commit

Permalink
feat: matches basic operator
Browse files Browse the repository at this point in the history
  • Loading branch information
sdo-1A committed Dec 14, 2023
1 parent a87118c commit 4a64581
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 10 deletions.
10 changes: 9 additions & 1 deletion docs/rules-engine/operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Example of usage :
| Number[] | allRangeNumber | all between | Check if every values of the variable are included in a specified range |
| Number[] | oneRangeNumber | one between | Check if one of the values of the variable is included in a specified range |
| String | inString | within | Check if the text variable is part of the specified value |
| String | matchesPattern | matches | Check if the text variable matches a specific pattern |
| String | notInString | not within | Check if the text variable is not part in the specified value |
| String | stringContains | contains | Check if the specified text value is included in the text variable |
| String | notStringContains | does not contain | Check if the specified text value is not included in the text variable |
Expand All @@ -71,6 +72,14 @@ Example of usage :
| All[] | lengthGreaterThanOrEquals | number of ≥ | Check if the number of values of the variable is greater or equal to a specific value |
| All[] | lengthGreaterThan | number of > | Check if the number of values of the variable is greater than a specific value |

> **Note**: For the operators comparing a text variable to a pattern (such as `matchesPattern`, `oneMatches`, and `allMatch`),
> we support the ES RegExp `/^myRegExp.*$/i` (containing the pattern and optional flags) or just the RegExp content `^myregexp.*$`.
>
> The special characters used in the pattern should contain a double backslash (`\\`).
> For example, to check if a string contains a `\t`, the pattern would need to include `\\t`.
>
> > Also, to avoid the wrong detection of an ES RegExp instead of RegExp content, a content beginning with a slash `/` character
> > (such as a path `/path/to/file`) should be preceded by a double backslash `\\` (for example `\\/path/to/file`)
You can create your own operator in your application and add it to the engine.
Note that the @title provides a string for the operator that can be displayed in an edition UI for better clarity (ex: < for lessThan).
Expand Down Expand Up @@ -180,4 +189,3 @@ ngOnInit() {
```

Here, `CurrentTimeFactsService` also provides a `tick()` method that, when called, it recomputes the current time. It is up to the application to decide how ofter the current time should be recomputed (at a given time interval, on page navigation, etc).

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {of} from 'rxjs';
import {executeOperator, isRangeNumber, isString, isSupportedSimpleTypes, isValidDate, isValidDateInput, isValidDateRange, numberValidator} from './operator.helpers';
import {executeOperator, isRangeNumber, isString, isSupportedSimpleTypes, isValidDate, isValidDateInput, isValidDateRange, numberValidator, parseRegExp} from './operator.helpers';
import {Operator} from './operator.interface';

describe('Operator helpers', () => {
Expand Down Expand Up @@ -224,4 +224,11 @@ describe('Operator helpers', () => {
expect(isString('')).toBeTruthy();
});
});

describe('parseRegExp', () => {
it('should validate input properly', () => {
expect(parseRegExp('test')).toEqual(new RegExp('test'));
expect(parseRegExp('/test/gs')).toEqual(new RegExp('test', 'gs'));
});
});
});
16 changes: 16 additions & 0 deletions packages/@o3r/rules-engine/src/engine/operator/operator.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,19 @@ export function isSupportedSimpleTypes(value: unknown): value is SupportedSimple
export function isString(value: unknown): value is string {
return typeof value === 'string';
}

/**
* Parse input to return RegExp
*
* @param value value to test whether pattern exists (can be string or array of strings)
* @param inputString regexp pattern
*/
export function parseRegExp(inputRegExp: string) {
if (inputRegExp.startsWith('/')) {
const finalSlash = inputRegExp.lastIndexOf('/');
const regexpPattern = inputRegExp.slice(1, finalSlash);
const regexpFlags = inputRegExp.slice(finalSlash + 1);
return new RegExp(regexpPattern, regexpFlags);
}
return new RegExp(inputRegExp);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {isRangeNumber, isString, isSupportedSimpleTypes, numberValidator} from '../operator.helpers';
import {isRangeNumber, isString, isSupportedSimpleTypes, numberValidator, parseRegExp} from '../operator.helpers';
import { Operator, SupportedSimpleTypes } from '../operator.interface';

/**
Expand Down Expand Up @@ -118,8 +118,8 @@ export const allLower: Operator<number[], number | string> = {
*/
export const allMatch: Operator<string[], string> = {
name: 'allMatch',
evaluator: (array, inputString) => {
const regExp = new RegExp(inputString);
evaluator: (array, inputRegExp) => {
const regExp = parseRegExp(inputRegExp);
return array.every((elementValue) => regExp.test(elementValue));
},
validateLhs: Array.isArray,
Expand Down Expand Up @@ -196,8 +196,8 @@ export const oneLower: Operator<number[], number | string> = {
*/
export const oneMatches: Operator<string[], string> = {
name: 'oneMatches',
evaluator: (arrayString, value) => {
const regExp = new RegExp(value);
evaluator: (arrayString, inputRegExp) => {
const regExp = parseRegExp(inputRegExp);
return arrayString.some((elementValue) => regExp.test(elementValue));
},
validateLhs: Array.isArray,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {equals, inArray, inString, isDefined, isUndefined, notEquals, notInArray, notInString} from './basic.operators';
import {equals, inArray, inString, isDefined, isUndefined, matchesPattern, notEquals, notInArray, notInString} from './basic.operators';

describe('Basic operator', () => {
describe('equals', () => {
Expand Down Expand Up @@ -161,4 +161,33 @@ describe('Basic operator', () => {
expect(isUndefined.evaluator(undefined as any)).toBeTruthy();
});
});

describe('matchesPattern', () => {
test('should have a valid name', () => {
expect(matchesPattern.name).toBe('matchesPattern');
});

test('should exclude non-string right hand operand', () => {
expect(matchesPattern.validateRhs).toBeDefined();
if (!matchesPattern.validateRhs) {
return;
}

expect(matchesPattern.validateRhs('test')).toBeTruthy();
expect(matchesPattern.validateRhs(/string/ as any)).toBeFalsy();
expect(matchesPattern.validateRhs(null as any)).toBeFalsy();
expect(matchesPattern.validateRhs(undefined as any)).toBeFalsy();
expect(matchesPattern.validateRhs(123 as any)).toBeFalsy();
});

test('should pass if the left hand value matches the right hand value', () => {
expect(matchesPattern.evaluator('test', 'T')).toBeFalsy();
expect(matchesPattern.evaluator('test', '/T/i')).toBeTruthy();
expect(matchesPattern.evaluator('test', '^t[ea]st')).toBeTruthy();
expect(matchesPattern.evaluator('test', 'notTest')).toBeFalsy();
expect(matchesPattern.evaluator('test/path/to/file', '\\/path/to/file')).toBeTruthy();
expect(matchesPattern.evaluator('8000', '^8[0-9]{3}')).toBeTruthy();
expect(matchesPattern.evaluator('8000', '/^8[0-9]{3}$/')).toBeTruthy();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isString, isSupportedSimpleTypes } from '../operator.helpers';
import { isString, isSupportedSimpleTypes, parseRegExp } from '../operator.helpers';
import { Operator, SupportedSimpleTypes, UnaryOperator } from '../operator.interface';

/**
Expand Down Expand Up @@ -91,7 +91,22 @@ export const isUndefined: UnaryOperator<any> = {
evaluator: (input) => input === undefined || input === null
};

/**
* Check if the text variable matches the specified RegExp pattern
*
* @title matches the pattern
*/
export const matchesPattern: Operator<string, string> = {
name: 'matchesPattern',
evaluator: (value, inputRegExp) => {
const regExp = parseRegExp(inputRegExp);
return regExp.test(value);
},
validateLhs: isString,
validateRhs: isString
};

/** List of all default basic operators */
export const basicOperators = [
equals, inArray, inString, isDefined, isUndefined, notEquals, notInArray, notInString
equals, inArray, inString, isDefined, isUndefined, matchesPattern, notEquals, notInArray, notInString
];

0 comments on commit 4a64581

Please sign in to comment.