Skip to content

Commit

Permalink
Workflow Library: Implementing Library.evaluate and first androidTest…
Browse files Browse the repository at this point in the history
… suite (#1326)

* Operator.evaluateLibrary function
Generalization of FhirEngineRetrieveProvider

* Fixes from spotlessApply

* Moving COVID tests to its own directory.

* Removing redundant (contextPath != "id")

* Using Truth library and clearing up the logs

* BugFix on assetPath and Organization ID

* Fixing the JSON structure of the test to become a Bundle instead of a Composite.

* Updating LibraryEvaluate Tests to the new FhirEngineProviderTestRule

* Update workflow/src/main/java/com/google/android/fhir/workflow/FhirOperator.kt

Improving documentation as suggested by the reviewer.

Co-authored-by: Jing Tang <[email protected]>

* Adding a new line before the function definition.

* Adding a new line before the function for consistency.

* deleting unnecessary test files

* Simplifying Test Case

* Removing new line

* Fixing Documentation of evaluateLibrary

* Improving the explanation of the test case for Library Evaluate

* Improving documentation of the Library Evaluate test case by describing the CQLEvaluator steps being tested

* Running spotless check

* update to trigger github actions after OutOfMemory exception

* removing spaces to pass spotless

* Improving documentation

* Use a cached version of R4

Co-authored-by: Jing Tang <[email protected]>
  • Loading branch information
vitorpamplona and jingtang10 authored May 24, 2022
1 parent d806535 commit e8bd02a
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 121 deletions.
6 changes: 5 additions & 1 deletion workflow/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ android {
multiDexEnabled = true
}

sourceSets { getByName("test").apply { resources.setSrcDirs(listOf("testdata")) } }
sourceSets {
getByName("test").apply { resources.setSrcDirs(listOf("testdata")) }
getByName("androidTest").apply { resources.setSrcDirs(listOf("testdata")) }
}

// Added this for fixing out of memory issue in running test cases
tasks.withType<Test>().configureEach {
Expand Down Expand Up @@ -130,6 +133,7 @@ dependencies {
androidTestImplementation(Dependencies.AndroidxTest.workTestingRuntimeKtx)
androidTestImplementation(Dependencies.junit)
androidTestImplementation(Dependencies.truth)
androidTestImplementation(project(":testing"))

api(Dependencies.HapiFhir.structuresR4) { exclude(module = "junit") }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright 2021 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 androidx.test.ext.junit.runners.AndroidJUnit4
import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
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 java.io.InputStream
import kotlinx.coroutines.runBlocking
import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.Parameters
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class FhirOperatorLibraryEvaluateTest {

@get:Rule val fhirEngineProviderRule = FhirEngineProviderTestRule()

private lateinit var fhirEngine: FhirEngine
private lateinit var fhirOperator: FhirOperator

private val fhirContext = FhirContext.forCached(FhirVersionEnum.R4)
private val jsonParser = fhirContext.newJsonParser()

private fun open(asset: String): InputStream? {
return javaClass.getResourceAsStream(asset)
}

private fun load(asset: String): Bundle {
return jsonParser.parseResource(open(asset)) as Bundle
}

@Before
fun setUp() = runBlocking {
fhirEngine = FhirEngineProvider.getInstance(ApplicationProvider.getApplicationContext())
fhirOperator = FhirOperator(fhirContext, fhirEngine)
}

/**
* Evaluates a compiled CQL that was exported to Jxson and included inside a FHIRLibrary Json. The
* compiled CQL file is encoded in Base64 and placed inside the JSON Library. The expression
* `CompletedImmunization` simply checks if a vaccination protocol has been finished as below.
*
* This test requires the CQLEvaluator to
* 1. load the patient using a `FhirEngineRetrieveProvider`,
* 2. load the Immunization records of that patient,
* 3. load the CQL Library using a `FhirEngineLibraryContentProvider`
* 4. evaluate if the immunization record presents a Protocol where the number of doses taken
* matches the number of required doses or if the number of required doses is null.
*
* ```
* library ImmunityCheck version '1.0.0'
*
* using FHIR version '4.0.0'
* include "FHIRHelpers" version '4.0.0' called FHIRHelpers
* context Immunization
*
* define "CompletedImmunization":
* exists(GetFinalDose) or exists(GetSingleDose)
*
* define "GetFinalDose":
* [Immunization] I
* where exists(I.protocolApplied)
* and I.protocolApplied.doseNumber.value = I.protocolApplied.seriesDoses.value
*
* define "GetSingleDose":
* [Immunization] I
* where exists(I.protocolApplied)
* and exists(I.protocolApplied.doseNumber.value)
* and not exists(I.protocolApplied.seriesDoses.value)
* ```
*/
@Test
fun evaluateImmunityCheck() = runBlocking {
// Load patient
val patientImmunizationHistory = load("/immunity-check/ImmunizationHistory.json")
for (entry in patientImmunizationHistory.entry) {
fhirEngine.create(entry.resource)
}

// Load Library that checks if Patient has taken a vaccine
fhirOperator.loadLibs(load("/immunity-check/ImmunityCheck.json"))

// Evaluates a specific Patient
val results =
fhirOperator.evaluateLibrary(
"http://localhost/Library/ImmunityCheck|1.0.0",
"d4d35004-24f8-40e4-8084-1ad75924514f",
setOf("CompletedImmunization")
) as
Parameters

assertThat(results.getParameterBool("CompletedImmunization")).isTrue()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,13 @@

package com.google.android.fhir.workflow

import ca.uhn.fhir.rest.gclient.ReferenceClientParam
import ca.uhn.fhir.rest.gclient.TokenClientParam
import com.google.android.fhir.FhirEngine
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.CarePlan
import org.hl7.fhir.r4.model.Condition
import org.hl7.fhir.r4.model.DiagnosticReport
import org.hl7.fhir.r4.model.Encounter
import org.hl7.fhir.r4.model.EpisodeOfCare
import org.hl7.fhir.r4.model.Observation
import org.hl7.fhir.r4.model.Patient
import org.hl7.fhir.r4.model.ServiceRequest
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
import org.opencds.cqf.cql.engine.retrieve.TerminologyAwareRetrieveProvider
import org.opencds.cqf.cql.engine.runtime.Code
import org.opencds.cqf.cql.engine.runtime.Interval
Expand All @@ -48,111 +43,30 @@ class FhirEngineRetrieveProvider(val fhirEngine: FhirEngine) : TerminologyAwareR
dateRange: Interval?
): Iterable<Any> {
return runBlocking {
when (dataType) {
"Patient" -> {
if (contextValue is String) {
mutableListOf(fhirEngine.get<Patient>(contextValue))
} else {
val patients =
fhirEngine.search<Patient> { filter(Patient.ACTIVE, { value = of(true) }) }
patients.toMutableList()
}
}
"EpisodeOfCare" -> {
if (contextValue is String) {
val patientsEpisodesOfCare =
fhirEngine.search<EpisodeOfCare> {
filter(EpisodeOfCare.PATIENT, { value = "$context/$contextValue" })
}
patientsEpisodesOfCare.toMutableList()
} else {
val patientsEpisodesOfCare =
fhirEngine.search<EpisodeOfCare> { filter(Patient.ACTIVE, { value = of(true) }) }
patientsEpisodesOfCare.toMutableList()
}
}
"Encounter" -> {
if (contextValue is String) {
val encounters =
fhirEngine.search<Encounter> {
filter(Encounter.SUBJECT, { value = "$context/$contextValue" })
}
encounters.toMutableList()
} else {
val encounters =
fhirEngine.search<Encounter> { filter(Patient.ACTIVE, { value = of(true) }) }
encounters.toMutableList()
}
}
"Condition" -> {
if (contextValue is String) {
val conditions =
fhirEngine.search<Condition> {
filter(Condition.SUBJECT, { value = "$context/$contextValue" })
}
conditions.toMutableList()
} else {
val conditions =
fhirEngine.search<Condition> { filter(Patient.ACTIVE, { value = of(true) }) }
conditions.toMutableList()
}
}
"Observation" -> {
if (contextValue is String) {
val observations =
fhirEngine.search<Observation> {
filter(Observation.SUBJECT, { value = "$context/$contextValue" })
}
observations.toMutableList()
} else {
val observations =
fhirEngine.search<Observation> { filter(Patient.ACTIVE, { value = of(true) }) }
observations.toMutableList()
}
}
"DiagnosticReport" -> {
if (contextValue is String) {
val diagnosis =
fhirEngine.search<DiagnosticReport> {
filter(DiagnosticReport.SUBJECT, { value = "$context/$contextValue" })
}
diagnosis.toMutableList()
} else {
val diagnosis =
fhirEngine.search<DiagnosticReport> { filter(Patient.ACTIVE, { value = of(true) }) }
diagnosis.toMutableList()
}
}
"ServiceRequest" -> {
if (contextValue is String) {
val serviceRequests =
fhirEngine.search<ServiceRequest> {
filter(ServiceRequest.SUBJECT, { value = "$context/$contextValue" })
}
serviceRequests.toMutableList()
} else {
val serviceRequests =
fhirEngine.search<ServiceRequest> { filter(Patient.ACTIVE, { value = of(true) }) }
serviceRequests.toMutableList()
}
}
"CarePlan" -> {
if (contextValue is String) {
val careplan =
fhirEngine.search<CarePlan> {
filter(CarePlan.SUBJECT, { value = "$context/$contextValue" })
}
careplan.toMutableList()
} else {
val careplan =
fhirEngine.search<CarePlan> { filter(Patient.ACTIVE, { value = of(true) }) }
careplan.toMutableList()
}
}
else -> {
throw NotImplementedError("Not implemented yet")
if (contextPath == "id" && contextValue is String) {
mutableListOf(fhirEngine.get(ResourceType.fromCode(dataType), contextValue))
} else if (contextPath is String && context is String && contextValue is String) {
val search = Search(ResourceType.fromCode(dataType))
search.filter(ReferenceClientParam(contextPath), { value = "$context/$contextValue" })
fhirEngine.search<Resource>(search).toMutableList()
} else {
val search = Search(ResourceType.fromCode(dataType))
if (hasField(dataType, "active")) {
// TODO: I am not sure why the default search is only for active entities
search.filter(TokenClientParam("active"), { value = of(true) })
}
fhirEngine.search<Resource>(search).toMutableList()
}
}
}

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
}
}
}
Loading

0 comments on commit e8bd02a

Please sign in to comment.