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

EnableWhenExpression context literal fhirpath supplement implementation #1957

Merged
merged 14 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from 8 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
75 changes: 75 additions & 0 deletions catalog/src/main/assets/behavior_enable_when_expression.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"resourceType": "Questionnaire",
"item": [
{
"linkId": "1",
"type": "choice",
"text": "Has the client received this year's seasonal flu vaccine?",
"answerOption": [
{
"valueCoding": {
"code": "Y",
"display": "Yes",
"system": "custom"
}
},
{
"valueCoding": {
"code": "N",
"display": "No",
"system": "custom"
}
},
{
"valueCoding": {
"code": "unknown",
"display": "Unknown",
"system": "custom"
}
}
]
},
{
"text": "Date of Vaccination?",
"type": "date",
"linkId": "yes",
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression",
"valueExpression": {
"expression": "%resource.descendants().where(linkId='1').answer.value.display.lower() = %context.linkId",
"language": "text/fhirpath"
}
}
]
},
{
"text": "Date of vaccination not provided",
"type": "display",
"linkId": "no",
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression",
"valueExpression": {
"expression": "%resource.descendants().where(linkId='1').answer.value.display.lower() = %context.linkId",
"language": "text/fhirpath"
}
}
]
},
{
"text": "Date of vaccination not known",
"type": "display",
"linkId": "unknown",
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression",
"valueExpression": {
"expression": "%resource.descendants().where(linkId='1').answer.value.where(display.lower() = %context.linkId).exists()",
"language": "text/fhirpath"
}
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ class BehaviorListViewModel(application: Application) : AndroidViewModel(applica
R.string.behavior_name_calculated_expression,
"behavior_calculated_expression.json"
),
ENABLE_WHEN_EXPRESSION(
R.drawable.ic_skiplogic_behavior,
R.string.behavior_name_enable_when_expression,
"behavior_enable_when_expression.json"
),
SKIP_LOGIC(
R.drawable.ic_skiplogic_behavior,
R.string.behavior_name_skip_logic,
Expand Down
3 changes: 3 additions & 0 deletions catalog/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@
<string
name="behavior_name_skip_logic_info"
>If Yes is selected, a follow-up question is displayed. If No is selected, no follow-up questions are displayed.</string>
<string
name="behavior_name_enable_when_expression"
>Enable When Expression</string>
maimoonak marked this conversation as resolved.
Show resolved Hide resolved
<string
name="behavior_name_calculated_expression_info"
>Input age to automatically calculate birthdate until birthdate is updated manually.</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package com.google.android.fhir.datacapture.enablement
import com.google.android.fhir.compareTo
import com.google.android.fhir.datacapture.extensions.allItems
import com.google.android.fhir.datacapture.extensions.enableWhenExpression
import com.google.android.fhir.datacapture.fhirpath.fhirPathEngine
import com.google.android.fhir.datacapture.fhirpath.evaluateToBoolean
import com.google.android.fhir.equals
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
Expand Down Expand Up @@ -110,8 +110,10 @@ internal class EnablementEvaluator(val questionnaireResponse: QuestionnaireRespo

// Evaluate `enableWhenExpression`.
if (enableWhenExpression != null && enableWhenExpression.hasExpression()) {
return fhirPathEngine.convertToBoolean(
fhirPathEngine.evaluate(questionnaireResponse, enableWhenExpression.expression)
return evaluateToBoolean(
questionnaireResponse,
questionnaireResponseItem,
enableWhenExpression.expression
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package com.google.android.fhir.datacapture.fhirpath
import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.utils.FHIRPathEngine

Expand All @@ -34,3 +36,22 @@ internal val fhirPathEngine: FHIRPathEngine =
*/
internal fun evaluateToDisplay(expressions: List<String>, data: Resource) =
expressions.joinToString(" ") { fhirPathEngine.evaluateToString(data, it) }

jingtang10 marked this conversation as resolved.
Show resolved Hide resolved
/**
* Evaluates the expression and returns the boolean result. The resources [QuestionnaireResponse]
* and [QuestionnaireResponseItemComponent] are passed as fhirPath supplements as defined in fhir
* specs https://build.fhir.org/ig/HL7/sdc/expressions.html#fhirpath-supplements
*
* %resource = [QuestionnaireResponse], %context = [QuestionnaireResponseItemComponent]
*/
internal fun evaluateToBoolean(
questionnaireResponse: QuestionnaireResponse,
questionnaireResponseItemComponent: QuestionnaireResponseItemComponent,
expression: String
) =
fhirPathEngine.evaluateToBoolean(
questionnaireResponse,
null,
questionnaireResponseItemComponent,
expression
)
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.P])
class EnablementEvaluatorTest {
val iParser: IParser = FhirContext.forR4Cached().newJsonParser()

@Test
fun evaluate_noEnableWhen_shouldReturnTrue() {
assertEnableWhen().isTrue()
Expand Down Expand Up @@ -159,8 +161,6 @@ class EnablementEvaluatorTest {
}
""".trimIndent()

val iParser: IParser = FhirContext.forR4().newJsonParser()

val questionnaire =
iParser.parseResource(Questionnaire::class.java, questionnaireJson) as Questionnaire

Expand Down Expand Up @@ -241,8 +241,6 @@ class EnablementEvaluatorTest {
}
""".trimIndent()

val iParser: IParser = FhirContext.forR4().newJsonParser()

val questionnaire =
iParser.parseResource(Questionnaire::class.java, questionnaireJson) as Questionnaire

Expand All @@ -263,6 +261,85 @@ class EnablementEvaluatorTest {
.isFalse()
}

@Test
fun `evaluate() should evaluate enableWhenExpression with context fhirpath supplement literal`() =
runBlocking {
@Language("JSON")
val questionnaireJson =
"""
{
"resourceType": "Questionnaire",
"item": [
{
"linkId": "1",
"definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.gender",
"type": "choice",
"text": "Gender"
},
{
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression",
"valueExpression": {
"language": "text/fhirpath",
"expression": "%resource.repeat(item).where(linkId='1').answer.value.code = %context.linkId"
}
}
],
"linkId" : "female",
"text": "Have you had mammogram before?(enableWhenExpression = only when gender is female)",
"type": "choice",
"answerValueSet": "http://hl7.org/fhir/ValueSet/yesnodontknow"
}
]
}
""".trimIndent()

@Language("JSON")
val questionnaireResponseJson =
"""
{
"resourceType": "QuestionnaireResponse",
"item": [
{
"linkId": "1",
"answer": [
{
"valueCoding": {
"system": "http://hl7.org/fhir/administrative-gender",
"code": "female",
"display": "Female"
}
}
]
},
{
"linkId": "female"
}
]
}
""".trimIndent()

val questionnaire =
iParser.parseResource(Questionnaire::class.java, questionnaireJson) as Questionnaire

val questionnaireItem: Questionnaire.QuestionnaireItemComponent =
questionnaire.item.find { it.linkId == "female" }!!

val questionnaireResponse =
iParser.parseResource(QuestionnaireResponse::class.java, questionnaireResponseJson)
as QuestionnaireResponse

assertThat(
EnablementEvaluator(questionnaireResponse)
.evaluate(
questionnaireItem,
questionnaireResponse.item[1],
)
)
.isTrue()
}

@Test
fun evaluate_expectAnswerDoesNotExist_answerDoesNotExist_shouldReturnTrue() {
assertEnableWhen(
Expand Down