Skip to content

Commit

Permalink
Evaluating variables in answerExpressions (#2066)
Browse files Browse the repository at this point in the history
* Adding support for variables in answerExpressions.

* Fixing tests and adding a test for new feature.

* Modifying to test with mulitple variables in the expression.

* Resolving comments by some refactoring and adding suggested changes.

* Removing too many param docs.

* Fixing test cases
  • Loading branch information
MJ1998 authored Jul 23, 2023
1 parent ebc8511 commit b8836f8
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ import kotlinx.coroutines.flow.update
import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.Element
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent
import org.hl7.fhir.r4.model.QuestionnaireResponse
Expand Down Expand Up @@ -577,16 +576,42 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
): List<Questionnaire.QuestionnaireItemAnswerOptionComponent> {
// Check cache first for database queries
val answerExpression = item.answerExpression ?: return emptyList()
if (answerExpression.isXFhirQuery && answerExpressionMap.contains(answerExpression.expression)
) {
return answerExpressionMap[answerExpression.expression]!!
}

val options = loadAnswerExpressionOptions(item, answerExpression)
return when {
answerExpression.isXFhirQuery -> {
xFhirQueryResolver?.let { xFhirQueryResolver ->
val xFhirExpressionString =
ExpressionEvaluator.createXFhirQueryFromExpression(
questionnaire,
questionnaireResponse,
item,
questionnaireItemParentMap,
answerExpression,
questionnaireLaunchContextMap
)
if (answerExpressionMap.containsKey(xFhirExpressionString)) {
answerExpressionMap[xFhirExpressionString]
}

if (answerExpression.isXFhirQuery) answerExpressionMap[answerExpression.expression] = options
val data = xFhirQueryResolver.resolve(xFhirExpressionString)
val options = item.extractAnswerOptions(data)

return options
answerExpressionMap[xFhirExpressionString] = options
options
}
?: error(
"XFhirQueryResolver cannot be null. Please provide the XFhirQueryResolver via DataCaptureConfig."
)
}
answerExpression.isFhirPath -> {
val data = fhirPathEngine.evaluate(questionnaireResponse, answerExpression.expression)
item.extractAnswerOptions(data)
}
else ->
throw UnsupportedOperationException(
"${answerExpression.language} not supported for answer-expression yet"
)
}
}

private fun resolveCqfExpression(
Expand All @@ -609,33 +634,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
)
}

private suspend fun loadAnswerExpressionOptions(
item: QuestionnaireItemComponent,
expression: Expression,
): List<Questionnaire.QuestionnaireItemAnswerOptionComponent> {
val data =
if (expression.isXFhirQuery) {
checkNotNull(xFhirQueryResolver) {
"XFhirQueryResolver cannot be null. Please provide the XFhirQueryResolver via DataCaptureConfig."
}

val xFhirExpressionString =
ExpressionEvaluator.createXFhirQueryFromExpression(
expression,
questionnaireLaunchContextMap
)
xFhirQueryResolver!!.resolve(xFhirExpressionString)
} else if (expression.isFhirPath) {
fhirPathEngine.evaluate(questionnaireResponse, expression.expression)
} else {
throw UnsupportedOperationException(
"${expression.language} not supported for answer-expression yet"
)
}

return item.extractAnswerOptions(data)
}

/**
* Traverses through the list of questionnaire items, the list of questionnaire response items and
* the list of items in the questionnaire response answer list and populates
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,19 +279,36 @@ object ExpressionEvaluator {
}

/**
* Creates an x-fhir-query string for evaluation
*
* @param expression x-fhir-query expression
* @param launchContextMap if passed, the launch context to evaluate the expression against
* Creates an x-fhir-query string for evaluation. For this, it evaluates both variables and
* fhir-paths in the expression.
*/
internal fun createXFhirQueryFromExpression(
questionnaire: Questionnaire,
questionnaireResponse: QuestionnaireResponse,
questionnaireItem: QuestionnaireItemComponent,
questionnaireItemParentMap: Map<QuestionnaireItemComponent, QuestionnaireItemComponent>,
expression: Expression,
launchContextMap: Map<String, Resource>?
): String {
if (launchContextMap == null) {
return expression.expression
}
return evaluateXFhirEnhancement(expression, launchContextMap).fold(expression.expression) {
// get all dependent variables and their evaluated values
val variablesEvaluatedPairs =
mutableMapOf<String, Base?>()
.apply {
extractDependentVariables(
expression,
questionnaire,
questionnaireResponse,
questionnaireItemParentMap,
questionnaireItem,
this
)
}
.filterKeys { expression.expression.contains("{{%$it}}") }
.map { Pair("{{%${it.key}}}", it.value!!.primitiveValue()) }

val fhirPathsEvaluatedPairs =
launchContextMap?.let { evaluateXFhirEnhancement(expression, it) } ?: emptySequence()
return (fhirPathsEvaluatedPairs + variablesEvaluatedPairs).fold(expression.expression) {
acc: String,
pair: Pair<String, String> ->
acc.replace(pair.first, pair.second)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@

package com.google.android.fhir.datacapture.fhirpath

import com.google.android.fhir.datacapture.extensions.EXTENSION_ANSWER_EXPRESSION_URL
import com.google.android.fhir.datacapture.extensions.EXTENSION_CALCULATED_EXPRESSION_URL
import com.google.android.fhir.datacapture.extensions.EXTENSION_VARIABLE_URL
import com.google.android.fhir.datacapture.extensions.answerExpression
import com.google.android.fhir.datacapture.extensions.asStringValue
import com.google.android.fhir.datacapture.extensions.variableExpressions
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.detectExpressionCyclicDependency
Expand Down Expand Up @@ -686,6 +688,10 @@ class ExpressionEvaluatorTest {

val expressionsToEvaluate =
ExpressionEvaluator.createXFhirQueryFromExpression(
Questionnaire(),
QuestionnaireResponse(),
Questionnaire.QuestionnaireItemComponent(),
emptyMap(),
expression,
mapOf(Practitioner().resourceType.name.lowercase() to Practitioner())
)
Expand All @@ -710,6 +716,10 @@ class ExpressionEvaluatorTest {

val expressionsToEvaluate =
ExpressionEvaluator.createXFhirQueryFromExpression(
Questionnaire(),
QuestionnaireResponse(),
Questionnaire.QuestionnaireItemComponent(),
emptyMap(),
expression,
mapOf(practitioner.resourceType.name to practitioner)
)
Expand All @@ -734,6 +744,10 @@ class ExpressionEvaluatorTest {

val expressionsToEvaluate =
ExpressionEvaluator.createXFhirQueryFromExpression(
Questionnaire(),
QuestionnaireResponse(),
Questionnaire.QuestionnaireItemComponent(),
emptyMap(),
expression,
mapOf(practitioner.resourceType.name.lowercase() to practitioner)
)
Expand All @@ -758,6 +772,10 @@ class ExpressionEvaluatorTest {

val expressionsToEvaluate =
ExpressionEvaluator.createXFhirQueryFromExpression(
Questionnaire(),
QuestionnaireResponse(),
Questionnaire.QuestionnaireItemComponent(),
emptyMap(),
expression,
mapOf(practitioner.resourceType.name to practitioner)
)
Expand All @@ -782,6 +800,10 @@ class ExpressionEvaluatorTest {

val expressionsToEvaluate =
ExpressionEvaluator.createXFhirQueryFromExpression(
Questionnaire(),
QuestionnaireResponse(),
Questionnaire.QuestionnaireItemComponent(),
emptyMap(),
expression,
mapOf(patient.resourceType.name.lowercase() to patient)
)
Expand Down Expand Up @@ -820,6 +842,10 @@ class ExpressionEvaluatorTest {

val expressionsToEvaluate =
ExpressionEvaluator.createXFhirQueryFromExpression(
Questionnaire(),
QuestionnaireResponse(),
Questionnaire.QuestionnaireItemComponent(),
emptyMap(),
expression,
mapOf(
patient.resourceType.name.lowercase() to patient,
Expand All @@ -828,4 +854,67 @@ class ExpressionEvaluatorTest {
)
assertThat(expressionsToEvaluate).isEqualTo("Patient?family=John&address-city=NAIROBI")
}

@Test
fun `createXFhirQueryFromExpression() should evaluate variables in answer expression`() {
val questionnaire =
Questionnaire().apply {
id = "a-questionnaire"
addItem(
Questionnaire.QuestionnaireItemComponent().apply {
linkId = "a-group-item"
text = "a question"
type = Questionnaire.QuestionnaireItemType.GROUP
addExtension().apply {
url = EXTENSION_VARIABLE_URL
setValue(
Expression().apply {
name = "A"
language = "text/fhirpath"
expression = "1"
}
)
}
addExtension().apply {
url = EXTENSION_VARIABLE_URL
setValue(
Expression().apply {
name = "B"
language = "text/fhirpath"
expression = "2"
}
)
}
addItem(
Questionnaire.QuestionnaireItemComponent().apply {
linkId = "an-item"
text = "a question"
type = Questionnaire.QuestionnaireItemType.TEXT
addExtension().apply {
url = EXTENSION_ANSWER_EXPRESSION_URL
setValue(
Expression().apply {
language = "application/x-fhir-query"
expression = "Patient?address-city={{%A}}&gender={{%B}}"
}
)
}
}
)
}
)
}

val result =
ExpressionEvaluator.createXFhirQueryFromExpression(
questionnaire,
QuestionnaireResponse(),
questionnaire.item[0].item[0],
mapOf(questionnaire.item[0].item[0] to questionnaire.item[0]),
questionnaire.item[0].item[0].answerExpression!!,
null
)

assertThat(result).isEqualTo("Patient?address-city=1&gender=2")
}
}

0 comments on commit b8836f8

Please sign in to comment.