From eb9fbecdb60adb38cda827d5a836ae55c5d94e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=E2=89=A1ZRS?= <12814349+LZRS@users.noreply.github.com> Date: Fri, 27 Sep 2024 01:24:38 +0300 Subject: [PATCH] Use Dispatchers.Default to evaluate fhirpath expressions --- .../MoreQuestionnaireItemComponents.kt | 2 +- .../fhirpath/ExpressionEvaluator.kt | 36 ++-- .../fhir/datacapture/fhirpath/FhirPathUtil.kt | 109 +++++++--- .../datacapture/QuestionnaireViewModelTest.kt | 2 + .../MoreQuestionnaireItemComponentsTest.kt | 60 +++--- .../fhirpath/ExpressionEvaluatorTest.kt | 198 +++++++++--------- .../datacapture/fhirpath/FhirPathUtilTest.kt | 30 +-- .../TestExpressionValueEvaluator.kt | 2 +- 8 files changed, 241 insertions(+), 198 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt index 0acd26c93f..98f707f163 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt @@ -773,7 +773,7 @@ internal data class ChoiceColumn(val path: String, val label: String?, val forDi * resources [Resource], identifiers [Identifier] or codes [Coding] * @return list of answer options [Questionnaire.QuestionnaireItemAnswerOptionComponent] */ -internal fun QuestionnaireItemComponent.extractAnswerOptions( +internal suspend fun QuestionnaireItemComponent.extractAnswerOptions( dataList: List, ): List { return when (this.type) { diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt index 97e74233e6..457e1df1aa 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt @@ -24,12 +24,17 @@ import com.google.android.fhir.datacapture.extensions.isFhirPath import com.google.android.fhir.datacapture.extensions.isReferencedBy import com.google.android.fhir.datacapture.extensions.isXFhirQuery import com.google.android.fhir.datacapture.extensions.variableExpressions +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.fold +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge import org.hl7.fhir.exceptions.FHIRException import org.hl7.fhir.r4.model.Base import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent import org.hl7.fhir.r4.model.Expression -import org.hl7.fhir.r4.model.ExpressionNode import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent import org.hl7.fhir.r4.model.QuestionnaireResponse @@ -348,7 +353,7 @@ internal class ExpressionEvaluator( * Creates an x-fhir-query string for evaluation. For this, it evaluates both variables and * fhir-paths in the expression. */ - internal fun createXFhirQueryFromExpression( + internal suspend fun createXFhirQueryFromExpression( expression: Expression, variablesMap: Map = emptyMap(), ): String { @@ -357,6 +362,7 @@ internal class ExpressionEvaluator( variablesMap .filterKeys { expression.expression.contains("{{%$it}}") } .map { Pair("{{%${it.key}}}", it.value?.primitiveValue() ?: "") } + .asFlow() val fhirPathsEvaluatedPairs = questionnaireLaunchContextMap @@ -364,9 +370,9 @@ internal class ExpressionEvaluator( .takeIf { !it.isNullOrEmpty() } ?.also { it.put(questionnaireFhirPathSupplement, questionnaire) } ?.let { evaluateXFhirEnhancement(expression, it) } - ?: emptySequence() + ?: emptyFlow() - return (variablesEvaluatedPairs + fhirPathsEvaluatedPairs).fold(expression.expression) { + return merge(variablesEvaluatedPairs, fhirPathsEvaluatedPairs).fold(expression.expression) { acc: String, pair: Pair, -> @@ -383,21 +389,17 @@ internal class ExpressionEvaluator( * Practitioner?active=true&{{Practitioner.name.family}} * @param launchContextMap the launch context to evaluate the expression against */ - private fun evaluateXFhirEnhancement( + private suspend fun evaluateXFhirEnhancement( expression: Expression, launchContextMap: Map, - ): Sequence> = + ): Flow> = xFhirQueryEnhancementRegex .findAll(expression.expression) + .asFlow() .map { it.groupValues } .map { (fhirPathWithParentheses, fhirPath) -> - val expressionNode = extractExpressionNode(fhirPath) val evaluatedResult = - evaluateToString( - expression = expressionNode, - data = launchContextMap[extractResourceType(expressionNode)], - contextMap = launchContextMap, - ) + evaluateToString(contextMap = launchContextMap, fhirPathString = fhirPath) // If the result of evaluating the FHIRPath expressions is an invalid query, it returns // null. As per the spec: @@ -531,15 +533,5 @@ internal class ExpressionEvaluator( } } -/** - * Extract [ResourceType] string representation from constant or name property of given - * [ExpressionNode]. - */ -private fun extractResourceType(expressionNode: ExpressionNode): String? { - // TODO(omarismail94): See if FHIRPathEngine.check() can be used to distinguish invalid - // expression vs an expression that is valid, but does not return one resource only. - return expressionNode.constant?.primitiveValue()?.substring(1) ?: expressionNode.name?.lowercase() -} - /** Pair of a [Questionnaire.QuestionnaireItemComponent] with its evaluated answers */ internal typealias ItemToAnswersPair = Pair> diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtil.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtil.kt index 1e86e4b8fd..66ad0e7a8f 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtil.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtil.kt @@ -18,6 +18,9 @@ package com.google.android.fhir.datacapture.fhirpath import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext import org.hl7.fhir.r4.model.Base import org.hl7.fhir.r4.model.ExpressionNode @@ -26,32 +29,52 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComp import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.utils.FHIRPathEngine -private val fhirPathEngine: FHIRPathEngine = +private val fhirPathEngine: FHIRPathEngine by lazy { with(FhirContext.forCached(FhirVersionEnum.R4)) { FHIRPathEngine(HapiWorkerContext(this, this.validationSupport)).apply { hostServices = FHIRPathEngineHostServices } } +} + +internal var fhirPathEngineDefaultDispatcher: CoroutineContext = Dispatchers.Default /** * Evaluates the expressions over list of resources [Resource] and joins to space separated string */ -internal fun evaluateToDisplay(expressions: List, data: Resource) = - expressions.joinToString(" ") { fhirPathEngine.evaluateToString(data, it) } +internal suspend fun evaluateToDisplay(expressions: List, data: Resource) = + withContext(fhirPathEngineDefaultDispatcher) { + expressions.joinToString(" ") { fhirPathEngine.evaluateToString(data, it) } + } /** Evaluates the expression over resource [Resource] and returns string value */ -internal fun evaluateToString( +internal suspend fun evaluateToString( expression: ExpressionNode, data: Resource?, contextMap: Map, ) = - fhirPathEngine.evaluateToString( - /* appInfo = */ contextMap, - /* focusResource = */ null, - /* rootResource = */ null, - /* base = */ data, - /* node = */ expression, + withContext(fhirPathEngineDefaultDispatcher) { + fhirPathEngine.evaluateToString( + /* appInfo = */ contextMap, + /* focusResource = */ null, + /* rootResource = */ null, + /* base = */ data, + /* node = */ expression, + ) + } + +/** Evaluates FhirPath expression string over a contextMap and returns string value */ +internal suspend fun evaluateToString( + contextMap: Map, + fhirPathString: String, +): String { + val expressionNode = extractExpressionNode(fhirPathString) + return evaluateToString( + expression = expressionNode, + data = contextMap[extractResourceType(expressionNode)], + contextMap = contextMap, ) +} /** * Evaluates the expression and returns the boolean result. The resources [QuestionnaireResponse] @@ -60,20 +83,22 @@ internal fun evaluateToString( * * %resource = [QuestionnaireResponse], %context = [QuestionnaireResponseItemComponent] */ -internal fun evaluateToBoolean( +internal suspend fun evaluateToBoolean( questionnaireResponse: QuestionnaireResponse, questionnaireResponseItemComponent: QuestionnaireResponseItemComponent, expression: String, contextMap: Map = mapOf(), ): Boolean { - val expressionNode = fhirPathEngine.parse(expression) - return fhirPathEngine.evaluateToBoolean( - contextMap, - questionnaireResponse, - null, - questionnaireResponseItemComponent, - expressionNode, - ) + return withContext(fhirPathEngineDefaultDispatcher) { + val expressionNode = fhirPathEngine.parse(expression) + fhirPathEngine.evaluateToBoolean( + contextMap, + questionnaireResponse, + null, + questionnaireResponseItemComponent, + expressionNode, + ) + } } /** @@ -84,31 +109,47 @@ internal fun evaluateToBoolean( * * %resource = [QuestionnaireResponse], %context = [QuestionnaireResponseItemComponent] */ -internal fun evaluateToBase( +internal suspend fun evaluateToBase( questionnaireResponse: QuestionnaireResponse?, questionnaireResponseItem: QuestionnaireResponseItemComponent?, expression: String, contextMap: Map = mapOf(), ): List { - return fhirPathEngine.evaluate( - /* appContext = */ contextMap, - /* focusResource = */ questionnaireResponse, - /* rootResource = */ null, - /* base = */ questionnaireResponseItem, - /* path = */ expression, - ) + return withContext(fhirPathEngineDefaultDispatcher) { + fhirPathEngine.evaluate( + /* appContext = */ contextMap, + /* focusResource = */ questionnaireResponse, + /* rootResource = */ null, + /* base = */ questionnaireResponseItem, + /* path = */ expression, + ) + } } /** Evaluates the given expression and returns list of [Base] */ -internal fun evaluateToBase(base: Base, expression: String): List { - return fhirPathEngine.evaluate( - /* base = */ base, - /* path = */ expression, - ) +internal suspend fun evaluateToBase(base: Base, expression: String): List { + return withContext(fhirPathEngineDefaultDispatcher) { + fhirPathEngine.evaluate( + /* base = */ base, + /* path = */ expression, + ) + } } /** Evaluates the given list of [Base] elements and returns boolean result */ -internal fun convertToBoolean(items: List) = fhirPathEngine.convertToBoolean(items) +internal suspend fun convertToBoolean(items: List) = + withContext(fhirPathEngineDefaultDispatcher) { fhirPathEngine.convertToBoolean(items) } /** Parse the given expression into [ExpressionNode] */ -internal fun extractExpressionNode(fhirPath: String) = fhirPathEngine.parse(fhirPath) +internal suspend fun extractExpressionNode(fhirPath: String) = + withContext(fhirPathEngineDefaultDispatcher) { fhirPathEngine.parse(fhirPath) } + +/** + * Extract [ResourceType] string representation from constant or name property of given + * [ExpressionNode]. + */ +private fun extractResourceType(expressionNode: ExpressionNode): String? { + // TODO(omarismail94): See if FHIRPathEngine.check() can be used to distinguish invalid + // expression vs an expression that is valid, but does not return one resource only. + return expressionNode.constant?.primitiveValue()?.substring(1) ?: expressionNode.name?.lowercase() +} diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt index e644e4c6d6..40d248fb35 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt @@ -56,6 +56,7 @@ import com.google.android.fhir.datacapture.extensions.createNestedQuestionnaireR import com.google.android.fhir.datacapture.extensions.entryMode import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.datacapture.extensions.maxValue +import com.google.android.fhir.datacapture.fhirpath.fhirPathEngineDefaultDispatcher import com.google.android.fhir.datacapture.testing.DataCaptureTestApplication import com.google.android.fhir.datacapture.validation.Invalid import com.google.android.fhir.datacapture.validation.MAX_VALUE_EXTENSION_URL @@ -157,6 +158,7 @@ class QuestionnaireViewModelTest { "Few tests require a custom application class that implements DataCaptureConfig.Provider" } ReflectionHelpers.setStaticField(DataCapture::class.java, "configuration", null) + fhirPathEngineDefaultDispatcher = mainDispatcherRule.testDispatcher } // ==================================================================== // diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponentsTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponentsTest.kt index beaf16d400..699d1e26d9 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponentsTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponentsTest.kt @@ -27,6 +27,7 @@ import com.google.common.truth.Truth.assertThat import java.math.BigDecimal import java.util.Locale import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.Attachment import org.hl7.fhir.r4.model.BooleanType import org.hl7.fhir.r4.model.CodeType @@ -2220,7 +2221,7 @@ class MoreQuestionnaireItemComponentsTest { } @Test - fun `extractAnswerOptions should return answer options for coding`() { + fun `extractAnswerOptions should return answer options for coding`() = runTest { val questionItem = Questionnaire() .addItem( @@ -2253,7 +2254,7 @@ class MoreQuestionnaireItemComponentsTest { } @Test - fun `extractAnswerOptions should return answer options for resources`() { + fun `extractAnswerOptions should return answer options for resources`() = runTest { val questionItem = Questionnaire() .addItem( @@ -2302,34 +2303,35 @@ class MoreQuestionnaireItemComponentsTest { } @Test - fun `extractAnswerOptions should throw IllegalArgumentException when item type is not reference and data type is resource`() { - val questionItem = - Questionnaire() - .addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "full-name" - type = Questionnaire.QuestionnaireItemType.CHOICE - extension = - listOf( - Extension(EXTENSION_CHOICE_COLUMN_URL).apply { - addExtension(Extension("path", StringType("name.given"))) - addExtension(Extension("label", StringType("GIVEN"))) - addExtension(Extension("forDisplay", BooleanType(true))) - }, - ) - }, - ) - - assertThrows(IllegalArgumentException::class.java) { - questionItem.itemFirstRep.extractAnswerOptions(listOf(Patient())) - } - .run { - assertThat(this.message) - .isEqualTo( - "$EXTENSION_CHOICE_COLUMN_URL not applicable for 'choice'. Only type reference is allowed with resource.", + fun `extractAnswerOptions should throw IllegalArgumentException when item type is not reference and data type is resource`() = + runTest { + val questionItem = + Questionnaire() + .addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "full-name" + type = Questionnaire.QuestionnaireItemType.CHOICE + extension = + listOf( + Extension(EXTENSION_CHOICE_COLUMN_URL).apply { + addExtension(Extension("path", StringType("name.given"))) + addExtension(Extension("label", StringType("GIVEN"))) + addExtension(Extension("forDisplay", BooleanType(true))) + }, + ) + }, ) - } - } + + assertThrows(IllegalArgumentException::class.java) { + runBlocking { questionItem.itemFirstRep.extractAnswerOptions(listOf(Patient())) } + } + .run { + assertThat(this.message) + .isEqualTo( + "$EXTENSION_CHOICE_COLUMN_URL not applicable for 'choice'. Only type reference is allowed with resource.", + ) + } + } @Test fun `sliderStepValue should return the integer value in the sliderStepValue extension`() { diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt index 910cb0e36f..4f9f46d5b2 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt @@ -28,6 +28,7 @@ import java.util.Calendar import java.util.Date import java.util.UUID import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.Address import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.DateType @@ -893,7 +894,7 @@ class ExpressionEvaluatorTest { } @Test - fun `createXFhirQueryFromExpression() should capture all FHIR paths`() { + fun `createXFhirQueryFromExpression() should capture all FHIR paths`() = runTest { val expression = Expression().apply { this.language = Expression.ExpressionLanguage.APPLICATION_XFHIRQUERY.toCode() @@ -919,37 +920,38 @@ class ExpressionEvaluatorTest { } @Test - fun `createXFhirQueryFromExpression() should evaluate to empty string for field that does not exist in resource`() { - val practitioner = - Practitioner().apply { - id = UUID.randomUUID().toString() - active = true - addName(HumanName().apply { this.family = "John" }) - } + fun `createXFhirQueryFromExpression() should evaluate to empty string for field that does not exist in resource`() = + runTest { + val practitioner = + Practitioner().apply { + id = UUID.randomUUID().toString() + active = true + addName(HumanName().apply { this.family = "John" }) + } - val expression = - Expression().apply { - this.language = Expression.ExpressionLanguage.APPLICATION_XFHIRQUERY.toCode() - this.expression = "Practitioner?gender={{Practitioner.gender}}" - } + val expression = + Expression().apply { + this.language = Expression.ExpressionLanguage.APPLICATION_XFHIRQUERY.toCode() + this.expression = "Practitioner?gender={{Practitioner.gender}}" + } - val expressionEvaluator = - ExpressionEvaluator( - Questionnaire(), - QuestionnaireResponse(), - questionnaireItemParentMap = emptyMap(), - questionnaireLaunchContextMap = mapOf(practitioner.resourceType.name to practitioner), - ) + val expressionEvaluator = + ExpressionEvaluator( + Questionnaire(), + QuestionnaireResponse(), + questionnaireItemParentMap = emptyMap(), + questionnaireLaunchContextMap = mapOf(practitioner.resourceType.name to practitioner), + ) - val expressionsToEvaluate = - expressionEvaluator.createXFhirQueryFromExpression( - expression, - ) - assertThat(expressionsToEvaluate).isEqualTo("Practitioner?gender=") - } + val expressionsToEvaluate = + expressionEvaluator.createXFhirQueryFromExpression( + expression, + ) + assertThat(expressionsToEvaluate).isEqualTo("Practitioner?gender=") + } @Test - fun `createXFhirQueryFromExpression() should evaluate correct expression`() { + fun `createXFhirQueryFromExpression() should evaluate correct expression`() = runTest { val practitioner = Practitioner().apply { id = UUID.randomUUID().toString() @@ -981,38 +983,39 @@ class ExpressionEvaluatorTest { } @Test - fun `createXFhirQueryFromExpression() should return empty string if the resource provided does not match the type in the expression`() { - val practitioner = - Practitioner().apply { - id = UUID.randomUUID().toString() - active = true - gender = Enumerations.AdministrativeGender.MALE - addName(HumanName().apply { this.family = "John" }) - } + fun `createXFhirQueryFromExpression() should return empty string if the resource provided does not match the type in the expression`() = + runTest { + val practitioner = + Practitioner().apply { + id = UUID.randomUUID().toString() + active = true + gender = Enumerations.AdministrativeGender.MALE + addName(HumanName().apply { this.family = "John" }) + } - val expression = - Expression().apply { - this.language = Expression.ExpressionLanguage.APPLICATION_XFHIRQUERY.toCode() - this.expression = "Practitioner?gender={{%patient.gender}}" - } + val expression = + Expression().apply { + this.language = Expression.ExpressionLanguage.APPLICATION_XFHIRQUERY.toCode() + this.expression = "Practitioner?gender={{%patient.gender}}" + } - val expressionEvaluator = - ExpressionEvaluator( - Questionnaire(), - QuestionnaireResponse(), - questionnaireItemParentMap = emptyMap(), - questionnaireLaunchContextMap = mapOf(practitioner.resourceType.name to practitioner), - ) + val expressionEvaluator = + ExpressionEvaluator( + Questionnaire(), + QuestionnaireResponse(), + questionnaireItemParentMap = emptyMap(), + questionnaireLaunchContextMap = mapOf(practitioner.resourceType.name to practitioner), + ) - val expressionsToEvaluate = - expressionEvaluator.createXFhirQueryFromExpression( - expression, - ) - assertThat(expressionsToEvaluate).isEqualTo("Practitioner?gender=") - } + val expressionsToEvaluate = + expressionEvaluator.createXFhirQueryFromExpression( + expression, + ) + assertThat(expressionsToEvaluate).isEqualTo("Practitioner?gender=") + } @Test - fun `createXFhirQueryFromExpression() should evaluate fhirPath with percent sign`() { + fun `createXFhirQueryFromExpression() should evaluate fhirPath with percent sign`() = runTest { val patient = Patient().apply { id = UUID.randomUUID().toString() @@ -1043,53 +1046,54 @@ class ExpressionEvaluatorTest { } @Test - fun `createXFhirQueryFromExpression() should evaluate when multiple fhir paths are given`() { - val patient = - Patient().apply { - id = UUID.randomUUID().toString() - active = true - gender = Enumerations.AdministrativeGender.MALE - addName(HumanName().apply { this.family = "John" }) - } + fun `createXFhirQueryFromExpression() should evaluate when multiple fhir paths are given`() = + runTest { + val patient = + Patient().apply { + id = UUID.randomUUID().toString() + active = true + gender = Enumerations.AdministrativeGender.MALE + addName(HumanName().apply { this.family = "John" }) + } - val location = - Location().apply { - id = UUID.randomUUID().toString() - status = Location.LocationStatus.ACTIVE - mode = Location.LocationMode.INSTANCE - address = - Address().apply { - use = Address.AddressUse.HOME - type = Address.AddressType.PHYSICAL - city = "NAIROBI" - } - } + val location = + Location().apply { + id = UUID.randomUUID().toString() + status = Location.LocationStatus.ACTIVE + mode = Location.LocationMode.INSTANCE + address = + Address().apply { + use = Address.AddressUse.HOME + type = Address.AddressType.PHYSICAL + city = "NAIROBI" + } + } - val expression = - Expression().apply { - this.language = Expression.ExpressionLanguage.APPLICATION_XFHIRQUERY.toCode() - this.expression = - "Patient?family={{%patient.name.family}}&address-city={{%location.address.city}}" - } + val expression = + Expression().apply { + this.language = Expression.ExpressionLanguage.APPLICATION_XFHIRQUERY.toCode() + this.expression = + "Patient?family={{%patient.name.family}}&address-city={{%location.address.city}}" + } - val expressionEvaluator = - ExpressionEvaluator( - Questionnaire(), - QuestionnaireResponse(), - questionnaireItemParentMap = emptyMap(), - questionnaireLaunchContextMap = - mapOf( - patient.resourceType.name.lowercase() to patient, - location.resourceType.name.lowercase() to location, - ), - ) + val expressionEvaluator = + ExpressionEvaluator( + Questionnaire(), + QuestionnaireResponse(), + questionnaireItemParentMap = emptyMap(), + questionnaireLaunchContextMap = + mapOf( + patient.resourceType.name.lowercase() to patient, + location.resourceType.name.lowercase() to location, + ), + ) - val expressionsToEvaluate = - expressionEvaluator.createXFhirQueryFromExpression( - expression, - ) - assertThat(expressionsToEvaluate).isEqualTo("Patient?family=John&address-city=NAIROBI") - } + val expressionsToEvaluate = + expressionEvaluator.createXFhirQueryFromExpression( + expression, + ) + assertThat(expressionsToEvaluate).isEqualTo("Patient?family=John&address-city=NAIROBI") + } @Test fun `createXFhirQueryFromExpression() should evaluate variables in answer expression when launch context is null`() { diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtilTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtilTest.kt index dca7a71767..b40b485dc4 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtilTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtilTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 Google LLC + * Copyright 2022-2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package com.google.android.fhir.datacapture.fhirpath import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.HumanName import org.hl7.fhir.r4.model.Patient import org.junit.Test @@ -27,18 +28,19 @@ import org.robolectric.RobolectricTestRunner class FhirPathUtilTest { @Test - fun `evaluateToDisplay should return concatenated string for expressions evaluation on given resource`() { - val expressions = listOf("name.given", "name.family") - val resource = - Patient().apply { - addName( - HumanName().apply { - this.family = "Doe" - this.addGiven("John") - }, - ) - } + fun `evaluateToDisplay should return concatenated string for expressions evaluation on given resource`() = + runTest { + val expressions = listOf("name.given", "name.family") + val resource = + Patient().apply { + addName( + HumanName().apply { + this.family = "Doe" + this.addGiven("John") + }, + ) + } - assertThat(evaluateToDisplay(expressions, resource)).isEqualTo("John Doe") - } + assertThat(evaluateToDisplay(expressions, resource)).isEqualTo("John Doe") + } } diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/TestExpressionValueEvaluator.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/TestExpressionValueEvaluator.kt index 5cb104fb75..a6e329466c 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/TestExpressionValueEvaluator.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/TestExpressionValueEvaluator.kt @@ -26,7 +26,7 @@ object TestExpressionValueEvaluator { * Doesn't handle expressions containing FHIRPath supplements * https://build.fhir.org/ig/HL7/sdc/expressions.html#fhirpath-supplements */ - fun evaluate(base: Base, expression: Expression): Type? = + suspend fun evaluate(base: Base, expression: Expression): Type? = try { evaluateToBase(base, expression.expression).singleOrNull() as? Type } catch (_: Exception) {