Skip to content

Commit

Permalink
16052: enable FHIR transforms to change bundle with custom FHIR funct…
Browse files Browse the repository at this point in the history
…ions (#16053)

* 16052: enable FHIR transforms to change bundle with custom FHIR functions

* 16052: enable FHIR transforms to change bundle with custom FHIR functions

* fixup! Merge branch 'platform/kalish/16052-custom-fhir-function-for-transforms' of github.com:CDCgov/prime-reportstream into platform/kalish/16052-custom-fhir-function-for-transforms

* Fix Version generation and killFunc in build.gradle

* fixup! 16052: enable FHIR transforms to change bundle with custom FHIR functions

* fixup! 16052: enable FHIR transforms to change bundle with custom FHIR functions

* fixup! Fix Version generation and killFunc in build.gradle
  • Loading branch information
mkalish authored Oct 3, 2024
1 parent eb84e75 commit 81f5d3c
Show file tree
Hide file tree
Showing 9 changed files with 247 additions and 21 deletions.
34 changes: 20 additions & 14 deletions prime-router/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -271,16 +271,17 @@ sourceSets.create("testIntegration") {
runtimeClasspath += sourceSets["main"].output
}

// Add generated version object
sourceSets["main"].java.srcDir("$buildDir/generated-src/version")

val compileTestIntegrationKotlin: KotlinCompile by tasks
compileTestIntegrationKotlin.kotlinOptions.jvmTarget = appJvmTarget

val testIntegrationImplementation: Configuration by configurations.getting {
extendsFrom(configurations["testImplementation"])
}

tasks.withType<KotlinCompile> {
mustRunAfter("generateVersionObject")
}

configurations["testIntegrationRuntimeOnly"].extendsFrom(configurations["runtimeOnly"])

tasks.register<Test>("testIntegration") {
Expand Down Expand Up @@ -353,7 +354,7 @@ tasks.withType<Test>().configureEach {
}

tasks.processResources {
dependsOn("generateVersionObject")
mustRunAfter("generateVersionObject")
// Set the proper build values in the build.properties file
filesMatching("build.properties") {
val dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd")
Expand Down Expand Up @@ -427,7 +428,7 @@ tasks.register<JavaExec>("primeCLI") {
// Use arguments passed by another task in the project.extra["cliArgs"] property.
doFirst {
if (project.extra.has("cliArgs") && project.extra["cliArgs"] is List<*>) {
args = (project.extra["cliArgs"] as List<*>).filterIsInstance(String::class.java)
args = (project.extra["cliArgs"] as List<*>).filterIsInstance<String>()
} else if (args.isNullOrEmpty()) {
args = listOf("-h")
println("primeCLI Gradle task usage: gradle primeCLI --args='<args>'")
Expand Down Expand Up @@ -521,12 +522,13 @@ tasks.register("generateVersionFile") {
}
}

tasks.register("generateVersionObject") {
val sourceDir = file("$buildDir/generated-src/version")
val sourceFile = file("$sourceDir/Version.kt")
sourceDir.mkdirs()
sourceFile.writeText(
"""
val generateVersionObject = tasks.register("generateVersionObject") {
doLast {
val sourceDir = file("$buildDir/generated-src/version/src/main/kotlin/gov/cdc/prime/router")
val sourceFile = file("$sourceDir/Version.kt")
sourceDir.mkdirs()
sourceFile.writeText(
"""
package gov.cdc.prime.router.version
/**
Expand All @@ -537,7 +539,12 @@ tasks.register("generateVersionObject") {
const val commitId = "$commitId"
}
""".trimIndent()
)
)
}
}
sourceSets.getByName("main").kotlin.srcDir("$buildDir/generated-src/version/src/main/kotlin")
tasks.named("compileKotlin").configure {
dependsOn(generateVersionObject)
}

val azureResourcesTmpDir = File(buildDir, "$azureFunctionsDir-resources/$azureAppName")
Expand Down Expand Up @@ -645,7 +652,6 @@ task<Exec>("uploadSwaggerUI") {
}

tasks.register("killFunc") {
doLast {
val processName = "func"
if (org.gradle.internal.os.OperatingSystem.current().isWindows) {
exec {
Expand All @@ -658,7 +664,6 @@ tasks.register("killFunc") {
commandLine = listOf("sh", "-c", "pkill -9 $processName || true")
}
}
}
}

tasks.register("run") {
Expand Down Expand Up @@ -772,6 +777,7 @@ tasks.named<nu.studer.gradle.jooq.JooqGenerate>("generateJooq") {
tasks.register("compile") {
group = rootProject.description ?: ""
description = "Compile the code"
dependsOn("generateVersionObject")
dependsOn("compileKotlin")
}

Expand Down
3 changes: 2 additions & 1 deletion prime-router/docs/universal-pipeline/translate.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ The two kinds of transforms work the same at a high level. The schema enumerates

- contains a FHIR path to the resource that needs to be transformed
- a condition specifying whether the resource should be transformed
- how the resource should get transformed
- how the resource should get transformed; a resource can be transformed either by setting it to a value or applying a
FHIR function

The primary difference between the FHIR and HL7 schemas is that the HL7 converter has special handling for converting
a FHIR resource into an HL7 segment or component.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@
"type": "string"
}
},
"function": {
"type": "string"
},
"valueSet": {
"anyOf": [
{
Expand Down
3 changes: 3 additions & 0 deletions prime-router/src/main/kotlin/config/validation/Validations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ object FhirToFhirTransformValidation : KonformValidation<FhirTransformSchema>()
addConstraint("Invalid FHIR path: {value}", test = ::validFhirPath)
}
}
addConstraint("Value and function cannot both be set") { element ->
!(element.value != null && element.function != null)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,19 +114,32 @@ class FhirTransformer(
eligibleFocusResources.forEach { singleFocusResource ->
elementContext.focusResource = singleFocusResource
val value = getValue(element, bundle, singleFocusResource, elementContext)
val function = element.function
if (value != null && function != null) {
throw SchemaException("Element can only set function or value")
}
val bundleProperty = element.bundleProperty
?: throw SchemaException("bundleProperty must be set for element ${element.name}")
if (value != null) {
val bundleProperty = element.bundleProperty
?: throw SchemaException("bundleProperty must be set for element ${element.name}")
updateBundle(
bundleProperty,
value,
elementContext,
bundle,
singleFocusResource
)
} else if (function != null) {
updateBundle(
bundleProperty,
function,
elementContext,
bundle,
singleFocusResource
)
} else {
logger.warn(
"Element ${element.name} is updating a bundle property, but did not specify a value"
"Element ${element.name} is updating a bundle property," +
" but did not specify a value or function"
)
}
debugMsg += "condition: true, resourceType: ${singleFocusResource.fhirType()}, " +
Expand Down Expand Up @@ -298,7 +311,32 @@ class FhirTransformer(
focusResource,
null
)
setBundleProperty(penultimateElements, lastElement, value)
setBundleProperty(penultimateElements, lastElement, value, context)
}

/**
* Updates a bundle by setting a value at a specified spot
*
* @param bundleProperty the property to update
* @param function the function to apply to the bundle property
* @param context the context to evaluate the bundle under
* @param focusResource the focus resource for any FHIR path evaluations
*/
internal fun updateBundle(
bundleProperty: String,
function: String,
context: CustomContext,
bundle: Bundle,
focusResource: Base,
) {
val (lastElement, penultimateElements) = createMissingElementsInBundleProperty(
bundleProperty,
context,
bundle,
focusResource,
null
)
applyFunction(penultimateElements, lastElement, function, context, bundle)
}

/**
Expand Down Expand Up @@ -350,7 +388,29 @@ class FhirTransformer(
focusResource,
appendToElements
)
setBundleProperty(bundlePenultimateElements, lastBundlePropertyElement, value)
setBundleProperty(bundlePenultimateElements, lastBundlePropertyElement, value, context)
}

/**
* Updates a list of [Base] by applying the passed FHIR [function]
*
* @param elementsToUpdate the list of [Base] to update
* @param propertyName the property to set on each element
* @param function the function to apply
*/
private fun applyFunction(
elementsToUpdate: List<Base>,
propertyName: String,
function: String,
context: CustomContext,
bundle: Bundle,
) {
elementsToUpdate.forEach { penultimateElement ->
val propertyInfo = extractChildProperty(propertyName, context, penultimateElement)
FhirPathUtils.evaluate(
context, penultimateElement, bundle, "%resource.${propertyInfo.propertyString}.$function"
)
}
}

/**
Expand All @@ -364,8 +424,13 @@ class FhirTransformer(
elementsToUpdate: List<Base>,
propertyName: String,
value: Base,
context: CustomContext,
) {
elementsToUpdate.forEach { penultimateElement ->
val propertyInfo = extractChildProperty(propertyName, context, penultimateElement)
if (propertyInfo.index != null) {
throw SchemaException("Schema is attempting to set a value for a particular index which is not allowed")
}
val property = penultimateElement.getNamedProperty(propertyName)
val newValue = FhirBundleUtils.convertFhirType(value, value.fhirType(), property.typeCode, logger)
penultimateElement.setProperty(propertyName, newValue.copy())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class FhirTransformSchemaElement(
debug: Boolean = false,
var bundleProperty: String? = null,
val appendToProperty: String? = null,
var function: String? = null,

) : ConfigSchemaElement<Bundle, Bundle, FhirTransformSchemaElement, FhirTransformSchema>(
name = name,
Expand Down Expand Up @@ -105,6 +106,7 @@ class FhirTransformSchemaElement(
bundleProperty: String? = null,
action: FhirTransformSchemaElementAction,
appendToProperty: String? = null,
function: String? = null,
) : this(
name,
condition,
Expand All @@ -118,7 +120,8 @@ class FhirTransformSchemaElement(
valueSet,
debug,
bundleProperty,
appendToProperty
appendToProperty,
function
) {
this.action = action
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.BaseDateTimeType
import org.hl7.fhir.r4.model.BooleanType
import org.hl7.fhir.r4.model.DateTimeType
import org.hl7.fhir.r4.model.HumanName
import org.hl7.fhir.r4.model.IntegerType
import org.hl7.fhir.r4.model.StringType
import java.time.DateTimeException
Expand Down Expand Up @@ -37,6 +38,7 @@ object CustomFHIRFunctions : FhirPathFunctions {
HasPhoneNumberExtension,
ChangeTimezone,
ConvertDateToAge,
DeidentifyHumanName,
;

companion object {
Expand Down Expand Up @@ -122,6 +124,14 @@ object CustomFHIRFunctions : FhirPathFunctions {
)
}

CustomFHIRFunctionNames.DeidentifyHumanName -> {
FunctionDetails(
"removes PII from a name",
0,
1
)
}

else -> additionalFunctions?.resolveFunction(functionName)
}
}
Expand Down Expand Up @@ -185,6 +195,10 @@ object CustomFHIRFunctions : FhirPathFunctions {
convertDateToAge(focus, parameters)
}

CustomFHIRFunctionNames.DeidentifyHumanName -> {
deidentifyHumanName(focus, parameters)
}

else -> additionalFunctions?.executeFunction(focus, functionName, parameters)
?: throw IllegalStateException("Tried to execute invalid FHIR Path function $functionName")
}
Expand Down Expand Up @@ -353,6 +367,22 @@ object CustomFHIRFunctions : FhirPathFunctions {
return if (type != null) mutableListOf(StringType(type)) else mutableListOf()
}

fun deidentifyHumanName(focus: MutableList<Base>, parameters: MutableList<MutableList<Base>>?): MutableList<Base> {
val deidentifiedValue = parameters?.firstOrNull()?.filterIsInstance<StringType>()?.firstOrNull()?.value ?: ""
focus.filterIsInstance<HumanName>().forEach { name ->
if (deidentifiedValue.isNotEmpty()) {
val updatedGiven = name.given.map { StringType(deidentifiedValue) }
name.setGiven(updatedGiven.toMutableList())
} else {
name.setGiven(emptyList())
}

name.setFamily(deidentifiedValue)
}

return focus
}

/**
* Get the ID type for the value in [focus].
* @return a list with one value denoting the ID type, or an empty list
Expand Down
Loading

0 comments on commit 81f5d3c

Please sign in to comment.