Skip to content

Commit e680e7c

Browse files
LZRSpldSebaMutukudubdabasodubaellykits
authored
Validate extracted FHIR resources while in debug (#2874)
* Validate extracted fhir resources while in debug * Format validation error messages * Show validation result in dialog after submission * Use macos-13 for ci job runner * Fix failing QuestionnaireViewModelTest tests * add missing paren * Refactor to just send FhirValidator error messages to logcat --------- Co-authored-by: Peter Lubell-Doughtie <[email protected]> Co-authored-by: Sebastian <[email protected]> Co-authored-by: Benjamin Mwalimu <[email protected]> Co-authored-by: Elly Kitoto <[email protected]>
1 parent 9ebde05 commit e680e7c

File tree

8 files changed

+411
-28
lines changed

8 files changed

+411
-28
lines changed

android/engine/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ dependencies {
266266
implementation(Dependencies.HapiFhir.structuresR4) { exclude(module = "junit") }
267267
implementation(Dependencies.HapiFhir.guavaCaching)
268268
implementation(Dependencies.HapiFhir.validationR4)
269+
implementation(Dependencies.HapiFhir.validationR5)
269270
implementation(Dependencies.HapiFhir.validation) {
270271
exclude(module = "commons-logging")
271272
exclude(module = "httpclient")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2021-2024 Ona Systems, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.smartregister.fhircore.engine.di
18+
19+
import ca.uhn.fhir.context.FhirContext
20+
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport
21+
import ca.uhn.fhir.context.support.IValidationSupport
22+
import ca.uhn.fhir.validation.FhirValidator
23+
import dagger.Module
24+
import dagger.Provides
25+
import dagger.hilt.InstallIn
26+
import dagger.hilt.components.SingletonComponent
27+
import javax.inject.Singleton
28+
import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService
29+
import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport
30+
import org.hl7.fhir.common.hapi.validation.support.UnknownCodeSystemWarningValidationSupport
31+
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain
32+
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator
33+
34+
@Module
35+
@InstallIn(SingletonComponent::class)
36+
class FhirValidatorModule {
37+
38+
@Provides
39+
@Singleton
40+
fun provideFhirValidator(fhirContext: FhirContext): FhirValidator {
41+
val validationSupportChain =
42+
ValidationSupportChain(
43+
DefaultProfileValidationSupport(fhirContext),
44+
InMemoryTerminologyServerValidationSupport(fhirContext),
45+
CommonCodeSystemsTerminologyService(fhirContext),
46+
UnknownCodeSystemWarningValidationSupport(fhirContext).apply {
47+
setNonExistentCodeSystemSeverity(IValidationSupport.IssueSeverity.WARNING)
48+
},
49+
)
50+
val instanceValidator = FhirInstanceValidator(validationSupportChain)
51+
instanceValidator.isAssumeValidRestReferences = true
52+
instanceValidator.invalidateCaches()
53+
return fhirContext.newValidator().apply { registerValidatorModule(instanceValidator) }
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2021-2024 Ona Systems, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.smartregister.fhircore.engine.util.extension
18+
19+
import ca.uhn.fhir.validation.FhirValidator
20+
import ca.uhn.fhir.validation.ResultSeverityEnum
21+
import ca.uhn.fhir.validation.ValidationResult
22+
import kotlin.coroutines.coroutineContext
23+
import kotlinx.coroutines.withContext
24+
import org.hl7.fhir.r4.model.Resource
25+
import org.smartregister.fhircore.engine.BuildConfig
26+
import timber.log.Timber
27+
28+
data class ResourceValidationResult(
29+
val resource: Resource,
30+
val validationResult: ValidationResult,
31+
) {
32+
val errorMessages
33+
get() = buildString {
34+
val messages =
35+
validationResult.messages.filter {
36+
it.severity.ordinal >= ResultSeverityEnum.WARNING.ordinal
37+
}
38+
39+
for (validationMsg in messages) {
40+
appendLine(
41+
"${validationMsg.message} - ${validationMsg.locationString} -- (${validationMsg.severity})",
42+
)
43+
}
44+
}
45+
}
46+
47+
data class FhirValidatorResultsWrapper(val results: List<ResourceValidationResult> = emptyList()) {
48+
val errorMessages = results.map { it.errorMessages }
49+
}
50+
51+
suspend fun FhirValidator.checkResourceValid(
52+
vararg resource: Resource,
53+
isDebug: Boolean = BuildConfig.DEBUG,
54+
): FhirValidatorResultsWrapper {
55+
if (!isDebug) return FhirValidatorResultsWrapper()
56+
57+
return withContext(coroutineContext) {
58+
FhirValidatorResultsWrapper(
59+
results =
60+
resource.map {
61+
val result = this@checkResourceValid.validateWithResult(it)
62+
ResourceValidationResult(it, result)
63+
},
64+
)
65+
}
66+
}
67+
68+
fun FhirValidatorResultsWrapper.logErrorMessages() {
69+
results.forEach {
70+
if (it.errorMessages.isNotBlank()) {
71+
Timber.tag("$TAG (${it.resource.referenceValue()})").e(it.errorMessages)
72+
}
73+
}
74+
}
75+
76+
private const val TAG = "FhirValidator"

android/engine/src/test/java/org/smartregister/fhircore/engine/task/FhirCarePlanGeneratorTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,8 @@ class FhirCarePlanGeneratorTest : RobolectricTest() {
201201
fhirCarePlanGenerator =
202202
FhirCarePlanGenerator(
203203
fhirEngine = fhirEngine,
204-
transformSupportServices = transformSupportServices,
205204
fhirPathEngine = fhirPathEngine,
205+
transformSupportServices = transformSupportServices,
206206
defaultRepository = defaultRepository,
207207
fhirResourceUtil = fhirResourceUtil,
208208
workflowCarePlanGenerator = workflowCarePlanGenerator,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright 2021-2024 Ona Systems, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.smartregister.fhircore.engine.util.extension
18+
19+
import ca.uhn.fhir.validation.FhirValidator
20+
import dagger.hilt.android.testing.HiltAndroidRule
21+
import dagger.hilt.android.testing.HiltAndroidTest
22+
import io.mockk.spyk
23+
import io.mockk.verify
24+
import javax.inject.Inject
25+
import kotlinx.coroutines.test.runTest
26+
import org.hl7.fhir.instance.model.api.IBaseResource
27+
import org.hl7.fhir.r4.model.CarePlan
28+
import org.hl7.fhir.r4.model.Patient
29+
import org.hl7.fhir.r4.model.Reference
30+
import org.junit.Assert
31+
import org.junit.Before
32+
import org.junit.Rule
33+
import org.junit.Test
34+
import org.smartregister.fhircore.engine.robolectric.RobolectricTest
35+
36+
@HiltAndroidTest
37+
class FhirValidatorExtensionTest : RobolectricTest() {
38+
39+
@get:Rule var hiltRule = HiltAndroidRule(this)
40+
41+
@Inject lateinit var validator: FhirValidator
42+
43+
@Before
44+
fun setUp() {
45+
hiltRule.inject()
46+
}
47+
48+
@Test
49+
fun testCheckResourceValidRunsNoValidationWhenBuildTypeIsNotDebug() = runTest {
50+
val basicResource = CarePlan()
51+
val fhirValidatorSpy = spyk(validator)
52+
val resultsWrapper = fhirValidatorSpy.checkResourceValid(basicResource, isDebug = false)
53+
Assert.assertTrue(resultsWrapper.results.isEmpty())
54+
verify(exactly = 0) { fhirValidatorSpy.validateWithResult(any<IBaseResource>()) }
55+
verify(exactly = 0) { fhirValidatorSpy.validateWithResult(any<String>()) }
56+
}
57+
58+
@Test
59+
fun testCheckResourceValidValidatesResourceStructureWhenCarePlanResourceInvalid() = runTest {
60+
val basicCarePlan = CarePlan()
61+
val resultsWrapper = validator.checkResourceValid(basicCarePlan)
62+
Assert.assertTrue(
63+
resultsWrapper.errorMessages.any {
64+
it.contains(
65+
"CarePlan.status: minimum required = 1, but only found 0",
66+
ignoreCase = true,
67+
)
68+
},
69+
)
70+
Assert.assertTrue(
71+
resultsWrapper.errorMessages.any {
72+
it.contains(
73+
"CarePlan.intent: minimum required = 1, but only found 0",
74+
ignoreCase = true,
75+
)
76+
},
77+
)
78+
}
79+
80+
@Test
81+
fun testCheckResourceValidValidatesReferenceType() = runTest {
82+
val carePlan =
83+
CarePlan().apply {
84+
status = CarePlan.CarePlanStatus.ACTIVE
85+
intent = CarePlan.CarePlanIntent.PLAN
86+
subject = Reference("Task/unknown")
87+
}
88+
val resultsWrapper = validator.checkResourceValid(carePlan)
89+
Assert.assertEquals(1, resultsWrapper.errorMessages.size)
90+
Assert.assertTrue(
91+
resultsWrapper.errorMessages
92+
.first()
93+
.contains(
94+
"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",
95+
ignoreCase = true,
96+
),
97+
)
98+
}
99+
100+
@Test
101+
fun testCheckResourceValidValidatesReferenceWithNoType() = runTest {
102+
val carePlan =
103+
CarePlan().apply {
104+
status = CarePlan.CarePlanStatus.ACTIVE
105+
intent = CarePlan.CarePlanIntent.PLAN
106+
subject = Reference("unknown")
107+
}
108+
val resultsWrapper = validator.checkResourceValid(carePlan)
109+
Assert.assertEquals(1, resultsWrapper.errorMessages.size)
110+
Assert.assertTrue(
111+
resultsWrapper.errorMessages
112+
.first()
113+
.contains(
114+
"The syntax of the reference 'unknown' looks incorrect, and it should be checked - CarePlan.subject",
115+
ignoreCase = true,
116+
),
117+
)
118+
}
119+
120+
@Test
121+
fun testCheckResourceValidValidatesResourceCorrectly() = runTest {
122+
val patient = Patient()
123+
val carePlan =
124+
CarePlan().apply {
125+
status = CarePlan.CarePlanStatus.ACTIVE
126+
intent = CarePlan.CarePlanIntent.PLAN
127+
subject = Reference(patient)
128+
}
129+
val resultsWrapper = validator.checkResourceValid(carePlan)
130+
Assert.assertEquals(1, resultsWrapper.errorMessages.size)
131+
Assert.assertTrue(resultsWrapper.errorMessages.first().isBlank())
132+
}
133+
}

0 commit comments

Comments
 (0)