Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate extracted fhir resources while in debug #2874

Merged
merged 17 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions android/engine/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ dependencies {
implementation(Dependencies.HapiFhir.structuresR4) { exclude(module = "junit") }
implementation(Dependencies.HapiFhir.guavaCaching)
implementation(Dependencies.HapiFhir.validationR4)
implementation(Dependencies.HapiFhir.validationR5)
implementation(Dependencies.HapiFhir.validation) {
exclude(module = "commons-logging")
exclude(module = "httpclient")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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.di

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.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService
import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport
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

@Module
@InstallIn(SingletonComponent::class)
class FhirValidatorModule {

@Provides
@Singleton
fun provideFhirValidator(): FhirValidator {
val fhirContext = FhirContext.forR4()
LZRS marked this conversation as resolved.
Show resolved Hide resolved

val validationSupportChain =
ValidationSupportChain(
DefaultProfileValidationSupport(fhirContext),
InMemoryTerminologyServerValidationSupport(fhirContext),
CommonCodeSystemsTerminologyService(fhirContext),
UnknownCodeSystemWarningValidationSupport(fhirContext).apply {
setNonExistentCodeSystemSeverity(IValidationSupport.IssueSeverity.WARNING)
},
)
val instanceValidator = FhirInstanceValidator(validationSupportChain)
instanceValidator.isAssumeValidRestReferences = true
instanceValidator.invalidateCaches()
return fhirContext.newValidator().apply { registerValidatorModule(instanceValidator) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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

suspend fun FhirValidator.checkResourceValid(
vararg resource: Resource,
isDebug: Boolean = BuildConfig.BUILD_TYPE.contains("debug", ignoreCase = true),
LZRS marked this conversation as resolved.
Show resolved Hide resolved
): List<ValidationResult> {
if (!isDebug) return emptyList()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean validation will only be triggered for debug apks?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it would only be triggered for debug because the performance implications it might have for non-debug apps


return withContext(coroutineContext) {
resource.map { [email protected](it) }
}
}

val ValidationResult.errorMessages
get() = buildString {
for (validationMsg in
messages.filter { it.severity.ordinal >= ResultSeverityEnum.WARNING.ordinal }) {
appendLine(
"${validationMsg.message} - ${validationMsg.locationString} -- (${validationMsg.severity})",
LZRS marked this conversation as resolved.
Show resolved Hide resolved
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@ class FhirCarePlanGeneratorTest : RobolectricTest() {
fhirCarePlanGenerator =
FhirCarePlanGenerator(
fhirEngine = fhirEngine,
transformSupportServices = transformSupportServices,
fhirPathEngine = fhirPathEngine,
transformSupportServices = transformSupportServices,
defaultRepository = defaultRepository,
fhirResourceUtil = fhirResourceUtil,
workflowCarePlanGenerator = workflowCarePlanGenerator,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* 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 dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.spyk
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
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.smartregister.fhircore.engine.robolectric.RobolectricTest

@HiltAndroidTest
class FhirValidatorExtensionTest : RobolectricTest() {

@get:Rule var hiltRule = HiltAndroidRule(this)

@Inject lateinit var validator: FhirValidator

@Before
fun setUp() {
hiltRule.inject()
}

@Test
fun testCheckResourceValidRunsNoValidationWhenBuildTypeIsNotDebug() = runTest {
val basicResource = CarePlan()
val fhirValidatorSpy = spyk(validator)
val results = fhirValidatorSpy.checkResourceValid(basicResource, isDebug = false)
Assert.assertTrue(results.isEmpty())
verify(exactly = 0) { fhirValidatorSpy.validateWithResult(any<IBaseResource>()) }
verify(exactly = 0) { fhirValidatorSpy.validateWithResult(any<String>()) }
}

@Test
fun testCheckResourceValidValidatesResourceStructureWhenCarePlanResourceInvalid() = runTest {
val basicCarePlan = CarePlan()
val results = validator.checkResourceValid(basicCarePlan)
Assert.assertFalse(results.isEmpty())
Assert.assertTrue(
results.any {
it.errorMessages.contains(
"CarePlan.status: minimum required = 1, but only found 0",
ignoreCase = true,
)
},
)
Assert.assertTrue(
results.any {
it.errorMessages.contains(
"CarePlan.intent: minimum required = 1, but only found 0",
ignoreCase = true,
)
},
)
}

@Test
fun testCheckResourceValidValidatesReferenceType() = runTest {
val carePlan =
CarePlan().apply {
status = CarePlan.CarePlanStatus.ACTIVE
intent = CarePlan.CarePlanIntent.PLAN
subject = Reference("Task/unknown")
}
val results = validator.checkResourceValid(carePlan)
Assert.assertFalse(results.isEmpty())
Assert.assertEquals(1, results.size)
Assert.assertTrue(
results
.first()
.errorMessages
.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",
ignoreCase = true,
),
)
}

@Test
fun testCheckResourceValidValidatesReferenceWithNoType() = runTest {
val carePlan =
CarePlan().apply {
status = CarePlan.CarePlanStatus.ACTIVE
intent = CarePlan.CarePlanIntent.PLAN
subject = Reference("unknown")
}
val results = validator.checkResourceValid(carePlan)
Assert.assertFalse(results.isEmpty())
Assert.assertEquals(1, results.size)
Assert.assertTrue(
results
.first()
.errorMessages
.contains(
"The syntax of the reference 'unknown' looks incorrect, and it should be checked - CarePlan.subject",
ignoreCase = true,
),
)
}

@Test
fun testCheckResourceValidValidatesResourceCorrectly() = runTest {
val patient = Patient()
val carePlan =
CarePlan().apply {
status = CarePlan.CarePlanStatus.ACTIVE
intent = CarePlan.CarePlanIntent.PLAN
subject = Reference(patient)
}
val results = validator.checkResourceValid(carePlan)
Assert.assertFalse(results.isEmpty())
Assert.assertEquals(1, results.size)
Assert.assertTrue(results.first().errorMessages.isBlank())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import java.io.Serializable
import javax.inject.Inject
import kotlinx.coroutines.launch
import org.hl7.fhir.r4.model.IdType
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.Resource
Expand All @@ -52,9 +53,11 @@
import org.smartregister.fhircore.engine.util.extension.encodeResourceToString
import org.smartregister.fhircore.engine.util.extension.parcelable
import org.smartregister.fhircore.engine.util.extension.parcelableArrayList
import org.smartregister.fhircore.engine.util.extension.referenceValue
import org.smartregister.fhircore.engine.util.extension.showToast
import org.smartregister.fhircore.engine.util.location.LocationUtils
import org.smartregister.fhircore.engine.util.location.PermissionUtils
import org.smartregister.fhircore.quest.BuildConfig
import org.smartregister.fhircore.quest.R
import org.smartregister.fhircore.quest.databinding.QuestionnaireActivityBinding
import org.smartregister.fhircore.quest.util.ResourceUtils
Expand Down Expand Up @@ -336,26 +339,75 @@
questionnaireConfig = questionnaireConfig,
actionParameters = actionParameters,
context = this@QuestionnaireActivity,
) { idTypes, questionnaireResponse ->
) { idTypes, questionnaireResponse, extractedValidationErrors ->
// Dismiss progress indicator dialog, submit result then finish activity
// TODO Ensure this dialog is dismissed even when an exception is encountered
setProgressState(QuestionnaireProgressState.ExtractionInProgress(false))
setResult(
Activity.RESULT_OK,
Intent().apply {
putExtra(QUESTIONNAIRE_RESPONSE, questionnaireResponse as Serializable)
putExtra(QUESTIONNAIRE_SUBMISSION_EXTRACTED_RESOURCE_IDS, idTypes as Serializable)
putExtra(QUESTIONNAIRE_CONFIG, questionnaireConfig as Parcelable)
},
)
finish()

if (BuildConfig.BUILD_TYPE.contains("debug", ignoreCase = true)) {
val message =

Check warning on line 348 in android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt

View check run for this annotation

Codecov / codecov/patch

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt#L348

Added line #L348 was not covered by tests
if (extractedValidationErrors.isEmpty()) {
"Questionnaire submitted and saved successfully with no validation errors"

Check warning on line 350 in android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt

View check run for this annotation

Codecov / codecov/patch

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt#L350

Added line #L350 was not covered by tests
} else {
"""
Questionnaire `${questionnaire?.referenceValue()}` was submitted but had the following validation errors
LZRS marked this conversation as resolved.
Show resolved Hide resolved

${
buildString {
extractedValidationErrors.forEach {
appendLine("${it.key}: ")
append(it.value)
appendLine()

Check warning on line 360 in android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt

View check run for this annotation

Codecov / codecov/patch

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt#L356-L360

Added lines #L356 - L360 were not covered by tests
}
}
}
"""
.trimIndent()

Check warning on line 365 in android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt

View check run for this annotation

Codecov / codecov/patch

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt#L365

Added line #L365 was not covered by tests
}

AlertDialogue.showInfoAlert(
ellykits marked this conversation as resolved.
Show resolved Hide resolved
this@QuestionnaireActivity,
message,
"Questionnaire submitted",

Check warning on line 371 in android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt

View check run for this annotation

Codecov / codecov/patch

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt#L368-L371

Added lines #L368 - L371 were not covered by tests
{
it.dismiss()
finishOnQuestionnaireSubmission(
questionnaireResponse,
idTypes,

Check warning on line 376 in android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt

View check run for this annotation

Codecov / codecov/patch

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt#L373-L376

Added lines #L373 - L376 were not covered by tests
questionnaireConfig,
)
},

Check warning on line 379 in android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt

View check run for this annotation

Codecov / codecov/patch

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt#L379

Added line #L379 was not covered by tests
)
} else {
finishOnQuestionnaireSubmission(
questionnaireResponse,
idTypes,

Check warning on line 384 in android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt

View check run for this annotation

Codecov / codecov/patch

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt#L382-L384

Added lines #L382 - L384 were not covered by tests
questionnaireConfig,
)
}
}
}
}
}
}
}

private fun finishOnQuestionnaireSubmission(
questionnaireResponse: QuestionnaireResponse,
idTypes: List<IdType>,
questionnaireConfig: QuestionnaireConfig,
) {
setResult(
Activity.RESULT_OK,
Intent().apply {
putExtra(QUESTIONNAIRE_RESPONSE, questionnaireResponse as Serializable)
putExtra(QUESTIONNAIRE_SUBMISSION_EXTRACTED_RESOURCE_IDS, idTypes as Serializable)
putExtra(QUESTIONNAIRE_CONFIG, questionnaireConfig as Parcelable)
},

Check warning on line 406 in android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt

View check run for this annotation

Codecov / codecov/patch

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt#L400-L406

Added lines #L400 - L406 were not covered by tests
)
finish()

Check warning on line 408 in android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt

View check run for this annotation

Codecov / codecov/patch

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt#L408

Added line #L408 was not covered by tests
}

private fun handleBackPress() {
if (questionnaireConfig.isReadOnly()) {
finish()
Expand Down
Loading
Loading