Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Evaluating variables in answerExpressions #2066

Merged
merged 6 commits into from
Jul 23, 2023
Merged
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
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)
MJ1998 marked this conversation as resolved.
Show resolved Hide resolved
) {
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(
MJ1998 marked this conversation as resolved.
Show resolved Hide resolved
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}}") }
MJ1998 marked this conversation as resolved.
Show resolved Hide resolved
.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")
}
}