Skip to content

Commit

Permalink
Merge pull request #69 from EyeSeeTea/feature/coding-dojo
Browse files Browse the repository at this point in the history
feat:use dhis2 expression parser
MiquelAdell authored Nov 19, 2024
2 parents d38ec23 + 72dd7f2 commit b892c5a
Showing 8 changed files with 326 additions and 290 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@
"@dhis2/d2-i18n": "1.1.0",
"@dhis2/d2-i18n-extract": "1.0.8",
"@dhis2/d2-i18n-generate": "1.2.0",
"@dhis2/expression-parser": "^1.1.0",
"@dhis2/ui": "6.12.0",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
108 changes: 108 additions & 0 deletions src/data/entities/D2ExpressionParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import * as xp from "@dhis2/expression-parser";
import _c from "../../domain/entities/generic/Collection";
import { Either } from "../../domain/entities/generic/Either";

export class D2ExpressionParser {
public evaluateRuleEngineCondition(
ruleCondition: string,
variableValues: Map<ProgramRuleVariableName, ProgramRuleVariableValue>
): Either<Error, boolean> {
try {
const expressionParser = new xp.ExpressionJs(
ruleCondition,
xp.ExpressionMode.RULE_ENGINE_CONDITION
);

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
);

return Either.success(parsedResult);
} catch (error) {
return Either.error(error as Error);
}
}

private getVariableValueByType = (
type: ProgramRuleVariableType,
stringValue: xp.Nullable<string>
): xp.VariableValueJs => {
const valueType = VariableValueTypeMap[type];
return new xp.VariableValueJs(valueType, stringValue, [], null);
};

private mapProgramRuleVariables(
expressionParser: xp.ExpressionJs,
ruleVariables: Map<string, ProgramRuleVariableValue>
) {
const programRuleVariables = expressionParser.collectProgramRuleVariableNames();
const variablesValueMap = programRuleVariables.map(programRuleVariable => {
const currentProgramRuleVariableValue = ruleVariables.get(programRuleVariable);

if (!currentProgramRuleVariableValue)
return {
programRuleVariable: programRuleVariable,
value: new xp.VariableValueJs(xp.ValueType.STRING, null, [], null),
};

const variableValue = this.getVariableValueByType(
currentProgramRuleVariableValue.type,
currentProgramRuleVariableValue.value === ""
? null
: currentProgramRuleVariableValue.value
);

return {
programRuleVariable: programRuleVariable,
value: variableValue,
};
});

return new Map(
variablesValueMap.map(variable => [variable.programRuleVariable, variable.value])
);
}

private mapProgramVariables(expressionParser: xp.ExpressionJs) {
const programVariables = expressionParser.collectProgramVariablesNames();
const programVariableValues = programVariables.map(programVariable => {
switch (programVariable) {
case "current_date": {
const currentISODate = new Date().toISOString().split("T")[0];
const currentDate = this.getVariableValueByType("date", currentISODate);
return { programVariable: programVariable, value: currentDate };
}
default:
throw new Error(
`Unhandled Program variable of type : ${programVariable}. Please contact developer`
);
}
});

const programVariablesMap = new Map(
programVariableValues.map(variable => [variable.programVariable, variable.value])
);

return programVariablesMap;
}
}
export type ProgramRuleVariableType = "text" | "number" | "date" | "boolean";
export type ProgramRuleVariableName = string;
export type ProgramRuleVariableValue = {
type: ProgramRuleVariableType;
value: string;
};

const VariableValueTypeMap: Record<ProgramRuleVariableType, xp.ValueType> = {
text: xp.ValueType.STRING,
boolean: xp.ValueType.BOOLEAN,
date: xp.ValueType.DATE,
number: xp.ValueType.NUMBER,
};
25 changes: 2 additions & 23 deletions src/data/utils/ruleHelper.ts
Original file line number Diff line number Diff line change
@@ -39,28 +39,6 @@ export const getProgramRules = (
return attributeId;
}) || [];

const parsedCondition = condition.replace(
dataElementVariablePattern,
(match, programRuleVar) => {
const dataElementId = programRuleVariables?.find(
programRuleVariable => programRuleVariable.name === programRuleVar
)?.dataElement?.id;

return `#{${dataElementId}}`;
}
);

const attributeParsedCondition = parsedCondition.replace(
attributeVariablePattern,
(match, programRuleVar) => {
const attributeId = programRuleVariables?.find(
programRuleVariable => programRuleVariable.name === programRuleVar
)?.trackedEntityAttribute?.id;

return `#{${attributeId}}`;
}
);

const programRuleActionIds: string[] = actions.map(action => action.id);

const programRuleActions: D2ProgramRuleAction[] | undefined = programRuleActionsResponse
@@ -84,10 +62,11 @@ export const getProgramRules = (

return {
id: id,
condition: attributeParsedCondition.replace(/d2:/g, "fn:"), //replace d2: with fn: to decouple entity from DHIS
condition: condition,
dataElementIds: _(dataElementIds).uniq().compact().value(),
teAttributeIds: _(teaIds).uniq().compact().value(),
actions: programRuleActions || [],
programRuleVariables: programRuleVariables || [],
};
}) || []
);
21 changes: 13 additions & 8 deletions src/domain/entities/Questionnaire/Questionnaire.ts
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ import {
QuestionnaireQuestion,
isAntibioticQuestion,
} from "./QuestionnaireQuestion";
import { QuestionnaireRule, getApplicableRules } from "./QuestionnaireRules";
import { getApplicableRules, QuestionnaireRule } from "./QuestionnaireRules";
import { QuestionnaireSection, QuestionnaireSectionM } from "./QuestionnaireSection";

export interface QuestionnaireBase {
@@ -272,10 +272,14 @@ export class Questionnaire {
...allQsInQuestionnaireStages,
];

const allQsInQuestionnaireWithUpdatedQ = allQsInQuestionnaire.map(question =>
question.id === updatedQuestion.id ? updatedQuestion : question
);

const applicableRules = getApplicableRules(
updatedQuestion,
questionnaire.rules,
allQsInQuestionnaire
allQsInQuestionnaireWithUpdatedQ
);

if (initialLoad && applicableRules.length === 0) return questionnaire;
@@ -369,12 +373,13 @@ export class Questionnaire {
questionnaire: Questionnaire,
rules: QuestionnaireRule[]
): QuestionnaireEntity | undefined {
const updatedEntityQuestions = QuestionnaireQuestion.updateQuestions(
questionnaireEntity.questions,
updatedQuestion,
rules,
questionnaire
);
const updatedEntityQuestions = QuestionnaireQuestion.updateQuestions({
processedQuestions: [],
questions: questionnaireEntity.questions,
updatedQuestion: updatedQuestion,
rules: rules,
questionnaire: questionnaire,
});

return {
...questionnaireEntity,
Loading

0 comments on commit b892c5a

Please sign in to comment.