From 9fa64b64e3948a1f67c4409ec68f2d20660fdacb Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Mon, 31 Jul 2023 10:06:42 +0700 Subject: [PATCH 01/24] Use launchContext for initialExpression --- .../fhir/datacapture/QuestionnaireFragment.kt | 8 +- .../datacapture/QuestionnaireViewModel.kt | 8 +- .../extensions/MoreQuestionnaires.kt | 85 ++++++++++++------- .../datacapture/mapping/ResourceMapper.kt | 45 ++++------ 4 files changed, 79 insertions(+), 67 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt index 66d910042b..e4d8d8fb9c 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt @@ -319,9 +319,9 @@ class QuestionnaireFragment : Fragment() { * user, etc. is "in context" at the time the questionnaire response is being completed: * https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-launchContext.html * - * @param launchContexts list of serialized resources + * @param launchContexts map of launchContext name and serialized resources */ - fun setQuestionnaireLaunchContexts(launchContexts: List) = apply { + fun setQuestionnaireLaunchContexts(launchContexts: Map) = apply { args.add(EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS to launchContexts) } @@ -416,7 +416,9 @@ class QuestionnaireFragment : Fragment() { */ internal const val EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING = "questionnaire-response" - /** A list of JSON encoded strings extra for each questionnaire context. */ + /** + * A map of launchContext name and JSON encoded strings extra for each questionnaire context. + */ internal const val EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS = "questionnaire-launch-contexts" /** diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index aa0361e303..5e29e0f794 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -34,6 +34,7 @@ import com.google.android.fhir.datacapture.extensions.cqfExpression import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem import com.google.android.fhir.datacapture.extensions.entryMode import com.google.android.fhir.datacapture.extensions.flattened +import com.google.android.fhir.datacapture.extensions.getMatchingLaunchContexts import com.google.android.fhir.datacapture.extensions.hasDifferentAnswerSet import com.google.android.fhir.datacapture.extensions.isDisplayItem import com.google.android.fhir.datacapture.extensions.isFhirPath @@ -170,13 +171,14 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireLaunchContextMap = if (state.contains(QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS)) { - val launchContextJsonStrings: List = + val launchContextJsonStrings: Map = state[QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS]!! - val launchContexts = launchContextJsonStrings.map { parser.parseResource(it) as Resource } + val launchContexts = + launchContextJsonStrings.mapValues { parser.parseResource(it.value) as Resource } questionnaire.questionnaireLaunchContexts?.let { launchContextExtensions -> validateLaunchContextExtensions(launchContextExtensions) - launchContexts.associateBy { it.resourceType.name.lowercase() } + getMatchingLaunchContexts(launchContexts, launchContextExtensions) } } else { null diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt index 8ba5377701..384ba64dd8 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt @@ -22,6 +22,7 @@ import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.Questionnaire +import org.hl7.fhir.r4.model.Resource /** * The StructureMap url in the @@ -73,38 +74,46 @@ internal fun validateLaunchContextExtensions(launchContextExtensions: List, + launchContextExtensions: List +): Map { + val nameCodes = + launchContextExtensions.mapNotNull { extension -> + (extension.getExtensionByUrl("name").value as? Coding)?.code + } + + return launchContexts.filterKeys { nameCodes.contains(it) } +} + /** * The set of supported launch contexts, as per: http://hl7.org/fhir/uv/sdc/ValueSet/launchContext */ private enum class QuestionnaireLaunchContextSet( - val code: String, - val display: String, val system: String, val resourceType: String, ) { - PATIENT("patient", "Patient", EXTENSION_LAUNCH_CONTEXT, "Patient"), - ENCOUNTER("encounter", "Encounter", EXTENSION_LAUNCH_CONTEXT, "Encounter"), - LOCATION("location", "Location", EXTENSION_LAUNCH_CONTEXT, "Location"), - USER_AS_PATIENT("user", "User", EXTENSION_LAUNCH_CONTEXT, "Patient"), - USER_AS_PRACTITIONER("user", "User", EXTENSION_LAUNCH_CONTEXT, "Practitioner"), - USER_AS_PRACTITIONER_ROLE("user", "User", EXTENSION_LAUNCH_CONTEXT, "PractitionerRole"), - USER_AS_RELATED_PERSON("user", "User", EXTENSION_LAUNCH_CONTEXT, "RelatedPerson"), - STUDY("study", "ResearchStudy", EXTENSION_LAUNCH_CONTEXT, "ResearchStudy"), + PATIENT(EXTENSION_LAUNCH_CONTEXT, "Patient"), + ENCOUNTER(EXTENSION_LAUNCH_CONTEXT, "Encounter"), + LOCATION(EXTENSION_LAUNCH_CONTEXT, "Location"), + USER_AS_PATIENT(EXTENSION_LAUNCH_CONTEXT, "Patient"), + USER_AS_PRACTITIONER(EXTENSION_LAUNCH_CONTEXT, "Practitioner"), + USER_AS_PRACTITIONER_ROLE(EXTENSION_LAUNCH_CONTEXT, "PractitionerRole"), + USER_AS_RELATED_PERSON(EXTENSION_LAUNCH_CONTEXT, "RelatedPerson"), + STUDY(EXTENSION_LAUNCH_CONTEXT, "ResearchStudy"), } /** 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 929988033b..4055782ddb 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 @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022-2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ package com.google.android.fhir.datacapture.mapping import com.google.android.fhir.datacapture.DataCapture import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem +import com.google.android.fhir.datacapture.extensions.getMatchingLaunchContexts +import com.google.android.fhir.datacapture.extensions.questionnaireLaunchContexts import com.google.android.fhir.datacapture.extensions.targetStructureMap import com.google.android.fhir.datacapture.extensions.toCodeType import com.google.android.fhir.datacapture.extensions.toCoding @@ -217,9 +219,14 @@ object ResourceMapper { */ suspend fun populate( questionnaire: Questionnaire, - vararg resources: Resource + launchContexts: Map ): QuestionnaireResponse { - populateInitialValues(questionnaire.item, *resources) + val filteredLaunchContexts = + getMatchingLaunchContexts( + launchContexts, + questionnaire.questionnaireLaunchContexts ?: listOf() + ) + populateInitialValues(questionnaire.item, filteredLaunchContexts) return QuestionnaireResponse().apply { item = questionnaire.item.map { it.createQuestionnaireResponseItem() } } @@ -227,14 +234,14 @@ object ResourceMapper { private suspend fun populateInitialValues( questionnaireItems: List, - vararg resources: Resource + launchContexts: Map ) { - questionnaireItems.forEach { populateInitialValue(it, *resources) } + questionnaireItems.forEach { populateInitialValue(it, launchContexts) } } private suspend fun populateInitialValue( questionnaireItem: Questionnaire.QuestionnaireItemComponent, - vararg resources: Resource + launchContexts: Map ) { check(questionnaireItem.initial.isEmpty() || questionnaireItem.initialExpression == null) { "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." @@ -242,12 +249,7 @@ object ResourceMapper { questionnaireItem.initialExpression ?.let { - fhirPathEngine - .evaluate( - selectPopulationContext(resources.asList(), it), - it.expression.removePrefix("%") - ) - .singleOrNull() + fhirPathEngine.evaluate(launchContexts, null, null, null, it.expression).firstOrNull() } ?.let { // Set initial value for the questionnaire item. Questionnaire items should not have both @@ -258,24 +260,7 @@ object ResourceMapper { ) } - populateInitialValues(questionnaireItem.item, *resources) - } - - /** - * Returns the population context for the questionnaire/group. - * - * The resource of the same type as the expected type of the initial expression will be selected - * first. Otherwise, the first resource in the list will be selected. - * - * TODO: rewrite this using the launch context and population context. - */ - private fun selectPopulationContext( - resources: List, - initialExpression: Expression - ): Resource? { - val resourceType = initialExpression.expression.substringBefore(".").removePrefix("%") - return resources.singleOrNull { it.resourceType.name.lowercase() == resourceType.lowercase() } - ?: resources.firstOrNull() + populateInitialValues(questionnaireItem.item, launchContexts) } private val Questionnaire.QuestionnaireItemComponent.initialExpression: Expression? From 8b765fdb2d6c4d16362e6645c2e7b1f3bd309d97 Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Mon, 31 Jul 2023 10:07:16 +0700 Subject: [PATCH 02/24] Test ResourceMapper --- .../datacapture/mapping/ResourceMapperTest.kt | 334 +++++++++++++----- 1 file changed, 252 insertions(+), 82 deletions(-) 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 9c3af08f8d..e5c9d3f9f8 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 @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022-2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ import androidx.test.core.app.ApplicationProvider import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum import ca.uhn.fhir.parser.IParser +import com.google.android.fhir.datacapture.extensions.EXTENSION_LAUNCH_CONTEXT +import com.google.android.fhir.datacapture.extensions.EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT import com.google.android.fhir.datacapture.views.factories.localDate import com.google.common.truth.Truth.assertThat import java.math.BigDecimal @@ -32,23 +34,21 @@ import kotlin.test.assertFailsWith import kotlinx.coroutines.runBlocking import org.hl7.fhir.exceptions.FHIRException import org.hl7.fhir.r4.model.Address -import org.hl7.fhir.r4.model.Annotation import org.hl7.fhir.r4.model.Base import org.hl7.fhir.r4.model.BooleanType import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.ContactPoint import org.hl7.fhir.r4.model.DateType +import org.hl7.fhir.r4.model.Encounter import org.hl7.fhir.r4.model.Enumerations import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.HumanName import org.hl7.fhir.r4.model.Immunization -import org.hl7.fhir.r4.model.MarkdownType import org.hl7.fhir.r4.model.Observation import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse -import org.hl7.fhir.r4.model.RelatedPerson import org.hl7.fhir.r4.model.ResourceFactory import org.hl7.fhir.r4.model.StringType import org.hl7.fhir.r4.model.codesystems.AdministrativeGender @@ -801,6 +801,16 @@ class ResourceMapperTest { runBlocking { val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "mother", "Mother")), + Extension("type", StringType("Patient")) + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "patient-dob" @@ -820,7 +830,7 @@ class ResourceMapperTest { val patientId = UUID.randomUUID().toString() val patient = Patient().apply { id = "Patient/$patientId/_history/2" } - val questionnaireResponse = ResourceMapper.populate(questionnaire, patient) + val questionnaireResponse = ResourceMapper.populate(questionnaire, mapOf("mother" to patient)) assertThat((questionnaireResponse.item[0].answer[0].value as DateType).localDate) .isEqualTo((DateType(Date())).localDate) @@ -1173,6 +1183,25 @@ class ResourceMapperTest { { "resourceType": "Questionnaire", "id": "client-registration-sample", + "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext", + "extension": [ + { + "url": "name", + "valueCoding": { + "system": "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", + "code": "father", + "display": "Patient" + } + }, + { + "url": "type", + "valueCode": "Patient" + } + ] + } + ], "status": "active", "date": "2020-11-18T07:24:47.111Z", "subjectType": [ @@ -1194,7 +1223,7 @@ class ResourceMapperTest { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.name.given", + "expression": "%father.name.given", "name": "patientName" } } @@ -1210,7 +1239,7 @@ class ResourceMapperTest { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.name.family", + "expression": "%father.name.family", "name": "patientFamily" } } @@ -1228,7 +1257,7 @@ class ResourceMapperTest { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.birthDate", + "expression": "%father.birthDate", "name": "patientBirthDate" } } @@ -1245,7 +1274,7 @@ class ResourceMapperTest { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.gender.value", + "expression": "%father.gender.value", "name": "patientGender" } } @@ -1283,7 +1312,7 @@ class ResourceMapperTest { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.telecom.value", + "expression": "%father.telecom.value", "name": "patientTelecom" } } @@ -1306,7 +1335,7 @@ class ResourceMapperTest { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.address.city", + "expression": "%father.address.city", "name": "patientCity" } } @@ -1322,7 +1351,7 @@ class ResourceMapperTest { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.address.country", + "expression": "%father.address.country", "name": "patientCity" } } @@ -1340,7 +1369,7 @@ class ResourceMapperTest { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.active", + "expression": "%father.active", "name": "patientActive" } } @@ -1361,7 +1390,7 @@ class ResourceMapperTest { as Questionnaire val patient = createPatientResource() - val response = ResourceMapper.populate(uriTestQuestionnaire, patient) + val response = ResourceMapper.populate(uriTestQuestionnaire, mapOf("father" to patient)) val responseItem = response.item[0] assertThat(((responseItem.item[0].item[0].answer[0]).value as StringType).valueAsString) @@ -1385,29 +1414,44 @@ class ResourceMapperTest { @Test fun `populate() should fill QuestionnaireResponse with values when given multiple Resources`() = runBlocking { - val relatedPerson = - RelatedPerson().apply { - name = - listOf( - HumanName().apply { - given = listOf(StringType("John")) - family = "Doe" - } - ) - birthDate = "1990-05-20".toDateFromFormatYyyyMmDd() - } - - val observation = - Observation().apply { - value = StringType("Allergic to dairy products and proteins") - note = listOf(Annotation(MarkdownType("Patient Registration Comments"))) - } - val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), + Extension("type", StringType("Patient")) + ) + } + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "mother", "Mother")), + Extension("type", StringType("Patient")) + ) + } + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension( + "name", + Coding( + EXTENSION_LAUNCH_CONTEXT, + "registration-encounter", + "Registration Encounter" + ) + ), + Extension("type", StringType("Encounter")) + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { - linkId = "first-name" + linkId = "first-name-father" type = Questionnaire.QuestionnaireItemType.TEXT extension = listOf( @@ -1415,7 +1459,7 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient.name.given" + expression = "%father.name.given" } ) ) @@ -1423,38 +1467,15 @@ class ResourceMapperTest { ) .addItem( Questionnaire.QuestionnaireItemComponent().apply { - linkId = "related-person-details" - type = Questionnaire.QuestionnaireItemType.GROUP - item = - listOf( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "rp-family-name" - type = Questionnaire.QuestionnaireItemType.TEXT - extension = - listOf( - Extension( - ITEM_INITIAL_EXPRESSION_URL, - Expression().apply { - language = "text/fhirpath" - expression = "RelatedPerson.name.family" - } - ) - ) - } - ) - } - ) - .addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "related-person-dob" - type = Questionnaire.QuestionnaireItemType.DATE + linkId = "first-name-mother" + type = Questionnaire.QuestionnaireItemType.TEXT extension = listOf( Extension( ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "RelatedPerson.birthDate" + expression = "%mother.name.given" } ) ) @@ -1462,7 +1483,7 @@ class ResourceMapperTest { ) .addItem( Questionnaire.QuestionnaireItemComponent().apply { - linkId = "comments" + linkId = "encounter-reason" type = Questionnaire.QuestionnaireItemType.TEXT extension = listOf( @@ -1470,33 +1491,140 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Observation.value" + expression = "%registration-encounter.reasonCode[0].text" } ) ) } ) - val patient = createPatientResource() + val patientFather = + Patient().apply { + active = true + gender = Enumerations.AdministrativeGender.MALE + name = listOf(HumanName().apply { given = mutableListOf(StringType("Salman")) }) + } + + val patientMother = + Patient().apply { + active = true + gender = Enumerations.AdministrativeGender.FEMALE + name = listOf(HumanName().apply { given = mutableListOf(StringType("Fatima")) }) + } + + val encounter = + Encounter().apply { + addReasonCode().apply { addCoding().apply { text = "Registration Task" } } + } + val questionnaireResponse = - ResourceMapper.populate(questionnaire, patient, relatedPerson, observation) + ResourceMapper.populate( + questionnaire, + mapOf( + "father" to patientFather, + "mother" to patientMother, + "registration-encounter" to encounter + ) + ) assertThat((questionnaireResponse.item[0].answer[0].value as StringType).valueAsString) .isEqualTo("Salman") - assertThat( - ((questionnaireResponse.item[1].item[0].answer[0]).value as StringType).valueAsString + assertThat(((questionnaireResponse.item[1].answer[0]).value as StringType).valueAsString) + .isEqualTo("Fatima") + assertThat(((questionnaireResponse.item[2].answer[0]).value as StringType).valueAsString) + .isEqualTo("Registration Task") + } + + @Test + fun `populate() should not fill QuestionnaireResponse with values if the intended launch context extension is not declared`(): + Unit = runBlocking { + val questionnaire = + Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), + Extension("type", StringType("Patient")) + ) + } + } + .addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "first-name-father" + type = Questionnaire.QuestionnaireItemType.TEXT + extension = + listOf( + Extension( + ITEM_INITIAL_EXPRESSION_URL, + Expression().apply { + language = "text/fhirpath" + expression = "%father.name.given" + } + ) + ) + } ) - .isEqualTo("Doe") - assertThat(((questionnaireResponse.item[2].answer[0]).value as DateType).valueAsString) - .isEqualTo("1990-05-20") - assertThat(((questionnaireResponse.item[3].answer[0]).value as StringType).valueAsString) - .isEqualTo("Allergic to dairy products and proteins") + .addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "first-name-mother" + type = Questionnaire.QuestionnaireItemType.TEXT + extension = + listOf( + Extension( + ITEM_INITIAL_EXPRESSION_URL, + Expression().apply { + language = "text/fhirpath" + expression = "%mother.name.given" + } + ) + ) + } + ) + + val patientFather = + Patient().apply { + active = true + gender = Enumerations.AdministrativeGender.MALE + name = listOf(HumanName().apply { given = mutableListOf(StringType("Salman")) }) + } + + val patientMother = + Patient().apply { + active = true + gender = Enumerations.AdministrativeGender.FEMALE + name = listOf(HumanName().apply { given = mutableListOf(StringType("Fatima")) }) + } + + val questionnaireResponse = + ResourceMapper.populate( + questionnaire, + mapOf("father" to patientFather, "mother" to patientMother) + ) + + assertThat((questionnaireResponse.item[0].answer[0].value as StringType).valueAsString) + .isEqualTo("Salman") + assertFailsWith { + assertThat(((questionnaireResponse.item[1].answer[0]).value as StringType).valueAsString) + .isEqualTo("Fatima") } + } @Test fun `populate() should correctly populate IdType value in QuestionnaireResponse`() = runBlocking { val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), + Extension("type", StringType("Patient")) + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "patient-id" @@ -1507,7 +1635,7 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient.id" + expression = "%father.id" } ) ) @@ -1516,7 +1644,7 @@ class ResourceMapperTest { val patientId = UUID.randomUUID().toString() val patient = Patient().apply { id = "Patient/$patientId" } - val questionnaireResponse = ResourceMapper.populate(questionnaire, patient) + val questionnaireResponse = ResourceMapper.populate(questionnaire, mapOf("father" to patient)) assertThat((questionnaireResponse.item[0].answer[0].value as StringType).value) .isEqualTo(patientId) @@ -1527,6 +1655,16 @@ class ResourceMapperTest { runBlocking { val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), + Extension("type", StringType("Patient")) + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "patient-id" @@ -1537,7 +1675,7 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient.id" + expression = "%father.id" } ) ) @@ -1546,7 +1684,7 @@ class ResourceMapperTest { val patientId = UUID.randomUUID().toString() val patient = Patient().apply { id = "Patient/$patientId/_history/2" } - val questionnaireResponse = ResourceMapper.populate(questionnaire, patient) + val questionnaireResponse = ResourceMapper.populate(questionnaire, mapOf("father" to patient)) assertThat((questionnaireResponse.item[0].answer[0].value as StringType).value) .isEqualTo(patientId) @@ -1557,6 +1695,16 @@ class ResourceMapperTest { runBlocking { val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "mother", "Mother")), + Extension("type", StringType("Patient")) + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "patient-gender" @@ -1567,7 +1715,7 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient.gender" + expression = "%mother.gender" } ) ) @@ -1590,7 +1738,7 @@ class ResourceMapperTest { ) val patient = Patient().apply { gender = Enumerations.AdministrativeGender.FEMALE } - val questionnaireResponse = ResourceMapper.populate(questionnaire, patient) + val questionnaireResponse = ResourceMapper.populate(questionnaire, mapOf("mother" to patient)) assertThat((questionnaireResponse.item[0].answer[0].value as Coding).code).isEqualTo("female") assertThat((questionnaireResponse.item[0].answer[0].value as Coding).display) @@ -1601,6 +1749,16 @@ class ResourceMapperTest { fun `populate() should populate nested non-group questions`() = runBlocking { val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "mother", "Mother")), + Extension("type", StringType("Patient")) + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "patient-gender" @@ -1611,7 +1769,7 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient.gender" + expression = "%mother.gender" } ) ) @@ -1641,7 +1799,7 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient.id" + expression = "%mother.id" } ) ) @@ -1656,7 +1814,7 @@ class ResourceMapperTest { gender = Enumerations.AdministrativeGender.FEMALE id = "Patient/$patientId/_history/2" } - val questionnaireResponse = ResourceMapper.populate(questionnaire, patient) + val questionnaireResponse = ResourceMapper.populate(questionnaire, mapOf("mother" to patient)) assertThat((questionnaireResponse.item[0].answer[0].value as Coding).code).isEqualTo("female") assertThat((questionnaireResponse.item[0].answer[0].value as Coding).display) @@ -2533,6 +2691,16 @@ class ResourceMapperTest { Unit = runBlocking { val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), + Extension("type", StringType("Patient")) + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "patient-gender" @@ -2543,7 +2711,7 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient.gender" + expression = "%father.gender" } ) ) @@ -2553,7 +2721,9 @@ class ResourceMapperTest { val patient = Patient().apply { gender = Enumerations.AdministrativeGender.MALE } val errorMessage = - assertFailsWith { ResourceMapper.populate(questionnaire, patient) } + assertFailsWith { + ResourceMapper.populate(questionnaire, mapOf("father" to patient)) + } .localizedMessage assertThat(errorMessage) .isEqualTo( From 59c7d6a5f5be0f0b5169e31db1d91bc9cb2c4a56 Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Mon, 31 Jul 2023 14:43:15 +0700 Subject: [PATCH 03/24] Fix test --- .../fhir/datacapture/QuestionnaireViewModelTest.kt | 2 +- .../extensions/MoreQuestionnairesTest.kt | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) 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 d5b1d6b986..e3195b54a9 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 @@ -4171,7 +4171,7 @@ class QuestionnaireViewModelTest { state.set(EXTRA_QUESTIONNAIRE_JSON_STRING, printer.encodeResourceToString(questionnaire)) state.set( EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS, - listOf(printer.encodeResourceToString(patient)) + mapOf("patient" to printer.encodeResourceToString(patient)) ) val viewModel = QuestionnaireViewModel(context, state) diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt index 2229f53448..9d27817663 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt @@ -140,14 +140,11 @@ class MoreQuestionnairesTest { } @Test - fun `should throw exception if resource type in type extension is different to what is in name extension`() { + fun `should throw exception if system in name extension is different to the fhir standard`() { val launchContextExtension = Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") .apply { - addExtension( - "name", - Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "encounter", "Encounter") - ) + addExtension("name", Coding("http://idk-bro", "grandma", "Grandma")) addExtension("type", CodeType("Patient")) } @@ -217,12 +214,7 @@ class MoreQuestionnairesTest { fun `should throw exception if the name extension is not present`() { val launchContextExtension = Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") - .apply { - addExtension( - "name", - Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "user", "User") - ) - } + .apply { addExtension("type", CodeType("Patient")) } val errorMessage = assertFailsWith { From 45a6ae0426e0abf60fd343759726946d56a865c3 Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Mon, 31 Jul 2023 16:25:46 +0700 Subject: [PATCH 04/24] Fix launchContexts for demo app when editing patient --- .../new-patient-registration-paginated.json | 33 ++++++++++++++----- .../android/fhir/demo/EditPatientViewModel.kt | 14 ++++---- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/demo/src/main/assets/new-patient-registration-paginated.json b/demo/src/main/assets/new-patient-registration-paginated.json index 25640065ab..a9d05f79db 100644 --- a/demo/src/main/assets/new-patient-registration-paginated.json +++ b/demo/src/main/assets/new-patient-registration-paginated.json @@ -8,6 +8,23 @@ "Patient" ], "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext", + "extension": [ + { + "url": "name", + "valueCoding": { + "system": "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", + "code": "client", + "display": "Client as a Patient" + } + }, + { + "url": "type", + "valueCode": "Patient" + } + ] + }, { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemExtractionContext", "valueExpression": { @@ -51,7 +68,7 @@ "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.name.given", + "expression": "%client.name.given", "name": "patientName" } } @@ -85,7 +102,7 @@ "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.name.family", + "expression": "%client.name.family", "name": "patientFamily" } } @@ -122,7 +139,7 @@ "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.birthDate", + "expression": "%client.birthDate", "name": "patientBirthDate" } } @@ -161,7 +178,7 @@ "valueExpression": { "system": "http://hl7.org/fhir/administrative-gender", "language": "text/fhirpath", - "expression": "Patient.gender", + "expression": "%client.gender", "name": "patientGender" } }, @@ -237,7 +254,7 @@ "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.telecom.value", + "expression": "%client.telecom.value", "name": "patientTelecom" } } @@ -278,7 +295,7 @@ "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.address.city", + "expression": "%client.address.city", "name": "patientCity" } } @@ -312,7 +329,7 @@ "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.address.country", + "expression": "%client.address.country", "name": "patientCity" } } @@ -348,7 +365,7 @@ "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.active", + "expression": "%client.active", "name": "patientActive" } } diff --git a/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt index f966f94ed9..6b1c91db65 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2021-2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import kotlinx.coroutines.launch import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.Resource /** * The ViewModel helper class for [EditPatientFragment], that is responsible for preparing data for @@ -45,14 +46,15 @@ class EditPatientViewModel(application: Application, private val state: SavedSta private suspend fun prepareEditPatient(): Pair { val patient = fhirEngine.get(patientId) + val launchContexts = mapOf("client" to patient) val question = readFileFromAssets("new-patient-registration-paginated.json").trimIndent() val parser = FhirContext.forCached(FhirVersionEnum.R4).newJsonParser() val questionnaire = - parser.parseResource(org.hl7.fhir.r4.model.Questionnaire::class.java, question) as - Questionnaire + parser.parseResource(org.hl7.fhir.r4.model.Questionnaire::class.java, question) + as Questionnaire val questionnaireResponse: QuestionnaireResponse = - ResourceMapper.populate(questionnaire, patient) + ResourceMapper.populate(questionnaire, launchContexts) val questionnaireResponseJson = parser.encodeResourceToString(questionnaireResponse) return question to questionnaireResponseJson } @@ -63,8 +65,8 @@ class EditPatientViewModel(application: Application, private val state: SavedSta private val questionnaireResource: Questionnaire get() = - FhirContext.forCached(FhirVersionEnum.R4).newJsonParser().parseResource(questionnaire) as - Questionnaire + FhirContext.forCached(FhirVersionEnum.R4).newJsonParser().parseResource(questionnaire) + as Questionnaire private var questionnaireJson: String? = null From 9686da88e8ed807a418f7081771bff30cb2c7a32 Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Mon, 31 Jul 2023 16:27:18 +0700 Subject: [PATCH 05/24] spotlessApply --- .../java/com/google/android/fhir/demo/EditPatientViewModel.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt index 6b1c91db65..942b52afd8 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt @@ -49,9 +49,7 @@ class EditPatientViewModel(application: Application, private val state: SavedSta val launchContexts = mapOf("client" to patient) val question = readFileFromAssets("new-patient-registration-paginated.json").trimIndent() val parser = FhirContext.forCached(FhirVersionEnum.R4).newJsonParser() - val questionnaire = - parser.parseResource(org.hl7.fhir.r4.model.Questionnaire::class.java, question) - as Questionnaire + val questionnaire = parser.parseResource(Questionnaire::class.java, question) as Questionnaire val questionnaireResponse: QuestionnaireResponse = ResourceMapper.populate(questionnaire, launchContexts) From d2d53a2d5c7a92a503468a5b26f66b9691aa348e Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Wed, 2 Aug 2023 14:25:31 +0700 Subject: [PATCH 06/24] WIP --- .../assets/dynamic-answer-expression.json | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 demo/src/main/assets/dynamic-answer-expression.json diff --git a/demo/src/main/assets/dynamic-answer-expression.json b/demo/src/main/assets/dynamic-answer-expression.json new file mode 100644 index 0000000000..ca55a031fe --- /dev/null +++ b/demo/src/main/assets/dynamic-answer-expression.json @@ -0,0 +1,134 @@ +{ + "resourceType": "Questionnaire", + "id": "client-registration-sample", + "language": "en", + "status": "active", + "date": "2020-11-18T07:24:47.111Z", + "subjectType": [ + "Patient" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext", + "extension": [ + { + "url": "name", + "valueCoding": { + "system": "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", + "code": "giga-chada", + "display": "Giga Chad" + } + }, + { + "url": "type", + "valueCode": "Patient" + } + ] + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/variable", + "valueExpression": { + "name": "selected-answer", + "language": "text/fhirpath", + "expression": "%resource.descendants().where(linkId='1').answer.value" + } + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/variable", + "valueExpression": { + "name": "city", + "language": "text/fhirpath", + "expression": "iif(%selected-answer.exists(), %selected-answer.code, 'NOTHING')" + } + } + ], + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "drop-down", + "display": "Drop down" + } + ], + "text": "Drop down" + } + } + ], + "linkId": "1", + "text": "Select City", + "type": "choice", + "required": true, + "answerOption": [ + { + "valueCoding": { + "code": "JAKARTA", + "display": "JAKARTA" + } + }, + { + "valueCoding": { + "code": "NAIROBI", + "display": "NAIROBI" + } + } + ] + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "drop-down", + "display": "Drop down" + } + ], + "text": "Drop down" + } + }, + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-answerExpression", + "valueExpression": { + "expression": "Patient?name={{%giga-chad.name.given}}", + "language": "application/x-fhir-query" + } + }, + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-choiceColumn", + "extension": [ + { + "url": "path", + "valueString": "id" + }, + { + "url": "forDisplay", + "valueBoolean": true + } + ] + } + ], + "linkId": "2", + "text": "Find Patient From: X City", + "_text": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/fhirpath", + "expression": "'Find Patient From: ' + %city + ' City'" + } + } + ] + }, + "type": "reference", + "required": true + } + ] +} \ No newline at end of file From 9c4210d4bf8ae1d0b7e32cca303bb6e6252f6045 Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Wed, 23 Aug 2023 14:59:17 +0700 Subject: [PATCH 07/24] Revert "WIP" This reverts commit d2d53a2d5c7a92a503468a5b26f66b9691aa348e. --- .../assets/dynamic-answer-expression.json | 134 ------------------ 1 file changed, 134 deletions(-) delete mode 100644 demo/src/main/assets/dynamic-answer-expression.json diff --git a/demo/src/main/assets/dynamic-answer-expression.json b/demo/src/main/assets/dynamic-answer-expression.json deleted file mode 100644 index ca55a031fe..0000000000 --- a/demo/src/main/assets/dynamic-answer-expression.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "resourceType": "Questionnaire", - "id": "client-registration-sample", - "language": "en", - "status": "active", - "date": "2020-11-18T07:24:47.111Z", - "subjectType": [ - "Patient" - ], - "extension": [ - { - "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext", - "extension": [ - { - "url": "name", - "valueCoding": { - "system": "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", - "code": "giga-chada", - "display": "Giga Chad" - } - }, - { - "url": "type", - "valueCode": "Patient" - } - ] - }, - { - "url": "http://hl7.org/fhir/StructureDefinition/variable", - "valueExpression": { - "name": "selected-answer", - "language": "text/fhirpath", - "expression": "%resource.descendants().where(linkId='1').answer.value" - } - }, - { - "url": "http://hl7.org/fhir/StructureDefinition/variable", - "valueExpression": { - "name": "city", - "language": "text/fhirpath", - "expression": "iif(%selected-answer.exists(), %selected-answer.code, 'NOTHING')" - } - } - ], - "item": [ - { - "extension": [ - { - "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://hl7.org/fhir/questionnaire-item-control", - "code": "drop-down", - "display": "Drop down" - } - ], - "text": "Drop down" - } - } - ], - "linkId": "1", - "text": "Select City", - "type": "choice", - "required": true, - "answerOption": [ - { - "valueCoding": { - "code": "JAKARTA", - "display": "JAKARTA" - } - }, - { - "valueCoding": { - "code": "NAIROBI", - "display": "NAIROBI" - } - } - ] - }, - { - "extension": [ - { - "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://hl7.org/fhir/questionnaire-item-control", - "code": "drop-down", - "display": "Drop down" - } - ], - "text": "Drop down" - } - }, - { - "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-answerExpression", - "valueExpression": { - "expression": "Patient?name={{%giga-chad.name.given}}", - "language": "application/x-fhir-query" - } - }, - { - "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-choiceColumn", - "extension": [ - { - "url": "path", - "valueString": "id" - }, - { - "url": "forDisplay", - "valueBoolean": true - } - ] - } - ], - "linkId": "2", - "text": "Find Patient From: X City", - "_text": { - "extension": [ - { - "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", - "valueExpression": { - "language": "text/fhirpath", - "expression": "'Find Patient From: ' + %city + ' City'" - } - } - ] - }, - "type": "reference", - "required": true - } - ] -} \ No newline at end of file From 33b3d038ec7a98770aa3e26571e7294ee8cd5d4f Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Wed, 23 Aug 2023 15:01:04 +0700 Subject: [PATCH 08/24] spotlessApply --- .../google/android/fhir/datacapture/mapping/ResourceMapper.kt | 2 +- .../android/fhir/datacapture/mapping/ResourceMapperTest.kt | 1 - 2 files changed, 1 insertion(+), 2 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 22bb1f4edb..bf3b612ba1 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 @@ -18,8 +18,8 @@ package com.google.android.fhir.datacapture.mapping import com.google.android.fhir.datacapture.DataCapture import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem -import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.datacapture.extensions.getMatchingLaunchContexts +import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.datacapture.extensions.questionnaireLaunchContexts import com.google.android.fhir.datacapture.extensions.targetStructureMap import com.google.android.fhir.datacapture.extensions.toCodeType 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 6b00f8ca73..e4f6c3fca0 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 @@ -50,7 +50,6 @@ import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.Reference -import org.hl7.fhir.r4.model.RelatedPerson import org.hl7.fhir.r4.model.ResourceFactory import org.hl7.fhir.r4.model.StringType import org.hl7.fhir.r4.model.codesystems.AdministrativeGender From 45c6dbf56a0dd8624e6e9b6897db4194c338b560 Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Thu, 24 Aug 2023 11:13:59 +0700 Subject: [PATCH 09/24] Fix test --- .../datacapture/mapping/ResourceMapperTest.kt | 49 ++++++++++++++++--- 1 file changed, 41 insertions(+), 8 deletions(-) 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 e4f6c3fca0..b40c2818c7 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 @@ -1656,6 +1656,16 @@ class ResourceMapperTest { runBlocking { val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), + Extension("type", StringType("Patient")) + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "patient-id" @@ -1666,7 +1676,7 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient.id" + expression = "%father.id" } ) ) @@ -1675,7 +1685,7 @@ class ResourceMapperTest { val patientId = UUID.randomUUID().toString() val patient = Patient().apply { id = "Patient/$patientId" } - val questionnaireResponse = ResourceMapper.populate(questionnaire, patient) + val questionnaireResponse = ResourceMapper.populate(questionnaire, mapOf("father" to patient)) assertThat((questionnaireResponse.item[0].answer[0].value as Reference).reference) .isEqualTo(patient.id) @@ -1686,6 +1696,16 @@ class ResourceMapperTest { runBlocking { val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), + Extension("type", StringType("Patient")) + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "patient-id" @@ -1696,7 +1716,7 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient.gender" + expression = "%father.gender" } ) ) @@ -1711,7 +1731,9 @@ class ResourceMapperTest { } val errorMessage = - assertFailsWith { ResourceMapper.populate(questionnaire, patient) } + assertFailsWith { + ResourceMapper.populate(questionnaire, mapOf(Pair("father", patient))) + } .localizedMessage assertThat(errorMessage).isEqualTo("Expression supplied does not evaluate to IdType.") } @@ -1721,6 +1743,16 @@ class ResourceMapperTest { runBlocking { val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "patient", "Patient")), + Extension("type", StringType("Patient")) + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { type = Questionnaire.QuestionnaireItemType.REFERENCE @@ -1729,17 +1761,18 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient" + expression = "%patient.id" } ) ) } ) - val patient = Patient().apply { id = UUID.randomUUID().toString() } - val questionnaireResponse = ResourceMapper.populate(questionnaire, patient) + val patient = Patient().apply { id = "Patient/${UUID.randomUUID()}" } + val questionnaireResponse = + ResourceMapper.populate(questionnaire, mapOf("patient" to patient)) assertThat(questionnaireResponse.itemFirstRep.answerFirstRep.valueReference.reference) - .isEqualTo("Patient/${patient.id}") + .isEqualTo(patient.id) } @Test fun `populate() should correctly populate IdType value with history in QuestionnaireResponse`() = From 6a47a51bb979320e8deeec0ad500c8e26fd73803 Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Tue, 12 Sep 2023 18:42:28 +0700 Subject: [PATCH 10/24] Refactor validateLaunchContextExtension --- .../extensions/MoreQuestionnaires.kt | 39 ++++--------------- .../extensions/MoreResourceTypes.kt | 31 +++++++++++++++ 2 files changed, 38 insertions(+), 32 deletions(-) create mode 100644 datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreResourceTypes.kt diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt index 384ba64dd8..10a6f4f1e1 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt @@ -74,8 +74,8 @@ internal fun validateLaunchContextExtensions(launchContextExtensions: List Date: Tue, 12 Sep 2023 18:42:59 +0700 Subject: [PATCH 11/24] Remove QuestionnaireLaunchContextSet enum class --- .../extensions/MoreQuestionnaires.kt | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt index 10a6f4f1e1..d101aba531 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt @@ -114,23 +114,6 @@ internal fun getMatchingLaunchContexts( return launchContexts.filterKeys { nameCodes.contains(it) } } -/** - * The set of supported launch contexts, as per: http://hl7.org/fhir/uv/sdc/ValueSet/launchContext - */ -private enum class QuestionnaireLaunchContextSet( - val system: String, - val resourceType: String, -) { - PATIENT(EXTENSION_LAUNCH_CONTEXT, "Patient"), - ENCOUNTER(EXTENSION_LAUNCH_CONTEXT, "Encounter"), - LOCATION(EXTENSION_LAUNCH_CONTEXT, "Location"), - USER_AS_PATIENT(EXTENSION_LAUNCH_CONTEXT, "Patient"), - USER_AS_PRACTITIONER(EXTENSION_LAUNCH_CONTEXT, "Practitioner"), - USER_AS_PRACTITIONER_ROLE(EXTENSION_LAUNCH_CONTEXT, "PractitionerRole"), - USER_AS_RELATED_PERSON(EXTENSION_LAUNCH_CONTEXT, "RelatedPerson"), - STUDY(EXTENSION_LAUNCH_CONTEXT, "ResearchStudy"), -} - /** * See * [Extension: target structure map](http://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-targetStructureMap.html) From ddc1dfc5af9f30c3f55cec67337204359fad8a24 Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Tue, 12 Sep 2023 18:44:27 +0700 Subject: [PATCH 12/24] Rename vars and functions --- .../fhir/datacapture/QuestionnaireFragment.kt | 8 ++++---- .../fhir/datacapture/QuestionnaireViewModel.kt | 17 ++++++++++------- .../extensions/MoreQuestionnaires.kt | 6 +++--- .../fhir/datacapture/mapping/ResourceMapper.kt | 4 ++-- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt index e4d8d8fb9c..72add7c3ed 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt @@ -319,10 +319,10 @@ class QuestionnaireFragment : Fragment() { * user, etc. is "in context" at the time the questionnaire response is being completed: * https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-launchContext.html * - * @param launchContexts map of launchContext name and serialized resources + * @param launchContextMap map of launchContext name and serialized resources */ - fun setQuestionnaireLaunchContexts(launchContexts: Map) = apply { - args.add(EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS to launchContexts) + fun setQuestionnaireLaunchContextMap(launchContextMap: Map) = apply { + args.add(EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP_STRINGS to launchContextMap) } /** @@ -419,7 +419,7 @@ class QuestionnaireFragment : Fragment() { /** * A map of launchContext name and JSON encoded strings extra for each questionnaire context. */ - internal const val EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS = + internal const val EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP_STRINGS = "questionnaire-launch-contexts" /** * A [URI][android.net.Uri] extra for streaming a JSON encoded questionnaire response. diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index 5e29e0f794..9ad5c8b9d0 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -33,8 +33,8 @@ import com.google.android.fhir.datacapture.extensions.allItems import com.google.android.fhir.datacapture.extensions.cqfExpression import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem import com.google.android.fhir.datacapture.extensions.entryMode +import com.google.android.fhir.datacapture.extensions.filterLaunchContextsByCodeInNameExtension import com.google.android.fhir.datacapture.extensions.flattened -import com.google.android.fhir.datacapture.extensions.getMatchingLaunchContexts import com.google.android.fhir.datacapture.extensions.hasDifferentAnswerSet import com.google.android.fhir.datacapture.extensions.isDisplayItem import com.google.android.fhir.datacapture.extensions.isFhirPath @@ -169,16 +169,19 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat init { questionnaireLaunchContextMap = - if (state.contains(QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS)) { + if (state.contains(QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP_STRINGS)) { - val launchContextJsonStrings: Map = - state[QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS]!! + val launchContextMapString: Map = + state[QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP_STRINGS]!! - val launchContexts = - launchContextJsonStrings.mapValues { parser.parseResource(it.value) as Resource } + val launchContextMapResource = + launchContextMapString.mapValues { parser.parseResource(it.value) as Resource } questionnaire.questionnaireLaunchContexts?.let { launchContextExtensions -> validateLaunchContextExtensions(launchContextExtensions) - getMatchingLaunchContexts(launchContexts, launchContextExtensions) + filterLaunchContextsByCodeInNameExtension( + launchContextMapResource, + launchContextExtensions + ) } } else { null diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt index d101aba531..3a1bd03bbc 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt @@ -99,10 +99,10 @@ private fun validateLaunchContextExtension(launchExtension: Extension) { } /** - * Gets the launch contexts resource map, filtered based on the matching nameCode values found in - * the launchContextExtensions list. + * Filters the provided launch contexts by matching the keys with the `code` values found in the + * "name" extensions. */ -internal fun getMatchingLaunchContexts( +internal fun filterLaunchContextsByCodeInNameExtension( launchContexts: Map, launchContextExtensions: List ): Map { 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 bf3b612ba1..4a351e7758 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 @@ -18,7 +18,7 @@ package com.google.android.fhir.datacapture.mapping import com.google.android.fhir.datacapture.DataCapture import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem -import com.google.android.fhir.datacapture.extensions.getMatchingLaunchContexts +import com.google.android.fhir.datacapture.extensions.filterLaunchContextsByCodeInNameExtension import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.datacapture.extensions.questionnaireLaunchContexts import com.google.android.fhir.datacapture.extensions.targetStructureMap @@ -225,7 +225,7 @@ object ResourceMapper { launchContexts: Map ): QuestionnaireResponse { val filteredLaunchContexts = - getMatchingLaunchContexts( + filterLaunchContextsByCodeInNameExtension( launchContexts, questionnaire.questionnaireLaunchContexts ?: listOf() ) From 65cbee1f681d0ce920e343e232c4022f0db939dc Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Tue, 12 Sep 2023 18:58:41 +0700 Subject: [PATCH 13/24] Add code comment for MoreResourceTypes.kt --- .../extensions/MoreResourceTypes.kt | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreResourceTypes.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreResourceTypes.kt index 78f70e9270..9f741ee4fe 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreResourceTypes.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreResourceTypes.kt @@ -19,13 +19,28 @@ package com.google.android.fhir.datacapture.extensions import org.hl7.fhir.exceptions.FHIRException import org.hl7.fhir.r4.model.ResourceType +/** A utility object for working with FHIR Resource Types. */ internal object MoreResourceTypes { - internal fun isValidCode(code: String): Boolean { + + /** + * Checks if a given code is a valid FHIR Resource Type code. + * + * @param code The code to check for validity. + * @return `true` if the code is valid, `false` otherwise. + */ + internal fun isValidCode(code: String): Boolean = fromCodeOrNull(code) != null + + /** + * Converts a code into a ResourceType, returning null if the conversion fails. + * + * @param code The code to convert. + * @return The ResourceType if conversion is successful, or null if it fails. + */ + internal fun fromCodeOrNull(code: String): ResourceType? { return try { ResourceType.fromCode(code) - true } catch (exception: FHIRException) { - false + null } } } From 8f6a9c46ed279e7a476444412ba77bab889abebf Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Tue, 12 Sep 2023 18:59:03 +0700 Subject: [PATCH 14/24] Unit testing --- .../datacapture/QuestionnaireViewModelTest.kt | 4 +- .../extensions/MoreQuestionnairesTest.kt | 39 ++----------- .../extensions/MoreResourceTypesTest.kt | 58 +++++++++++++++++++ 3 files changed, 65 insertions(+), 36 deletions(-) create mode 100644 datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreResourceTypesTest.kt 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 e3195b54a9..bc9774f195 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 @@ -25,7 +25,7 @@ import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.parser.IParser import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_ENABLE_REVIEW_PAGE import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_JSON_STRING -import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS +import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP_STRINGS import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_READ_ONLY import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_SHOW_REVIEW_PAGE_FIRST @@ -4170,7 +4170,7 @@ class QuestionnaireViewModelTest { } state.set(EXTRA_QUESTIONNAIRE_JSON_STRING, printer.encodeResourceToString(questionnaire)) state.set( - EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS, + EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP_STRINGS, mapOf("patient" to printer.encodeResourceToString(patient)) ) diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt index 9d27817663..c49dc715ea 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt @@ -111,19 +111,15 @@ class MoreQuestionnairesTest { } @Test - fun `should throw exception if resource type in context is not part of launchContext value set`() { + fun `should throw exception if type in type extension is not a valid resource type`() { val launchContextExtension = Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") .apply { addExtension( "name", - Coding( - "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", - "observation", - "Observation" - ) + Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "me", "Me") ) - addExtension("type", CodeType("Observation")) + addExtension("type", CodeType("Avocado")) } val errorMessage = @@ -140,11 +136,11 @@ class MoreQuestionnairesTest { } @Test - fun `should throw exception if system in name extension is different to the fhir standard`() { + fun `should throw exception if system in name extension is not valid`() { val launchContextExtension = Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") .apply { - addExtension("name", Coding("http://idk-bro", "grandma", "Grandma")) + addExtension("name", Coding("http://wrong-system", "grandma", "Grandma")) addExtension("type", CodeType("Patient")) } @@ -161,31 +157,6 @@ class MoreQuestionnairesTest { ) } - @Test - fun `should throw exception if type extension is not a subset of User value set in name extension`() { - val launchContextExtension = - Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") - .apply { - addExtension( - "name", - Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "user", "User") - ) - addExtension("type", CodeType("Observation")) - } - - val errorMessage = - assertFailsWith { - validateLaunchContextExtensions(listOf(launchContextExtension)) - } - .localizedMessage - - assertThat(errorMessage) - .isEqualTo( - "The extension:name extension and/or extension:type extension do not follow " + - "the format specified in http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext" - ) - } - @Test fun `should throw exception if the type extension is not present`() { val launchContextExtension = diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreResourceTypesTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreResourceTypesTest.kt new file mode 100644 index 0000000000..d1929c2aec --- /dev/null +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreResourceTypesTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2022-2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.datacapture.extensions + +import android.os.Build +import com.google.common.truth.Truth.assertThat +import org.hl7.fhir.r4.model.ResourceType +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.P]) +class MoreResourceTypesTest { + + @Test + fun isValidCode_shouldReturnTrue_becauseOfValidCode() { + val code = "Patient" + val isValid = MoreResourceTypes.isValidCode(code) + assertThat(isValid).isTrue() + } + + @Test + fun isValidCode_shouldReturnFalse_becauseOfInvalidCode() { + val code = "WaitWhat" + val isValid = MoreResourceTypes.isValidCode(code) + assertThat(isValid).isFalse() + } + + @Test + fun fromCodeOrNull_shouldReturnResourceType_becauseOfValidCode() { + val code = "Patient" + val resourceType = MoreResourceTypes.fromCodeOrNull(code) + assertThat(resourceType).isEqualTo(ResourceType.Patient) + } + + @Test + fun fromCodeOrNull_shouldReturnNull_becauseOfInvalidCode() { + val code = "WaitWhat" + val resourceType = MoreResourceTypes.fromCodeOrNull(code) + assertThat(resourceType).isNull() + } +} From 3bff3b20f8bdc5db272f3dcd1b3683bd82da0d84 Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Wed, 13 Sep 2023 14:49:31 +0700 Subject: [PATCH 15/24] Fix post-merge-conflict --- .../android/fhir/datacapture/mapping/ResourceMapperTest.kt | 1 + 1 file changed, 1 insertion(+) 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 b40c2818c7..95aaf178b6 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 @@ -24,6 +24,7 @@ import ca.uhn.fhir.context.FhirVersionEnum import ca.uhn.fhir.parser.IParser import com.google.android.fhir.datacapture.extensions.EXTENSION_LAUNCH_CONTEXT import com.google.android.fhir.datacapture.extensions.EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT +import com.google.android.fhir.datacapture.extensions.ITEM_INITIAL_EXPRESSION_URL import com.google.android.fhir.datacapture.views.factories.localDate import com.google.common.truth.Truth.assertThat import java.math.BigDecimal From 1206cfab7db033ddbf827158b0aaeafa8f4f793d Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Wed, 13 Sep 2023 14:54:01 +0700 Subject: [PATCH 16/24] spotlessApply --- .../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 a1696941af..31f6e537c6 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 @@ -18,8 +18,8 @@ package com.google.android.fhir.datacapture.mapping import com.google.android.fhir.datacapture.DataCapture import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem -import com.google.android.fhir.datacapture.extensions.initialExpression import com.google.android.fhir.datacapture.extensions.filterLaunchContextsByCodeInNameExtension +import com.google.android.fhir.datacapture.extensions.initialExpression import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.datacapture.extensions.questionnaireLaunchContexts import com.google.android.fhir.datacapture.extensions.targetStructureMap From 28c86be67fa409a12b94b94cbbc3064a8f04723f Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Mon, 9 Oct 2023 16:27:00 +0700 Subject: [PATCH 17/24] Address review --- .../fhir/datacapture/QuestionnaireFragment.kt | 5 +- .../datacapture/QuestionnaireViewModel.kt | 11 +- .../extensions/MoreQuestionnaires.kt | 49 ++++---- .../extensions/MoreResourceTypes.kt | 46 ------- .../datacapture/mapping/ResourceMapper.kt | 4 +- .../datacapture/QuestionnaireViewModelTest.kt | 4 +- .../extensions/MoreQuestionnairesTest.kt | 119 +++++++++++++++--- .../extensions/MoreResourceTypesTest.kt | 58 --------- 8 files changed, 142 insertions(+), 154 deletions(-) delete mode 100644 datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreResourceTypes.kt delete mode 100644 datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreResourceTypesTest.kt diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt index 72add7c3ed..495c4e268d 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt @@ -322,7 +322,7 @@ class QuestionnaireFragment : Fragment() { * @param launchContextMap map of launchContext name and serialized resources */ fun setQuestionnaireLaunchContextMap(launchContextMap: Map) = apply { - args.add(EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP_STRINGS to launchContextMap) + args.add(EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP to launchContextMap) } /** @@ -419,8 +419,7 @@ class QuestionnaireFragment : Fragment() { /** * A map of launchContext name and JSON encoded strings extra for each questionnaire context. */ - internal const val EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP_STRINGS = - "questionnaire-launch-contexts" + internal const val EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP = "questionnaire-launch-contexts" /** * A [URI][android.net.Uri] extra for streaming a JSON encoded questionnaire response. * diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index ffe9ca27db..8f1a80af65 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -33,7 +33,7 @@ import com.google.android.fhir.datacapture.extensions.allItems import com.google.android.fhir.datacapture.extensions.cqfExpression import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem import com.google.android.fhir.datacapture.extensions.entryMode -import com.google.android.fhir.datacapture.extensions.filterLaunchContextsByCodeInNameExtension +import com.google.android.fhir.datacapture.extensions.filterByCodeInNameExtension import com.google.android.fhir.datacapture.extensions.flattened import com.google.android.fhir.datacapture.extensions.hasDifferentAnswerSet import com.google.android.fhir.datacapture.extensions.isDisplayItem @@ -167,19 +167,16 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat init { questionnaireLaunchContextMap = - if (state.contains(QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP_STRINGS)) { + if (state.contains(QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP)) { val launchContextMapString: Map = - state[QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP_STRINGS]!! + state[QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP]!! val launchContextMapResource = launchContextMapString.mapValues { parser.parseResource(it.value) as Resource } questionnaire.questionnaireLaunchContexts?.let { launchContextExtensions -> validateLaunchContextExtensions(launchContextExtensions) - filterLaunchContextsByCodeInNameExtension( - launchContextMapResource, - launchContextExtensions - ) + filterByCodeInNameExtension(launchContextMapResource, launchContextExtensions) } } else { null diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt index 3a1bd03bbc..1462df556c 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt @@ -16,6 +16,7 @@ package com.google.android.fhir.datacapture.extensions +import org.hl7.fhir.exceptions.FHIRException import org.hl7.fhir.r4.model.CanonicalType import org.hl7.fhir.r4.model.CodeType import org.hl7.fhir.r4.model.Coding @@ -23,6 +24,7 @@ import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.Resource +import org.hl7.fhir.r4.model.ResourceType /** * The StructureMap url in the @@ -65,12 +67,7 @@ internal fun Questionnaire.findVariableExpression(variableName: String): Express */ internal fun validateLaunchContextExtensions(launchContextExtensions: List) = launchContextExtensions.forEach { launchExtension -> - validateLaunchContextExtension( - Extension().apply { - addExtension(launchExtension.extension.firstOrNull { it.url == "name" }) - addExtension(launchExtension.extension.firstOrNull { it.url == "type" }) - } - ) + validateLaunchContextExtension(launchExtension) } /** @@ -79,21 +76,31 @@ internal fun validateLaunchContextExtensions(launchContextExtensions: List, launchContextExtensions: List ): Map { val nameCodes = - launchContextExtensions.mapNotNull { extension -> - (extension.getExtensionByUrl("name").value as? Coding)?.code - } + launchContextExtensions + .mapNotNull { extension -> (extension.getExtensionByUrl("name").value as? Coding)?.code } + .toSet() return launchContexts.filterKeys { nameCodes.contains(it) } } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreResourceTypes.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreResourceTypes.kt deleted file mode 100644 index 9f741ee4fe..0000000000 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreResourceTypes.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.android.fhir.datacapture.extensions - -import org.hl7.fhir.exceptions.FHIRException -import org.hl7.fhir.r4.model.ResourceType - -/** A utility object for working with FHIR Resource Types. */ -internal object MoreResourceTypes { - - /** - * Checks if a given code is a valid FHIR Resource Type code. - * - * @param code The code to check for validity. - * @return `true` if the code is valid, `false` otherwise. - */ - internal fun isValidCode(code: String): Boolean = fromCodeOrNull(code) != null - - /** - * Converts a code into a ResourceType, returning null if the conversion fails. - * - * @param code The code to convert. - * @return The ResourceType if conversion is successful, or null if it fails. - */ - internal fun fromCodeOrNull(code: String): ResourceType? { - return try { - ResourceType.fromCode(code) - } catch (exception: FHIRException) { - null - } - } -} 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 31f6e537c6..628efdf735 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 @@ -18,7 +18,7 @@ package com.google.android.fhir.datacapture.mapping import com.google.android.fhir.datacapture.DataCapture import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem -import com.google.android.fhir.datacapture.extensions.filterLaunchContextsByCodeInNameExtension +import com.google.android.fhir.datacapture.extensions.filterByCodeInNameExtension import com.google.android.fhir.datacapture.extensions.initialExpression import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.datacapture.extensions.questionnaireLaunchContexts @@ -226,7 +226,7 @@ object ResourceMapper { launchContexts: Map ): QuestionnaireResponse { val filteredLaunchContexts = - filterLaunchContextsByCodeInNameExtension( + filterByCodeInNameExtension( launchContexts, questionnaire.questionnaireLaunchContexts ?: listOf() ) 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 299317ef99..f9713f2127 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 @@ -25,7 +25,7 @@ import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.parser.IParser import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_ENABLE_REVIEW_PAGE import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_JSON_STRING -import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP_STRINGS +import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_READ_ONLY import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_SHOW_REVIEW_PAGE_FIRST @@ -4170,7 +4170,7 @@ class QuestionnaireViewModelTest { } state.set(EXTRA_QUESTIONNAIRE_JSON_STRING, printer.encodeResourceToString(questionnaire)) state.set( - EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP_STRINGS, + EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP, mapOf("patient" to printer.encodeResourceToString(patient)) ) diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt index c49dc715ea..b11b1602bc 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt @@ -111,7 +111,55 @@ class MoreQuestionnairesTest { } @Test - fun `should throw exception if type in type extension is not a valid resource type`() { + fun `validateLaunchContextExtensions should throw exception if child extensions of launch context extension is less than 2 in size`() { + val launchContextExtension = + Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") + .apply { + addExtension( + "name", + Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "me", "Me") + ) + } + + val errorMessage = + assertFailsWith { + validateLaunchContextExtensions(listOf(launchContextExtension)) + } + .localizedMessage + + assertThat(errorMessage) + .isEqualTo( + "Expected 2 extensions (name and type) in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT but found ${launchContextExtension.extension.size}." + ) + } + + @Test + fun `validateLaunchContextExtensions should throw exception if child extensions of launch context extension is more than 2 in size`() { + val launchContextExtension = + Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") + .apply { + addExtension( + "name", + Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "me", "Me") + ) + addExtension("type", CodeType("Patient")) + addExtension("waitwhat", CodeType("Patient")) + } + + val errorMessage = + assertFailsWith { + validateLaunchContextExtensions(listOf(launchContextExtension)) + } + .localizedMessage + + assertThat(errorMessage) + .isEqualTo( + "Expected 2 extensions (name and type) in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT but found ${launchContextExtension.extension.size}." + ) + } + + @Test + fun `validateLaunchContextExtensions should throw exception if type in type extension is not a valid resource type`() { val launchContextExtension = Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") .apply { @@ -130,19 +178,38 @@ class MoreQuestionnairesTest { assertThat(errorMessage) .isEqualTo( - "The extension:name extension and/or extension:type extension do not follow " + - "the format specified in http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext" + "The extension:name and/or extension:type do not follow the format specified in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT" ) } @Test - fun `should throw exception if system in name extension is not valid`() { + fun `validateLaunchContextExtensions should throw exception if system in name extension is not valid`() { val launchContextExtension = Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") .apply { addExtension("name", Coding("http://wrong-system", "grandma", "Grandma")) addExtension("type", CodeType("Patient")) } + val errorMessage = + assertFailsWith { + validateLaunchContextExtensions(listOf(launchContextExtension)) + } + .localizedMessage + + assertThat(errorMessage) + .isEqualTo( + "The extension:name and/or extension:type do not follow the format specified in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT" + ) + } + + @Test + fun `validateLaunchContextExtensions should throw exception if the type extension has wrong url`() { + val launchContextExtension = + Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") + .apply { + addExtension("name", Coding("http://wrong-system", "grandma", "Grandma")) + addExtension("waitwhat", StringType("Patient")) + } val errorMessage = assertFailsWith { @@ -152,20 +219,41 @@ class MoreQuestionnairesTest { assertThat(errorMessage) .isEqualTo( - "The extension:name extension and/or extension:type extension do not follow " + - "the format specified in http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext" + "The extension:type is missing or is not of type CodeType in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT" ) } @Test - fun `should throw exception if the type extension is not present`() { + fun `validateLaunchContextExtensions should throw exception if the type extension value is not CodeType`() { + val launchContextExtension = + Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") + .apply { + addExtension("name", Coding("http://wrong-system", "grandma", "Grandma")) + addExtension("type", StringType("Patient")) + } + + val errorMessage = + assertFailsWith { + validateLaunchContextExtensions(listOf(launchContextExtension)) + } + .localizedMessage + + assertThat(errorMessage) + .isEqualTo( + "The extension:type is missing or is not of type CodeType in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT" + ) + } + + @Test + fun `validateLaunchContextExtensions should throw exception if the name extension has wrong url`() { val launchContextExtension = Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") .apply { addExtension( - "name", - Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "user", "User") + "waitwhat", + Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "grandma", "Grandma") ) + addExtension("type", CodeType("Patient")) } val errorMessage = @@ -176,16 +264,18 @@ class MoreQuestionnairesTest { assertThat(errorMessage) .isEqualTo( - "The extension:name or extension:type extension is missing in " + - EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + "The extension:name is missing or is not of type Coding in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT" ) } @Test - fun `should throw exception if the name extension is not present`() { + fun `validateLaunchContextExtensions should throw exception if the name extension value is not Coding`() { val launchContextExtension = Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") - .apply { addExtension("type", CodeType("Patient")) } + .apply { + addExtension("name", StringType("waitwhat")) + addExtension("type", CodeType("Patient")) + } val errorMessage = assertFailsWith { @@ -195,8 +285,7 @@ class MoreQuestionnairesTest { assertThat(errorMessage) .isEqualTo( - "The extension:name or extension:type extension is missing in " + - EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + "The extension:name is missing or is not of type Coding in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT" ) } } diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreResourceTypesTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreResourceTypesTest.kt deleted file mode 100644 index d1929c2aec..0000000000 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreResourceTypesTest.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2022-2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.android.fhir.datacapture.extensions - -import android.os.Build -import com.google.common.truth.Truth.assertThat -import org.hl7.fhir.r4.model.ResourceType -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [Build.VERSION_CODES.P]) -class MoreResourceTypesTest { - - @Test - fun isValidCode_shouldReturnTrue_becauseOfValidCode() { - val code = "Patient" - val isValid = MoreResourceTypes.isValidCode(code) - assertThat(isValid).isTrue() - } - - @Test - fun isValidCode_shouldReturnFalse_becauseOfInvalidCode() { - val code = "WaitWhat" - val isValid = MoreResourceTypes.isValidCode(code) - assertThat(isValid).isFalse() - } - - @Test - fun fromCodeOrNull_shouldReturnResourceType_becauseOfValidCode() { - val code = "Patient" - val resourceType = MoreResourceTypes.fromCodeOrNull(code) - assertThat(resourceType).isEqualTo(ResourceType.Patient) - } - - @Test - fun fromCodeOrNull_shouldReturnNull_becauseOfInvalidCode() { - val code = "WaitWhat" - val resourceType = MoreResourceTypes.fromCodeOrNull(code) - assertThat(resourceType).isNull() - } -} From 287512cbf8aa887ca318b282507b3205158cc34a Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Mon, 9 Oct 2023 17:43:33 +0700 Subject: [PATCH 18/24] spotlessApply --- .../fhir/datacapture/QuestionnaireFragment.kt | 1 + .../extensions/MoreQuestionnaires.kt | 8 +- .../datacapture/mapping/ResourceMapper.kt | 8 +- .../extensions/MoreQuestionnairesTest.kt | 36 +-- .../datacapture/mapping/ResourceMapperTest.kt | 224 ++++++++++-------- .../android/fhir/demo/EditPatientViewModel.kt | 3 +- 6 files changed, 154 insertions(+), 126 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt index f818757bdb..4bead5d1bd 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt @@ -458,6 +458,7 @@ class QuestionnaireFragment : Fragment() { * A map of launchContext name and JSON encoded strings extra for each questionnaire context. */ internal const val EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP = "questionnaire-launch-contexts" + /** * A [URI][android.net.Uri] extra for streaming a JSON encoded questionnaire response. * diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt index ed542f7691..7604f9a977 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt @@ -82,13 +82,13 @@ private fun validateLaunchContextExtension(launchExtension: Extension) { val nameCoding = launchExtension.getExtensionByUrl("name")?.value as? Coding ?: error( - "The extension:name is missing or is not of type Coding in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT" + "The extension:name is missing or is not of type Coding in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT", ) val typeCodeType = launchExtension.getExtensionByUrl("type")?.value as? CodeType ?: error( - "The extension:type is missing or is not of type CodeType in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT" + "The extension:type is missing or is not of type CodeType in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT", ) val isValidResourceType = @@ -100,7 +100,7 @@ private fun validateLaunchContextExtension(launchExtension: Extension) { if (nameCoding.system != EXTENSION_LAUNCH_CONTEXT || !isValidResourceType) { error( - "The extension:name and/or extension:type do not follow the format specified in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT" + "The extension:name and/or extension:type do not follow the format specified in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT", ) } } @@ -111,7 +111,7 @@ private fun validateLaunchContextExtension(launchExtension: Extension) { */ internal fun filterByCodeInNameExtension( launchContexts: Map, - launchContextExtensions: List + launchContextExtensions: List, ): Map { val nameCodes = launchContextExtensions 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 ebc94a4074..ed6ac07801 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 @@ -220,12 +220,12 @@ object ResourceMapper { */ suspend fun populate( questionnaire: Questionnaire, - launchContexts: Map + launchContexts: Map, ): QuestionnaireResponse { val filteredLaunchContexts = filterByCodeInNameExtension( launchContexts, - questionnaire.questionnaireLaunchContexts ?: listOf() + questionnaire.questionnaireLaunchContexts ?: listOf(), ) populateInitialValues(questionnaire.item, filteredLaunchContexts) return QuestionnaireResponse().apply { @@ -235,14 +235,14 @@ object ResourceMapper { private suspend fun populateInitialValues( questionnaireItems: List, - launchContexts: Map + launchContexts: Map, ) { questionnaireItems.forEach { populateInitialValue(it, launchContexts) } } private suspend fun populateInitialValue( questionnaireItem: Questionnaire.QuestionnaireItemComponent, - launchContexts: Map + launchContexts: Map, ) { check(questionnaireItem.initial.isEmpty() || questionnaireItem.initialExpression == null) { "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." diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt index b11b1602bc..b4e6db7c12 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt @@ -43,8 +43,8 @@ class MoreQuestionnairesTest { listOf( Extension( "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-targetStructureMap", - CanonicalType(structureMapUrl) - ) + CanonicalType(structureMapUrl), + ), ) assertThat(questionnaire.targetStructureMap).isEqualTo(structureMapUrl) @@ -64,11 +64,11 @@ class MoreQuestionnairesTest { .addCoding( Coding() .setCode(DisplayItemControlType.PAGE.extensionCode) - .setSystem(EXTENSION_ITEM_CONTROL_SYSTEM) - ) - ) + .setSystem(EXTENSION_ITEM_CONTROL_SYSTEM), + ), + ), ) - } + }, ) } @@ -117,7 +117,7 @@ class MoreQuestionnairesTest { .apply { addExtension( "name", - Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "me", "Me") + Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "me", "Me"), ) } @@ -129,7 +129,7 @@ class MoreQuestionnairesTest { assertThat(errorMessage) .isEqualTo( - "Expected 2 extensions (name and type) in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT but found ${launchContextExtension.extension.size}." + "Expected 2 extensions (name and type) in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT but found ${launchContextExtension.extension.size}.", ) } @@ -140,7 +140,7 @@ class MoreQuestionnairesTest { .apply { addExtension( "name", - Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "me", "Me") + Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "me", "Me"), ) addExtension("type", CodeType("Patient")) addExtension("waitwhat", CodeType("Patient")) @@ -154,7 +154,7 @@ class MoreQuestionnairesTest { assertThat(errorMessage) .isEqualTo( - "Expected 2 extensions (name and type) in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT but found ${launchContextExtension.extension.size}." + "Expected 2 extensions (name and type) in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT but found ${launchContextExtension.extension.size}.", ) } @@ -165,7 +165,7 @@ class MoreQuestionnairesTest { .apply { addExtension( "name", - Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "me", "Me") + Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "me", "Me"), ) addExtension("type", CodeType("Avocado")) } @@ -178,7 +178,7 @@ class MoreQuestionnairesTest { assertThat(errorMessage) .isEqualTo( - "The extension:name and/or extension:type do not follow the format specified in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT" + "The extension:name and/or extension:type do not follow the format specified in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT", ) } @@ -198,7 +198,7 @@ class MoreQuestionnairesTest { assertThat(errorMessage) .isEqualTo( - "The extension:name and/or extension:type do not follow the format specified in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT" + "The extension:name and/or extension:type do not follow the format specified in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT", ) } @@ -219,7 +219,7 @@ class MoreQuestionnairesTest { assertThat(errorMessage) .isEqualTo( - "The extension:type is missing or is not of type CodeType in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT" + "The extension:type is missing or is not of type CodeType in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT", ) } @@ -240,7 +240,7 @@ class MoreQuestionnairesTest { assertThat(errorMessage) .isEqualTo( - "The extension:type is missing or is not of type CodeType in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT" + "The extension:type is missing or is not of type CodeType in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT", ) } @@ -251,7 +251,7 @@ class MoreQuestionnairesTest { .apply { addExtension( "waitwhat", - Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "grandma", "Grandma") + Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "grandma", "Grandma"), ) addExtension("type", CodeType("Patient")) } @@ -264,7 +264,7 @@ class MoreQuestionnairesTest { assertThat(errorMessage) .isEqualTo( - "The extension:name is missing or is not of type Coding in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT" + "The extension:name is missing or is not of type Coding in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT", ) } @@ -285,7 +285,7 @@ class MoreQuestionnairesTest { assertThat(errorMessage) .isEqualTo( - "The extension:name is missing or is not of type Coding in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT" + "The extension:name is missing or is not of type Coding in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT", ) } } 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 95aaf178b6..47b3250f03 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 @@ -339,7 +339,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() @Language("JSON") val questionnaireResponseJson = @@ -469,7 +470,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() val uriTestQuestionnaire = iParser.parseResource(Questionnaire::class.java, questionnaireJson) as Questionnaire @@ -529,7 +531,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() @Language("JSON") val questionnaireResponseJson = @@ -561,7 +564,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() val uriTestQuestionnaire = iParser.parseResource(Questionnaire::class.java, questionnaireJson) as Questionnaire @@ -627,7 +631,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() @Language("JSON") val questionnaireResponseJson = @@ -675,7 +680,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() val iParser: IParser = FhirContext.forR4().newJsonParser() @@ -745,7 +751,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() @Language("JSON") val questionnaireResponseJson = @@ -780,7 +787,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() val uriTestQuestionnaire = iParser.parseResource(Questionnaire::class.java, questionnaireJson) as Questionnaire @@ -809,7 +817,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "mother", "Mother")), - Extension("type", StringType("Patient")) + Extension("type", StringType("Patient")), ) } } @@ -824,10 +832,10 @@ class ResourceMapperTest { Expression().apply { language = "text/fhirpath" expression = "today()" - } - ) + }, + ), ) - } + }, ) val patientId = UUID.randomUUID().toString() @@ -964,7 +972,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() @Language("JSON") val questionnaireResponseJson = @@ -1057,7 +1066,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() val uriTestQuestionnaire = iParser.parseResource(Questionnaire::class.java, questionnaireJson) as Questionnaire @@ -1131,7 +1141,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() @Language("JSON") val questionnaireResponseJson = @@ -1159,7 +1170,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() val pulseOximetryQuestionnaire = iParser.parseResource(Questionnaire::class.java, questionnaireJson) as Questionnaire @@ -1385,7 +1397,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() val uriTestQuestionnaire = iParser.parseResource(org.hl7.fhir.r4.model.Questionnaire::class.java, questionnaireJson) @@ -1424,7 +1437,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), - Extension("type", StringType("Patient")) + Extension("type", StringType("Patient")), ) } addExtension().apply { @@ -1432,7 +1445,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "mother", "Mother")), - Extension("type", StringType("Patient")) + Extension("type", StringType("Patient")), ) } addExtension().apply { @@ -1444,10 +1457,10 @@ class ResourceMapperTest { Coding( EXTENSION_LAUNCH_CONTEXT, "registration-encounter", - "Registration Encounter" - ) + "Registration Encounter", + ), ), - Extension("type", StringType("Encounter")) + Extension("type", StringType("Encounter")), ) } } @@ -1462,10 +1475,10 @@ class ResourceMapperTest { Expression().apply { language = "text/fhirpath" expression = "%father.name.given" - } - ) + }, + ), ) - } + }, ) .addItem( Questionnaire.QuestionnaireItemComponent().apply { @@ -1478,10 +1491,10 @@ class ResourceMapperTest { Expression().apply { language = "text/fhirpath" expression = "%mother.name.given" - } - ) + }, + ), ) - } + }, ) .addItem( Questionnaire.QuestionnaireItemComponent().apply { @@ -1494,10 +1507,10 @@ class ResourceMapperTest { Expression().apply { language = "text/fhirpath" expression = "%registration-encounter.reasonCode[0].text" - } - ) + }, + ), ) - } + }, ) val patientFather = @@ -1525,8 +1538,8 @@ class ResourceMapperTest { mapOf( "father" to patientFather, "mother" to patientMother, - "registration-encounter" to encounter - ) + "registration-encounter" to encounter, + ), ) assertThat((questionnaireResponse.item[0].answer[0].value as StringType).valueAsString) @@ -1548,7 +1561,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), - Extension("type", StringType("Patient")) + Extension("type", StringType("Patient")), ) } } @@ -1563,10 +1576,10 @@ class ResourceMapperTest { Expression().apply { language = "text/fhirpath" expression = "%father.name.given" - } - ) + }, + ), ) - } + }, ) .addItem( Questionnaire.QuestionnaireItemComponent().apply { @@ -1579,10 +1592,10 @@ class ResourceMapperTest { Expression().apply { language = "text/fhirpath" expression = "%mother.name.given" - } - ) + }, + ), ) - } + }, ) val patientFather = @@ -1602,7 +1615,7 @@ class ResourceMapperTest { val questionnaireResponse = ResourceMapper.populate( questionnaire, - mapOf("father" to patientFather, "mother" to patientMother) + mapOf("father" to patientFather, "mother" to patientMother), ) assertThat((questionnaireResponse.item[0].answer[0].value as StringType).valueAsString) @@ -1623,7 +1636,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), - Extension("type", StringType("Patient")) + Extension("type", StringType("Patient")), ) } } @@ -1638,10 +1651,10 @@ class ResourceMapperTest { Expression().apply { language = "text/fhirpath" expression = "%father.id" - } - ) + }, + ), ) - } + }, ) val patientId = UUID.randomUUID().toString() @@ -1663,7 +1676,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), - Extension("type", StringType("Patient")) + Extension("type", StringType("Patient")), ) } } @@ -1678,10 +1691,10 @@ class ResourceMapperTest { Expression().apply { language = "text/fhirpath" expression = "%father.id" - } - ) + }, + ), ) - } + }, ) val patientId = UUID.randomUUID().toString() @@ -1703,7 +1716,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), - Extension("type", StringType("Patient")) + Extension("type", StringType("Patient")), ) } } @@ -1718,10 +1731,10 @@ class ResourceMapperTest { Expression().apply { language = "text/fhirpath" expression = "%father.gender" - } - ) + }, + ), ) - } + }, ) val patientId = UUID.randomUUID().toString() @@ -1750,7 +1763,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "patient", "Patient")), - Extension("type", StringType("Patient")) + Extension("type", StringType("Patient")), ) } } @@ -1763,10 +1776,10 @@ class ResourceMapperTest { Expression().apply { language = "text/fhirpath" expression = "%patient.id" - } - ) + }, + ), ) - } + }, ) val patient = Patient().apply { id = "Patient/${UUID.randomUUID()}" } val questionnaireResponse = @@ -1775,6 +1788,7 @@ class ResourceMapperTest { assertThat(questionnaireResponse.itemFirstRep.answerFirstRep.valueReference.reference) .isEqualTo(patient.id) } + @Test fun `populate() should correctly populate IdType value with history in QuestionnaireResponse`() = runBlocking { @@ -1786,7 +1800,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), - Extension("type", StringType("Patient")) + Extension("type", StringType("Patient")), ) } } @@ -1801,10 +1815,10 @@ class ResourceMapperTest { Expression().apply { language = "text/fhirpath" expression = "%father.id" - } - ) + }, + ), ) - } + }, ) val patientId = UUID.randomUUID().toString() @@ -1826,7 +1840,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "mother", "Mother")), - Extension("type", StringType("Patient")) + Extension("type", StringType("Patient")), ) } } @@ -1841,8 +1855,8 @@ class ResourceMapperTest { Expression().apply { language = "text/fhirpath" expression = "%mother.gender" - } - ) + }, + ), ) answerOption = listOf( @@ -1850,16 +1864,16 @@ class ResourceMapperTest { Coding().apply { code = AdministrativeGender.MALE.toCode() display = AdministrativeGender.MALE.display - } + }, ), Questionnaire.QuestionnaireItemAnswerOptionComponent( Coding().apply { code = AdministrativeGender.FEMALE.toCode() display = AdministrativeGender.FEMALE.display - } - ) + }, + ), ) - } + }, ) val patient = Patient().apply { gender = Enumerations.AdministrativeGender.FEMALE } @@ -1880,7 +1894,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "mother", "Mother")), - Extension("type", StringType("Patient")) + Extension("type", StringType("Patient")), ) } } @@ -1895,8 +1909,8 @@ class ResourceMapperTest { Expression().apply { language = "text/fhirpath" expression = "%mother.gender" - } - ) + }, + ), ) answerOption = listOf( @@ -1904,14 +1918,14 @@ class ResourceMapperTest { Coding().apply { code = AdministrativeGender.MALE.toCode() display = AdministrativeGender.MALE.display - } + }, ), Questionnaire.QuestionnaireItemAnswerOptionComponent( Coding().apply { code = AdministrativeGender.FEMALE.toCode() display = AdministrativeGender.FEMALE.display - } - ) + }, + ), ) item = listOf( @@ -1925,12 +1939,12 @@ class ResourceMapperTest { Expression().apply { language = "text/fhirpath" expression = "%mother.id" - } - ) + }, + ), ) - } + }, ) - } + }, ) val patientId = UUID.randomUUID().toString() @@ -1945,7 +1959,7 @@ class ResourceMapperTest { assertThat((questionnaireResponse.item[0].answer[0].value as Coding).display) .isEqualTo(AdministrativeGender.FEMALE.display) assertThat( - (questionnaireResponse.item[0].answer[0].item[0].answer[0].value as StringType).value + (questionnaireResponse.item[0].answer[0].item[0].answer[0].value as StringType).value, ) .isEqualTo(patientId) } @@ -1960,14 +1974,14 @@ class ResourceMapperTest { Address().apply { city = "Lahore" country = "Pakistan" - } + }, ) name = listOf( HumanName().apply { given = mutableListOf(StringType("Salman")) family = "Ali" - } + }, ) telecom = listOf(ContactPoint().apply { value = "12345" }) } @@ -2075,7 +2089,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() @Language("JSON") val questionnaireResponseJson = @@ -2200,7 +2215,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() val mapping = """map "http://hl7.org/fhir/StructureMap/PatientRegistration" = 'PatientRegistration' @@ -2279,7 +2295,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() @Language("JSON") val questionnaireResponseJson = @@ -2298,7 +2315,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() val mapping = """map "http://hl7.org/fhir/StructureMap/ImmunizationReg" = 'ImmunizationReg' @@ -2332,7 +2350,7 @@ class ResourceMapperTest { uriTestQuestionnaireResponse, StructureMapExtractionContext(context, transformSupportServices) { _, worker -> StructureMapUtilities(worker).parse(mapping, "") - } + }, ) assertThat(bundle.entry.get(0).resource).isInstanceOf(Immunization::class.java) @@ -2467,7 +2485,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() @Language("JSON") val response = @@ -2508,7 +2527,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() val temperatureQuestionnaire = iParser.parseResource(Questionnaire::class.java, questionnaire) as Questionnaire val temperatureQuestionnaireResponse = @@ -2576,7 +2596,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() @Language("JSON") val questionnaireResponseJson = @@ -2604,7 +2625,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() val questionnaire = iParser.parseResource(Questionnaire::class.java, questionnaireJson) as Questionnaire val response = @@ -2719,7 +2741,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() @Language("JSON") val questionnaireResponseJson = @@ -2783,7 +2806,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() val questionnaire = iParser.parseResource(Questionnaire::class.java, questionnaireJson) as Questionnaire val response = @@ -2822,7 +2846,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), - Extension("type", StringType("Patient")) + Extension("type", StringType("Patient")), ) } } @@ -2837,11 +2861,11 @@ class ResourceMapperTest { Expression().apply { language = "text/fhirpath" expression = "%father.gender" - } - ) + }, + ), ) initial = listOf(Questionnaire.QuestionnaireItemInitialComponent(StringType("female"))) - } + }, ) val patient = Patient().apply { gender = Enumerations.AdministrativeGender.MALE } @@ -2852,7 +2876,7 @@ class ResourceMapperTest { .localizedMessage assertThat(errorMessage) .isEqualTo( - "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." + "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.", ) } @@ -2899,7 +2923,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() @Language("JSON") val response = @@ -2929,7 +2954,8 @@ class ResourceMapperTest { } ] } - """.trimIndent() + """ + .trimIndent() val questionnaireObj = iParser.parseResource(Questionnaire::class.java, questionnaire) as Questionnaire val temperatureQuestionnaireResponse = diff --git a/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt index 47ae72efbc..456f480cf9 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt @@ -79,7 +79,8 @@ class EditPatientViewModel(application: Application, private val state: SavedSta val entry = ResourceMapper.extract(questionnaireResource, questionnaireResponse).entryFirstRep if (entry.resource !is Patient) return@launch val patient = entry.resource as Patient - if (patient.hasName() && + if ( + patient.hasName() && patient.name[0].hasGiven() && patient.name[0].hasFamily() && patient.hasBirthDate() && From 8fec91ec00b33405faf84447fb12c62b5ca15dd8 Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Mon, 9 Oct 2023 17:55:09 +0700 Subject: [PATCH 19/24] Revert un-intended changes --- .../android/fhir/datacapture/mapping/ResourceMapperTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 47b3250f03..c102b40596 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 @@ -1088,7 +1088,7 @@ class ResourceMapperTest { } @Test - fun `extract_updateIntegerObservationForDecimalDefination_shouldUpdateAsDecimal() `() = + fun `extract_updateIntegerObservationForDecimalDefinition_shouldUpdateAsDecimal() `() = runBlocking { @Language("JSON") val questionnaireJson = From a21e6f7e6f8e632377bd699382ef74d52148f9a5 Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Mon, 9 Oct 2023 20:17:26 +0700 Subject: [PATCH 20/24] Fix failing checks --- .../android/fhir/catalog/DemoQuestionnaireFragment.kt | 4 ++-- .../android/fhir/datacapture/QuestionnaireViewModelTest.kt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/DemoQuestionnaireFragment.kt b/catalog/src/main/java/com/google/android/fhir/catalog/DemoQuestionnaireFragment.kt index cbc0e6f49b..b9f8309e27 100644 --- a/catalog/src/main/java/com/google/android/fhir/catalog/DemoQuestionnaireFragment.kt +++ b/catalog/src/main/java/com/google/android/fhir/catalog/DemoQuestionnaireFragment.kt @@ -168,11 +168,11 @@ class DemoQuestionnaireFragment : Fragment() { R.id.container, QuestionnaireFragment.builder() .setQuestionnaire(questionnaireJsonString) - .setQuestionnaireLaunchContexts( + .setQuestionnaireLaunchContextMap( FhirContext.forR4Cached() .newJsonParser() .encodeResourceToString(Patient().apply { id = "P1" }) - .let { listOf(it) }, + .let { mapOf("patient" to it) }, ) .build(), QUESTIONNAIRE_FRAGMENT_TAG, 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 48e44cd743..e1261f8027 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 @@ -25,7 +25,7 @@ import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.parser.IParser import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_ENABLE_REVIEW_PAGE import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_JSON_STRING -import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS +import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_READ_ONLY import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_SHOW_CANCEL_BUTTON @@ -4410,8 +4410,8 @@ class QuestionnaireViewModelTest { } state.set(EXTRA_QUESTIONNAIRE_JSON_STRING, printer.encodeResourceToString(questionnaire)) state.set( - EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS, - listOf(printer.encodeResourceToString(patient)), + EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP, + mapOf("patient" to printer.encodeResourceToString(patient)), ) val viewModel = QuestionnaireViewModel(context, state) From 8130aefed39247bda2af82d1f51cfea5cafa8a7f Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Fri, 13 Oct 2023 09:06:21 +0700 Subject: [PATCH 21/24] Remove check of must contain 2 sub-extensions --- .../extensions/MoreQuestionnaires.kt | 4 -- .../extensions/MoreQuestionnairesTest.kt | 48 ------------------- 2 files changed, 52 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt index 7604f9a977..82ecb6a2e7 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt @@ -75,10 +75,6 @@ internal fun validateLaunchContextExtensions(launchContextExtensions: List { - validateLaunchContextExtensions(listOf(launchContextExtension)) - } - .localizedMessage - - assertThat(errorMessage) - .isEqualTo( - "Expected 2 extensions (name and type) in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT but found ${launchContextExtension.extension.size}.", - ) - } - - @Test - fun `validateLaunchContextExtensions should throw exception if child extensions of launch context extension is more than 2 in size`() { - val launchContextExtension = - Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") - .apply { - addExtension( - "name", - Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "me", "Me"), - ) - addExtension("type", CodeType("Patient")) - addExtension("waitwhat", CodeType("Patient")) - } - - val errorMessage = - assertFailsWith { - validateLaunchContextExtensions(listOf(launchContextExtension)) - } - .localizedMessage - - assertThat(errorMessage) - .isEqualTo( - "Expected 2 extensions (name and type) in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT but found ${launchContextExtension.extension.size}.", - ) - } - @Test fun `validateLaunchContextExtensions should throw exception if type in type extension is not a valid resource type`() { val launchContextExtension = From f6f6b3cefb1548d4c9d5af9cb5944c0aadb7f035 Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Fri, 13 Oct 2023 09:06:33 +0700 Subject: [PATCH 22/24] Update Kdoc --- .../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 ed6ac07801..b7b6765105 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 @@ -216,7 +216,7 @@ object ResourceMapper { * Performs * [Expression-based population](http://build.fhir.org/ig/HL7/sdc/populate.html#expression-based-population) * and returns a [QuestionnaireResponse] for the [questionnaire] that is populated from the - * [resources]. + * [launchContexts]. */ suspend fun populate( questionnaire: Questionnaire, From ad51a709f020074d34a310811bf03a695a16c0fe Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Fri, 13 Oct 2023 09:59:42 +0700 Subject: [PATCH 23/24] Revert --- .../android/fhir/datacapture/mapping/ResourceMapperTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 31b9ac8e14..23a0903052 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 @@ -1781,7 +1781,7 @@ class ResourceMapperTest { ) }, ) - val patient = Patient().apply { id = "Patient/${UUID.randomUUID()}" } + val patient = Patient().apply { id = UUID.randomUUID().toString() } val questionnaireResponse = ResourceMapper.populate(questionnaire, mapOf("patient" to patient)) From 41a957d70e6e2795e7791eb55ecd26dd2b1fdafc Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Fri, 13 Oct 2023 10:00:16 +0700 Subject: [PATCH 24/24] validate launch context when using populate public API --- .../datacapture/mapping/ResourceMapper.kt | 2 ++ .../datacapture/mapping/ResourceMapperTest.kt | 27 ++++++++++--------- 2 files changed, 16 insertions(+), 13 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 75f29b0cce..eeb37f4ca3 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 @@ -27,6 +27,7 @@ import com.google.android.fhir.datacapture.extensions.toCodeType import com.google.android.fhir.datacapture.extensions.toCoding import com.google.android.fhir.datacapture.extensions.toIdType import com.google.android.fhir.datacapture.extensions.toUriType +import com.google.android.fhir.datacapture.extensions.validateLaunchContextExtensions import com.google.android.fhir.datacapture.fhirpath.fhirPathEngine import java.lang.reflect.Field import java.lang.reflect.Method @@ -222,6 +223,7 @@ object ResourceMapper { questionnaire: Questionnaire, launchContexts: Map, ): QuestionnaireResponse { + validateLaunchContextExtensions(questionnaire.questionnaireLaunchContexts ?: listOf()) val filteredLaunchContexts = filterByCodeInNameExtension( launchContexts, 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 23a0903052..4c88f02408 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 @@ -37,6 +37,7 @@ import org.hl7.fhir.exceptions.FHIRException 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.Coding import org.hl7.fhir.r4.model.ContactPoint import org.hl7.fhir.r4.model.DateType @@ -817,7 +818,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "mother", "Mother")), - Extension("type", StringType("Patient")), + Extension("type", CodeType("Patient")), ) } } @@ -1437,7 +1438,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), - Extension("type", StringType("Patient")), + Extension("type", CodeType("Patient")), ) } addExtension().apply { @@ -1445,7 +1446,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "mother", "Mother")), - Extension("type", StringType("Patient")), + Extension("type", CodeType("Patient")), ) } addExtension().apply { @@ -1460,7 +1461,7 @@ class ResourceMapperTest { "Registration Encounter", ), ), - Extension("type", StringType("Encounter")), + Extension("type", CodeType("Encounter")), ) } } @@ -1561,7 +1562,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), - Extension("type", StringType("Patient")), + Extension("type", CodeType("Patient")), ) } } @@ -1636,7 +1637,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), - Extension("type", StringType("Patient")), + Extension("type", CodeType("Patient")), ) } } @@ -1676,7 +1677,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), - Extension("type", StringType("Patient")), + Extension("type", CodeType("Patient")), ) } } @@ -1716,7 +1717,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), - Extension("type", StringType("Patient")), + Extension("type", CodeType("Patient")), ) } } @@ -1763,7 +1764,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "patient", "Patient")), - Extension("type", StringType("Patient")), + Extension("type", CodeType("Patient")), ) } } @@ -1800,7 +1801,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), - Extension("type", StringType("Patient")), + Extension("type", CodeType("Patient")), ) } } @@ -1840,7 +1841,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "mother", "Mother")), - Extension("type", StringType("Patient")), + Extension("type", CodeType("Patient")), ) } } @@ -1894,7 +1895,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "mother", "Mother")), - Extension("type", StringType("Patient")), + Extension("type", CodeType("Patient")), ) } } @@ -2846,7 +2847,7 @@ class ResourceMapperTest { extension = listOf( Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), - Extension("type", StringType("Patient")), + Extension("type", CodeType("Patient")), ) } }