diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/di/FhirValidatorModule.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/di/FhirValidatorModule.kt index 53af79a8a9..4e49ded6ba 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/di/FhirValidatorModule.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/di/FhirValidatorModule.kt @@ -20,6 +20,7 @@ import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.support.DefaultProfileValidationSupport import ca.uhn.fhir.context.support.IValidationSupport import ca.uhn.fhir.validation.FhirValidator +import dagger.Lazy import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -30,6 +31,8 @@ import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerVali import org.hl7.fhir.common.hapi.validation.support.UnknownCodeSystemWarningValidationSupport import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator +import org.smartregister.fhircore.engine.util.DispatcherProvider +import org.smartregister.fhircore.engine.util.validation.ResourceValidationRequestHandler @Module @InstallIn(SingletonComponent::class) @@ -52,4 +55,13 @@ class FhirValidatorModule { instanceValidator.invalidateCaches() return fhirContext.newValidator().apply { registerValidatorModule(instanceValidator) } } + + @Provides + @Singleton + fun provideResourceValidationRequestHandler( + fhirValidatorProvider: Lazy, + dispatcherProvider: DispatcherProvider, + ): ResourceValidationRequestHandler { + return ResourceValidationRequestHandler(fhirValidatorProvider.get(), dispatcherProvider) + } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirValidatorExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirValidatorExtension.kt deleted file mode 100644 index 3a7c28f60f..0000000000 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirValidatorExtension.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2021-2024 Ona Systems, Inc - * - * 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 org.smartregister.fhircore.engine.util.extension - -import ca.uhn.fhir.validation.FhirValidator -import ca.uhn.fhir.validation.ResultSeverityEnum -import ca.uhn.fhir.validation.ValidationResult -import kotlin.coroutines.coroutineContext -import kotlinx.coroutines.withContext -import org.hl7.fhir.r4.model.Resource -import org.smartregister.fhircore.engine.BuildConfig -import timber.log.Timber - -data class ResourceValidationResult( - val resource: Resource, - val validationResult: ValidationResult, -) { - val errorMessages - get() = buildString { - val messages = - validationResult.messages.filter { - it.severity.ordinal >= ResultSeverityEnum.WARNING.ordinal - } - - for (validationMsg in messages) { - appendLine( - "${validationMsg.message} - ${validationMsg.locationString} -- (${validationMsg.severity})", - ) - } - } -} - -data class FhirValidatorResultsWrapper(val results: List = emptyList()) { - val errorMessages = results.map { it.errorMessages } -} - -suspend fun FhirValidator.checkResourceValid( - vararg resource: Resource, - isDebug: Boolean = BuildConfig.DEBUG, -): FhirValidatorResultsWrapper { - if (!isDebug) return FhirValidatorResultsWrapper() - - return withContext(coroutineContext) { - FhirValidatorResultsWrapper( - results = - resource.map { - val result = this@checkResourceValid.validateWithResult(it) - ResourceValidationResult(it, result) - }, - ) - } -} - -fun FhirValidatorResultsWrapper.logErrorMessages() { - results.forEach { - if (it.errorMessages.isNotBlank()) { - Timber.tag("$TAG (${it.resource.referenceValue()})").e(it.errorMessages) - } - } -} - -private const val TAG = "FhirValidator" diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/validation/ResourceValidationRequest.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/validation/ResourceValidationRequest.kt new file mode 100644 index 0000000000..ba398a5bf8 --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/validation/ResourceValidationRequest.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * 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 org.smartregister.fhircore.engine.util.validation + +import ca.uhn.fhir.validation.FhirValidator +import ca.uhn.fhir.validation.ResultSeverityEnum +import ca.uhn.fhir.validation.ValidationResult +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.hl7.fhir.r4.model.Resource +import org.smartregister.fhircore.engine.util.DispatcherProvider +import org.smartregister.fhircore.engine.util.extension.referenceValue +import timber.log.Timber + +data class ResourceValidationRequest(val resources: List) { + constructor(vararg resource: Resource) : this(resource.toList()) +} + +class ResourceValidationRequestHandler( + private val fhirValidator: FhirValidator, + private val dispatcherProvider: DispatcherProvider, +) { + fun handleResourceValidationRequest(request: ResourceValidationRequest) { + CoroutineScope(dispatcherProvider.io()).launch { + val resources = request.resources + fhirValidator.checkResources(resources).logErrorMessages() + } + } +} + +internal data class ResourceValidationResult( + val resource: Resource, + val validationResult: ValidationResult, +) { + val errorMessages + get() = buildString { + val messages = + validationResult.messages.filter { + it.severity.ordinal >= ResultSeverityEnum.WARNING.ordinal + } + if (messages.isNotEmpty()) { + appendLine(resource.referenceValue()) + } + for (validationMsg in messages) { + appendLine( + "${validationMsg.locationString} - ${validationMsg.message} -- (${validationMsg.severity})", + ) + } + } +} + +internal class FhirValidatorResultsWrapper( + val results: List = emptyList(), +) { + val errorMessages = results.map { it.errorMessages } + + fun logErrorMessages() { + results.forEach { + if (it.errorMessages.isNotBlank()) { + Timber.tag(TAG).e(it.errorMessages) + } + } + } + + companion object { + private const val TAG = "FhirValidatorResult" + } +} + +internal fun FhirValidator.checkResources( + resources: List, +): FhirValidatorResultsWrapper { + return FhirValidatorResultsWrapper( + results = + resources.map { + val result = this@checkResources.validateWithResult(it) + ResourceValidationResult(it, result) + }, + ) +} diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/FhirValidatorExtensionTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/util/validation/ResourceValidationRequestTest.kt similarity index 65% rename from android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/FhirValidatorExtensionTest.kt rename to android/engine/src/test/java/org/smartregister/fhircore/engine/util/validation/ResourceValidationRequestTest.kt index f2dff7fe0d..0e3d8d2a8f 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/FhirValidatorExtensionTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/util/validation/ResourceValidationRequestTest.kt @@ -14,16 +14,16 @@ * limitations under the License. */ -package org.smartregister.fhircore.engine.util.extension +package org.smartregister.fhircore.engine.util.validation import ca.uhn.fhir.validation.FhirValidator import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import io.mockk.spyk +import io.mockk.mockkObject +import io.mockk.unmockkObject import io.mockk.verify import javax.inject.Inject import kotlinx.coroutines.test.runTest -import org.hl7.fhir.instance.model.api.IBaseResource import org.hl7.fhir.r4.model.CarePlan import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.Reference @@ -32,33 +32,51 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.smartregister.fhircore.engine.robolectric.RobolectricTest +import timber.log.Timber @HiltAndroidTest -class FhirValidatorExtensionTest : RobolectricTest() { - +class ResourceValidationRequestTest : RobolectricTest() { @get:Rule var hiltRule = HiltAndroidRule(this) @Inject lateinit var validator: FhirValidator + @Inject lateinit var resourceValidationRequestHandler: ResourceValidationRequestHandler + @Before fun setUp() { hiltRule.inject() } @Test - fun testCheckResourceValidRunsNoValidationWhenBuildTypeIsNotDebug() = runTest { - val basicResource = CarePlan() - val fhirValidatorSpy = spyk(validator) - val resultsWrapper = fhirValidatorSpy.checkResourceValid(basicResource, isDebug = false) - Assert.assertTrue(resultsWrapper.results.isEmpty()) - verify(exactly = 0) { fhirValidatorSpy.validateWithResult(any()) } - verify(exactly = 0) { fhirValidatorSpy.validateWithResult(any()) } + fun testHandleResourceValidationRequestValidatesInvalidResourceLoggingErrors() = runTest { + mockkObject(Timber) + val resource = + CarePlan().apply { + id = "test-careplan" + status = CarePlan.CarePlanStatus.ACTIVE + intent = CarePlan.CarePlanIntent.PLAN + subject = Reference("f4bd3e29-f0f8-464e-97af-923b83664ccc") + } + val validationRequest = ResourceValidationRequest(resource) + resourceValidationRequestHandler.handleResourceValidationRequest(validationRequest) + verify { + Timber.e( + withArg { + Assert.assertTrue( + it.contains( + "CarePlan.subject - The syntax of the reference 'f4bd3e29-f0f8-464e-97af-923b83664ccc' looks incorrect, and it should be checked -- (WARNING)", + ), + ) + }, + ) + } + unmockkObject(Timber) } @Test fun testCheckResourceValidValidatesResourceStructureWhenCarePlanResourceInvalid() = runTest { val basicCarePlan = CarePlan() - val resultsWrapper = validator.checkResourceValid(basicCarePlan) + val resultsWrapper = validator.checkResources(listOf(basicCarePlan)) Assert.assertTrue( resultsWrapper.errorMessages.any { it.contains( @@ -85,13 +103,13 @@ class FhirValidatorExtensionTest : RobolectricTest() { intent = CarePlan.CarePlanIntent.PLAN subject = Reference("Task/unknown") } - val resultsWrapper = validator.checkResourceValid(carePlan) + val resultsWrapper = validator.checkResources(listOf(carePlan)) Assert.assertEquals(1, resultsWrapper.errorMessages.size) Assert.assertTrue( resultsWrapper.errorMessages .first() .contains( - "The type 'Task' implied by the reference URL Task/unknown is not a valid Target for this element (must be one of [Group, Patient]) - CarePlan.subject", + "CarePlan.subject - The type 'Task' implied by the reference URL Task/unknown is not a valid Target for this element (must be one of [Group, Patient])", ignoreCase = true, ), ) @@ -105,13 +123,13 @@ class FhirValidatorExtensionTest : RobolectricTest() { intent = CarePlan.CarePlanIntent.PLAN subject = Reference("unknown") } - val resultsWrapper = validator.checkResourceValid(carePlan) + val resultsWrapper = validator.checkResources(listOf(carePlan)) Assert.assertEquals(1, resultsWrapper.errorMessages.size) Assert.assertTrue( resultsWrapper.errorMessages .first() .contains( - "The syntax of the reference 'unknown' looks incorrect, and it should be checked - CarePlan.subject", + "CarePlan.subject - The syntax of the reference 'unknown' looks incorrect, and it should be checked", ignoreCase = true, ), ) @@ -126,7 +144,7 @@ class FhirValidatorExtensionTest : RobolectricTest() { intent = CarePlan.CarePlanIntent.PLAN subject = Reference(patient) } - val resultsWrapper = validator.checkResourceValid(carePlan) + val resultsWrapper = validator.checkResources(listOf(carePlan)) Assert.assertEquals(1, resultsWrapper.errorMessages.size) Assert.assertTrue(resultsWrapper.errorMessages.first().isBlank()) } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index fa4d91a472..2bb53a31e0 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -217,59 +217,53 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { } private fun renderQuestionnaire() { + if (supportFragmentManager.findFragmentByTag(QUESTIONNAIRE_FRAGMENT_TAG) != null) return + lifecycleScope.launch { - var questionnaireFragment: QuestionnaireFragment? = null - if (supportFragmentManager.findFragmentByTag(QUESTIONNAIRE_FRAGMENT_TAG) == null) { - viewModel.setProgressState(QuestionnaireProgressState.QuestionnaireLaunch(true)) - with(viewBinding) { - questionnaireToolbar.apply { - setNavigationIcon(R.drawable.ic_cancel) - setNavigationOnClickListener { handleBackPress() } - } - questionnaireTitle.apply { text = questionnaireConfig.title } - clearAll.apply { - visibility = if (questionnaireConfig.showClearAll) View.VISIBLE else View.GONE - setOnClickListener { questionnaireFragment?.clearAllAnswers() } - } - } + viewModel.setProgressState(QuestionnaireProgressState.QuestionnaireLaunch(true)) - questionnaire = viewModel.retrieveQuestionnaire(questionnaireConfig) + viewBinding.questionnaireToolbar.setNavigationIcon(R.drawable.ic_cancel) + viewBinding.questionnaireToolbar.setNavigationOnClickListener { handleBackPress() } + viewBinding.questionnaireTitle.text = questionnaireConfig.title + viewBinding.clearAll.visibility = + if (questionnaireConfig.showClearAll) View.VISIBLE else View.GONE - try { - val questionnaireFragmentBuilder = - buildQuestionnaireFragment( - questionnaire = questionnaire!!, - questionnaireConfig = questionnaireConfig, - ) + questionnaire = viewModel.retrieveQuestionnaire(questionnaireConfig) - questionnaireFragment = questionnaireFragmentBuilder.build() - supportFragmentManager.commit { - setReorderingAllowed(true) - add(R.id.container, questionnaireFragment, QUESTIONNAIRE_FRAGMENT_TAG) - } + if (questionnaire == null) { + showToast(getString(R.string.questionnaire_not_found)) + finish() + return@launch + } + if (questionnaire!!.subjectType.isNullOrEmpty()) { + val subjectRequiredMessage = getString(R.string.missing_subject_type) + showToast(subjectRequiredMessage) + Timber.e(subjectRequiredMessage) + finish() + return@launch + } - registerFragmentResultListener() - } catch (nullPointerException: NullPointerException) { - showToast(getString(R.string.questionnaire_not_found)) - finish() - } finally { - viewModel.setProgressState(QuestionnaireProgressState.QuestionnaireLaunch(false)) - } + val questionnaireFragment = + getQuestionnaireFragmentBuilder( + questionnaire = questionnaire!!, + questionnaireConfig = questionnaireConfig, + ) + .build() + viewBinding.clearAll.setOnClickListener { questionnaireFragment.clearAllAnswers() } + supportFragmentManager.commit { + setReorderingAllowed(true) + add(R.id.container, questionnaireFragment, QUESTIONNAIRE_FRAGMENT_TAG) } + registerFragmentResultListener() + + viewModel.setProgressState(QuestionnaireProgressState.QuestionnaireLaunch(false)) } } - private suspend fun buildQuestionnaireFragment( + private suspend fun getQuestionnaireFragmentBuilder( questionnaire: Questionnaire, questionnaireConfig: QuestionnaireConfig, ): QuestionnaireFragment.Builder { - if (questionnaire.subjectType.isNullOrEmpty()) { - val subjectRequiredMessage = getString(R.string.missing_subject_type) - showToast(subjectRequiredMessage) - Timber.e(subjectRequiredMessage) - finish() - } - val (questionnaireResponse, launchContextResources) = viewModel.populateQuestionnaire(questionnaire, this.questionnaireConfig, actionParameters) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt index 9b83444ff9..55b4a5b59d 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt @@ -23,7 +23,6 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import ca.uhn.fhir.context.FhirContext -import ca.uhn.fhir.validation.FhirValidator import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.datacapture.mapping.ResourceMapper import com.google.android.fhir.datacapture.mapping.StructureMapExtractionContext @@ -34,12 +33,12 @@ import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.search.Search import com.google.android.fhir.search.filter.TokenParamFilterCriterion import com.google.android.fhir.workflow.FhirOperator +import dagger.Lazy import dagger.hilt.android.lifecycle.HiltViewModel import java.util.Date import java.util.LinkedList import java.util.UUID import javax.inject.Inject -import javax.inject.Provider import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -83,7 +82,6 @@ import org.smartregister.fhircore.engine.util.extension.appendPractitionerInfo import org.smartregister.fhircore.engine.util.extension.appendRelatedEntityLocation import org.smartregister.fhircore.engine.util.extension.asReference import org.smartregister.fhircore.engine.util.extension.batchedSearch -import org.smartregister.fhircore.engine.util.extension.checkResourceValid import org.smartregister.fhircore.engine.util.extension.clearText import org.smartregister.fhircore.engine.util.extension.cqfLibraryUrls import org.smartregister.fhircore.engine.util.extension.extractByStructureMap @@ -92,7 +90,6 @@ import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid import org.smartregister.fhircore.engine.util.extension.find import org.smartregister.fhircore.engine.util.extension.generateMissingId import org.smartregister.fhircore.engine.util.extension.isIn -import org.smartregister.fhircore.engine.util.extension.logErrorMessages import org.smartregister.fhircore.engine.util.extension.packRepeatedGroups import org.smartregister.fhircore.engine.util.extension.prepopulateWithComputedConfigValues import org.smartregister.fhircore.engine.util.extension.questionnaireResponseStatus @@ -100,6 +97,8 @@ import org.smartregister.fhircore.engine.util.extension.showToast import org.smartregister.fhircore.engine.util.extension.updateLastUpdated import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor import org.smartregister.fhircore.engine.util.helper.TransformSupportServices +import org.smartregister.fhircore.engine.util.validation.ResourceValidationRequest +import org.smartregister.fhircore.engine.util.validation.ResourceValidationRequestHandler import org.smartregister.fhircore.quest.R import timber.log.Timber @@ -114,7 +113,7 @@ constructor( val transformSupportServices: TransformSupportServices, val sharedPreferencesHelper: SharedPreferencesHelper, val fhirOperator: FhirOperator, - val fhirValidatorProvider: Provider, + val fhirValidatorRequestHandlerProvider: Lazy, val fhirPathDataExtractor: FhirPathDataExtractor, val configurationRegistry: ConfigurationRegistry, ) : ViewModel() { @@ -264,9 +263,17 @@ constructor( } } - suspend fun validateWithFhirValidator(vararg resource: Resource) { - val fhirValidator = fhirValidatorProvider.get() - fhirValidator.checkResourceValid(*resource).logErrorMessages() + fun validateWithFhirValidator(vararg resource: Resource) { + if (BuildConfig.DEBUG) { + fhirValidatorRequestHandlerProvider + .get() + .handleResourceValidationRequest( + request = + ResourceValidationRequest( + *resource, + ), + ) + } } suspend fun retireUsedQuestionnaireUniqueId( @@ -345,6 +352,7 @@ constructor( ) { questionnaireResponse.subject = this.logicalId.asReference(subjectType) } + if (questionnaireConfig.isEditable()) { if (resourceType == subjectType) { this.id = questionnaireResponse.subject.extractId() @@ -360,19 +368,23 @@ constructor( .fhirPathExpression val currentResourceIdentifier = - fhirPathDataExtractor.extractValue( - base = this, - expression = fhirPathExpression, - ) + withContext(dispatcherProvider.default()) { + fhirPathDataExtractor.extractValue( + base = this@run, + expression = fhirPathExpression, + ) + } // Search for resource with property value matching extracted value val resource = previouslyExtractedResources.getValue(resourceType).find { val extractedValue = - fhirPathDataExtractor.extractValue( - base = it, - expression = fhirPathExpression, - ) + withContext(dispatcherProvider.default()) { + fhirPathDataExtractor.extractValue( + base = it, + expression = fhirPathExpression, + ) + } extractedValue.isNotEmpty() && extractedValue.equals(currentResourceIdentifier, true) } @@ -629,25 +641,27 @@ constructor( ): Bundle = kotlin .runCatching { - if (extractByStructureMap) { - ResourceMapper.extract( - questionnaire = questionnaire, - questionnaireResponse = questionnaireResponse, - structureMapExtractionContext = - StructureMapExtractionContext( - transformSupportServices = transformSupportServices, - structureMapProvider = { structureMapUrl: String?, _: IWorkerContext -> - structureMapUrl?.substringAfterLast("/")?.let { - defaultRepository.loadResource(it) - } - }, - ), - ) - } else { - ResourceMapper.extract( - questionnaire = questionnaire, - questionnaireResponse = questionnaireResponse, - ) + withContext(dispatcherProvider.default()) { + if (extractByStructureMap) { + ResourceMapper.extract( + questionnaire = questionnaire, + questionnaireResponse = questionnaireResponse, + structureMapExtractionContext = + StructureMapExtractionContext( + transformSupportServices = transformSupportServices, + structureMapProvider = { structureMapUrl: String?, _: IWorkerContext -> + structureMapUrl?.substringAfterLast("/")?.let { + defaultRepository.loadResource(it) + } + }, + ), + ) + } else { + ResourceMapper.extract( + questionnaire = questionnaire, + questionnaireResponse = questionnaireResponse, + ) + } } } .onFailure { exception -> @@ -778,18 +792,20 @@ constructor( } } - return QuestionnaireResponseValidator.validateQuestionnaireResponse( - questionnaire = Questionnaire().apply { item = validQuestionnaireItems }, - questionnaireResponse = - QuestionnaireResponse().apply { - item = validQuestionnaireResponseItems - packRepeatedGroups() - }, - context = context, - ) - .values - .flatten() - .all { it is Valid || it is NotValidated } + return withContext(dispatcherProvider.default()) { + QuestionnaireResponseValidator.validateQuestionnaireResponse( + questionnaire = Questionnaire().apply { item = validQuestionnaireItems }, + questionnaireResponse = + QuestionnaireResponse().apply { + item = validQuestionnaireResponseItems + packRepeatedGroups() + }, + context = context, + ) + .values + .flatten() + .all { it is Valid || it is NotValidated } + } } suspend fun executeCql( @@ -1120,10 +1136,12 @@ constructor( computedValues, ) - fhirPathDataExtractor.extractValue( - base = uniqueIdResource, - expression = uniqueIdAssignmentConfig.idFhirPathExpression, - ) + withContext(dispatcherProvider.default()) { + fhirPathDataExtractor.extractValue( + base = uniqueIdResource, + expression = uniqueIdAssignmentConfig.idFhirPathExpression, + ) + } }, ) @@ -1213,13 +1231,8 @@ constructor( it.resourceType != null && it.value.isNotEmpty() } - .mapNotNull { - try { - loadResource(it.resourceType!!, it.value) - } catch (resourceNotFoundException: ResourceNotFoundException) { - null - } - } + .distinctBy { "${it.resourceType?.name}${it.value}" } + .mapNotNull { loadResource(it.resourceType!!, it.value) } } /** Load [Resource] of type [ResourceType] for the provided [resourceIdentifier] */ diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt index 5be9e2b455..bac9f9b788 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt @@ -19,7 +19,6 @@ package org.smartregister.fhircore.quest.ui.questionnaire import android.app.Application import androidx.test.core.app.ApplicationProvider import ca.uhn.fhir.parser.IParser -import ca.uhn.fhir.validation.FhirValidator import com.google.android.fhir.FhirEngine import com.google.android.fhir.SearchResult import com.google.android.fhir.datacapture.extensions.logicalId @@ -29,6 +28,7 @@ import com.google.android.fhir.get import com.google.android.fhir.knowledge.KnowledgeManager import com.google.android.fhir.search.Search import com.google.android.fhir.workflow.FhirOperator +import dagger.Lazy import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import io.mockk.coEvery @@ -46,7 +46,6 @@ import java.io.File import java.util.Date import java.util.UUID import javax.inject.Inject -import javax.inject.Provider import kotlin.test.assertEquals import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -57,6 +56,7 @@ import org.hl7.fhir.r4.model.Attachment import org.hl7.fhir.r4.model.Basic import org.hl7.fhir.r4.model.BooleanType import org.hl7.fhir.r4.model.Bundle +import org.hl7.fhir.r4.model.CarePlan import org.hl7.fhir.r4.model.CodeableConcept import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.Consent @@ -116,6 +116,7 @@ import org.smartregister.fhircore.engine.util.extension.questionnaireResponseSta import org.smartregister.fhircore.engine.util.extension.valueToString import org.smartregister.fhircore.engine.util.extension.yesterday import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor +import org.smartregister.fhircore.engine.util.validation.ResourceValidationRequestHandler import org.smartregister.fhircore.quest.app.fakes.Faker import org.smartregister.fhircore.quest.assertResourceEquals import org.smartregister.fhircore.quest.robolectric.RobolectricTest @@ -131,7 +132,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { @Inject lateinit var sharedPreferencesHelper: SharedPreferencesHelper - @Inject lateinit var fhirValidatorProvider: Provider + @Inject lateinit var fhirValidatorRequestHandlerProvider: Lazy @Inject lateinit var configService: ConfigService @@ -212,7 +213,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { resourceDataRulesExecutor = resourceDataRulesExecutor, transformSupportServices = mockk(), sharedPreferencesHelper = sharedPreferencesHelper, - fhirValidatorProvider = fhirValidatorProvider, + fhirValidatorRequestHandlerProvider = fhirValidatorRequestHandlerProvider, fhirOperator = fhirOperator, fhirPathDataExtractor = fhirPathDataExtractor, configurationRegistry = configurationRegistry, @@ -652,7 +653,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { transformSupportServices = mockk(), sharedPreferencesHelper = sharedPreferencesHelper, fhirOperator = fhirOperator, - fhirValidatorProvider = fhirValidatorProvider, + fhirValidatorRequestHandlerProvider = fhirValidatorRequestHandlerProvider, fhirPathDataExtractor = fhirPathDataExtractor, configurationRegistry = configurationRegistry, ) @@ -1171,6 +1172,9 @@ class QuestionnaireViewModelTest : RobolectricTest() { fun testGenerateCarePlan() = runTest { val bundle = Bundle().apply { addEntry(Bundle.BundleEntryComponent().apply { resource = patient }) } + coEvery { + fhirCarePlanGenerator.generateOrUpdateCarePlan(any(), any(), any(), any()) + } returns CarePlan() val questionnaireConfig = questionnaireConfig.copy(planDefinitions = listOf("planDefId")) questionnaireViewModel.generateCarePlan(patient, bundle, questionnaireConfig) @@ -1949,7 +1953,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { transformSupportServices = mockk(), sharedPreferencesHelper = sharedPreferencesHelper, fhirOperator = fhirOperator, - fhirValidatorProvider = fhirValidatorProvider, + fhirValidatorRequestHandlerProvider = fhirValidatorRequestHandlerProvider, fhirPathDataExtractor = fhirPathDataExtractor, configurationRegistry = configurationRegistry, ) @@ -2011,7 +2015,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { transformSupportServices = mockk(), sharedPreferencesHelper = sharedPreferencesHelper, fhirOperator = fhirOperator, - fhirValidatorProvider = fhirValidatorProvider, + fhirValidatorRequestHandlerProvider = fhirValidatorRequestHandlerProvider, fhirPathDataExtractor = fhirPathDataExtractor, configurationRegistry = configurationRegistry, ) @@ -2086,7 +2090,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { transformSupportServices = mockk(), sharedPreferencesHelper = sharedPreferencesHelper, fhirOperator = fhirOperator, - fhirValidatorProvider = fhirValidatorProvider, + fhirValidatorRequestHandlerProvider = fhirValidatorRequestHandlerProvider, fhirPathDataExtractor = fhirPathDataExtractor, configurationRegistry = configurationRegistry, )