From fc5e93ff9cb89749a7635aa7f24086a02f96a66a Mon Sep 17 00:00:00 2001 From: Parth Panchal Date: Thu, 5 Dec 2024 22:15:47 -0800 Subject: [PATCH 1/6] populate initial with initialExpression result collection --- .../datacapture/mapping/ResourceMapper.kt | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt index f1f47495e9..7553318e8f 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt @@ -251,19 +251,22 @@ object ResourceMapper { questionnaireItem.initialExpression ?.let { evaluateToBase( - questionnaireResponse = null, - questionnaireResponseItem = null, - expression = it.expression, - contextMap = launchContexts, - ) - .firstOrNull() + questionnaireResponse = null, + questionnaireResponseItem = null, + expression = it.expression, + contextMap = launchContexts, + ) } ?.let { // Set initial value for the questionnaire item. Questionnaire items should not have both // initial value and initial expression. - val value = it.asExpectedType(questionnaireItem.type) - questionnaireItem.initial = - mutableListOf(Questionnaire.QuestionnaireItemInitialComponent().setValue(value)) + if (it.isNotEmpty()) { + questionnaireItem.initial = + it.map { + val value = it.asExpectedType(questionnaireItem.type) + Questionnaire.QuestionnaireItemInitialComponent().setValue(value) + } + } } populateInitialValues(questionnaireItem.item, launchContexts) From ac017b33f2616841b07d42bb31233053e1d9660a Mon Sep 17 00:00:00 2001 From: Parth Panchal Date: Fri, 6 Dec 2024 18:50:58 -0800 Subject: [PATCH 2/6] handle invariant rules for initialExpression --- .../datacapture/mapping/ResourceMapper.kt | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt index 7553318e8f..233bdd749c 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt @@ -19,6 +19,7 @@ package com.google.android.fhir.datacapture.mapping import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem import com.google.android.fhir.datacapture.extensions.filterByCodeInNameExtension import com.google.android.fhir.datacapture.extensions.initialExpression +import com.google.android.fhir.datacapture.extensions.initialSelected import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.datacapture.extensions.questionnaireLaunchContexts import com.google.android.fhir.datacapture.extensions.targetStructureMap @@ -248,6 +249,16 @@ object ResourceMapper { "QuestionnaireItem item is not allowed to have both initial.value and initial expression. See rule at http://build.fhir.org/ig/HL7/sdc/expressions.html#initialExpression." } + // Initial values can't be specified for groups or display items + if (questionnaireItem.initial.isNotEmpty() || questionnaireItem.initialExpression != null) { + check( + questionnaireItem.type != Questionnaire.QuestionnaireItemType.GROUP && + questionnaireItem.type != Questionnaire.QuestionnaireItemType.DISPLAY, + ) { + "QuestionnaireItem item is not allowed to have initial value or initial expression for groups or display items." + } + } + questionnaireItem.initialExpression ?.let { evaluateToBase( @@ -257,14 +268,28 @@ object ResourceMapper { contextMap = launchContexts, ) } - ?.let { - // Set initial value for the questionnaire item. Questionnaire items should not have both - // initial value and initial expression. - if (it.isNotEmpty()) { + ?.let { evaluatedExpressionResult -> + // Set initial value for the questionnaire item. + if (evaluatedExpressionResult.isEmpty()) return@let + + if (questionnaireItem.answerOption.isNotEmpty()) { + questionnaireItem.answerOption.forEach { answerOption -> + answerOption.initialSelected = + evaluatedExpressionResult.any { answerOption.value.equalsDeep(it) } + } + } else { + // Handle initial values based on whether the questionnaire item repeats questionnaireItem.initial = - it.map { - val value = it.asExpectedType(questionnaireItem.type) - Questionnaire.QuestionnaireItemInitialComponent().setValue(value) + if (questionnaireItem.repeats) { + evaluatedExpressionResult.map { + Questionnaire.QuestionnaireItemInitialComponent() + .setValue( + it.asExpectedType(questionnaireItem.type), + ) + } + } else { + val value = evaluatedExpressionResult.first().asExpectedType(questionnaireItem.type) + listOf(Questionnaire.QuestionnaireItemInitialComponent().setValue(value)) } } } From f1586004708f0839537f4be3c94e2f75c15dfb14 Mon Sep 17 00:00:00 2001 From: Parth Panchal Date: Sat, 7 Dec 2024 19:56:34 -0800 Subject: [PATCH 3/6] convert evaluatedExpressionResult to questionnaireItem.type --- .../android/fhir/datacapture/mapping/ResourceMapper.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt index 233bdd749c..25cb905d15 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt @@ -268,9 +268,11 @@ object ResourceMapper { contextMap = launchContexts, ) } - ?.let { evaluatedExpressionResult -> + ?.let { // Set initial value for the questionnaire item. - if (evaluatedExpressionResult.isEmpty()) return@let + if (it.isEmpty()) return@let + + val evaluatedExpressionResult = it.map { it.asExpectedType(questionnaireItem.type) } if (questionnaireItem.answerOption.isNotEmpty()) { questionnaireItem.answerOption.forEach { answerOption -> @@ -284,11 +286,11 @@ object ResourceMapper { evaluatedExpressionResult.map { Questionnaire.QuestionnaireItemInitialComponent() .setValue( - it.asExpectedType(questionnaireItem.type), + it, ) } } else { - val value = evaluatedExpressionResult.first().asExpectedType(questionnaireItem.type) + val value = evaluatedExpressionResult.first() listOf(Questionnaire.QuestionnaireItemInitialComponent().setValue(value)) } } From bd7a356e53c972703a48836de17bdff181e38958 Mon Sep 17 00:00:00 2001 From: Parth Panchal Date: Sat, 7 Dec 2024 20:08:17 -0800 Subject: [PATCH 4/6] updated answerOptions in test to match initialExpressions' result --- .../android/fhir/datacapture/mapping/ResourceMapperTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt index 52d29f390e..04068741c2 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt @@ -1955,12 +1955,14 @@ class ResourceMapperTest { listOf( Questionnaire.QuestionnaireItemAnswerOptionComponent( Coding().apply { + system = AdministrativeGender.MALE.system code = AdministrativeGender.MALE.toCode() display = AdministrativeGender.MALE.display }, ), Questionnaire.QuestionnaireItemAnswerOptionComponent( Coding().apply { + system = AdministrativeGender.FEMALE.system code = AdministrativeGender.FEMALE.toCode() display = AdministrativeGender.FEMALE.display }, @@ -2011,12 +2013,14 @@ class ResourceMapperTest { Coding().apply { code = AdministrativeGender.MALE.toCode() display = AdministrativeGender.MALE.display + system = AdministrativeGender.MALE.system }, ), Questionnaire.QuestionnaireItemAnswerOptionComponent( Coding().apply { code = AdministrativeGender.FEMALE.toCode() display = AdministrativeGender.FEMALE.display + system = AdministrativeGender.MALE.system }, ), ) From 7e803047a400023c81febdeb52fb0a4d88b08236 Mon Sep 17 00:00:00 2001 From: Parth Panchal Date: Thu, 12 Dec 2024 00:06:53 -0800 Subject: [PATCH 5/6] added rule url in check message for initial value on group or display items. --- .../google/android/fhir/datacapture/mapping/ResourceMapper.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt index 25cb905d15..2e82134b49 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt @@ -255,7 +255,7 @@ object ResourceMapper { questionnaireItem.type != Questionnaire.QuestionnaireItemType.GROUP && questionnaireItem.type != Questionnaire.QuestionnaireItemType.DISPLAY, ) { - "QuestionnaireItem item is not allowed to have initial value or initial expression for groups or display items." + "QuestionnaireItem item is not allowed to have initial value or initial expression for groups or display items. See rule at http://build.fhir.org/ig/HL7/sdc/expressions.html#initialExpression." } } From 0af4de40254cf8cd071c6bcc84598a0d7d18cbe0 Mon Sep 17 00:00:00 2001 From: Parth Panchal Date: Thu, 12 Dec 2024 00:16:27 -0800 Subject: [PATCH 6/6] added test for invariant rule que-8 & codings without system match in initialExpression --- .../datacapture/mapping/ResourceMapperTest.kt | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt index 04068741c2..3e057da1f6 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt @@ -42,6 +42,7 @@ import org.hl7.fhir.r4.model.Address import org.hl7.fhir.r4.model.Base import org.hl7.fhir.r4.model.BooleanType import org.hl7.fhir.r4.model.CodeType +import org.hl7.fhir.r4.model.CodeableConcept import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.ContactPoint import org.hl7.fhir.r4.model.DateType @@ -2977,6 +2978,160 @@ class ResourceMapperTest { ) } + @Test + fun `populate() should fail with IllegalStateException when QuestionnaireItem of group or display item has a initial value`(): + Unit = runBlocking { + val questionnaire = + Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(CODE_SYSTEM_LAUNCH_CONTEXT, "father", "Father")), + Extension("type", CodeType("Patient")), + ) + } + } + .addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "patient-gender-group-initial-expression" + type = Questionnaire.QuestionnaireItemType.GROUP + extension = + listOf( + Extension( + ITEM_INITIAL_EXPRESSION_URL, + Expression().apply { + language = "text/fhirpath" + expression = "%father.gender" + }, + ), + ) + item = + mutableListOf( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "patient-gender-display-initial" + type = Questionnaire.QuestionnaireItemType.DISPLAY + initial = + listOf(Questionnaire.QuestionnaireItemInitialComponent(StringType("male"))) + }, + ) + }, + ) + .addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "patient-gender-group-initial" + type = Questionnaire.QuestionnaireItemType.GROUP + initial = listOf(Questionnaire.QuestionnaireItemInitialComponent(StringType("male"))) + item = + mutableListOf( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "patient-gender-display-initial-expression" + type = Questionnaire.QuestionnaireItemType.DISPLAY + extension = + listOf( + Extension( + ITEM_INITIAL_EXPRESSION_URL, + Expression().apply { + language = "text/fhirpath" + expression = "%father.gender" + }, + ), + ) + }, + ) + }, + ) + + val patient = Patient().apply { gender = Enumerations.AdministrativeGender.MALE } + val errorMessage = + assertFailsWith { + ResourceMapper.populate(questionnaire, mapOf("father" to patient)) + } + .localizedMessage + assertThat(errorMessage) + .isEqualTo( + "QuestionnaireItem item is not allowed to have initial value or initial expression for groups or display items. See rule at http://build.fhir.org/ig/HL7/sdc/expressions.html#initialExpression.", + ) + } + + @Test + fun `populate() should select answerOption of type coding(without system) if initialExpression result matches its coding(without system)`() = + runBlocking { + val questionnaire = + Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension( + "name", + Coding( + CODE_SYSTEM_LAUNCH_CONTEXT, + "observation", + "Test Observation", + ), + ), + Extension("type", CodeType("Observation")), + ) + } + } + .addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "observation-choice" + type = Questionnaire.QuestionnaireItemType.CHOICE + extension = + listOf( + Extension( + ITEM_INITIAL_EXPRESSION_URL, + Expression().apply { + language = "text/fhirpath" + expression = "%observation.value.coding" + }, + ), + ) + answerOption = + listOf( + Questionnaire.QuestionnaireItemAnswerOptionComponent( + Coding().apply { + code = "correct-code-val" + display = "correct-display-val" + }, + ), + Questionnaire.QuestionnaireItemAnswerOptionComponent( + Coding().apply { + code = "wrong-code-val" + display = "wrong-display-val" + }, + ), + ) + }, + ) + + val observation = + Observation().apply { + value = + CodeableConcept().apply { + coding = + mutableListOf( + Coding().apply { + code = "correct-code-val" + display = "correct-display-val" + }, + ) + } + } + + val questionnaireResponse = + ResourceMapper.populate(questionnaire, mapOf("observation" to observation)) + + assertThat((questionnaireResponse.item[0].answer[0].value as Coding).code) + .isEqualTo("correct-code-val") + assertThat((questionnaireResponse.item[0].answer[0].value as Coding).display) + .isEqualTo("correct-display-val") + } + @Test fun `extract() definition based extraction should extract multiple values of a list field in a group`(): Unit = runBlocking {