Skip to content

Commit 8a36cfd

Browse files
committed
Validate extracted fhir resources while in debug
1 parent 7241a7a commit 8a36cfd

File tree

10 files changed

+332
-16
lines changed

10 files changed

+332
-16
lines changed

android/engine/build.gradle.kts

+4
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ dependencies {
156156
exclude(group = "ca.uhn.hapi.fhir")
157157
}
158158

159+
implementation(libs.hapi.fhir.validation) { exclude(module = "commons-logging") }
160+
implementation(libs.hapi.fhir.validation.resources.r4)
161+
implementation(libs.hapi.fhir.validation.resources.r5)
162+
159163
// Shared dependencies
160164
api(libs.bundles.datastore.kt)
161165
api(libs.glide)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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(): FhirValidator {
41+
val fhirContext = FhirContext.forR4()
42+
43+
val validationSupportChain =
44+
ValidationSupportChain(
45+
DefaultProfileValidationSupport(fhirContext),
46+
InMemoryTerminologyServerValidationSupport(fhirContext),
47+
CommonCodeSystemsTerminologyService(fhirContext),
48+
UnknownCodeSystemWarningValidationSupport(fhirContext).apply {
49+
setNonExistentCodeSystemSeverity(IValidationSupport.IssueSeverity.WARNING)
50+
},
51+
)
52+
val instanceValidator = FhirInstanceValidator(validationSupportChain)
53+
instanceValidator.isAssumeValidRestReferences = true
54+
// instanceValidator.validatorResourceFetcher
55+
// instanceValidator.setCustomExtensionDomains()
56+
instanceValidator.invalidateCaches()
57+
return fhirContext.newValidator().apply { registerValidatorModule(instanceValidator) }
58+
}
59+
}

android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt

+24-2
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ package org.smartregister.fhircore.engine.task
1919
import androidx.annotation.VisibleForTesting
2020
import ca.uhn.fhir.context.FhirContext
2121
import ca.uhn.fhir.util.TerserUtil
22+
import ca.uhn.fhir.validation.FhirValidator
2223
import com.google.android.fhir.FhirEngine
2324
import com.google.android.fhir.get
2425
import com.google.android.fhir.logicalId
2526
import com.google.android.fhir.search.search
2627
import java.util.Date
2728
import javax.inject.Inject
29+
import javax.inject.Provider
2830
import javax.inject.Singleton
2931
import org.hl7.fhir.r4.model.ActivityDefinition
3032
import org.hl7.fhir.r4.model.Base
@@ -56,7 +58,9 @@ import org.smartregister.fhircore.engine.configuration.event.EventType
5658
import org.smartregister.fhircore.engine.data.local.DefaultRepository
5759
import org.smartregister.fhircore.engine.util.extension.addResourceParameter
5860
import org.smartregister.fhircore.engine.util.extension.asReference
61+
import org.smartregister.fhircore.engine.util.extension.checkResourceValid
5962
import org.smartregister.fhircore.engine.util.extension.encodeResourceToString
63+
import org.smartregister.fhircore.engine.util.extension.errorMessages
6064
import org.smartregister.fhircore.engine.util.extension.extractFhirpathDuration
6165
import org.smartregister.fhircore.engine.util.extension.extractFhirpathPeriod
6266
import org.smartregister.fhircore.engine.util.extension.extractId
@@ -75,6 +79,7 @@ constructor(
7579
val transformSupportServices: TransformSupportServices,
7680
val defaultRepository: DefaultRepository,
7781
val fhirResourceUtil: FhirResourceUtil,
82+
val fhirValidatorProvider: Provider<FhirValidator>,
7883
val workflowCarePlanGenerator: WorkflowCarePlanGenerator,
7984
) {
8085
private val structureMapUtilities by lazy {
@@ -193,7 +198,22 @@ constructor(
193198

194199
val carePlanTasks = output.contained.filterIsInstance<Task>()
195200

196-
if (carePlanModified) saveCarePlan(output)
201+
if (carePlanModified) {
202+
fhirValidatorProvider
203+
.get()
204+
.checkResourceValid(output)
205+
.filterNot { it.errorMessages.isBlank() }
206+
.takeIf { it.isNotEmpty() }
207+
?.let {
208+
val errors = buildString {
209+
it.forEach { validationResult -> appendLine(validationResult.errorMessages) }
210+
}
211+
212+
throw IllegalStateException(errors)
213+
}
214+
215+
saveCarePlan(output)
216+
}
197217

198218
if (carePlanTasks.isNotEmpty()) {
199219
fhirResourceUtil.updateUpcomingTasksToDue(
@@ -215,7 +235,9 @@ constructor(
215235
carePlan.contained.clear()
216236

217237
// Save CarePlan only if it has activity, otherwise just save contained/dependent resources
218-
if (output.hasActivity()) defaultRepository.create(true, carePlan)
238+
if (output.hasActivity()) {
239+
defaultRepository.create(true, carePlan)
240+
}
219241

220242
dependents.forEach { defaultRepository.create(true, it) }
221243

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
27+
suspend fun FhirValidator.checkResourceValid(
28+
vararg resource: Resource,
29+
isDebug: Boolean = BuildConfig.BUILD_TYPE.contains("debug", ignoreCase = true),
30+
): List<ValidationResult> {
31+
if (!isDebug) return emptyList()
32+
33+
return withContext(coroutineContext) {
34+
resource.map { this@checkResourceValid.validateWithResult(it) }
35+
}
36+
}
37+
38+
val ValidationResult.errorMessages
39+
get() = buildString {
40+
for (validationMsg in
41+
messages.filter { it.severity.ordinal >= ResultSeverityEnum.WARNING.ordinal }) {
42+
appendLine("${validationMsg.message} - ${validationMsg.locationString}")
43+
}
44+
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import ca.uhn.fhir.context.FhirContext
2121
import ca.uhn.fhir.context.FhirVersionEnum
2222
import ca.uhn.fhir.parser.IParser
2323
import ca.uhn.fhir.rest.gclient.ReferenceClientParam
24+
import ca.uhn.fhir.validation.FhirValidator
2425
import com.google.android.fhir.FhirEngine
2526
import com.google.android.fhir.SearchResult
2627
import com.google.android.fhir.get
@@ -45,6 +46,7 @@ import java.util.Calendar
4546
import java.util.Date
4647
import java.util.UUID
4748
import javax.inject.Inject
49+
import javax.inject.Provider
4850
import kotlin.time.Duration.Companion.seconds
4951
import kotlinx.coroutines.Dispatchers
5052
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -129,6 +131,8 @@ class FhirCarePlanGeneratorTest : RobolectricTest() {
129131

130132
@Inject lateinit var workflowCarePlanGenerator: WorkflowCarePlanGenerator
131133

134+
@Inject lateinit var fhirValidatorProvider: Provider<FhirValidator>
135+
132136
@Inject lateinit var fhirPathEngine: FHIRPathEngine
133137

134138
@Inject lateinit var fhirEngine: FhirEngine
@@ -169,6 +173,7 @@ class FhirCarePlanGeneratorTest : RobolectricTest() {
169173
fhirPathEngine = fhirPathEngine,
170174
defaultRepository = defaultRepository,
171175
fhirResourceUtil = fhirResourceUtil,
176+
fhirValidatorProvider = fhirValidatorProvider,
172177
workflowCarePlanGenerator = workflowCarePlanGenerator,
173178
)
174179

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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 results = fhirValidatorSpy.checkResourceValid(basicResource, isDebug = false)
53+
Assert.assertTrue(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 results = validator.checkResourceValid(basicCarePlan)
62+
Assert.assertFalse(results.isEmpty())
63+
Assert.assertTrue(
64+
results.any {
65+
it.errorMessages.contains(
66+
"CarePlan.status: minimum required = 1, but only found 0",
67+
ignoreCase = true,
68+
)
69+
},
70+
)
71+
Assert.assertTrue(
72+
results.any {
73+
it.errorMessages.contains(
74+
"CarePlan.intent: minimum required = 1, but only found 0",
75+
ignoreCase = true,
76+
)
77+
},
78+
)
79+
}
80+
81+
@Test
82+
fun testCheckResourceValidValidatesReferenceType() = runTest {
83+
val carePlan =
84+
CarePlan().apply {
85+
status = CarePlan.CarePlanStatus.ACTIVE
86+
intent = CarePlan.CarePlanIntent.PLAN
87+
subject = Reference("Task/unknown")
88+
}
89+
val results = validator.checkResourceValid(carePlan)
90+
Assert.assertFalse(results.isEmpty())
91+
Assert.assertEquals(1, results.size)
92+
Assert.assertTrue(
93+
results
94+
.first()
95+
.errorMessages
96+
.contains(
97+
"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",
98+
ignoreCase = true,
99+
),
100+
)
101+
}
102+
103+
@Test
104+
fun testCheckResourceValidValidatesReferenceWithNoType() = runTest {
105+
val carePlan =
106+
CarePlan().apply {
107+
status = CarePlan.CarePlanStatus.ACTIVE
108+
intent = CarePlan.CarePlanIntent.PLAN
109+
subject = Reference("unknown")
110+
}
111+
val results = validator.checkResourceValid(carePlan)
112+
Assert.assertFalse(results.isEmpty())
113+
Assert.assertEquals(1, results.size)
114+
Assert.assertTrue(
115+
results
116+
.first()
117+
.errorMessages
118+
.contains(
119+
"The syntax of the reference 'unknown' looks incorrect, and it should be checked - CarePlan.subject",
120+
ignoreCase = true,
121+
),
122+
)
123+
}
124+
125+
@Test
126+
fun testCheckResourceValidValidatesResourceCorrectly() = runTest {
127+
val patient = Patient()
128+
val carePlan =
129+
CarePlan().apply {
130+
status = CarePlan.CarePlanStatus.ACTIVE
131+
intent = CarePlan.CarePlanIntent.PLAN
132+
subject = Reference(patient)
133+
}
134+
val results = validator.checkResourceValid(carePlan)
135+
Assert.assertFalse(results.isEmpty())
136+
Assert.assertEquals(1, results.size)
137+
Assert.assertTrue(results.first().errorMessages.isBlank())
138+
}
139+
}

android/gradle/libs.versions.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ dagger-hilt = "2.45"
111111
jetbrains-dokka = "1.8.20"
112112
navigation-safeargs = "2.4.2"
113113
diffplug-spotless = "6.19.0"
114-
114+
hapi-fhir = "6.0.1"
115115

116116
[libraries]
117117
accompanist-flowlayout = { group = "com.google.accompanist", name = "accompanist-flowlayout", version.ref = "accompanist-flowlayout" }
@@ -222,6 +222,9 @@ work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version
222222
work-testing = { group = "androidx.work", name = "work-testing", version.ref = "work-testing" }
223223
workflow = { group = "org.smartregister", name = "workflow", version.ref = "workflow" }
224224
xercesImpl = { group = "xerces", name = "xercesImpl", version.ref = "xercesImpl" }
225+
hapi-fhir-validation = {group = "ca.uhn.hapi.fhir", name="hapi-fhir-validation", version.ref = "hapi-fhir"}
226+
hapi-fhir-validation-resources-r4 = {group = "ca.uhn.hapi.fhir", name="hapi-fhir-validation-resources-r4", version.ref = "hapi-fhir"}
227+
hapi-fhir-validation-resources-r5 = {group = "ca.uhn.hapi.fhir", name="hapi-fhir-validation-resources-r5", version.ref = "hapi-fhir"}
225228

226229
[plugins]
227230
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "org-jetbrains-kotlin-android" }

0 commit comments

Comments
 (0)