Skip to content

Commit

Permalink
Add fetch resource by reference in RulesEngine (#3704)
Browse files Browse the repository at this point in the history
      * Add rule to fetch resource by reference

* Only save base resource if update made

* Refactor fetch resource by reference

- Add unit tests

* Rename test

* Add test for migration rule condition

* ⬆️ Update app version

* Refactor dispatcher handler

* Run spotlessApply

---------

Co-authored-by: Allan O <[email protected]>
  • Loading branch information
qiarie and allan-on authored Jan 30, 2025
1 parent e2d5d97 commit 9608436
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 17 deletions.
4 changes: 2 additions & 2 deletions android/buildSrc/src/main/kotlin/BuildConfigs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ object BuildConfigs {
const val minSdk = 26
const val compileSdk = 34
const val targetSdk = 34
const val versionCode = 13
const val versionName = "2.1.0"
const val versionCode = 14
const val versionName = "2.1.1"
const val applicationId = "org.smartregister.opensrp"
const val jvmToolchain = 17
const val kotlinCompilerExtensionVersion = "1.5.8"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package org.smartregister.fhircore.engine.rulesengine
import android.content.Context
import ca.uhn.fhir.context.FhirContext
import com.google.android.fhir.datacapture.extensions.logicalId
import com.google.android.fhir.db.ResourceNotFoundException
import com.google.android.fhir.search.Order
import com.jayway.jsonpath.Configuration
import com.jayway.jsonpath.JsonPath
Expand All @@ -32,12 +33,13 @@ import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.system.measureTimeMillis
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.apache.commons.jexl3.JexlEngine
import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.Enumerations.DataType
import org.hl7.fhir.r4.model.Reference
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
import org.hl7.fhir.r4.model.Task
import org.jeasy.rules.api.Facts
import org.jeasy.rules.api.Rule
Expand Down Expand Up @@ -752,7 +754,8 @@ constructor(
fhirContext
.newJsonParser()
.parseResource(resource::class.java, updatedResourceDocument.jsonString())
CoroutineScope(dispatcherProvider.io()).launch {

runBlocking {
if (purgeAffectedResources) {
defaultRepository.purge(updatedResource as Resource, forcePurge = true)
}
Expand All @@ -774,6 +777,39 @@ constructor(
}
}
}

fun getResourceByReference(
resourceReference: String?,
): Resource? {
if (resourceReference.isNullOrEmpty()) {
return null
}

return runBlocking {
try {
defaultRepository.loadResource(Reference().apply { reference = resourceReference })
} catch (e: ResourceNotFoundException) {
null
}
}
}

fun getResourceByIdAndType(
resourceId: String?,
resourceType: String?,
): Resource? {
if (resourceId.isNullOrEmpty() || resourceType.isNullOrEmpty()) {
return null
}

return runBlocking {
try {
defaultRepository.loadResource(resourceId, ResourceType.valueOf(resourceType))
} catch (e: ResourceNotFoundException) {
null
}
}
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,21 @@

package org.smartregister.fhircore.engine.rulesengine

import com.google.android.fhir.datacapture.extensions.logicalId
import com.google.android.fhir.db.ResourceNotFoundException
import com.google.android.fhir.search.Order
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.coEvery
import io.mockk.coVerify
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject
import kotlinx.coroutines.test.runTest
import org.hl7.fhir.r4.model.CarePlan
import org.hl7.fhir.r4.model.Enumerations
import org.hl7.fhir.r4.model.Patient
import org.hl7.fhir.r4.model.Period
import org.hl7.fhir.r4.model.ResourceType
import org.hl7.fhir.r4.model.Task
Expand All @@ -34,6 +40,7 @@ import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.smartregister.fhircore.engine.data.local.DefaultRepository
import org.smartregister.fhircore.engine.domain.model.RelatedResourceCount
import org.smartregister.fhircore.engine.domain.model.ServiceStatus
import org.smartregister.fhircore.engine.robolectric.RobolectricTest
Expand All @@ -43,7 +50,11 @@ class RulesEngineServiceTest : RobolectricTest() {
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)

@Inject lateinit var rulesFactory: RulesFactory

@Inject lateinit var defaultRepository: DefaultRepository

private lateinit var rulesEngineService: RulesFactory.RulesEngineService

private val tasks =
listOf(
Task().apply {
Expand Down Expand Up @@ -447,4 +458,106 @@ class RulesEngineServiceTest : RobolectricTest() {
)
Assert.assertEquals(resources[2].period.end, (filteredResources.last() as CarePlan).period.end)
}

@Test
fun testGetResourceByReferenceReturnsNullWhenResourceNotFound() = runTest {
val resourceId = "uuid"

coEvery { defaultRepository.fhirEngine.get(ResourceType.Patient, resourceId) } throws
ResourceNotFoundException(ResourceType.Patient.name, resourceId)

Assert.assertNull(rulesEngineService.getResourceByReference("Patient/uuid"))

coVerify { defaultRepository.fhirEngine.get(ResourceType.Patient, resourceId) }
}

@Test
fun testGetResourceByReferenceReturnsNullWhenReferenceIsNull() = runTest {
Assert.assertNull(rulesEngineService.getResourceByReference(null))
}

@Test
fun testGetResourceByReferenceReturnsNullWhenReferenceIsEmpty() = runTest {
Assert.assertNull(rulesEngineService.getResourceByReference(""))
}

@Test
fun testGetResourceByReferenceReturnsResourceWhenReferenceIsValid() = runTest {
val expectedResource = Patient().apply { id = "uuid" }

coEvery {
defaultRepository.fhirEngine.get(ResourceType.Patient, expectedResource.logicalId)
} answers { expectedResource }

val result = rulesEngineService.getResourceByReference("Patient/uuid")

Assert.assertEquals(expectedResource, result)

coVerify {
defaultRepository.fhirEngine.get(expectedResource.resourceType, expectedResource.logicalId)
}
}

@Test
fun testGetResourceByIdAndTypeReturnsNullWhenResourceIdIsNullAndResourceTypeIsValid() = runTest {
Assert.assertNull(
rulesEngineService.getResourceByIdAndType(null, ResourceType.Patient.name),
)
}

@Test
fun testGetResourceByIdAndTypeReturnsNullWhenResourceIdIsEmptyAndResourceTypeIsValid() = runTest {
Assert.assertNull(
rulesEngineService.getResourceByIdAndType("", ResourceType.Patient.name),
)
}

@Test
fun testGetResourceByIdAndTypeReturnsNullWhenResourceIdIsValidAndResourceTypeIsNull() = runTest {
Assert.assertNull(
rulesEngineService.getResourceByIdAndType("uuid", null),
)
}

@Test
fun testGetResourceByIdAndTypeReturnsNullWhenResourceIdIsValidAndResourceTypeIsEmpty() = runTest {
Assert.assertNull(
rulesEngineService.getResourceByIdAndType("uuid", ""),
)
}

@Test
fun testGetResourceByIdAndTypeReturnsNullWhenResourceNotFound() = runTest {
val resourceId = "uuid"

coEvery { defaultRepository.fhirEngine.get(ResourceType.Patient, resourceId) } throws
ResourceNotFoundException(ResourceType.Patient.name, resourceId)

Assert.assertNull(
rulesEngineService.getResourceByIdAndType(resourceId, ResourceType.Patient.name),
)

coVerify { defaultRepository.fhirEngine.get(ResourceType.Patient, resourceId) }
}

@Test
fun testGetResourceByIdAndTypeReturnsNullWhenResourceIdAndTypeAreValid() = runTest {
val expectedResource = Patient().apply { id = "uuid" }

coEvery {
defaultRepository.fhirEngine.get(ResourceType.Patient, expectedResource.logicalId)
} answers { expectedResource }

val result =
rulesEngineService.getResourceByIdAndType(
expectedResource.logicalId,
ResourceType.Patient.name,
)

Assert.assertEquals(expectedResource, result)

coVerify {
defaultRepository.fhirEngine.get(expectedResource.resourceType, expectedResource.logicalId)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ constructor(
val resource = repositoryResourceData.resource
val jsonParse = JsonPath.using(conf).parse(resource.encodeResourceToString())
val rules = rulesExecutor.rulesFactory.generateRules(migrationConfig.rules)
var baseResourceUpdated = false
val updatedResourceDocument =
jsonParse.apply {
migrationConfig.updateValues.forEach { updateExpression ->
Expand All @@ -187,6 +188,7 @@ constructor(
)
if (updateExpression.jsonPathExpression.startsWith("\$") && value != null) {
set(updateExpression.jsonPathExpression, value)
baseResourceUpdated = true
}
if (
updateExpression.jsonPathExpression.startsWith(
Expand All @@ -198,23 +200,26 @@ constructor(
updateExpression.jsonPathExpression.replace(resource.resourceType.name, "\$"),
value,
)
baseResourceUpdated = true
}
}
}

val resourceDefinition: Class<out IBaseResource>? =
FhirContext.forR4Cached().getResourceDefinition(resource).implementingClass
if (baseResourceUpdated) {
val resourceDefinition: Class<out IBaseResource>? =
FhirContext.forR4Cached().getResourceDefinition(resource).implementingClass

val updatedResource =
parser.parseResource(resourceDefinition, updatedResourceDocument.jsonString())
withContext(dispatcherProvider.io()) {
if (migrationConfig.purgeAffectedResources) {
defaultRepository.purge(updatedResource as Resource, forcePurge = true)
}
if (migrationConfig.createLocalChangeEntitiesAfterPurge) {
defaultRepository.addOrUpdate(resource = updatedResource as Resource)
} else {
defaultRepository.createRemote(resource = arrayOf(updatedResource as Resource))
val updatedResource =
parser.parseResource(resourceDefinition, updatedResourceDocument.jsonString())
withContext(dispatcherProvider.io()) {
if (migrationConfig.purgeAffectedResources) {
defaultRepository.purge(updatedResource as Resource, forcePurge = true)
}
if (migrationConfig.createLocalChangeEntitiesAfterPurge) {
defaultRepository.addOrUpdate(resource = updatedResource as Resource)
} else {
defaultRepository.createRemote(resource = arrayOf(updatedResource as Resource))
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,51 @@ class DataMigrationTest : RobolectricTest() {
preferenceDataStore.read(PreferenceDataStore.MIGRATION_VERSION).first(),
)
}

@Test
fun testMigrateShouldNotUpdateResourceIfRuleConditionIsFalse() =
runTest(timeout = 60.seconds) {
defaultRepository.create(addResourceTags = true, patient)

dataMigration.migrate(
migrationConfigs =
listOf(
MigrationConfig(
resourceConfig =
FhirResourceConfig(
baseResource = ResourceConfig(resource = ResourceType.Patient),
),
version = 2,
rules =
listOf(
RuleConfig(
name = "ruleCondition",
actions = listOf("data.put('ruleCondition', 'false')"),
),
RuleConfig(
name = "updateValue",
actions = listOf("data.put('updateValue', 'female')"),
condition = "data.get('ruleCondition')",
),
),
updateValues =
listOf(
UpdateValueConfig(
jsonPathExpression = "\$.gender",
computedValueKey = "updateValue",
),
),
),
),
previousVersion = 0,
)

val updatedPatient = defaultRepository.loadResource<Patient>(patient.logicalId)

Assert.assertEquals(Enumerations.AdministrativeGender.MALE, updatedPatient?.gender)
Assert.assertEquals(
2,
preferenceDataStore.read(PreferenceDataStore.MIGRATION_VERSION).first(),
)
}
}

0 comments on commit 9608436

Please sign in to comment.