From 6fbc25166cadf18c270526bd347156510e4b88c3 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Fri, 23 Sep 2022 11:50:11 +0500 Subject: [PATCH 1/2] Fhir data retrieve changes --- .../workflow/FhirEngineRetrieveProvider.kt | 46 ++++++++++++++----- .../android/fhir/workflow/FhirOperatorTest.kt | 38 +++++++-------- .../group-measure/patients-bundle.json | 1 + 3 files changed, 55 insertions(+), 30 deletions(-) diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt index c2a7fd62dd..2fa29015fc 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package com.google.android.fhir.workflow +import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.rest.gclient.ReferenceClientParam import ca.uhn.fhir.rest.gclient.TokenClientParam import com.google.android.fhir.FhirEngine @@ -23,14 +24,19 @@ import com.google.android.fhir.get import com.google.android.fhir.search.Search import com.google.android.fhir.search.search import kotlinx.coroutines.runBlocking +import org.hl7.fhir.r4.model.CodeableConcept +import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.Group import org.hl7.fhir.r4.model.ResourceType +import org.hl7.fhir.r4.model.Task import org.opencds.cqf.cql.engine.retrieve.TerminologyAwareRetrieveProvider import org.opencds.cqf.cql.engine.runtime.Code import org.opencds.cqf.cql.engine.runtime.Interval class FhirEngineRetrieveProvider(private val fhirEngine: FhirEngine) : TerminologyAwareRetrieveProvider() { + val fhirContext = FhirContext.forR4Cached() + override fun retrieve( context: String?, contextPath: String?, @@ -50,17 +56,35 @@ class FhirEngineRetrieveProvider(private val fhirEngine: FhirEngine) : listOf(fhirEngine.get(ResourceType.fromCode(dataType), contextValue)) } else { val search = Search(ResourceType.fromCode(dataType)) - if (search.type != ResourceType.Group && // added special treatment for Group type - contextPath is String && context is String && contextValue is String - ) { - search.filter(ReferenceClientParam(contextPath), { value = "$context/$contextValue" }) - } else { - if (search.type == ResourceType.Patient && // filtering active patients - hasField(dataType, "active") - ) { - search.filter(TokenClientParam("active"), { value = of(true) }) + + if (codePath is String) + codes?.map { Coding(it.system, it.code, it.display) }?.let { + // codePath is coded property name hence get the search param + val codeParam = + fhirContext.getResourceDefinition(dataType).searchParams.find { + it.path.endsWith(codePath) + }!! + .name + search.filter( + TokenClientParam(codeParam), + { value = of(CodeableConcept().apply { coding = it }) } + ) } - } + + if (contextPath is String && context is String && contextValue is String) + when { + // in case of Task ignore the contextPath which is requester and use subject to filter + // by task beneficiary + search.type == ResourceType.Task -> + search.filter( + ReferenceClientParam(Task.SP_SUBJECT), + { value = "$context/$contextValue" } + ) + // in case of Group contextPath is member.entity which might not be intended filter + search.type != ResourceType.Group -> + search.filter(ReferenceClientParam(contextPath), { value = "$context/$contextValue" }) + } + fhirEngine.search(search) } } diff --git a/workflow/src/test/java/com/google/android/fhir/workflow/FhirOperatorTest.kt b/workflow/src/test/java/com/google/android/fhir/workflow/FhirOperatorTest.kt index 05fdb66c23..6e2a2d6c42 100644 --- a/workflow/src/test/java/com/google/android/fhir/workflow/FhirOperatorTest.kt +++ b/workflow/src/test/java/com/google/android/fhir/workflow/FhirOperatorTest.kt @@ -204,11 +204,11 @@ class FhirOperatorTest { with(measureReport.group[0]) { assertThat(id).isEqualTo("groups") assertThat(population[0].id).isEqualTo("initial-population") - assertThat(population[0].count).isEqualTo(17) + assertThat(population[0].count).isEqualTo(20) assertThat(population[1].id).isEqualTo("denominator") - assertThat(population[1].count).isEqualTo(17) + assertThat(population[1].count).isEqualTo(20) assertThat(population[2].id).isEqualTo("numerator") - assertThat(population[2].count).isEqualTo(17) + assertThat(population[2].count).isEqualTo(20) assertThat(measureScore.value.toPlainString()).isEqualTo("1.0") assertThat(stratifierFirstRep.id).isNull() assertThat(stratifierFirstRep.stratum).isEmpty() @@ -216,12 +216,12 @@ class FhirOperatorTest { with(measureReport.group[1]) { assertThat(id).isEqualTo("males") assertThat(population[0].id).isEqualTo("initial-population") - assertThat(population[0].count).isEqualTo(17) + assertThat(population[0].count).isEqualTo(20) assertThat(population[1].id).isEqualTo("denominator") - assertThat(population[1].count).isEqualTo(17) + assertThat(population[1].count).isEqualTo(20) assertThat(population[2].id).isEqualTo("numerator") - assertThat(population[2].count).isEqualTo(7) - assertThat(measureScore.value.toPlainString()).isEqualTo("0.4117647058823529") + assertThat(population[2].count).isEqualTo(9) + assertThat(measureScore.value.toPlainString()).isEqualTo("0.45") assertThat(stratifierFirstRep.id).isEqualTo("by-age") with(stratifierFirstRep.stratum[0]) { assertThat(value.text).isEqualTo("P0Y") @@ -244,18 +244,18 @@ class FhirOperatorTest { with(stratifierFirstRep.stratum[2]) { assertThat(value.text).isEqualTo("P15-49Y") assertThat(population[0].id).isEqualTo("initial-population") - assertThat(population[0].count).isEqualTo(2) + assertThat(population[0].count).isEqualTo(4) assertThat(population[1].id).isEqualTo("denominator") - assertThat(population[1].count).isEqualTo(2) + assertThat(population[1].count).isEqualTo(4) assertThat(population[2].id).isEqualTo("numerator") - assertThat(population[2].count).isEqualTo(1) + assertThat(population[2].count).isEqualTo(3) } with(stratifierFirstRep.stratum[3]) { assertThat(value.text).isEqualTo("P6-14Y") assertThat(population[0].id).isEqualTo("initial-population") - assertThat(population[0].count).isEqualTo(2) + assertThat(population[0].count).isEqualTo(3) assertThat(population[1].id).isEqualTo("denominator") - assertThat(population[1].count).isEqualTo(2) + assertThat(population[1].count).isEqualTo(3) assertThat(population[2].id).isEqualTo("numerator") assertThat(population[2].count).isEqualTo(0) } @@ -272,12 +272,12 @@ class FhirOperatorTest { with(measureReport.group[2]) { assertThat(id).isEqualTo("females") assertThat(population[0].id).isEqualTo("initial-population") - assertThat(population[0].count).isEqualTo(17) + assertThat(population[0].count).isEqualTo(20) assertThat(population[1].id).isEqualTo("denominator") - assertThat(population[1].count).isEqualTo(17) + assertThat(population[1].count).isEqualTo(20) assertThat(population[2].id).isEqualTo("numerator") assertThat(population[2].count).isEqualTo(10) - assertThat(measureScore.value.toPlainString()).isEqualTo("0.5882352941176471") + assertThat(measureScore.value.toPlainString()).isEqualTo("0.5") assertThat(stratifierFirstRep.id).isEqualTo("by-age") with(stratifierFirstRep.stratum[0]) { assertThat(value.text).isEqualTo("P0Y") @@ -300,18 +300,18 @@ class FhirOperatorTest { with(stratifierFirstRep.stratum[2]) { assertThat(value.text).isEqualTo("P15-49Y") assertThat(population[0].id).isEqualTo("initial-population") - assertThat(population[0].count).isEqualTo(2) + assertThat(population[0].count).isEqualTo(4) assertThat(population[1].id).isEqualTo("denominator") - assertThat(population[1].count).isEqualTo(2) + assertThat(population[1].count).isEqualTo(4) assertThat(population[2].id).isEqualTo("numerator") assertThat(population[2].count).isEqualTo(1) } with(stratifierFirstRep.stratum[3]) { assertThat(value.text).isEqualTo("P6-14Y") assertThat(population[0].id).isEqualTo("initial-population") - assertThat(population[0].count).isEqualTo(2) + assertThat(population[0].count).isEqualTo(3) assertThat(population[1].id).isEqualTo("denominator") - assertThat(population[1].count).isEqualTo(2) + assertThat(population[1].count).isEqualTo(3) assertThat(population[2].id).isEqualTo("numerator") assertThat(population[2].count).isEqualTo(2) } diff --git a/workflow/testdata/group-measure/patients-bundle.json b/workflow/testdata/group-measure/patients-bundle.json index b766b8d401..64c7c9dc17 100644 --- a/workflow/testdata/group-measure/patients-bundle.json +++ b/workflow/testdata/group-measure/patients-bundle.json @@ -15,6 +15,7 @@ "status": "generated", "div": "
" }, + "birthDate": "2011-01-01", "telecom": [ { "system": "phone", From 6be1909922f4fabd4c901d92d5c2f0da8f7a710d Mon Sep 17 00:00:00 2001 From: maimoonak Date: Fri, 23 Sep 2022 13:46:32 +0500 Subject: [PATCH 2/2] Add unit tests --- .../workflow/FhirEngineRetrieveProvider.kt | 10 - .../FhirEngineRetrieveProviderTest.kt | 210 ++++++++++++++++++ 2 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineRetrieveProviderTest.kt diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt index 2fa29015fc..e8a1637107 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt @@ -89,14 +89,4 @@ class FhirEngineRetrieveProvider(private val fhirEngine: FhirEngine) : } } } - - private fun hasField(dataType: String?, field: String): Boolean { - if (dataType == null) return false - return try { - Class.forName("org.hl7.fhir.r4.model.$dataType").getDeclaredField(field) - true - } catch (e: NoSuchFieldException) { - false - } - } } diff --git a/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineRetrieveProviderTest.kt b/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineRetrieveProviderTest.kt new file mode 100644 index 0000000000..b0044cd64b --- /dev/null +++ b/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineRetrieveProviderTest.kt @@ -0,0 +1,210 @@ +/* + * Copyright 2022 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.workflow + +import androidx.test.core.app.ApplicationProvider +import com.google.android.fhir.FhirEngine +import com.google.android.fhir.FhirEngineProvider +import com.google.android.fhir.testing.FhirEngineProviderTestRule +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.runBlocking +import org.hl7.fhir.r4.model.CodeableConcept +import org.hl7.fhir.r4.model.Coding +import org.hl7.fhir.r4.model.Condition +import org.hl7.fhir.r4.model.Encounter +import org.hl7.fhir.r4.model.Patient +import org.hl7.fhir.r4.model.Reference +import org.hl7.fhir.r4.model.Task +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.opencds.cqf.cql.engine.runtime.Code +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class FhirEngineRetrieveProviderTest { + + @get:Rule val fhirEngineProviderRule = FhirEngineProviderTestRule() + private lateinit var fhirEngine: FhirEngine + private lateinit var fhirEngineRetrieveProvider: FhirEngineRetrieveProvider + + @Before + fun setupTest() { + fhirEngine = FhirEngineProvider.getInstance(ApplicationProvider.getApplicationContext()) + fhirEngineRetrieveProvider = FhirEngineRetrieveProvider(fhirEngine) + } + + @Test + fun `retrieve should return filtered Encounters with reason code`() = runBlocking { + val encounters = + listOf( + Encounter().apply { + id = "Encounter/E1" + addReasonCode().apply { this.addCoding(Coding("http://codesystem.org", "c1", "C 1")) } + }, + Encounter().apply { + id = "Encounter/E2" + addReasonCode().apply { this.addCoding(Coding("http://codesystem.org", "c2", "C 2")) } + } + ) + + fhirEngine.create(*encounters.toTypedArray()) + val result = + fhirEngineRetrieveProvider.retrieve( + context = "Patient", + contextPath = "subject", + contextValue = null, + dataType = "Encounter", + templateId = null, + codePath = "reasonCode", + codes = mutableListOf(Code().withCode("c2").withSystem("http://codesystem.org")), + valueSet = null, + datePath = null, + dateLowPath = null, + dateHighPath = null, + dateRange = null + ) + .map { it as Encounter } + + assertThat(result.size).isEqualTo(1) + assertThat(result.first().id).isEqualTo("Encounter/E2") + assertThat(result.first().reasonCode.first().codingFirstRep.code).isEqualTo("c2") + } + + @Test + fun `retrieve should return filtered Conditions with patient id`() = runBlocking { + val conditions = + listOf( + Condition().apply { + id = "Condition/C1" + code = + CodeableConcept().apply { this.addCoding(Coding("http://codesystem.org", "c1", "C 1")) } + subject = Reference().apply { reference = "Patient/P1" } + }, + Condition().apply { + id = "Condition/C2" + code = + CodeableConcept().apply { this.addCoding(Coding("http://codesystem.org", "c2", "C 2")) } + subject = Reference().apply { reference = "Patient/P2" } + } + ) + + fhirEngine.create(*conditions.toTypedArray()) + + val result = + fhirEngineRetrieveProvider.retrieve( + context = "Patient", + contextPath = "subject", + contextValue = "P2", + dataType = "Condition", + templateId = null, + codePath = null, + codes = null, + valueSet = null, + datePath = null, + dateLowPath = null, + dateHighPath = null, + dateRange = null + ) + .map { it as Condition } + + assertThat(result.size).isEqualTo(1) + assertThat(result.first().id).isEqualTo("Condition/C2") + assertThat(result.first().subject.reference).isEqualTo("Patient/P2") + } + + @Test + fun `retrieve should return filtered Tasks with patient id`() = runBlocking { + val tasks = + listOf( + Task().apply { + id = "Task/T1" + code = + CodeableConcept().apply { this.addCoding(Coding("http://codesystem.org", "c1", "C 1")) } + `for` = Reference().apply { reference = "Patient/P1" } + }, + Task().apply { + id = "Task/T2" + code = + CodeableConcept().apply { this.addCoding(Coding("http://codesystem.org", "c2", "C 2")) } + `for` = Reference().apply { reference = "Patient/P2" } + } + ) + + fhirEngine.create(*tasks.toTypedArray()) + + val result = + fhirEngineRetrieveProvider.retrieve( + context = "Patient", + contextPath = "requester", + contextValue = "P2", + dataType = "Task", + templateId = null, + codePath = null, + codes = null, + valueSet = null, + datePath = null, + dateLowPath = null, + dateHighPath = null, + dateRange = null + ) + .map { it as Task } + + assertThat(result.size).isEqualTo(1) + assertThat(result.first().id).isEqualTo("Task/T2") + assertThat(result.first().`for`.reference).isEqualTo("Patient/P2") + } + + @Test + fun `retrieve should return Patients with no filter on active`() = runBlocking { + val patients = + listOf( + Patient().apply { + id = "Patient/P1" + active = false + }, + Patient().apply { + id = "Patient/P2" + active = true + } + ) + + fhirEngine.create(*patients.toTypedArray()) + + val result = + fhirEngineRetrieveProvider.retrieve( + context = "Patient", + contextPath = null, + contextValue = null, + dataType = "Patient", + templateId = null, + codePath = null, + codes = null, + valueSet = null, + datePath = null, + dateLowPath = null, + dateHighPath = null, + dateRange = null + ) + .map { it as Patient } + + assertThat(result.size).isEqualTo(2) + assertThat(result.first().id).isEqualTo("Patient/P1") + assertThat(result.last().id).isEqualTo("Patient/P2") + } +}