Skip to content

Commit

Permalink
Configs Versioning (#3189)
Browse files Browse the repository at this point in the history
* Configs versioning setup

* init implementation guide version in settings screen

* remove unused tag

* add string resource

* fetch composition referenced in IG

* add implementationguide to test

* Add ImplementationGuide resource

* update tests

* Update implementation_guide_config.json

* Refactor flow

* Rename implementationGuideUrl to implementationGuideId

- Code cleanup

* Format implementation_guide_config.json

* Run spotlessApply

* Remove implementationGuideId entry from app config

- Add name field to IG config
- Update variable name

* Refactor implementation from config registry

* refactor to use IG by version

* Run spotlessApply

* Refactors and bug fixes

* Update sample ImplementationGuide URL and version

* Update fetchRemoteIG app URL

* Remove flavour from versionName when fetching IG

* Save IG after fetch

* save ImplementationGuide to database

* introduce fetchConfiguration IG test skeletons

* Update IG URL to FQDN

* find and sort IG by context-quantity

* fix failing tests

* update IG tests

* use implmentaionGuide extension guide

* use version code for context-quantity

* empty commit to trigger checks

* run spotlessApply

* remove unnecessary implementationGuide details from userSettingsScreen

* spotlessApply

* initiate Implementation Guide documentation

* Update strings.xml

Co-authored-by: Peter Lubell-Doughtie <[email protected]>

---------

Co-authored-by: Simon Njoroge <[email protected]>
Co-authored-by: Benjamin Mwalimu <[email protected]>
Co-authored-by: Peter Lubell-Doughtie <[email protected]>
  • Loading branch information
4 people authored May 6, 2024
1 parent d6b014f commit 99f974e
Show file tree
Hide file tree
Showing 9 changed files with 311 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.Binary
import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.Composition
import org.hl7.fhir.r4.model.ImplementationGuide
import org.hl7.fhir.r4.model.ListResource
import org.hl7.fhir.r4.model.MetadataResource
import org.hl7.fhir.r4.model.Resource
Expand Down Expand Up @@ -397,7 +398,7 @@ constructor(
sharedPreferencesHelper.read(SharedPreferenceKey.APP_ID.name, null)?.let { appId ->
val parsedAppId = appId.substringBefore(TYPE_REFERENCE_DELIMITER).trim()
val patientRelatedResourceTypes = mutableListOf<ResourceType>()
val compositionResource = fetchRemoteComposition(parsedAppId)
val compositionResource = fetchRemoteCompositionByAppId(parsedAppId)
compositionResource?.let { composition ->
composition
.retrieveCompositionSections()
Expand Down Expand Up @@ -444,11 +445,43 @@ constructor(
}
}

suspend fun fetchRemoteComposition(appId: String?): Composition? {
Timber.i("Fetching configs for app $appId")
suspend fun fetchRemoteImplementationGuideByAppId(
appId: String?,
appVersionCode: Int?,
): ImplementationGuide? {
Timber.i("Fetching ImplementationGuide config for app $appId version $appVersionCode")

val urlPath =
"${ResourceType.Composition.name}?${Composition.SP_IDENTIFIER}=$appId&_count=$DEFAULT_COUNT"
"ImplementationGuide?&name=$appId&context-quantity=le$appVersionCode&_sort=-context-quantity&_count=1"
return fhirResourceDataSource.getResource(urlPath).entryFirstRep.let {
if (!it.hasResource()) {
Timber.w("No response for ImplementationGuide resource on path $urlPath")
return null
}

it.resource as ImplementationGuide
}
}

suspend fun fetchRemoteCompositionById(
id: String?,
version: String?,
): Composition? {
Timber.i("Fetching Composition config id $id version $version")
val urlPath = "Composition/$id/_history/$version"
return fhirResourceDataSource.getResource(urlPath).entryFirstRep.let {
if (!it.hasResource()) {
Timber.w("No response for composition resource on path $urlPath")
return null
}

it.resource as Composition
}
}

suspend fun fetchRemoteCompositionByAppId(appId: String?): Composition? {
Timber.i("Fetching Composition config for app $appId")
val urlPath = "Composition?identifier=$appId&_count=$DEFAULT_COUNT"
return fhirResourceDataSource.getResource(urlPath).entryFirstRep.let {
if (!it.hasResource()) {
Timber.w("No response for composition resource on path $urlPath")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import org.hl7.fhir.r4.model.Flag
import org.hl7.fhir.r4.model.Group
import org.hl7.fhir.r4.model.HumanName
import org.hl7.fhir.r4.model.Immunization
import org.hl7.fhir.r4.model.ImplementationGuide
import org.hl7.fhir.r4.model.Location
import org.hl7.fhir.r4.model.Observation
import org.hl7.fhir.r4.model.Patient
Expand Down Expand Up @@ -382,6 +383,14 @@ fun isValidResourceType(resourceCode: String): Boolean {
}
}

fun ImplementationGuide.retrieveImplementationGuideDefinitionResources():
List<ImplementationGuide.ImplementationGuideDefinitionResourceComponent> {
val resources =
mutableListOf<ImplementationGuide.ImplementationGuideDefinitionResourceComponent>()
this.definition.resource.forEach { resources.add(it) }
return resources
}

/**
* Composition sections can be nested. This function retrieves all the nested composition sections
* and returns a flattened list of all [Composition.SectionComponent] for the given [Composition]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"resourceType": "ImplementationGuide",
"id": "12967310",
"url": "https://fhir.zeir.smartregister.org/fhir/ImplementationGuide/quest",
"version": "1.1.0",
"name": "quest",
"title": "Quest Implementation Guide",
"status": "draft",
"packageId": "org.smartregister.fhircore",
"date": "2024-04-01",
"publisher": "Ona Systems, Inc.",
"fhirVersion": [
{
"code": {
"coding": [
{
"system": "http://hl7.org/fhir/FHIR-version",
"code": "4.3.0",
"display": "4.3.0"
}
],
"text": "FHIR Release 4B."
}
}
],
"useContext": {
"code": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/usage-context-type",
"code": "program",
"display": "Program"
}
],
"text": "Program"
},
"valueRange": {
"low": {
"value": 1
},
"high": {
"value": 10
}
}
},
"definition": {
"resource": [
{
"reference": {
"reference": "Composition/8294"
}
}
]
}
}
1 change: 0 additions & 1 deletion android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,3 @@ android.defaults.buildfeatures.buildconfig=true
android.suppressUnsupportedCompileSdk=34
android.jetifier.ignorelist=jackson-core
org.gradle.warning.mode=all
android.suppressUnsupportedCompileSdk=34
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,13 @@ import org.smartregister.fhircore.engine.util.extension.extractId
import org.smartregister.fhircore.engine.util.extension.getActivity
import org.smartregister.fhircore.engine.util.extension.launchActivityWithNoBackStackHistory
import org.smartregister.fhircore.engine.util.extension.retrieveCompositionSections
import org.smartregister.fhircore.engine.util.extension.retrieveImplementationGuideDefinitionResources
import org.smartregister.fhircore.quest.ui.login.LoginActivity
import retrofit2.HttpException
import timber.log.Timber

typealias QuestBuildConfig = org.smartregister.fhircore.quest.BuildConfig

@HiltViewModel
class AppSettingViewModel
@Inject
Expand Down Expand Up @@ -102,10 +105,40 @@ constructor(
viewModelScope.launch {
try {
showProgressBar.postValue(true)
Timber.i("Fetching configs for app $appId")
val compositionResource =
withContext(dispatcherProvider.io()) {
configurationRegistry.fetchRemoteComposition(appId)

Timber.i(
"Fetching configs for app $appId with highest context-quantity ${QuestBuildConfig.VERSION_CODE}",
)

val compositionResource: Composition?

val implementationGuideResource =
configurationRegistry.fetchRemoteImplementationGuideByAppId(
appId,
QuestBuildConfig.VERSION_CODE,
)

compositionResource =
if (implementationGuideResource != null) {
configurationRegistry.addOrUpdate(implementationGuideResource)

val compositionReference =
implementationGuideResource
.retrieveImplementationGuideDefinitionResources()[0]
.reference
.reference

val compositionIdWithHistory = compositionReference?.substringAfter('/')
val compositionId = compositionIdWithHistory?.substringBefore('/')
val compositionVersion = compositionIdWithHistory?.substringAfterLast('/', "")

withContext(dispatcherProvider.io()) {
configurationRegistry.fetchRemoteCompositionById(compositionId, compositionVersion)
}
} else {
withContext(dispatcherProvider.io()) {
configurationRegistry.fetchRemoteCompositionByAppId(appId)
}
}

if (compositionResource == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import kotlinx.coroutines.runBlocking
import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.Composition
import org.hl7.fhir.r4.model.Identifier
import org.hl7.fhir.r4.model.ImplementationGuide
import org.hl7.fhir.r4.model.ListResource
import org.hl7.fhir.r4.model.Reference
import org.hl7.fhir.r4.model.StructureMap
Expand Down Expand Up @@ -120,7 +121,8 @@ class ConfigurationRegistryTest : RobolectricTest() {
}

every { secureSharedPreference.retrieveSessionUsername() } returns "demo"
coEvery { configurationRegistry.fetchRemoteComposition(any()) } returns composition
coEvery { configurationRegistry.fetchRemoteCompositionByAppId(any()) } returns composition
coEvery { configurationRegistry.fhirResourceDataSource.getResource(any()) } returns bundle
coEvery { configurationRegistry.fhirResourceDataSource.post(any(), any()) } returns bundle
every { sharedPreferencesHelper.read(SharedPreferenceKey.APP_ID.name, null) } returns "demo"
coEvery { configurationRegistry.saveSyncSharedPreferences(any()) } just runs
Expand All @@ -131,6 +133,21 @@ class ConfigurationRegistryTest : RobolectricTest() {

@Test
fun testFetchListResourceNonProxy() = runBlocking {
val implementationGuide =
ImplementationGuide().apply {
url = "ImplementationGuide/1"
name = "testImplementationGuide"
definition =
ImplementationGuide.ImplementationGuideDefinitionComponent().apply {
resource =
mutableListOf(
ImplementationGuide.ImplementationGuideDefinitionResourceComponent(
Reference().apply { reference = "Composition" },
),
)
}
}

val composition =
Composition().apply {
addSection().apply {
Expand All @@ -148,8 +165,7 @@ class ConfigurationRegistryTest : RobolectricTest() {

configurationRegistry.setNonProxy(true)
every { secureSharedPreference.retrieveSessionUsername() } returns "demo"
coEvery { configurationRegistry.fetchRemoteComposition(any()) } returns composition

coEvery { configurationRegistry.fetchRemoteCompositionByAppId(any()) } returns composition
coEvery { configurationRegistry.fhirResourceDataSource.getResource(any()) } returns bundle
every { sharedPreferencesHelper.read(SharedPreferenceKey.APP_ID.name, null) } returns "demo"
coEvery { configurationRegistry.saveSyncSharedPreferences(any()) } just runs
Expand Down Expand Up @@ -184,7 +200,7 @@ class ConfigurationRegistryTest : RobolectricTest() {
}

every { secureSharedPreference.retrieveSessionUsername() } returns "demo"
coEvery { configurationRegistry.fetchRemoteComposition(any()) } returns composition
coEvery { configurationRegistry.fetchRemoteCompositionByAppId(any()) } returns composition
coEvery {
fhirResourceService.getResourceWithGatewayModeHeader(
ConfigurationRegistry.FHIR_GATEWAY_MODE_HEADER_VALUE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import org.hl7.fhir.r4.model.Binary
import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.Composition
import org.hl7.fhir.r4.model.Identifier
import org.hl7.fhir.r4.model.ImplementationGuide
import org.hl7.fhir.r4.model.Reference
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
Expand Down Expand Up @@ -159,7 +160,16 @@ class AppSettingViewModelTest : RobolectricTest() {
@Test
fun `fetchConfigurations() should call configurationRegistry#processResultBundleBinaries with correct values`() =
runTest {
coEvery { appSettingViewModel.configurationRegistry.fetchRemoteComposition(any()) } returns
coEvery {
appSettingViewModel.configurationRegistry.fetchRemoteImplementationGuideByAppId(
any(),
any(),
)
} returns null

coEvery {
appSettingViewModel.configurationRegistry.fetchRemoteCompositionByAppId(any())
} returns
Composition().apply {
addSection().apply {
this.focus =
Expand Down Expand Up @@ -222,7 +232,7 @@ class AppSettingViewModelTest : RobolectricTest() {

val binarySlot = slot<Binary>()

coVerify { appSettingViewModel.configurationRegistry.fetchRemoteComposition(any()) }
coVerify { appSettingViewModel.configurationRegistry.fetchRemoteCompositionByAppId(any()) }
coVerify { fhirResourceDataSource.post(any(), any()) }
coVerify { defaultRepository.createRemote(any(), any()) }
coVerify {
Expand Down Expand Up @@ -472,8 +482,12 @@ class AppSettingViewModelTest : RobolectricTest() {
coEvery { appSettingViewModel.loadConfigurations(any()) } just runs
coEvery { appSettingViewModel.isNonProxy() } returns false
coEvery { appSettingViewModel.appId } returns MutableLiveData(appId)
coEvery { appSettingViewModel.configurationRegistry.fetchRemoteComposition(appId) } returns
composition
coEvery {
appSettingViewModel.configurationRegistry.fetchRemoteImplementationGuideByAppId(any(), any())
} returns null
coEvery {
appSettingViewModel.configurationRegistry.fetchRemoteCompositionByAppId(appId)
} returns composition
coEvery { appSettingViewModel.defaultRepository.createRemote(any(), any()) } just runs
coEvery { appSettingViewModel.configurationRegistry.saveSyncSharedPreferences(any()) } just runs
coEvery {
Expand Down Expand Up @@ -599,4 +613,71 @@ class AppSettingViewModelTest : RobolectricTest() {
slot.captured,
)
}

@Test
fun `fetchConfigurations() with an ImplementationGuide should call fetchRemoteCompositionById()`() {
runBlocking {
appSettingViewModel.run {
onApplicationIdChanged("app")
fetchConfigurations(context)
}
val implementationGuide =
ImplementationGuide().apply {
url = "ImplementationGuide/1"
name = "testImplementationGuide"
definition =
ImplementationGuide.ImplementationGuideDefinitionComponent().apply {
resource =
mutableListOf(
ImplementationGuide.ImplementationGuideDefinitionResourceComponent(
Reference().apply { reference = "Composition/_history/1" },
),
)
}
}
val composition = Composition().apply { id = "1" }
coEvery {
appSettingViewModel.configurationRegistry.fetchRemoteImplementationGuideByAppId(
any(),
any(),
)
} returns implementationGuide
coEvery { appSettingViewModel.configurationRegistry.addOrUpdate(any()) } just runs
coEvery {
appSettingViewModel.configurationRegistry.fetchRemoteCompositionById(any(), any())
} returns composition
coEvery { appSettingViewModel.configurationRegistry.saveSyncSharedPreferences(any()) } just
runs
coEvery { appSettingViewModel.defaultRepository.createRemote(any(), any()) } just runs
appSettingViewModel.fetchConfigurations(context)
coVerify {
appSettingViewModel.configurationRegistry.fetchRemoteCompositionById(any(), any())
}
}
}

@Test
fun `fetchConfigurations() without ImplementationGuide should call fetchRemoteCompositionByAppId()`() {
runBlocking {
appSettingViewModel.run {
onApplicationIdChanged("app")
fetchConfigurations(context)
}
val composition = Composition().apply { id = "123" }
coEvery {
appSettingViewModel.configurationRegistry.fetchRemoteImplementationGuideByAppId(
any(),
any(),
)
} returns null
coEvery {
appSettingViewModel.configurationRegistry.fetchRemoteCompositionByAppId(any())
} returns composition
coEvery { appSettingViewModel.configurationRegistry.saveSyncSharedPreferences(any()) } just
runs
coEvery { appSettingViewModel.defaultRepository.createRemote(any(), any()) } just runs
appSettingViewModel.fetchConfigurations(context)
coVerify { appSettingViewModel.configurationRegistry.fetchRemoteCompositionByAppId(any()) }
}
}
}
Loading

0 comments on commit 99f974e

Please sign in to comment.