From 5d5ff04c724b94b24e42057887c26ae96d393f11 Mon Sep 17 00:00:00 2001 From: mohamed yahia Date: Tue, 22 Oct 2024 18:44:02 +0300 Subject: [PATCH] Add data condition variable (#6239) * Add data condition variable * Format --------- Co-authored-by: Artur Arseniev --- .../ConditionStatement.ts | 13 ++ .../conditional_variables/DataCondition.ts | 50 +++++ .../LogicalGroupStatement.ts | 15 ++ .../evaluateCondition.ts | 53 ++++++ .../operators/GenericOperator.ts | 54 ++++++ .../operators/LogicalOperator.ts | 28 +++ .../operators/NumberOperator.ts | 35 ++++ .../operators/StringOperations.ts | 29 +++ .../conditional_variables/operators/index.ts | 3 + .../conditional_variables/DataCondition.ts | 174 ++++++++++++++++++ .../operators/GenericOperator.ts | 150 +++++++++++++++ .../operators/LogicalOperator.ts | 59 ++++++ .../operators/NumberOperator.ts | 95 ++++++++++ .../operators/StringOperator.ts | 81 ++++++++ 14 files changed, 839 insertions(+) create mode 100644 packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts create mode 100644 packages/core/src/data_sources/model/conditional_variables/DataCondition.ts create mode 100644 packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts create mode 100644 packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts create mode 100644 packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts create mode 100644 packages/core/src/data_sources/model/conditional_variables/operators/LogicalOperator.ts create mode 100644 packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts create mode 100644 packages/core/src/data_sources/model/conditional_variables/operators/StringOperations.ts create mode 100644 packages/core/src/data_sources/model/conditional_variables/operators/index.ts create mode 100644 packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts create mode 100644 packages/core/test/specs/data_sources/model/conditional_variables/operators/GenericOperator.ts create mode 100644 packages/core/test/specs/data_sources/model/conditional_variables/operators/LogicalOperator.ts create mode 100644 packages/core/test/specs/data_sources/model/conditional_variables/operators/NumberOperator.ts create mode 100644 packages/core/test/specs/data_sources/model/conditional_variables/operators/StringOperator.ts diff --git a/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts b/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts new file mode 100644 index 000000000..93fd786b9 --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts @@ -0,0 +1,13 @@ +import { Operator } from './operators'; + +export class ConditionStatement { + constructor( + private leftValue: any, + private operator: Operator, + private rightValue: any, + ) {} + + evaluate(): boolean { + return this.operator.evaluate(this.leftValue, this.rightValue); + } +} diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts new file mode 100644 index 000000000..fbcf89196 --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -0,0 +1,50 @@ +import { NumberOperation } from './operators/NumberOperator'; +import { StringOperation } from './operators/StringOperations'; +import { GenericOperation } from './operators/GenericOperator'; +import { Model } from '../../../common'; +import { LogicalOperation } from './operators/LogicalOperator'; +import { evaluateCondition } from './evaluateCondition'; + +export const ConditionalVariableType = 'conditional-variable'; +export type Expression = { + left: any; + operator: GenericOperation | StringOperation | NumberOperation; + right: any; +}; + +export type LogicGroup = { + logicalOperator: LogicalOperation; + statements: (Expression | LogicGroup | boolean)[]; +}; + +export class DataCondition extends Model { + private conditionResult: boolean; + + defaults() { + return { + type: ConditionalVariableType, + condition: false, + }; + } + + constructor( + private condition: Expression | LogicGroup | boolean, + private ifTrue: any, + private ifFalse: any, + ) { + super(); + this.conditionResult = this.evaluate(); + } + + evaluate() { + return evaluateCondition(this.condition); + } + + getDataValue(): any { + return this.conditionResult ? this.ifTrue : this.ifFalse; + } + + reevaluate(): void { + this.conditionResult = this.evaluate(); + } +} diff --git a/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts b/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts new file mode 100644 index 000000000..4d4796c0d --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts @@ -0,0 +1,15 @@ +import { LogicalOperator } from './operators/LogicalOperator'; +import { Expression, LogicGroup } from './DataCondition'; +import { evaluateCondition } from './evaluateCondition'; + +export class LogicalGroupStatement { + constructor( + private operator: LogicalOperator, + private statements: (Expression | LogicGroup | boolean)[], + ) {} + + evaluate(): boolean { + const results = this.statements.map((statement) => evaluateCondition(statement)); + return this.operator.evaluate(results); + } +} diff --git a/packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts b/packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts new file mode 100644 index 000000000..ee5dee77c --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts @@ -0,0 +1,53 @@ +import { ConditionStatement } from './ConditionStatement'; +import { Expression, LogicGroup } from './DataCondition'; +import { LogicalGroupStatement } from './LogicalGroupStatement'; +import { Operator } from './operators'; +import { GenericOperation, GenericOperator } from './operators/GenericOperator'; +import { LogicalOperator } from './operators/LogicalOperator'; +import { NumberOperation, NumberOperator } from './operators/NumberOperator'; +import { StringOperation, StringOperator } from './operators/StringOperations'; + +export function evaluateCondition(condition: any): boolean { + if (typeof condition === 'boolean') { + return condition; + } + + if (isLogicGroup(condition)) { + const { logicalOperator, statements } = condition; + const op = new LogicalOperator(logicalOperator); + const logicalGroup = new LogicalGroupStatement(op, statements); + return logicalGroup.evaluate(); + } + + if (isCondition(condition)) { + const { left, operator, right } = condition; + const op = operatorFactory(left, operator); + const statement = new ConditionStatement(left, op, right); + return statement.evaluate(); + } + + throw new Error('Invalid condition type.'); +} + +function operatorFactory(left: any, operator: string): Operator { + if (isOperatorInEnum(operator, GenericOperation)) { + return new GenericOperator(operator as GenericOperation); + } else if (typeof left === 'number') { + return new NumberOperator(operator as NumberOperation); + } else if (typeof left === 'string') { + return new StringOperator(operator as StringOperation); + } + throw new Error(`Unsupported data type: ${typeof left}`); +} + +function isOperatorInEnum(operator: string, enumObject: any): boolean { + return Object.values(enumObject).includes(operator); +} + +function isLogicGroup(condition: any): condition is LogicGroup { + return condition && typeof condition.logicalOperator !== 'undefined' && Array.isArray(condition.statements); +} + +function isCondition(condition: any): condition is Expression { + return condition && typeof condition.left !== 'undefined' && typeof condition.operator === 'string'; +} diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts new file mode 100644 index 000000000..45ab4943d --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts @@ -0,0 +1,54 @@ +import DataVariable from '../../DataVariable'; +import { Operator } from '.'; + +export enum GenericOperation { + equals = 'equals', + isTruthy = 'isTruthy', + isFalsy = 'isFalsy', + isDefined = 'isDefined', + isNull = 'isNull', + isUndefined = 'isUndefined', + isArray = 'isArray', + isObject = 'isObject', + isString = 'isString', + isNumber = 'isNumber', + isBoolean = 'isBoolean', + isDefaultValue = 'isDefaultValue', // For Datasource variables +} + +export class GenericOperator extends Operator { + constructor(private operator: GenericOperation) { + super(); + } + + evaluate(left: any, right: any): boolean { + switch (this.operator) { + case 'equals': + return left === right; + case 'isTruthy': + return !!left; + case 'isFalsy': + return !left; + case 'isDefined': + return left !== undefined && left !== null; + case 'isNull': + return left === null; + case 'isUndefined': + return left === undefined; + case 'isArray': + return Array.isArray(left); + case 'isObject': + return typeof left === 'object' && left !== null; + case 'isString': + return typeof left === 'string'; + case 'isNumber': + return typeof left === 'number'; + case 'isBoolean': + return typeof left === 'boolean'; + case 'isDefaultValue': + return left instanceof DataVariable && left.get('default') === right; + default: + throw new Error(`Unsupported generic operator: ${this.operator}`); + } + } +} diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/LogicalOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/LogicalOperator.ts new file mode 100644 index 000000000..5c3aced38 --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/operators/LogicalOperator.ts @@ -0,0 +1,28 @@ +import { Operator } from '.'; + +export enum LogicalOperation { + and = 'and', + or = 'or', + xor = 'xor', +} + +export class LogicalOperator extends Operator { + constructor(private operator: LogicalOperation) { + super(); + } + + evaluate(statements: boolean[]): boolean { + if (!statements.length) throw new Error('Expected one or more statments, got none'); + + switch (this.operator) { + case LogicalOperation.and: + return statements.every(Boolean); + case LogicalOperation.or: + return statements.some(Boolean); + case LogicalOperation.xor: + return statements.filter(Boolean).length === 1; + default: + throw new Error(`Unsupported logical operator: ${this.operator}`); + } + } +} diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts new file mode 100644 index 000000000..db054680b --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts @@ -0,0 +1,35 @@ +import { Operator } from '.'; + +export enum NumberOperation { + greaterThan = '>', + lessThan = '<', + greaterThanOrEqual = '>=', + lessThanOrEqual = '<=', + equals = '=', + notEquals = '!=', +} + +export class NumberOperator extends Operator { + constructor(private operator: NumberOperation) { + super(); + } + + evaluate(left: number, right: number): boolean { + switch (this.operator) { + case NumberOperation.greaterThan: + return left > right; + case NumberOperation.lessThan: + return left < right; + case NumberOperation.greaterThanOrEqual: + return left >= right; + case NumberOperation.lessThanOrEqual: + return left <= right; + case NumberOperation.equals: + return left === right; + case NumberOperation.notEquals: + return left !== right; + default: + throw new Error(`Unsupported number operator: ${this.operator}`); + } + } +} diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/StringOperations.ts b/packages/core/src/data_sources/model/conditional_variables/operators/StringOperations.ts new file mode 100644 index 000000000..d7ac89ab6 --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/operators/StringOperations.ts @@ -0,0 +1,29 @@ +import { Operator } from '.'; + +export enum StringOperation { + contains = 'contains', + startsWith = 'startsWith', + endsWith = 'endsWith', + matchesRegex = 'matchesRegex', +} + +export class StringOperator extends Operator { + constructor(private operator: StringOperation) { + super(); + } + + evaluate(left: string, right: string): boolean { + switch (this.operator) { + case StringOperation.contains: + return left.includes(right); + case StringOperation.startsWith: + return left.startsWith(right); + case StringOperation.endsWith: + return left.endsWith(right); + case StringOperation.matchesRegex: + return new RegExp(right).test(left); + default: + throw new Error(`Unsupported string operator: ${this.operator}`); + } + } +} diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/index.ts b/packages/core/src/data_sources/model/conditional_variables/operators/index.ts new file mode 100644 index 000000000..5ae1f2879 --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/operators/index.ts @@ -0,0 +1,3 @@ +export abstract class Operator { + abstract evaluate(left: any, right: any): boolean; +} diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts new file mode 100644 index 000000000..c3f7da595 --- /dev/null +++ b/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts @@ -0,0 +1,174 @@ +import { + DataCondition, + Expression, + LogicGroup, +} from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; +import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; +import { LogicalOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/LogicalOperator'; +import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; +import { StringOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/StringOperations'; + +describe('DataCondition', () => { + describe('Basic Functionality Tests', () => { + test('should evaluate a simple boolean condition', () => { + const condition = true; + const dataCondition = new DataCondition(condition, 'Yes', 'No'); + + expect(dataCondition.getDataValue()).toBe('Yes'); + }); + + test('should return ifFalse when condition evaluates to false', () => { + const condition = false; + const dataCondition = new DataCondition(condition, 'Yes', 'No'); + + expect(dataCondition.getDataValue()).toBe('No'); + }); + }); + + describe('Operator Tests', () => { + test('should evaluate using GenericOperation operators', () => { + const condition: Expression = { left: 5, operator: GenericOperation.equals, right: 5 }; + const dataCondition = new DataCondition(condition, 'Equal', 'Not Equal'); + + expect(dataCondition.getDataValue()).toBe('Equal'); + }); + + test('equals (false)', () => { + const condition: Expression = { + left: 'hello', + operator: GenericOperation.equals, + right: 'world', + }; + const dataCondition = new DataCondition(condition, 'true', 'false'); + expect(dataCondition.evaluate()).toBe(false); + }); + + test('should evaluate using StringOperation operators', () => { + const condition: Expression = { left: 'apple', operator: StringOperation.contains, right: 'app' }; + const dataCondition = new DataCondition(condition, 'Contains', "Doesn't contain"); + + expect(dataCondition.getDataValue()).toBe('Contains'); + }); + + test('should evaluate using NumberOperation operators', () => { + const condition: Expression = { left: 10, operator: NumberOperation.lessThan, right: 15 }; + const dataCondition = new DataCondition(condition, 'Valid', 'Invalid'); + + expect(dataCondition.getDataValue()).toBe('Valid'); + }); + + test('should evaluate using LogicalOperation operators', () => { + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.and, + statements: [ + { left: true, operator: GenericOperation.equals, right: true }, + { left: 5, operator: NumberOperation.greaterThan, right: 3 }, + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'Pass', 'Fail'); + expect(dataCondition.getDataValue()).toBe('Pass'); + }); + }); + + describe('Edge Case Tests', () => { + test('should throw error for invalid condition type', () => { + const invalidCondition: any = { randomField: 'randomValue' }; + expect(() => new DataCondition(invalidCondition, 'Yes', 'No')).toThrow('Invalid condition type.'); + }); + + test('should evaluate complex nested conditions', () => { + const nestedLogicGroup: LogicGroup = { + logicalOperator: LogicalOperation.or, + statements: [ + { + logicalOperator: LogicalOperation.and, + statements: [ + { left: 1, operator: NumberOperation.lessThan, right: 5 }, + { left: 'test', operator: GenericOperation.equals, right: 'test' }, + ], + }, + { left: 10, operator: NumberOperation.greaterThan, right: 100 }, + ], + }; + + const dataCondition = new DataCondition(nestedLogicGroup, 'Nested Pass', 'Nested Fail'); + expect(dataCondition.getDataValue()).toBe('Nested Pass'); + }); + }); + + describe('LogicalGroup Tests', () => { + test('should correctly handle AND logical operator', () => { + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.and, + statements: [ + { left: true, operator: GenericOperation.equals, right: true }, + { left: 5, operator: NumberOperation.greaterThan, right: 3 }, + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false'); + expect(dataCondition.getDataValue()).toBe('All true'); + }); + + test('should correctly handle OR logical operator', () => { + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.or, + statements: [ + { left: true, operator: GenericOperation.equals, right: false }, + { left: 5, operator: NumberOperation.greaterThan, right: 3 }, + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'At least one true', 'All false'); + expect(dataCondition.getDataValue()).toBe('At least one true'); + }); + + test('should correctly handle XOR logical operator', () => { + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.xor, + statements: [ + { left: true, operator: GenericOperation.equals, right: true }, + { left: 5, operator: NumberOperation.lessThan, right: 3 }, + { left: false, operator: GenericOperation.equals, right: true }, + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'Exactly one true', 'Multiple true or all false'); + expect(dataCondition.getDataValue()).toBe('Exactly one true'); + }); + + test('should handle nested logical groups', () => { + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.and, + statements: [ + { left: true, operator: GenericOperation.equals, right: true }, + { + logicalOperator: LogicalOperation.or, + statements: [ + { left: 5, operator: NumberOperation.greaterThan, right: 3 }, + { left: false, operator: GenericOperation.equals, right: true }, + ], + }, + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false'); + expect(dataCondition.getDataValue()).toBe('All true'); + }); + + test('should handle groups with false conditions', () => { + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.and, + statements: [ + { left: true, operator: GenericOperation.equals, right: true }, + { left: false, operator: GenericOperation.equals, right: true }, + { left: 5, operator: NumberOperation.greaterThan, right: 3 }, + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false'); + expect(dataCondition.getDataValue()).toBe('One or more false'); + }); + }); +}); diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/operators/GenericOperator.ts b/packages/core/test/specs/data_sources/model/conditional_variables/operators/GenericOperator.ts new file mode 100644 index 000000000..516bb5ce4 --- /dev/null +++ b/packages/core/test/specs/data_sources/model/conditional_variables/operators/GenericOperator.ts @@ -0,0 +1,150 @@ +import { + GenericOperator, + GenericOperation, +} from '../../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; + +describe('GenericOperator', () => { + describe('Operator: equals', () => { + test('should return true when values are equal', () => { + const operator = new GenericOperator(GenericOperation.equals); + expect(operator.evaluate(5, 5)).toBe(true); + }); + + test('should return false when values are not equal', () => { + const operator = new GenericOperator(GenericOperation.equals); + expect(operator.evaluate(5, 10)).toBe(false); + }); + }); + + describe('Operator: isTruthy', () => { + test('should return true for truthy value', () => { + const operator = new GenericOperator(GenericOperation.isTruthy); + expect(operator.evaluate('non-empty', null)).toBe(true); + }); + + test('should return false for falsy value', () => { + const operator = new GenericOperator(GenericOperation.isTruthy); + expect(operator.evaluate('', null)).toBe(false); + }); + }); + + describe('Operator: isFalsy', () => { + test('should return true for falsy value', () => { + const operator = new GenericOperator(GenericOperation.isFalsy); + expect(operator.evaluate(0, null)).toBe(true); + }); + + test('should return false for truthy value', () => { + const operator = new GenericOperator(GenericOperation.isFalsy); + expect(operator.evaluate(1, null)).toBe(false); + }); + }); + + describe('Operator: isDefined', () => { + test('should return true for defined value', () => { + const operator = new GenericOperator(GenericOperation.isDefined); + expect(operator.evaluate(10, null)).toBe(true); + }); + + test('should return false for undefined value', () => { + const operator = new GenericOperator(GenericOperation.isDefined); + expect(operator.evaluate(undefined, null)).toBe(false); + }); + }); + + describe('Operator: isNull', () => { + test('should return true for null value', () => { + const operator = new GenericOperator(GenericOperation.isNull); + expect(operator.evaluate(null, null)).toBe(true); + }); + + test('should return false for non-null value', () => { + const operator = new GenericOperator(GenericOperation.isNull); + expect(operator.evaluate(0, null)).toBe(false); + }); + }); + + describe('Operator: isUndefined', () => { + test('should return true for undefined value', () => { + const operator = new GenericOperator(GenericOperation.isUndefined); + expect(operator.evaluate(undefined, null)).toBe(true); + }); + + test('should return false for defined value', () => { + const operator = new GenericOperator(GenericOperation.isUndefined); + expect(operator.evaluate(0, null)).toBe(false); + }); + }); + + describe('Operator: isArray', () => { + test('should return true for array', () => { + const operator = new GenericOperator(GenericOperation.isArray); + expect(operator.evaluate([1, 2, 3], null)).toBe(true); + }); + + test('should return false for non-array', () => { + const operator = new GenericOperator(GenericOperation.isArray); + expect(operator.evaluate('not an array', null)).toBe(false); + }); + }); + + describe('Operator: isObject', () => { + test('should return true for object', () => { + const operator = new GenericOperator(GenericOperation.isObject); + expect(operator.evaluate({ key: 'value' }, null)).toBe(true); + }); + + test('should return false for non-object', () => { + const operator = new GenericOperator(GenericOperation.isObject); + expect(operator.evaluate(42, null)).toBe(false); + }); + }); + + describe('Operator: isString', () => { + test('should return true for string', () => { + const operator = new GenericOperator(GenericOperation.isString); + expect(operator.evaluate('Hello', null)).toBe(true); + }); + + test('should return false for non-string', () => { + const operator = new GenericOperator(GenericOperation.isString); + expect(operator.evaluate(42, null)).toBe(false); + }); + }); + + describe('Operator: isNumber', () => { + test('should return true for number', () => { + const operator = new GenericOperator(GenericOperation.isNumber); + expect(operator.evaluate(42, null)).toBe(true); + }); + + test('should return false for non-number', () => { + const operator = new GenericOperator(GenericOperation.isNumber); + expect(operator.evaluate('not a number', null)).toBe(false); + }); + }); + + describe('Operator: isBoolean', () => { + test('should return true for boolean', () => { + const operator = new GenericOperator(GenericOperation.isBoolean); + expect(operator.evaluate(true, null)).toBe(true); + }); + + test('should return false for non-boolean', () => { + const operator = new GenericOperator(GenericOperation.isBoolean); + expect(operator.evaluate(1, null)).toBe(false); + }); + }); + + describe('Edge Case Tests', () => { + test('should handle null as input gracefully', () => { + const operator = new GenericOperator(GenericOperation.isNull); + expect(operator.evaluate(null, null)).toBe(true); + }); + + test('should throw error for unsupported operator', () => { + const operator = new GenericOperator('unsupported' as GenericOperation); + expect(() => operator.evaluate(1, 2)).toThrow('Unsupported generic operator: unsupported'); + }); + }); +}); diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/operators/LogicalOperator.ts b/packages/core/test/specs/data_sources/model/conditional_variables/operators/LogicalOperator.ts new file mode 100644 index 000000000..a81809858 --- /dev/null +++ b/packages/core/test/specs/data_sources/model/conditional_variables/operators/LogicalOperator.ts @@ -0,0 +1,59 @@ +import { + LogicalOperator, + LogicalOperation, +} from '../../../../../../src/data_sources/model/conditional_variables/operators/LogicalOperator'; + +describe('LogicalOperator', () => { + describe('Operator: and', () => { + test('should return true when all statements are true', () => { + const operator = new LogicalOperator(LogicalOperation.and); + expect(operator.evaluate([true, true, true])).toBe(true); + }); + + test('should return false when at least one statement is false', () => { + const operator = new LogicalOperator(LogicalOperation.and); + expect(operator.evaluate([true, false, true])).toBe(false); + }); + }); + + describe('Operator: or', () => { + test('should return true when at least one statement is true', () => { + const operator = new LogicalOperator(LogicalOperation.or); + expect(operator.evaluate([false, true, false])).toBe(true); + }); + + test('should return false when all statements are false', () => { + const operator = new LogicalOperator(LogicalOperation.or); + expect(operator.evaluate([false, false, false])).toBe(false); + }); + }); + + describe('Operator: xor', () => { + test('should return true when exactly one statement is true', () => { + const operator = new LogicalOperator(LogicalOperation.xor); + expect(operator.evaluate([true, false, false])).toBe(true); + }); + + test('should return false when more than one statement is true', () => { + const operator = new LogicalOperator(LogicalOperation.xor); + expect(operator.evaluate([true, true, false])).toBe(false); + }); + + test('should return false when no statement is true', () => { + const operator = new LogicalOperator(LogicalOperation.xor); + expect(operator.evaluate([false, false, false])).toBe(false); + }); + }); + + describe('Edge Case Tests', () => { + test('should return false for xor with all false inputs', () => { + const operator = new LogicalOperator(LogicalOperation.xor); + expect(operator.evaluate([false, false])).toBe(false); + }); + + test('should throw error for unsupported operator', () => { + const operator = new LogicalOperator('unsupported' as LogicalOperation); + expect(() => operator.evaluate([true, false])).toThrow('Unsupported logical operator: unsupported'); + }); + }); +}); diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/operators/NumberOperator.ts b/packages/core/test/specs/data_sources/model/conditional_variables/operators/NumberOperator.ts new file mode 100644 index 000000000..2c719338c --- /dev/null +++ b/packages/core/test/specs/data_sources/model/conditional_variables/operators/NumberOperator.ts @@ -0,0 +1,95 @@ +import { + NumberOperator, + NumberOperation, +} from '../../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; + +describe('NumberOperator', () => { + describe('Operator: greaterThan', () => { + test('should return true when left is greater than right', () => { + const operator = new NumberOperator(NumberOperation.greaterThan); + expect(operator.evaluate(5, 3)).toBe(true); + }); + + test('should return false when left is not greater than right', () => { + const operator = new NumberOperator(NumberOperation.greaterThan); + expect(operator.evaluate(2, 3)).toBe(false); + }); + }); + + describe('Operator: lessThan', () => { + test('should return true when left is less than right', () => { + const operator = new NumberOperator(NumberOperation.lessThan); + expect(operator.evaluate(2, 3)).toBe(true); + }); + + test('should return false when left is not less than right', () => { + const operator = new NumberOperator(NumberOperation.lessThan); + expect(operator.evaluate(5, 3)).toBe(false); + }); + }); + + describe('Operator: greaterThanOrEqual', () => { + test('should return true when left is greater than or equal to right', () => { + const operator = new NumberOperator(NumberOperation.greaterThanOrEqual); + expect(operator.evaluate(3, 3)).toBe(true); + }); + + test('should return false when left is not greater than or equal to right', () => { + const operator = new NumberOperator(NumberOperation.greaterThanOrEqual); + expect(operator.evaluate(2, 3)).toBe(false); + }); + }); + + describe('Operator: lessThanOrEqual', () => { + test('should return true when left is less than or equal to right', () => { + const operator = new NumberOperator(NumberOperation.lessThanOrEqual); + expect(operator.evaluate(3, 3)).toBe(true); + }); + + test('should return false when left is not less than or equal to right', () => { + const operator = new NumberOperator(NumberOperation.lessThanOrEqual); + expect(operator.evaluate(5, 3)).toBe(false); + }); + }); + + describe('Operator: equals', () => { + test('should return true when numbers are equal', () => { + const operator = new NumberOperator(NumberOperation.equals); + expect(operator.evaluate(4, 4)).toBe(true); + }); + + test('should return false when numbers are not equal', () => { + const operator = new NumberOperator(NumberOperation.equals); + expect(operator.evaluate(4, 5)).toBe(false); + }); + }); + + describe('Operator: notEquals', () => { + test('should return true when numbers are not equal', () => { + const operator = new NumberOperator(NumberOperation.notEquals); + expect(operator.evaluate(4, 5)).toBe(true); + }); + + test('should return false when numbers are equal', () => { + const operator = new NumberOperator(NumberOperation.notEquals); + expect(operator.evaluate(4, 4)).toBe(false); + }); + }); + + describe('Edge Case Tests', () => { + test('should handle boundary values correctly', () => { + const operator = new NumberOperator(NumberOperation.lessThan); + expect(operator.evaluate(Number.MIN_VALUE, 1)).toBe(true); + }); + + test('should return false for NaN comparisons', () => { + const operator = new NumberOperator(NumberOperation.equals); + expect(operator.evaluate(NaN, NaN)).toBe(false); + }); + + test('should throw error for unsupported operator', () => { + const operator = new NumberOperator('unsupported' as NumberOperation); + expect(() => operator.evaluate(1, 2)).toThrow('Unsupported number operator: unsupported'); + }); + }); +}); diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/operators/StringOperator.ts b/packages/core/test/specs/data_sources/model/conditional_variables/operators/StringOperator.ts new file mode 100644 index 000000000..6e291cf94 --- /dev/null +++ b/packages/core/test/specs/data_sources/model/conditional_variables/operators/StringOperator.ts @@ -0,0 +1,81 @@ +import { + StringOperator, + StringOperation, +} from '../../../../../../src/data_sources/model/conditional_variables/operators/StringOperations'; + +describe('StringOperator', () => { + describe('Operator: contains', () => { + test('should return true when left contains right', () => { + const operator = new StringOperator(StringOperation.contains); + expect(operator.evaluate('hello world', 'world')).toBe(true); + }); + + test('should return false when left does not contain right', () => { + const operator = new StringOperator(StringOperation.contains); + expect(operator.evaluate('hello world', 'moon')).toBe(false); + }); + }); + + describe('Operator: startsWith', () => { + test('should return true when left starts with right', () => { + const operator = new StringOperator(StringOperation.startsWith); + expect(operator.evaluate('hello world', 'hello')).toBe(true); + }); + + test('should return false when left does not start with right', () => { + const operator = new StringOperator(StringOperation.startsWith); + expect(operator.evaluate('hello world', 'world')).toBe(false); + }); + }); + + describe('Operator: endsWith', () => { + test('should return true when left ends with right', () => { + const operator = new StringOperator(StringOperation.endsWith); + expect(operator.evaluate('hello world', 'world')).toBe(true); + }); + + test('should return false when left does not end with right', () => { + const operator = new StringOperator(StringOperation.endsWith); + expect(operator.evaluate('hello world', 'hello')).toBe(false); + }); + }); + + describe('Operator: matchesRegex', () => { + test('should return true when left matches the regex right', () => { + const operator = new StringOperator(StringOperation.matchesRegex); + expect(operator.evaluate('hello world', '^hello')).toBe(true); + }); + + test('should return false when left does not match the regex right', () => { + const operator = new StringOperator(StringOperation.matchesRegex); + expect(operator.evaluate('hello world', '^world')).toBe(false); + }); + }); + + describe('Edge Case Tests', () => { + test('should return false for contains with empty right string', () => { + const operator = new StringOperator(StringOperation.contains); + expect(operator.evaluate('hello world', '')).toBe(true); // Empty string is included in any string + }); + + test('should return true for startsWith with empty right string', () => { + const operator = new StringOperator(StringOperation.startsWith); + expect(operator.evaluate('hello world', '')).toBe(true); // Any string starts with an empty string + }); + + test('should return true for endsWith with empty right string', () => { + const operator = new StringOperator(StringOperation.endsWith); + expect(operator.evaluate('hello world', '')).toBe(true); // Any string ends with an empty string + }); + + test('should throw error for invalid regex', () => { + const operator = new StringOperator(StringOperation.matchesRegex); + expect(() => operator.evaluate('hello world', '[')).toThrow(); + }); + + test('should throw error for unsupported operator', () => { + const operator = new StringOperator('unsupported' as StringOperation); + expect(() => operator.evaluate('test', 'test')).toThrow('Unsupported string operator: unsupported'); + }); + }); +});