Skip to content

Commit

Permalink
feat: Add support for ASSIGN actions in Program Rules
Browse files Browse the repository at this point in the history
  • Loading branch information
MatiasArriola committed Jan 9, 2025
1 parent 753b3c0 commit 68c2cc3
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 16 deletions.
40 changes: 30 additions & 10 deletions src/data/entities/D2ExpressionParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,42 @@ export class D2ExpressionParser {
ruleCondition,
xp.ExpressionMode.RULE_ENGINE_CONDITION
);
const expressionData = this.getExpressionDataJs(expressionParser, variableValues);
const parsedResult: boolean = expressionParser.evaluate(() => {}, expressionData);
return Either.success(parsedResult);
} catch (error) {
return Either.error(error as Error);
}
}

const ruleVariables = this.mapProgramRuleVariables(expressionParser, variableValues);
const genericVariables = this.mapProgramVariables(expressionParser);
const variables = new Map([...ruleVariables, ...genericVariables]);

const expressionData = new xp.ExpressionDataJs(variables);

const parsedResult: boolean = expressionParser.evaluate(
() => console.debug(""),
expressionData
public evaluateActionExpression(
expression: string,
variableValues: Map<ProgramRuleVariableName, ProgramRuleVariableValue>
): Either<Error, EvaluatedExpressionResult> {
try {
const expressionParser = new xp.ExpressionJs(
expression,
xp.ExpressionMode.RULE_ENGINE_ACTION
);

const expressionData = this.getExpressionDataJs(expressionParser, variableValues);
const parsedResult: EvaluatedExpressionResult = expressionParser.evaluate(() => {},
expressionData);
return Either.success(parsedResult);
} catch (error) {
return Either.error(error as Error);
}
}

private getExpressionDataJs = (
expressionParser: xp.ExpressionJs,
variableValues: Map<ProgramRuleVariableName, ProgramRuleVariableValue>
): xp.ExpressionDataJs => {
const ruleVariables = this.mapProgramRuleVariables(expressionParser, variableValues);
const genericVariables = this.mapProgramVariables(expressionParser);
const variables = new Map([...ruleVariables, ...genericVariables]);
return new xp.ExpressionDataJs(variables);
};

private getVariableValueByType = (
type: ProgramRuleVariableType,
stringValue: xp.Nullable<string>
Expand Down Expand Up @@ -106,3 +124,5 @@ const VariableValueTypeMap: Record<ProgramRuleVariableType, xp.ValueType> = {
date: xp.ValueType.DATE,
number: xp.ValueType.NUMBER,
};

export type EvaluatedExpressionResult = boolean | string | number | Date | null;
63 changes: 60 additions & 3 deletions src/domain/entities/Questionnaire/QuestionnaireQuestion.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Maybe, assertUnreachable } from "../../../utils/ts-utils";
import { Id, NamedRef } from "../Ref";
import { getApplicableRules, QuestionnaireRule } from "./QuestionnaireRules";
import {
getApplicableRules,
getQuestionValueFromEvaluatedExpression,
QuestionnaireRule,
} from "./QuestionnaireRules";
import _ from "../generic/Collection";
import { Questionnaire } from "./Questionnaire";

Expand Down Expand Up @@ -267,18 +271,71 @@ export class QuestionnaireQuestion {
return finalUpdatesWithSideEffects;
}

private static updateQuestion(question: Question, rules: QuestionnaireRule[]): Question {
private static updateQuestion<T extends Question>(question: T, rules: QuestionnaireRule[]): T {
const updatedIsVisible = this.isQuestionVisible(question, rules);
const updatedErrors = this.getQuestionWarningsAndErrors(question, rules);

const updatedIsDisabled = this.isQuestionDisabled(question, rules);
const updatedValue = this.getQuestionAssignValue(question, rules);
return {
...question,
isVisible: updatedIsVisible,
errors: updatedErrors,
disabled: updatedIsDisabled,
value: updatedValue,
...(question.isVisible !== updatedIsVisible ? { value: undefined } : {}),
};
}

private static getRulesWithAssignActionForQuestion(
question: Question,
rules: QuestionnaireRule[]
) {
return rules.filter(
rule =>
rule.parsedResult &&
rule.actions.filter(
action =>
action.programRuleActionType === "ASSIGN" &&
((action.dataElement && action.dataElement.id === question.id) ||
(action.trackedEntityAttribute &&
action.trackedEntityAttribute.id === question.id))
).length > 0
);
}

private static isQuestionDisabled(
question: Question,
rules: QuestionnaireRule[]
): boolean | undefined {
const applicableRules = this.getRulesWithAssignActionForQuestion(question, rules);
return applicableRules.length > 0 ? true : question.disabled;
}

private static getQuestionAssignValue(
question: Question,
rules: QuestionnaireRule[]
): Question["value"] {
const applicableActions = this.getRulesWithAssignActionForQuestion(question, rules).flatMap(
rule => rule.actions
);
if (applicableActions.length === 0) {
return question.value;
} else {
if (applicableActions.length > 1) {
console.warn(
"Multiple ASSIGN actions found for question: ",
question,
"Applying first rule:",
applicableActions[0]
);
}
return getQuestionValueFromEvaluatedExpression(
question,
applicableActions[0]?.dataEvaluated
);
}
}

private static isQuestionVisible(question: Question, rules: QuestionnaireRule[]): boolean {
//Check of there are any rules applicable to the current question
//with hide field action
Expand Down
72 changes: 69 additions & 3 deletions src/domain/entities/Questionnaire/QuestionnaireRules.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
D2ExpressionParser,
EvaluatedExpressionResult,
ProgramRuleVariableName,
ProgramRuleVariableValue,
} from "../../../data/entities/D2ExpressionParser";
Expand Down Expand Up @@ -31,7 +32,8 @@ export interface QuestionnaireRuleAction {
trackedEntityAttribute?: {
id: Id | undefined; // to hide
};
data?: string; // to assign
data?: string; // to assign (expression raw value)
dataEvaluated?: EvaluatedExpressionResult; // to assign (calculated/evaluated value)
programStageSection?: {
id: Id | undefined; // to hide/show
};
Expand Down Expand Up @@ -65,10 +67,18 @@ export const getApplicableRules = (
);

//2. Run the rule conditions and return rules with parsed results
// Also augment rule actions with results of the `data` expression evaluation
const parsedApplicableRules = applicableRules.map(rule => {
const expressionParserResult = parseConditionWithExpressionParser(rule, questions);

return { ...rule, parsedResult: expressionParserResult };
const actionsWithEvaluatedDataExpressions = getActionsWithEvaluatedDataExpression(
rule,
questions
);
return {
...rule,
parsedResult: expressionParserResult,
actions: actionsWithEvaluatedDataExpressions,
};
});

return parsedApplicableRules;
Expand Down Expand Up @@ -101,6 +111,25 @@ export const getQuestionValueByType = (question: Question): string => {
}
};

export function getQuestionValueFromEvaluatedExpression(
question: Question,
dataEvaluated?: EvaluatedExpressionResult
): Question["value"] {
// TODO: handle possible mismatches between question value type and dataEvaluated type
// e.g. question.type is "date" but dataEvaluated is a number
if (dataEvaluated === null) {
return undefined;
} else if (question.type === "select") {
const option = question.options.find(option => option.code === dataEvaluated);
if (!option) console.warn("Option not found in question for code:", dataEvaluated);
return option;
} else if (typeof dataEvaluated === "number") {
return dataEvaluated.toString();
} else {
return dataEvaluated;
}
}

function getProgramRuleVariableValues(
programRuleVariables: Maybe<D2ProgramRuleVariable[]>,
questions: Question[]
Expand Down Expand Up @@ -154,3 +183,40 @@ const parseConditionWithExpressionParser = (
},
});
};

/**
* Get the actions from the rule, augmenting them with the `dataEvaluated` property - Only for ASSIGN actions
* `dataEvaluated` is set with the results of running the D2ExpressionParser evaluation
*/
const getActionsWithEvaluatedDataExpression = (
rule: QuestionnaireRule,
questions: Question[]
): QuestionnaireRuleAction[] => {
const programRuleVariableValues = getProgramRuleVariableValues(
rule.programRuleVariables,
questions
);
const parser = new D2ExpressionParser();

return rule.actions.map(action => {
if (!action.data || action.programRuleActionType !== "ASSIGN") {
return action;
}
return {
...action,
dataEvaluated: parser
.evaluateActionExpression(action.data, programRuleVariableValues)
.match({
success: evaluationResult => evaluationResult,
error: errMsg => {
console.error(
"Error evaluating ASSIGN data expression",
action.data,
errMsg
);
return null;
},
}),
};
});
};

0 comments on commit 68c2cc3

Please sign in to comment.