Skip to content

Commit

Permalink
Add datasource support to conditional variables (#6270)
Browse files Browse the repository at this point in the history
  • Loading branch information
mohamedsalem401 authored Oct 28, 2024
1 parent 32278ea commit e43dcfc
Show file tree
Hide file tree
Showing 6 changed files with 353 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,8 @@ export default class DynamicVariableListenerManager {
this.dataListeners.forEach((ls) => model.stopListening(ls.obj, ls.event, this.onChange));
this.dataListeners = [];
}

destroy() {
this.removeListeners();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import EditorModel from '../../../editor/model/Editor';
import DataVariable from '../DataVariable';
import { evaluateVariable, isDataVariable } from '../utils';
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 { NumberOperator, NumberOperation } from './operators/NumberOperator';
import { StringOperator, StringOperation } from './operators/StringOperations';

export class Condition {
private condition: Expression | LogicGroup | boolean;
private em: EditorModel;

constructor(condition: Expression | LogicGroup | boolean, opts: { em: EditorModel }) {
this.condition = condition;
this.em = opts.em;
}

evaluate(): boolean {
return this.evaluateCondition(this.condition);
}

/**
* Recursively evaluates conditions and logic groups.
*/
private evaluateCondition(condition: any): boolean {
if (typeof condition === 'boolean') return condition;

if (this.isLogicGroup(condition)) {
const { logicalOperator, statements } = condition;
const operator = new LogicalOperator(logicalOperator);
const logicalGroup = new LogicalGroupStatement(operator, statements, { em: this.em });
return logicalGroup.evaluate();
}

if (this.isExpression(condition)) {
const { left, operator, right } = condition;
const evaluateLeft = evaluateVariable(left, this.em);
const evaluateRight = evaluateVariable(right, this.em);
const op = this.getOperator(evaluateLeft, operator);

const evaluated = op.evaluate(evaluateLeft, evaluateRight);
return evaluated;
}

throw new Error('Invalid condition type.');
}

/**
* Factory method for creating operators based on the data type.
*/
private getOperator(left: any, operator: string): Operator {
if (this.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}`);
}

/**
* Extracts all data variables from the condition, including nested ones.
*/
getDataVariables(): DataVariable[] {
const variables: DataVariable[] = [];
this.extractVariables(this.condition, variables);
return variables;
}

/**
* Recursively extracts variables from expressions or logic groups.
*/
private extractVariables(condition: boolean | LogicGroup | Expression, variables: DataVariable[]): void {
if (this.isExpression(condition)) {
if (isDataVariable(condition.left)) variables.push(condition.left);
if (isDataVariable(condition.right)) variables.push(condition.right);
} else if (this.isLogicGroup(condition)) {
condition.statements.forEach((stmt) => this.extractVariables(stmt, variables));
}
}

/**
* Checks if a condition is a LogicGroup.
*/
private isLogicGroup(condition: any): condition is LogicGroup {
return condition && typeof condition.logicalOperator !== 'undefined' && Array.isArray(condition.statements);
}

/**
* Checks if a condition is an Expression.
*/
private isExpression(condition: any): condition is Expression {
return condition && typeof condition.left !== 'undefined' && typeof condition.operator === 'string';
}

/**
* Checks if an operator exists in a specific enum.
*/
private isOperatorInEnum(operator: string, enumObject: any): boolean {
return Object.values(enumObject).includes(operator);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import { StringOperation } from './operators/StringOperations';
import { GenericOperation } from './operators/GenericOperator';
import { Model } from '../../../common';
import { LogicalOperation } from './operators/LogicalOperator';
import { evaluateCondition } from './evaluateCondition';
import DynamicVariableListenerManager from '../DataVariableListenerManager';
import EditorModel from '../../../editor/model/Editor';
import { Condition } from './Condition';
import DataVariable from '../DataVariable';
import { evaluateVariable, isDataVariable } from '../utils';

export const ConditionalVariableType = 'conditional-variable';
export const DataConditionType = 'conditional-variable';
export type Expression = {
left: any;
operator: GenericOperation | StringOperation | NumberOperation;
Expand All @@ -19,32 +23,75 @@ export type LogicGroup = {

export class DataCondition extends Model {
private conditionResult: boolean;
private condition: Condition;
private em: EditorModel;
private variableListeners: DynamicVariableListenerManager[] = [];

defaults() {
return {
type: ConditionalVariableType,
type: DataConditionType,
condition: false,
};
}

constructor(
private condition: Expression | LogicGroup | boolean,
condition: Expression | LogicGroup | boolean,
private ifTrue: any,
private ifFalse: any,
opts: { em: EditorModel },
) {
super();
this.condition = new Condition(condition, { em: opts.em });
this.em = opts.em;
this.conditionResult = this.evaluate();
this.listenToDataVariables();
}

evaluate() {
return evaluateCondition(this.condition);
return this.condition.evaluate();
}

getDataValue(): any {
return this.conditionResult ? this.ifTrue : this.ifFalse;
return this.conditionResult ? evaluateVariable(this.ifTrue, this.em) : evaluateVariable(this.ifFalse, this.em);
}

reevaluate(): void {
this.conditionResult = this.evaluate();
}

toJSON() {
return {
condition: this.condition,
ifTrue: this.ifTrue,
ifFalse: this.ifFalse,
};
}

private listenToDataVariables() {
if (!this.em) return;

// Clear previous listeners to avoid memory leaks
this.cleanupListeners();

const dataVariables = this.condition.getDataVariables();
if (isDataVariable(this.ifTrue)) dataVariables.push(this.ifTrue);
if (isDataVariable(this.ifFalse)) dataVariables.push(this.ifFalse);

dataVariables.forEach((variable) => {
const variableInstance = new DataVariable(variable, { em: this.em });
const listener = new DynamicVariableListenerManager({
model: this as any,
em: this.em!,
dataVariable: variableInstance,
updateValueFromDataVariable: this.reevaluate.bind(this),
});

this.variableListeners.push(listener);
});
}

private cleanupListeners() {
this.variableListeners.forEach((listener) => listener.destroy());
this.variableListeners = [];
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { LogicalOperator } from './operators/LogicalOperator';
import { Expression, LogicGroup } from './DataCondition';
import { evaluateCondition } from './evaluateCondition';
import { Condition } from './Condition';
import EditorModel from '../../../editor/model/Editor';

export class LogicalGroupStatement {
private em: EditorModel;

constructor(
private operator: LogicalOperator,
private statements: (Expression | LogicGroup | boolean)[],
) {}
opts: { em: EditorModel },
) {
this.em = opts.em;
}

evaluate(): boolean {
const results = this.statements.map((statement) => evaluateCondition(statement));
const results = this.statements.map((statement) => {
const condition = new Condition(statement, { em: this.em });
return condition.evaluate();
});
return this.operator.evaluate(results);
}
}
15 changes: 15 additions & 0 deletions packages/core/src/data_sources/model/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import EditorModel from '../../editor/model/Editor';
import { DataConditionType } from './conditional_variables/DataCondition';
import DataVariable, { DataVariableType } from './DataVariable';

export function isDataVariable(variable: any) {
return variable?.type === DataVariableType;
}

export function isDataCondition(variable: any) {
return variable?.type === DataConditionType;
}

export function evaluateVariable(variable: any, em: EditorModel) {
return isDataVariable(variable) ? new DataVariable(variable, { em }).getDataValue() : variable;
}
Loading

0 comments on commit e43dcfc

Please sign in to comment.