Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
433 changes: 386 additions & 47 deletions README.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions api/rule-engine.api
Original file line number Diff line number Diff line change
Expand Up @@ -474,11 +474,13 @@ public abstract interface class org/hisp/dhis/rules/models/RuleVariable {
public abstract fun getName ()Ljava/lang/String;
public fun getOptionName (Ljava/lang/String;)Ljava/lang/String;
public abstract fun getOptions ()Ljava/util/List;
public fun getOptionsByCode ()Ljava/util/Map;
public abstract fun getUseCodeForOptionSet ()Z
}

public final class org/hisp/dhis/rules/models/RuleVariable$DefaultImpls {
public static fun getOptionName (Lorg/hisp/dhis/rules/models/RuleVariable;Ljava/lang/String;)Ljava/lang/String;
public static fun getOptionsByCode (Lorg/hisp/dhis/rules/models/RuleVariable;)Ljava/util/Map;
}

public final class org/hisp/dhis/rules/models/RuleVariableAttribute : org/hisp/dhis/rules/models/RuleVariable {
Expand All @@ -497,6 +499,7 @@ public final class org/hisp/dhis/rules/models/RuleVariableAttribute : org/hisp/d
public fun getName ()Ljava/lang/String;
public fun getOptionName (Ljava/lang/String;)Ljava/lang/String;
public fun getOptions ()Ljava/util/List;
public fun getOptionsByCode ()Ljava/util/Map;
public fun getUseCodeForOptionSet ()Z
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
Expand All @@ -510,6 +513,7 @@ public final class org/hisp/dhis/rules/models/RuleVariableCalculatedValue : org/
public fun getName ()Ljava/lang/String;
public fun getOptionName (Ljava/lang/String;)Ljava/lang/String;
public fun getOptions ()Ljava/util/List;
public fun getOptionsByCode ()Ljava/util/Map;
public fun getUseCodeForOptionSet ()Z
}

Expand All @@ -521,6 +525,7 @@ public final class org/hisp/dhis/rules/models/RuleVariableCurrentEvent : org/his
public fun getName ()Ljava/lang/String;
public fun getOptionName (Ljava/lang/String;)Ljava/lang/String;
public fun getOptions ()Ljava/util/List;
public fun getOptionsByCode ()Ljava/util/Map;
public fun getUseCodeForOptionSet ()Z
}

Expand All @@ -532,6 +537,7 @@ public final class org/hisp/dhis/rules/models/RuleVariableNewestEvent : org/hisp
public fun getName ()Ljava/lang/String;
public fun getOptionName (Ljava/lang/String;)Ljava/lang/String;
public fun getOptions ()Ljava/util/List;
public fun getOptionsByCode ()Ljava/util/Map;
public fun getUseCodeForOptionSet ()Z
}

Expand All @@ -543,6 +549,7 @@ public final class org/hisp/dhis/rules/models/RuleVariableNewestStageEvent : org
public fun getName ()Ljava/lang/String;
public fun getOptionName (Ljava/lang/String;)Ljava/lang/String;
public fun getOptions ()Ljava/util/List;
public fun getOptionsByCode ()Ljava/util/Map;
public final fun getProgramStage ()Ljava/lang/String;
public fun getUseCodeForOptionSet ()Z
}
Expand All @@ -555,6 +562,7 @@ public final class org/hisp/dhis/rules/models/RuleVariablePreviousEvent : org/hi
public fun getName ()Ljava/lang/String;
public fun getOptionName (Ljava/lang/String;)Ljava/lang/String;
public fun getOptions ()Ljava/util/List;
public fun getOptionsByCode ()Ljava/util/Map;
public fun getUseCodeForOptionSet ()Z
}

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ repositories {
maven { url = uri("https://central.sonatype.com/repository/maven-snapshots/") }
}

version = "3.8.0-SNAPSHOT"
version = "3.8.1-SNAPSHOT"
group = "org.hisp.dhis.rules"

if (project.hasProperty("removeSnapshotSuffix")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,25 @@ package org.hisp.dhis.rules.api

import org.hisp.dhis.rules.models.Rule
import org.hisp.dhis.rules.models.RuleVariable
import org.hisp.dhis.rules.utils.filterRules
import org.hisp.dhis.rules.utils.mergeSorted

data class RuleEngineContext(
val rules: List<Rule>,
val ruleVariables: List<RuleVariable> = emptyList(),
val ruleSupplementaryData: RuleSupplementaryData = RuleSupplementaryData(),
val constantsValues: Map<String, String> = emptyMap(),
)
) {
// Rules that apply to enrollments and to events with no stage-specific override,
// sorted by priority. Computed once so evaluate() calls pay no per-call cost.
internal val enrollmentRules: List<Rule> = filterRules(rules).sorted()

// For each program stage: the merged, sorted list of enrollment rules + stage rules.
// enrollmentRules is already sorted; stage rules are sorted separately and then merged
// in O(n+m) rather than re-sorting the full combined list from scratch.
// Events whose stage has no entry fall back to enrollmentRules.
internal val rulesByStage: Map<String, List<Rule>> = rules
.filter { !it.programStage.isNullOrEmpty() }
.groupBy { it.programStage!! }
.mapValues { (_, stageRules) -> mergeSorted(enrollmentRules, stageRules.sorted()) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import org.hisp.dhis.rules.api.RuleContextRequirements
import org.hisp.dhis.rules.api.RuleEngine
import org.hisp.dhis.rules.api.RuleEngineContext
import org.hisp.dhis.rules.models.*
import org.hisp.dhis.rules.utils.filterRules
import org.hisp.dhis.rules.utils.orderEvents

internal class DefaultRuleEngine : RuleEngine {
Expand All @@ -33,7 +32,7 @@ internal class DefaultRuleEngine : RuleEngine {
target.event,
valueMap,
RuleConditionEvaluator.convertSupplementaryData(executionContext.ruleSupplementaryData),
filterRules(executionContext.rules, target).sorted(),
executionContext.rulesByStage[target.programStage] ?: executionContext.enrollmentRules,
AttributeType.DATA_ELEMENT,
)
}
Expand All @@ -55,7 +54,7 @@ internal class DefaultRuleEngine : RuleEngine {
target.enrollment,
valueMap,
RuleConditionEvaluator.convertSupplementaryData(executionContext.ruleSupplementaryData),
filterRules(executionContext.rules).sorted(),
executionContext.enrollmentRules,
AttributeType.TRACKED_ENTITY_ATTRIBUTE,
)
}
Expand All @@ -69,9 +68,8 @@ internal class DefaultRuleEngine : RuleEngine {
RuleVariableValueMapBuilder()
.multipleBuild(executionContext.constantsValues, executionContext.ruleVariables, eventsTarget.toSet(), enrollmentTarget)
return RuleEngineMultipleExecution().execute(
executionContext.rules,
executionContext,
valueMap,
executionContext.ruleSupplementaryData,
)
}

Expand Down Expand Up @@ -102,10 +100,10 @@ internal class DefaultRuleEngine : RuleEngine {
): RuleValidationResult =
try {
val validationMap: Map<String, ValueType> = dataItemStore.mapValues { e -> e.value.valueType.toValueType() }
Expression(expression, mode, false).validate(validationMap)
val parsed = Expression(expression, mode, false)
parsed.validate(validationMap)
val displayNames: Map<String, String> = dataItemStore.mapValues { e -> e.value.displayName }
val description = Expression(expression, mode, false).describe(displayNames)
RuleValidationResult(valid = true, description = description)
RuleValidationResult(valid = true, description = parsed.describe(displayNames))
} catch (ex: IllegalExpressionException) {
RuleValidationResult(
valid = false,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
package org.hisp.dhis.rules.engine

import org.hisp.dhis.rules.api.RuleSupplementaryData
import org.hisp.dhis.rules.api.RuleEngineContext
import org.hisp.dhis.rules.models.*
import org.hisp.dhis.rules.utils.filterRules

internal class RuleEngineMultipleExecution {
fun execute(
rules: List<Rule>,
executionContext: RuleEngineContext,
ruleVariableValueMap: RuleVariableValueMap,
ruleSupplementaryData: RuleSupplementaryData,
): List<RuleEffects> {
val supplementaryMap = RuleConditionEvaluator.convertSupplementaryData(ruleSupplementaryData)
val supplementaryMap = RuleConditionEvaluator.convertSupplementaryData(executionContext.ruleSupplementaryData)
val evaluator = RuleConditionEvaluator()
val ruleEffects: MutableList<RuleEffects> = ArrayList()
val enrollmentRules: List<Rule> = filterRules(rules).sorted()
val rulesByStage: Map<String, List<Rule>> = rules
.filter { !it.programStage.isNullOrEmpty() }
.groupBy { it.programStage!! }
.mapValues { (_, stageRules) -> (enrollmentRules + stageRules).sorted() }
val enrollmentRules = executionContext.enrollmentRules
val rulesByStage = executionContext.rulesByStage
for ((enrollment, valueMap) in ruleVariableValueMap.enrollmentMap) {
val enrollmentRuleEffects =
evaluator.getEvaluatedAndErrorRuleEffects(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,12 @@ internal class RuleVariableValueMapBuilder {
for (j in dataValues.indices) {
val ruleDataValue = dataValues[j]

// push new list if it is not there for the given data element
if (!allEventsValues.containsKey(ruleDataValue.dataElement)) {
allEventsValues[ruleDataValue.dataElement] = ArrayList(events.size)
}

// append data value to the list
allEventsValues[ruleDataValue.dataElement]?.add(
RuleDataValueHistory(ruleDataValue.value, events[i].eventDate, events[i].resolvedCreatedDate, events[i].programStage),
allEventsValues.getOrPut(ruleDataValue.dataElement) { ArrayList(events.size) }
.add(RuleDataValueHistory(
ruleDataValue.value,
events[i].eventDate,
events[i].resolvedCreatedDate,
events[i].programStage),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ data class RuleEvent(
val organisationUnitCode: String?,
val dataValues: List<RuleDataValue>,
): Comparable<RuleEvent> {
internal val resolvedCreatedDate get() = (createdAtClientDate ?: createdDate).instant
internal val resolvedCreatedDate = (createdAtClientDate ?: createdDate).instant

override fun compareTo(other: RuleEvent): Int {
val dateComparison = this.eventDate.compareTo(other.eventDate)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ interface RuleVariable {
currentEnrollmentValues: Map<String, RuleAttributeValue>,
): RuleVariableValue

fun getOptionName(value: String): String {
return options
.find { (_, code): Option -> value == code }?.name ?: value
}
// Concrete classes should override this with a stored val so the map is built once.
// The default recomputes on every call, which is safe for external implementations.
val optionsByCode: Map<String, String> get() = options.associate { it.code to it.name }

fun getOptionName(value: String): String = optionsByCode[value] ?: value
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ data class RuleVariableAttribute(
override val field: String,
override val fieldType: RuleValueType,
) : RuleVariable {
override val optionsByCode: Map<String, String> = options.associate { it.code to it.name }
override fun createValues(
ruleEvent: RuleEvent?,
allEventValues: Map<String, List<RuleDataValueHistory>>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class RuleVariableCurrentEvent(
override val field: String,
override val fieldType: RuleValueType,
) : RuleVariable {
override val optionsByCode: Map<String, String> = options.associate { it.code to it.name }
override fun createValues(
ruleEvent: RuleEvent?,
allEventValues: Map<String, List<RuleDataValueHistory>>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class RuleVariableNewestEvent(
override val field: String,
override val fieldType: RuleValueType,
) : RuleVariable {
override val optionsByCode: Map<String, String> = options.associate { it.code to it.name }
override fun createValues(
ruleEvent: RuleEvent?,
allEventValues: Map<String, List<RuleDataValueHistory>>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class RuleVariableNewestStageEvent(
override val fieldType: RuleValueType,
val programStage: String,
) : RuleVariable {
override val optionsByCode: Map<String, String> = options.associate { it.code to it.name }
override fun createValues(
ruleEvent: RuleEvent?,
allEventValues: Map<String, List<RuleDataValueHistory>>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class RuleVariablePreviousEvent(
override val field: String,
override val fieldType: RuleValueType,
) : RuleVariable {
override val optionsByCode: Map<String, String> = options.associate { it.code to it.name }
override fun createValues(
ruleEvent: RuleEvent?,
allEventValues: Map<String, List<RuleDataValueHistory>>,
Expand Down
12 changes: 12 additions & 0 deletions src/commonMain/kotlin/org/hisp/dhis/rules/utils/RuleEventUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ internal fun getPreviousDataValue(dataValues: List<RuleDataValueHistory>, ruleEv
internal fun filterRules(rules: List<Rule>): List<Rule> =
rules.filter { it.programStage.isNullOrEmpty() }

/** Merges two already-sorted lists into a single sorted list in O(n+m). */
internal fun <T : Comparable<T>> mergeSorted(a: List<T>, b: List<T>): List<T> {
val result = ArrayList<T>(a.size + b.size)
var i = 0; var j = 0
while (i < a.size && j < b.size) {
if (a[i] <= b[j]) result.add(a[i++]) else result.add(b[j++])
}
while (i < a.size) result.add(a[i++])
while (j < b.size) result.add(b[j++])
return result
}

internal fun filterRules(
rules: List<Rule>,
ruleEvent: RuleEvent,
Expand Down