diff --git a/efsity/.gitignore b/efsity/.gitignore index 635edfdf..79acd051 100644 --- a/efsity/.gitignore +++ b/efsity/.gitignore @@ -9,3 +9,5 @@ fhircore-tooling.iml # build files bin target +generated-structure-map.txt +generated-json-map.json diff --git a/efsity/README.md b/efsity/README.md index 32fb06e7..964e5704 100644 --- a/efsity/README.md +++ b/efsity/README.md @@ -172,6 +172,36 @@ $ fct validateFileStructure -i ~/Workspace/fhir-resources/ -s ~/path/to If the file structure matches the schema then a positive result is printed to the terminal, otherwise an error is thrown showing the issue. +### Generating a structure map +To generate a Structure Map, you need to provide the questionnaire, the configuration path, and the questionnaire response path. The output will be based on the specified inputs +```console +$ fct generateStructureMap --questionnaire /path/to/questionnaire.json --configPath /path/to/config/StructureMap.xls --questionnaireResponsePath /path/to/questionnaire-response.json +``` +This process will generate two output files: a .map file and its JSON equivalent +**Options** +``` +-q or --questionnaire Path to the Questionnaire JSON file. +-c or --configPath Path to the Structure Map configuration file (XLS format). +-qr or --questionnaireResponsePath Path to the Questionnaire Response JSON file. +``` +**Sample Configuration File** + +To help users get started with the Structure Map generation process, we provide a sample configuration file in XLS format. You can download the sample from the link below: + +[Download Sample Structure Map Configuration (XLS)](https://github.com/onaio/fhir-tooling/blob/main/sm-gen/src/main/resources/StructureMap%20XLS.xls) + +Alternatively, if you have cloned the repository, the sample file can be found in the following path: + +console +``` +/path/to/your/repository/src/main/resources/StructureMapXLS.xls +``` + +This sample XLS form includes the required structure and format to define mappings between questionnaire fields and FHIR resources. You can customize it according to your specific needs. + +For more documentation please refer to this [documentation](https://github.com/onaio/fhir-tooling/blob/main/efsity/Structure_map_gen.md) + + ### Localization Tool that supports localization by the use of the translation extension diff --git a/efsity/Structure_map_gen.md b/efsity/Structure_map_gen.md new file mode 100644 index 00000000..7204b2b9 --- /dev/null +++ b/efsity/Structure_map_gen.md @@ -0,0 +1,38 @@ +# StructureMap tooling + +The StructureMap Tooling is a utility designed to generate Structure Maps for FHIR (Fast Healthcare Interoperability Resources) transformations. Structure Maps are used to define how one FHIR resource is transformed into another. This tooling leverages the HL7 FHIRPath language to provide support for creating and managing these transformations. + +## Features +**FHIRPath Engine**: Utilizes the FHIRPath engine to evaluate and transform FHIR resources. + +**Transformation Support**: Provides services to support the transformation process, ensuring that resources are accurately converted according to the specified Structure Map. + +**Automation**: Simplifies the process of generating Structure Maps through automated tooling. + +## Files +**FhirPathEngineHostServices.kt**: Contains services for hosting and running the FHIRPath engine. + +**Main.kt**: The entry point of the application. It orchestrates the process of reading input, processing it through the FHIRPath engine, and generating the Structure Map. + +**TransformSupportServices.kt**: Provides additional support services required for the transformation process. + +**Utils.kt**: Contains the main logic for generating the Structure Maps using the FHIRPath engine and transformation support services. + +## Prerequisites +- Questionnaire JSON +- XLS form with the required information based on the questionnaire + +### Installation +1. Clone the Repository: + +```console +git clone https://github.com/your-repo/structuremap-tooling.git +cd structuremap-tooling +``` +2. Once the `structuremap-tooling` folder, click on run. +3. A prompt will appear on the CLI `Kindly enter the XLS filepath`: Enter the absolute path of the file's location. Click `Enter` +4. Another prompt will appear on the CLI `Kindly enter the questionnaire filepath`: Enter the absolute path of the file's location. Click `Enter` +5. The structureMap will be generated in the CLI and two complete files in the folder containing the `.json` and `.map` files + +## Contributing +Contributions are welcome! Please open an issue or submit a pull request for any enhancements or bug fixes. diff --git a/efsity/build.gradle.kts b/efsity/build.gradle.kts index 705d7dff..ef712c65 100644 --- a/efsity/build.gradle.kts +++ b/efsity/build.gradle.kts @@ -83,6 +83,8 @@ dependencies { implementation(deps.picocli) implementation(deps.xstream) implementation(deps.icu4j) + implementation(deps.poi) + implementation(deps.poiooxml) implementation(deps.javafaker) { exclude(group = "org.yaml") } implementation(deps.snakeyaml) implementation("ca.uhn.hapi.fhir:hapi-fhir-validation:6.8.0") @@ -90,6 +92,8 @@ dependencies { testImplementation(kotlin("test")) testImplementation("junit:junit:4.13.2") testImplementation("org.mockito:mockito-inline:3.12.4") + testImplementation("io.mockk:mockk:1.13.7") + testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") } tasks.withType { options.encoding = deps.versions.project.build.sourceEncoding.get() } diff --git a/efsity/libs.versions.toml b/efsity/libs.versions.toml index 77cde84c..e2778c90 100644 --- a/efsity/libs.versions.toml +++ b/efsity/libs.versions.toml @@ -19,6 +19,8 @@ snakeyaml-version="2.3" spotless-version ="6.20.0" xstream="1.4.20" icu4j-version = "75.1" +poi-version = "4.1.1" +poi-ooxml-version = "4.1.1" [libraries] caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine-version" } @@ -50,6 +52,8 @@ picocli = { module = "info.picocli:picocli", version.ref = "info-picocli-version snakeyaml = { module="org.yaml:snakeyaml", version.ref ="snakeyaml-version"} xstream = { module = "com.thoughtworks.xstream:xstream", version.ref = "xstream" } icu4j = { module="com.ibm.icu:icu4j", version.ref = "icu4j-version" } +poi = { module="org.apache.poi:poi", version.ref = "poi-version" } +poiooxml = { module="org.apache.poi:poi-ooxml", version.ref = "poi-ooxml-version" } [bundles] cqf-cql = ["cql-to-elm","elm","elm-jackson","model","model-jackson"] diff --git a/efsity/src/main/java/org/smartregister/Main.java b/efsity/src/main/java/org/smartregister/Main.java index 679d1dbb..6b5f3673 100644 --- a/efsity/src/main/java/org/smartregister/Main.java +++ b/efsity/src/main/java/org/smartregister/Main.java @@ -21,7 +21,8 @@ TranslateCommand.class, QuestionnaireResponseGeneratorCommand.class, ValidateFileStructureCommand.class, - PublishFhirResourcesCommand.class + PublishFhirResourcesCommand.class, + GenerateStructureMapCommand.class }) public class Main implements Runnable { public static void main(String[] args) { diff --git a/efsity/src/main/java/org/smartregister/command/GenerateStructureMapCommand.java b/efsity/src/main/java/org/smartregister/command/GenerateStructureMapCommand.java new file mode 100644 index 00000000..de8a4359 --- /dev/null +++ b/efsity/src/main/java/org/smartregister/command/GenerateStructureMapCommand.java @@ -0,0 +1,48 @@ +package org.smartregister.command; + +import java.io.IOException; +import org.smartregister.structuremaptool.GenerateStructureMapServiceKt; +import org.smartregister.util.FctUtils; +import picocli.CommandLine; + +@CommandLine.Command(name = "generateStructureMap") +public class GenerateStructureMapCommand implements Runnable { + @CommandLine.Option( + names = {"-q", "--questionnaire"}, + description = "Questionnaire", + required = true) + private String questionnairePath; + + @CommandLine.Option( + names = {"-c", "--configPath"}, + description = "StructureMap generation configuration in an excel sheet", + required = true) + private String configPath; + + @CommandLine.Option( + names = {"-qr", "--questionnaireResponsePath"}, + description = "Questionnaire response", + required = true) + private String questionnaireResponsePath; + + @Override + public void run() { + if (configPath != null) { + try { + generateStructureMap(configPath, questionnairePath, questionnaireResponsePath); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + public static void generateStructureMap( + String configPath, String questionnairePath, String questionnaireResponsePath) + throws IOException { + long start = System.currentTimeMillis(); + FctUtils.printInfo("Starting StructureMap generation"); + GenerateStructureMapServiceKt.main(configPath, questionnairePath, questionnaireResponsePath); + + FctUtils.printCompletedInDuration(start); + } +} diff --git a/efsity/src/main/kotlin/org/smartregister/external/TransformSupportServices.kt b/efsity/src/main/kotlin/org/smartregister/external/TransformSupportServices.kt index 458d76b6..a2cc81a3 100644 --- a/efsity/src/main/kotlin/org/smartregister/external/TransformSupportServices.kt +++ b/efsity/src/main/kotlin/org/smartregister/external/TransformSupportServices.kt @@ -55,6 +55,7 @@ class TransformSupportServices @Inject constructor(val simpleWorkerContext: Simp override fun createType(appInfo: Any, name: String): Base { return when (name) { "RiskAssessment_Prediction" -> RiskAssessmentPredictionComponent() + "RiskAssessment\$RiskAssessmentPredictionComponent" -> RiskAssessmentPredictionComponent() "Immunization_AppliedProtocol" -> Immunization.ImmunizationProtocolAppliedComponent() "Immunization_Reaction" -> Immunization.ImmunizationReactionComponent() "EpisodeOfCare_Diagnosis" -> EpisodeOfCare.DiagnosisComponent() diff --git a/efsity/src/main/kotlin/org/smartregister/structuremaptool/GenerateStructureMapService.kt b/efsity/src/main/kotlin/org/smartregister/structuremaptool/GenerateStructureMapService.kt new file mode 100644 index 00000000..340cf1e9 --- /dev/null +++ b/efsity/src/main/kotlin/org/smartregister/structuremaptool/GenerateStructureMapService.kt @@ -0,0 +1,254 @@ +package org.smartregister.structuremaptool + +import ca.uhn.fhir.context.FhirContext +import ca.uhn.fhir.context.FhirVersionEnum +import ca.uhn.fhir.parser.IParser +import java.io.File +import java.io.FileInputStream +import java.nio.charset.Charset +import java.util.* +import org.apache.commons.io.FileUtils +import org.apache.poi.ss.usermodel.CellType +import org.apache.poi.ss.usermodel.Row +import org.apache.poi.ss.usermodel.WorkbookFactory +import org.hl7.fhir.r4.context.SimpleWorkerContext +import org.hl7.fhir.r4.model.Bundle +import org.hl7.fhir.r4.model.Parameters +import org.hl7.fhir.r4.model.Questionnaire +import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager +import org.hl7.fhir.utilities.npm.ToolsVersion +import org.smartregister.external.TransformSupportServices +import org.smartregister.structuremaptool.extensions.addIndentation +import org.smartregister.structuremaptool.extensions.clean + +fun main( + xlsConfigFilePath: String, + questionnaireFilePath: String, + questionnaireResponsePath: String +) { + val contextR4 = FhirContext.forR4() + val fhirJsonParser = contextR4.newJsonParser() + val questionnaire: Questionnaire = + fhirJsonParser.parseResource( + Questionnaire::class.java, + FileUtils.readFileToString(File(questionnaireFilePath), Charset.defaultCharset()) + ) + val questionnaireResponse: QuestionnaireResponse = + fhirJsonParser.parseResource( + QuestionnaireResponse::class.java, + FileUtils.readFileToString(File(questionnaireResponsePath), Charset.defaultCharset()) + ) + + // reads the xls + val xlsConfigFile = FileInputStream(xlsConfigFilePath) + val xlWorkbook = WorkbookFactory.create(xlsConfigFile) + + // TODO: Check that all the Resource(s) ub the Resource column are the correct name and type eg. + // RiskFlag in the previous XLSX was not valid + // TODO: Check that all the path's and other entries in the excel sheet are valid + // TODO: Add instructions for adding embedded classes like + // `RiskAssessment$RiskAssessmentPredictionComponent` to the TransformSupportServices + + // read the settings sheet + val settingsWorkbook = xlWorkbook.getSheet("Settings") + var questionnaireId: String? = null + + for (i in 0..settingsWorkbook.lastRowNum) { + val cell = settingsWorkbook.getRow(i).getCell(0) + if (cell.stringCellValue == "questionnaire-id") { + questionnaireId = settingsWorkbook.getRow(i).getCell(1).stringCellValue + } + } + + /* + TODO: Fix Groups calling sequence so that Groups that depend on other resources to be generated need to be called first + We can also throw an exception if to figure out cyclic dependency. Good candidate for Floyd's tortoise and/or topological sorting 😁. Cool!!!! + */ + + val questionnaireResponseItemIds = questionnaireResponse.item.map { it.id } + if (questionnaireId != null && questionnaireResponseItemIds.isNotEmpty()) { + + val sb = StringBuilder() + val structureMapHeader = + """ + map "http://hl7.org/fhir/StructureMap/$questionnaireId" = '${questionnaireId.clean()}' + + uses "http://hl7.org/fhir/StructureDefinition/QuestionnaireReponse" as source + uses "http://hl7.org/fhir/StructureDefinition/Bundle" as target + """ + .trimIndent() + + val structureMapBody = + """ + group ${questionnaireId.clean()}(source src : QuestionnaireResponse, target bundle: Bundle) { + src -> bundle.id = uuid() "rule_c"; + src -> bundle.type = 'collection' "rule_b"; + src -> bundle.entry as entry then """ + .trimIndent() + + val resourceConversionInstructions = hashMapOf>() + + // Group the rules according to the resource + val fieldMappingsSheet = xlWorkbook.getSheet("Field Mappings") + fieldMappingsSheet.forEachIndexed { index, row -> + if (index == 0) return@forEachIndexed + if (row.isEmpty()) { + return@forEachIndexed + } + + val instruction = row.getInstruction() + val xlsId = instruction.responseFieldId + val comparedResponseAndXlsId = questionnaireResponseItemIds.contains(xlsId) + if (instruction.resource.isNotEmpty() && comparedResponseAndXlsId) { + resourceConversionInstructions + .computeIfAbsent(instruction.searchKey(), { key -> mutableListOf() }) + .add(instruction) + } + } + + // val resource = ?: Class.forName("org.hl7.fhir.r4.model.$resourceName").newInstance() as + // Resource + + // Perform the extraction for the row + /*generateStructureMapLine(structureMapBody, row, resource, extractionResources) + + extractionResources[resourceName + resourceIndex] = resource*/ + + sb.append(structureMapHeader) + sb.appendNewLine(2) + sb.append(structureMapBody) + + // Fix the question path + val questionsPath = getQuestionsPath(questionnaire) + + // TODO: Generate the links to the group names here + var index = 0 + var len = resourceConversionInstructions.size + var resourceName = "" + resourceConversionInstructions.forEach { entry -> + resourceName = + entry.key.replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() + } + if (index++ != 0) sb.append(",") + if (resourceName.isNotEmpty()) sb.append("Extract$resourceName(src, bundle)") + } + sb.append(""" "rule_a";""".trimMargin()) + sb.appendNewLine() + sb.append("}") + + // Add the embedded instructions + val groupNames = mutableListOf() + sb.appendNewLine(3) + + resourceConversionInstructions.forEach { + Group(it, sb, questionsPath).generateGroup(questionnaireResponse) + } + + val structureMapString = sb.toString() + try { + val simpleWorkerContext = + SimpleWorkerContext().apply { + setExpansionProfile(Parameters()) + isCanRunWithoutTerminology = true + } + val transformSupportServices = TransformSupportServices(simpleWorkerContext) + val scu = + org.hl7.fhir.r4.utils.StructureMapUtilities(simpleWorkerContext, transformSupportServices) + val structureMap = scu.parse(structureMapString, questionnaireId.clean()) + // DataFormatException | FHIRLexerException + + try { + val bundle = Bundle() + scu.transform(contextR4, questionnaireResponse, structureMap, bundle) + val jsonParser = FhirContext.forR4().newJsonParser() + + println(jsonParser.encodeResourceToString(bundle)) + } catch (e: Exception) { + e.printStackTrace() + } + } catch (ex: Exception) { + println("The generated StructureMap has a formatting error") + ex.printStackTrace() + } + + var finalStructureMap = sb.toString() + finalStructureMap = finalStructureMap.addIndentation() + println(finalStructureMap) + writeStructureMapOutput(sb.toString().addIndentation()) + } +} + +fun Row.isEmpty(): Boolean { + return getCell(0) == null && getCell(1) == null && getCell(2) == null +} + +fun Row.getCellAsString(cellnum: Int): String? { + val cell = getCell(cellnum) ?: return null + return when (cell.cellTypeEnum) { + CellType.STRING -> cell.stringCellValue + CellType.BLANK -> null + CellType.BOOLEAN -> cell.booleanCellValue.toString() + CellType.NUMERIC -> cell.numericCellValue.toString() + else -> null + } +} + +fun Row.getInstruction(): Instruction { + return Instruction().apply { + responseFieldId = getCell(0)?.stringCellValue + constantValue = getCellAsString(1) + resource = getCell(2).stringCellValue + resourceIndex = getCell(3)?.numericCellValue?.toInt() ?: 0 + fieldPath = getCell(4)?.stringCellValue ?: "" + fullFieldPath = fieldPath + field = getCell(5)?.stringCellValue + conversion = getCell(6)?.stringCellValue + fhirPathStructureMapFunctions = getCell(7)?.stringCellValue + } +} + +class Instruction { + var responseFieldId: String? = null + var constantValue: String? = null + var resource: String = "" + var resourceIndex: Int = 0 + var fieldPath: String = "" + var field: String? = null + var conversion: String? = null + var fhirPathStructureMapFunctions: String? = null + + // TODO: Clean the following properties + var fullFieldPath = "" + + fun fullPropertyPath(): String = "$resource.$fullFieldPath" + + fun searchKey() = resource + resourceIndex +} + +fun Instruction.copyFrom(instruction: Instruction) { + constantValue = instruction.constantValue + resource = instruction.resource + resourceIndex = instruction.resourceIndex + fieldPath = instruction.fieldPath + fullFieldPath = instruction.fullFieldPath + field = instruction.field + conversion = instruction.conversion + fhirPathStructureMapFunctions = instruction.fhirPathStructureMapFunctions +} + +fun writeStructureMapOutput(structureMap: String) { + File("generated-structure-map.txt").writeText(structureMap.addIndentation()) + val pcm = FilesystemPackageCacheManager(true, ToolsVersion.TOOLS_VERSION) + val contextR5 = SimpleWorkerContext.fromPackage(pcm.loadPackage("hl7.fhir.r4.core", "4.0.1")) + contextR5.setExpansionProfile(Parameters()) + contextR5.isCanRunWithoutTerminology = true + val transformSupportServices = TransformSupportServices(contextR5) + val scu = org.hl7.fhir.r4.utils.StructureMapUtilities(contextR5, transformSupportServices) + val map = scu.parse(structureMap, "LocationRegistration") + val iParser: IParser = + FhirContext.forCached(FhirVersionEnum.R4).newJsonParser().setPrettyPrint(true) + val mapString = iParser.encodeResourceToString(map) + File("generated-json-map.json").writeText(mapString) +} diff --git a/efsity/src/main/kotlin/org/smartregister/structuremaptool/Utils.kt b/efsity/src/main/kotlin/org/smartregister/structuremaptool/Utils.kt new file mode 100644 index 00000000..18cdf6b0 --- /dev/null +++ b/efsity/src/main/kotlin/org/smartregister/structuremaptool/Utils.kt @@ -0,0 +1,462 @@ +package org.smartregister.structuremaptool + +import ca.uhn.fhir.context.FhirContext +import ca.uhn.fhir.context.FhirVersionEnum +import java.lang.reflect.Field +import java.lang.reflect.ParameterizedType +import org.apache.poi.ss.usermodel.Row +import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext +import org.hl7.fhir.r4.model.Enumeration +import org.hl7.fhir.r4.model.Questionnaire +import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent +import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.Resource +import org.hl7.fhir.r4.utils.FHIRPathEngine +import org.smartregister.structuremaptool.extensions.* + +// Get the hl7 resources +val contextR4 = FhirContext.forR4() +val fhirResources = contextR4.resourceTypes + +fun getQuestionsPath(questionnaire: Questionnaire): HashMap { + val questionsMap = hashMapOf() + + questionnaire.item.forEach { itemComponent -> getQuestionNames("", itemComponent, questionsMap) } + return questionsMap +} + +fun getQuestionNames( + parentName: String, + item: QuestionnaireItemComponent, + questionsMap: HashMap +) { + val currParentName = if (parentName.isEmpty()) "" else parentName + questionsMap.put(item.linkId, currParentName) + + item.item.forEach { itemComponent -> + getQuestionNames( + currParentName + ".where(linkId = '${item.linkId}').item", + itemComponent, + questionsMap + ) + } +} + +class Group( + entry: Map.Entry>, + val stringBuilder: StringBuilder, + val questionsPath: HashMap +) { + + var lineCounter = 0 + var groupName = entry.key + val instructions = entry.value + + private fun generateReference(resourceName: String, resourceIndex: String): String { + // Generate the reference based on the resourceName and resourceIndex + val sb = StringBuilder() + sb.append("create('Reference') as reference then {") + sb.appendNewLine() + sb.append( + "src-> reference.reference = evaluate(bundle, \$this.entry.where(resourceType = '$resourceName/$resourceIndex'))" + ) + sb.append(""" "rule_d";""".trimMargin()) + sb.appendNewLine() + sb.append("}") + return sb.toString() + } + + fun generateGroup(questionnaireResponse: QuestionnaireResponse) { + if (fhirResources.contains(groupName.dropLast(1))) { + val resourceName = instructions[0].resource + + // add target of reference to function if reference is not null + val structureMapFunctionHead = + "group Extract$groupName(source src : QuestionniareResponse, target bundle: Bundle) {" + stringBuilder.appendNewLine() + stringBuilder.append(structureMapFunctionHead).appendNewLine() + stringBuilder + .append( + "src -> bundle.entry as entry, entry.resource = create('$resourceName') as entity1 then {" + ) + .appendNewLine() + + val mainNest = Nest() + mainNest.fullPath = "" + mainNest.name = "" + mainNest.resourceName = resourceName + + instructions.forEachIndexed { index, instruction -> mainNest.add(instruction) } + + mainNest.buildStructureMap(0, questionnaireResponse) + + stringBuilder.append("} ") + addRuleNo() + stringBuilder.appendNewLine() + stringBuilder.append("}") + stringBuilder.appendNewLine() + stringBuilder.appendNewLine() + } else { + println("$groupName is not a valid hl7 resource name") + } + } + + fun addRuleNo() { + stringBuilder.append(""" "${groupName}_${lineCounter++}"; """) + } + + fun Instruction.getPropertyPath(): String { + return questionsPath.getOrDefault(responseFieldId, "") + } + + fun Instruction.getAnswerExpression(questionnaireResponse: QuestionnaireResponse): String { + + // 1. If the answer is static/literal, just return it here + // TODO: We should infer the resource element and add the correct conversion or code to assign + // this correctly + if (constantValue != null) { + return when { + fieldPath == "id" -> "create('id') as id, id.value = '$constantValue'" + fieldPath == "rank" -> { + val constValue = constantValue!!.replace(".0", "") + "create('positiveInt') as rank, rank.value = '$constValue'" + } + else -> "'$constantValue'" + } + } + + // 2. If the answer is from the QuestionnaireResponse, get the ID of the item in the + // "Questionnaire Response Field Id" and + // get its value using FHIR Path expressions + if (responseFieldId != null) { + // TODO: Fix the 1st param inside the evaluate expression + var expression = + "${"$"}this.item${getPropertyPath()}.where(linkId = '$responseFieldId').answer.value" + // TODO: Fix these to use infer + if (fieldPath == "id" || fieldPath == "rank") { + expression = + "create('${if (fieldPath == "id") "id" else "positiveInt"}') as $fieldPath, $fieldPath.value = evaluate(src, $expression)" + } else { + + // TODO: Infer the resource property type and answer to perform other conversions + // TODO: Extend this to cover other corner cases + if (expression.isCoding(questionnaireResponse) && fieldPath.isEnumeration(this)) { + expression = expression.replace("answer.value", "answer.value.code") + } else if (inferType(fullPropertyPath()) == "CodeableConcept") { + return "''" + } + expression = "evaluate(src, $expression)" + } + return expression + } + + // 3. If it's a FHIR Path/StructureMap function, add the contents directly from here to the + // StructureMap + if (fhirPathStructureMapFunctions != null && fhirPathStructureMapFunctions!!.isNotEmpty()) { + // TODO: Fix the 2nd param inside the evaluate expression --> Not sure what this is but check + // this + return fhirPathStructureMapFunctions!! + } + // 4. If the answer is a conversion, (Assume this means it's being converted to a reference) + if (conversion != null && conversion!!.isNotBlank() && conversion!!.isNotEmpty()) { + println("current resource to reference is $conversion") + + val resourceName = conversion!!.replace("$", "") + var resourceIndex = conversion!!.replace("$$resourceName", "") + if (resourceIndex.isNotEmpty()) { + resourceIndex = "[$resourceIndex]" + } + val reference = generateReference(resourceName = resourceName, resourceIndex = resourceIndex) + return reference + } + + /* + 5. You can use $Resource eg $Patient to reference another resource being extracted here, + but how do we actually get its instance so that we can use it???? - This should be handled elsewhere + */ + + return "''" + } + + inner class Nest { + var instruction: Instruction? = null + + // We can change this to a linked list + val nests = ArrayList() + lateinit var name: String + lateinit var fullPath: String + lateinit var resourceName: String + + fun add(instruction: Instruction) { + /*if (instruction.fieldPath.startsWith(fullPath)) { + + }*/ + val remainingPath = instruction.fieldPath.replace(fullPath, "") + + remainingPath.run { + if (contains(".")) { + val parts = split(".") + val partName = parts[0].ifEmpty { parts[1] } + + // Search for the correct property to put this nested property + nests.forEach { + if (partName.startsWith(it.name)) { + val nextInstruction = + Instruction().apply { + copyFrom(instruction) + var newFieldPath = "" + parts.forEachIndexed { index, s -> + if (index != 0) { + newFieldPath += s + } + + if (index > 0 && index < parts.size - 1) { + newFieldPath += "." + } + } + + fieldPath = newFieldPath + } + + it.add(nextInstruction) + + return@run + } + } + + // If no match is found, let's create a new one + val newNest = + Nest().apply { + name = partName + + fullPath = + if (this@Nest.fullPath.isNotEmpty()) { + "${this@Nest.fullPath}.$partName" + } else { + partName + } + resourceName = inferType("${this@Nest.resourceName}.$fullPath") ?: "" + + if ( + (parts[0].isEmpty() && parts.size > 2) || (parts[0].isNotEmpty() && parts.size > 1) + ) { + val nextInstruction = + Instruction().apply { + copyFrom(instruction) + var newFieldPath = "" + parts.forEachIndexed { index, s -> + if (index != 0) { + newFieldPath += s + } + } + + fieldPath = newFieldPath + } + add(nextInstruction) + } else { + this@apply.instruction = instruction + } + } + nests.add(newNest) + } else { + this@Nest.nests.add( + Nest().apply { + name = remainingPath + fullPath = instruction.fieldPath + this@apply.instruction = instruction + resourceName = inferType("${this@Nest.resourceName}.$fullPath") ?: "" + } + ) + } + } + } + + fun buildStructureMap(currLevel: Int, questionnaireResponse: QuestionnaireResponse) { + if (instruction != null) { + val answerExpression = instruction?.getAnswerExpression(questionnaireResponse) + + if (answerExpression != null) { + if ( + answerExpression.isNotEmpty() && + answerExpression.isNotBlank() && + answerExpression != "''" + ) { + val propertyType = inferType(instruction!!.fullPropertyPath()) + val answerType = answerExpression.getAnswerType(questionnaireResponse) + + if ( + propertyType != "Type" && + answerType != propertyType && + propertyType?.canHandleConversion(answerType ?: "")?.not() == true && + answerExpression.startsWith("evaluate") + ) { + println( + "Failed type matching --> ${instruction!!.fullPropertyPath()} of type $answerType != $propertyType" + ) + stringBuilder.append("src -> entity$currLevel.${instruction!!.fieldPath} = ") + stringBuilder.append( + "create('${propertyType.getFhirType()}') as randomVal, randomVal.value = " + ) + stringBuilder.append(answerExpression) + addRuleNo() + stringBuilder.appendNewLine() + return + } + + stringBuilder.append("src -> entity$currLevel.${instruction!!.fieldPath} = ") + stringBuilder.append(answerExpression) + addRuleNo() + stringBuilder.appendNewLine() + } + } + } else if (nests.size > 0) { + // val resourceType = inferType("entity$currLevel.$name", instruction) + + if (!name.equals("")) { + val resourceType = resourceName + stringBuilder.append( + "src -> entity$currLevel.$name = create('$resourceType') as entity${currLevel + 1} then {" + ) + stringBuilder.appendNewLine() + } else { + // stringBuilder.append("src -> entity$currLevel.$name = create('$resourceType') as + // entity${currLevel + 1} then {") + + } + + nests.forEach { it.buildStructureMap(currLevel + 1, questionnaireResponse) } + + // nest!!.buildStructureMap(currLevel + 1) + + if (!name.equals("")) { + stringBuilder.append("}") + addRuleNo() + } else { + // addRuleNo() + } + stringBuilder.appendNewLine() + } else { + throw Exception("nest & instruction are null inside Nest object") + } + } + } +} + +fun generateStructureMapLine( + structureMapBody: StringBuilder, + row: Row, + resource: Resource, + extractionResources: HashMap +) { + row.forEachIndexed { index, cell -> + val cellValue = cell.stringCellValue + val fieldPath = row.getCell(4).stringCellValue + val targetDataType = determineFhirDataType(cellValue) + structureMapBody.append("src -> entity.${fieldPath}=") + + when (targetDataType) { + "string" -> { + structureMapBody.append("create('string').value ='$cellValue'") + } + "integer" -> { + structureMapBody.append("create('integer').value = $cellValue") + } + "boolean" -> { + val booleanValue = if (cellValue.equals("true", ignoreCase = true)) "true" else "false" + structureMapBody.append("create('boolean').value = $booleanValue") + } + else -> { + structureMapBody.append("create('unsupportedDataType').value = '$cellValue'") + } + } + structureMapBody.appendNewLine() + } +} + +fun determineFhirDataType(cellValue: String): String { + val cleanedValue = cellValue.trim().toLowerCase() + + when { + cleanedValue == "true" || cleanedValue == "false" -> return "boolean" + cleanedValue.matches(Regex("-?\\d+")) -> return "boolean" + cleanedValue.matches(Regex("-?\\d*\\.\\d+")) -> return "decimal" + cleanedValue.matches(Regex("\\d{4}-\\d{2}-\\d{2}")) -> return "date" + cleanedValue.matches(Regex("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}")) -> return "dateTime" + else -> { + return "string" + } + } +} + +fun StringBuilder.appendNewLine(count: Int = 1): StringBuilder { + var counter = 0 + while (counter < count) { + append(System.lineSeparator()) + counter++ + } + return this +} + +private val Field.isList: Boolean + get() = isParameterized && type == List::class.java + +private val Field.isParameterized: Boolean + get() = genericType is ParameterizedType + +/** The non-parameterized type of this field (e.g. `String` for a field of type `List`). */ +private val Field.nonParameterizedType: Class<*> + get() = + if (isParameterized) (genericType as ParameterizedType).actualTypeArguments[0] as Class<*> + else type + +private fun Class<*>.getFieldOrNull(name: String): Field? { + return try { + getDeclaredField(name) + } catch (ex: NoSuchFieldException) { + superclass?.getFieldOrNull(name) + } +} + +internal val fhirPathEngine: FHIRPathEngine = + with(FhirContext.forCached(FhirVersionEnum.R4)) { + FHIRPathEngine(HapiWorkerContext(this, this.validationSupport)).apply { + // TODO update to link to already existing FHIRPathEngineHostService in the external folder + // hostServices = FHIRPathEngineHostServices + } + } + +/** + * Infer's the type and return the short class name eg `HumanName` for org.fhir.hl7.r4.model.Patient + * when given the path `Patient.name` + */ +fun inferType(propertyPath: String): String? { + // TODO: Handle possible errors + // TODO: Handle inferring nested types + val parts = propertyPath.split(".") + val parentResourceClassName = parts[0] + lateinit var parentClass: Class<*> + + if (fhirResources.contains(parentResourceClassName)) { + parentClass = Class.forName("org.hl7.fhir.r4.model.$parentResourceClassName") + return inferType(parentClass, parts, 1) + } else { + return null + } +} + +fun inferType(parentClass: Class<*>?, parts: List, index: Int): String? { + val resourcePropertyName = parts[index] + val propertyField = parentClass?.getFieldOrNull(resourcePropertyName) + + val propertyType = + if (propertyField?.isList == true) propertyField.nonParameterizedType + // TODO: Check if this is required + else if (propertyField?.type == Enumeration::class.java) + // TODO: Check if this works + propertyField.nonParameterizedType + else propertyField?.type + + return if (parts.size > index + 1) { + return inferType(propertyType, parts, index + 1) + } else propertyType?.name?.replace("org.hl7.fhir.r4.model.", "") +} diff --git a/efsity/src/main/kotlin/org/smartregister/structuremaptool/extensions/StringExtensions.kt b/efsity/src/main/kotlin/org/smartregister/structuremaptool/extensions/StringExtensions.kt new file mode 100644 index 00000000..2f56bc24 --- /dev/null +++ b/efsity/src/main/kotlin/org/smartregister/structuremaptool/extensions/StringExtensions.kt @@ -0,0 +1,121 @@ +package org.smartregister.structuremaptool.extensions + +import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.Type +import org.smartregister.structuremaptool.* +import org.smartregister.structuremaptool.fhirPathEngine + +fun String.addIndentation(): String { + var currLevel = 0 + val lines = split("\n") + + val sb = StringBuilder() + lines.forEach { line -> + if (line.endsWith("{")) { + sb.append(line.addIndentation(currLevel)) + sb.appendNewLine() + currLevel++ + } else if (line.startsWith("}")) { + currLevel-- + sb.append(line.addIndentation(currLevel)) + sb.appendNewLine() + } else { + sb.append(line.addIndentation(currLevel)) + sb.appendNewLine() + } + } + return sb.toString() +} + +fun String.addIndentation(times: Int): String { + var processedString = "" + for (k in 1..times) { + processedString += "\t" + } + + processedString += this + return processedString +} + +fun String.clean(): String { + return this.replace("-", "").replace("_", "").replace(" ", "") +} + +fun String.isCoding(questionnaireResponse: QuestionnaireResponse): Boolean { + val answerType = getType(questionnaireResponse) + return if (answerType != null) { + answerType == "org.hl7.fhir.r4.model.Coding" + } else { + false + } +} + +fun String.getType(questionnaireResponse: QuestionnaireResponse): String? { + val answer = fhirPathEngine.evaluate(questionnaireResponse, this) + + return answer.firstOrNull()?.javaClass?.name +} + +fun String.getAnswerType(questionnaireResponse: QuestionnaireResponse): String? { + return if (isEvaluateExpression()) { + val fhirPath = substring(indexOf(",") + 1, length - 1) + + fhirPath.getType(questionnaireResponse)?.replace("org.hl7.fhir.r4.model.", "") + } else { + // TODO: WE can run the actual line against StructureMapUtilities.runTransform to get the actual + // one that is generated and confirm if we need more conversions + "StringType" + } +} + +fun String.isEnumeration(instruction: Instruction): Boolean { + return inferType(instruction.fullPropertyPath())?.contains("Enumeration") ?: false +} + +fun String.isEvaluateExpression(): Boolean = startsWith("evaluate(") + +fun String.isMultipleTypes(): Boolean = this == "Type" + +// TODO: Finish this. Use the annotation @Chid.type +fun String.getPossibleTypes(): List { + return listOf() +} + +fun String.canHandleConversion(sourceType: String): Boolean { + val propertyClass = Class.forName("org.hl7.fhir.r4.model.$this") + val targetType2 = + if (sourceType == "StringType") String::class.java + else Class.forName("org.hl7.fhir.r4.model.$sourceType") + + val possibleConversions = + listOf( + "BooleanType" to "StringType", + "DateType" to "StringType", + "DecimalType" to "IntegerType", + "AdministrativeGender" to "CodeType" + ) + + possibleConversions.forEach { + if (this.contains(it.first) && sourceType == it.second) { + return true + } + } + + try { + propertyClass.getDeclaredMethod("fromCode", targetType2) + } catch (ex: NoSuchMethodException) { + return false + } + + return true +} + +fun String.getParentResource(): String? { + return substring(0, lastIndexOf('.')) +} + +fun String.getResourceProperty(): String? { + return substring(lastIndexOf('.') + 1) +} + +fun String.getFhirType(): String = replace("Type", "").lowercase() diff --git a/efsity/src/test/kotlin/external/FhirPathEngineHostServicesTest.kt b/efsity/src/test/kotlin/external/FhirPathEngineHostServicesTest.kt new file mode 100644 index 00000000..4f3a1062 --- /dev/null +++ b/efsity/src/test/kotlin/external/FhirPathEngineHostServicesTest.kt @@ -0,0 +1,96 @@ +package external + +import org.hl7.fhir.r4.model.StringType +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.smartregister.external.FhirPathEngineHostServices + +class FhirPathEngineHostServicesTest { + + @Test + fun `test Resolve Constant with Valid Key returns Value`() { + val appContext = mapOf("test" to mutableListOf(StringType("Test Value"))) + val result = FhirPathEngineHostServices.resolveConstant(appContext, "test", false) + + assertNotNull(result) + assertTrue(result is MutableList<*>) + assertEquals("Test Value", (result!![0] as StringType).value) + } + + @Test + fun `test Resolve Constant with Invalid Key returns Null`() { + val appContext = mapOf("test" to mutableListOf(StringType("Test Value"))) + val result = FhirPathEngineHostServices.resolveConstant(appContext, "invalidKey", false) + + assertNull(result) + } + + @Test + fun `test Resolve Constant with NonMapAppContext returns Null`() { + val appContext = "invalidAppContext" + val result = FhirPathEngineHostServices.resolveConstant(appContext, "test", false) + + assertNull(result) + } + + @Test + fun `test Resolve Constant Type throws Unsupported Operation Exception`() { + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.resolveConstantType(null, "test") + } + } + + @Test + fun `test Log throws Unsupported Operation Exception`() { + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.log("Test log message", mutableListOf()) + } + } + + @Test + fun `test Resolve Function throws Unsupported Operation Exception`() { + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.resolveFunction("testFunction") + } + } + + @Test + fun `test Check Function throws Unsupported Operation Exception`() { + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.checkFunction(null, "testFunction", mutableListOf()) + } + } + + @Test + fun `test ExecuteFunction throws Unsupported Operation Exception`() { + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.executeFunction( + null, + mutableListOf(), + "testFunction", + mutableListOf() + ) + } + } + + @Test + fun `test Resolve Reference throws Unsupported Operation Exception`() { + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.resolveReference(null, "http://example.com", null) + } + } + + @Test + fun `test Conforms To Profile throws Unsupported Operation Exception`() { + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.conformsToProfile(null, null, "http://example.com") + } + } + + @Test + fun `test Resolve ValueSet throws Unsupported Operation Exception`() { + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.resolveValueSet(null, "http://example.com") + } + } +} diff --git a/efsity/src/test/kotlin/external/TransformSupportServicesTest.kt b/efsity/src/test/kotlin/external/TransformSupportServicesTest.kt new file mode 100644 index 00000000..714ff886 --- /dev/null +++ b/efsity/src/test/kotlin/external/TransformSupportServicesTest.kt @@ -0,0 +1,137 @@ +package org.smartregister.fhir.structuremaptool + +import io.mockk.mockk +import kotlin.test.Test +import org.hl7.fhir.exceptions.FHIRException +import org.hl7.fhir.r4.model.CarePlan +import org.hl7.fhir.r4.model.Encounter +import org.hl7.fhir.r4.model.EpisodeOfCare +import org.hl7.fhir.r4.model.Immunization +import org.hl7.fhir.r4.model.Observation +import org.hl7.fhir.r4.model.Patient +import org.hl7.fhir.r4.model.RiskAssessment +import org.hl7.fhir.r4.model.TimeType +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.smartregister.external.TransformSupportServices + +class TransformSupportServicesTest { + lateinit var transformSupportServices: TransformSupportServices + + @BeforeEach + fun setUp() { + transformSupportServices = TransformSupportServices(mockk()) + } + + @Test + fun `createType() should return RiskAssessmentPrediction when given RiskAssessment_Prediction`() { + assertTrue( + transformSupportServices.createType("", "RiskAssessment_Prediction") + is RiskAssessment.RiskAssessmentPredictionComponent, + ) + } + + @Test + fun `createType() should return ImmunizationProtocol when given Immunization_VaccinationProtocol`() { + assertTrue( + transformSupportServices.createType("", "Immunization_AppliedProtocol") + is Immunization.ImmunizationProtocolAppliedComponent, + ) + } + + @Test + fun `createType() should return ImmunizationReaction when given Immunization_Reaction`() { + assertTrue( + transformSupportServices.createType("", "Immunization_Reaction") + is Immunization.ImmunizationReactionComponent, + ) + } + + @Test + fun `createType() should return Diagnosis when given EpisodeOfCare_Diagnosis`() { + assertTrue( + transformSupportServices.createType("", "EpisodeOfCare_Diagnosis") + is EpisodeOfCare.DiagnosisComponent, + ) + } + + @Test + fun `createType() should return Diagnosis when given Encounter_Diagnosis`() { + assertTrue( + transformSupportServices.createType("", "Encounter_Diagnosis") + is Encounter.DiagnosisComponent, + ) + } + + @Test + fun `createType() should return EncounterParticipant when given Encounter_Participant`() { + assertTrue( + transformSupportServices.createType("", "Encounter_Participant") + is Encounter.EncounterParticipantComponent, + ) + } + + @Test + fun `createType() should return CarePlanActivity when given CarePlan_Activity`() { + assertTrue( + transformSupportServices.createType("", "CarePlan_Activity") + is CarePlan.CarePlanActivityComponent, + ) + } + + @Test + fun `createType() should return CarePlanActivityDetail when given CarePlan_ActivityDetail`() { + assertTrue( + transformSupportServices.createType("", "CarePlan_ActivityDetail") + is CarePlan.CarePlanActivityDetailComponent, + ) + } + + @Test + fun `createType() should return PatientLink when given Patient_Link`() { + assertTrue( + transformSupportServices.createType("", "Patient_Link") is Patient.PatientLinkComponent, + ) + } + + @Test + fun `createType() should return ObservationComponentComponent when given Observation_Component`() { + assertTrue( + transformSupportServices.createType("", "Observation_Component") + is Observation.ObservationComponentComponent, + ) + } + + @Test + fun `createType() should return Time when given time`() { + assertTrue(transformSupportServices.createType("", "time") is TimeType) + } + + @Test + fun `createResource() should add resource into output when given Patient and atRootOfTransForm as True`() { + assertEquals(transformSupportServices.outputs.size, 0) + transformSupportServices.createResource("", Patient(), true) + assertEquals(transformSupportServices.outputs.size, 1) + } + + @Test + fun `createResource() should not add resource into output when given Patient and atRootOfTransForm as False`() { + assertEquals(transformSupportServices.outputs.size, 0) + transformSupportServices.createResource("", Patient(), false) + assertEquals(transformSupportServices.outputs.size, 0) + } + + @Test + fun `resolveReference should throw FHIRException when given url`() { + assertThrows(FHIRException::class.java) { + transformSupportServices.resolveReference("", "https://url.com") + } + } + + @Test + fun `performSearch() should throw FHIRException this is not supported yet when given url`() { + assertThrows(FHIRException::class.java) { + transformSupportServices.performSearch("", "https://url.com") + } + } +} diff --git a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/FhirPathEngineHostServices.kt b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/FhirPathEngineHostServices.kt index fd4f5c15..9d970769 100644 --- a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/FhirPathEngineHostServices.kt +++ b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/FhirPathEngineHostServices.kt @@ -51,7 +51,7 @@ internal object FhirPathEngineHostServices : FHIRPathEngine.IEvaluationContext { } override fun resolveFunction( - functionName: String? + functionName: String?, ): FHIRPathEngine.IEvaluationContext.FunctionDetails { logger.info("Resolving function: ${functionName ?: "Unknown"}") return functionCache.getOrPut(functionName ?: "") { diff --git a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Main.kt b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Main.kt index fd244ae6..8fae7a31 100644 --- a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Main.kt +++ b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Main.kt @@ -51,7 +51,7 @@ class Application : CliktCommand() { val questionnaire: Questionnaire = fhirJsonParser.parseResource( Questionnaire::class.java, - FileUtils.readFileToString(File(questionnairefile), Charset.defaultCharset()) + FileUtils.readFileToString(File(questionnairefile), Charset.defaultCharset()), ) val questionnaireResponseFile = File(javaClass.classLoader.getResource("questionnaire-response.json")?.file.toString()) @@ -59,7 +59,7 @@ class Application : CliktCommand() { questionnaireResponse = fhirJsonParser.parseResource( QuestionnaireResponse::class.java, - questionnaireResponseFile.readText(Charset.defaultCharset()) + questionnaireResponseFile.readText(Charset.defaultCharset()), ) } else { println("File not found: questionnaire-response.json") diff --git a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Utils.kt b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Utils.kt index 8c018da4..cdc96e87 100644 --- a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Utils.kt +++ b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Utils.kt @@ -28,7 +28,7 @@ fun getQuestionsPath(questionnaire: Questionnaire): HashMap { fun getQuestionNames( parentName: String, item: QuestionnaireItemComponent, - questionsMap: HashMap + questionsMap: HashMap, ) { val currParentName = if (parentName.isEmpty()) "" else parentName questionsMap.put(item.linkId, currParentName) @@ -37,7 +37,7 @@ fun getQuestionNames( getQuestionNames( currParentName + ".where(linkId = '${item.linkId}').item", itemComponent, - questionsMap + questionsMap, ) } } @@ -58,7 +58,7 @@ class Group( sb.append("create('Reference') as reference then {") sb.appendNewLine() sb.append( - "src-> reference.reference = evaluate(bundle, \$this.entry.where(resourceType = '$resourceName/$resourceIndex'))" + "src-> reference.reference = evaluate(bundle, \$this.entry.where(resourceType = '$resourceName/$resourceIndex'))", ) sb.append(""" "rule_d";""".trimMargin()) sb.appendNewLine() @@ -77,7 +77,7 @@ class Group( stringBuilder.append(structureMapFunctionHead).appendNewLine() stringBuilder .append( - "src -> bundle.entry as entry, entry.resource = create('$resourceName') as entity1 then {" + "src -> bundle.entry as entry, entry.resource = create('$resourceName') as entity1 then {", ) .appendNewLine() @@ -293,11 +293,11 @@ class Group( answerExpression.startsWith("evaluate") ) { println( - "Failed type matching --> ${instruction!!.fullPropertyPath()} of type $answerType != $propertyType" + "Failed type matching --> ${instruction!!.fullPropertyPath()} of type $answerType != $propertyType", ) stringBuilder.append("src -> entity$currLevel.${instruction!!.fieldPath} = ") stringBuilder.append( - "create('${propertyType.getFhirType()}') as randomVal, randomVal.value = " + "create('${propertyType.getFhirType()}') as randomVal, randomVal.value = ", ) stringBuilder.append(answerExpression) addRuleNo() @@ -317,7 +317,7 @@ class Group( if (!name.equals("")) { val resourceType = resourceName stringBuilder.append( - "src -> entity$currLevel.$name = create('$resourceType') as entity${currLevel + 1} then {" + "src -> entity$currLevel.$name = create('$resourceType') as entity${currLevel + 1} then {", ) stringBuilder.appendNewLine() } else { @@ -509,8 +509,9 @@ fun String.getPossibleTypes(): List { fun String.canHandleConversion(sourceType: String): Boolean { val propertyClass = Class.forName("org.hl7.fhir.r4.model.$this") val targetType2 = - if (sourceType == "StringType") String::class.java - else Class.forName("org.hl7.fhir.r4.model.$sourceType") + if (sourceType == "StringType") { + String::class.java + } else Class.forName("org.hl7.fhir.r4.model.$sourceType") val possibleConversions = listOf( diff --git a/sm-gen/src/main/resources/StructureMap XLS.xls b/sm-gen/src/main/resources/StructureMap.xls similarity index 100% rename from sm-gen/src/main/resources/StructureMap XLS.xls rename to sm-gen/src/main/resources/StructureMap.xls diff --git a/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/FhirPathEngineHostServicesTest.kt b/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/FhirPathEngineHostServicesTest.kt index c1146e87..1f40c23a 100644 --- a/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/FhirPathEngineHostServicesTest.kt +++ b/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/FhirPathEngineHostServicesTest.kt @@ -34,7 +34,7 @@ class FhirPathEngineHostServicesTest { assertEquals( "Test Value", (result as StringType).value, - "The resolved constant should match the expected value" + "The resolved constant should match the expected value", ) } @@ -88,7 +88,7 @@ class FhirPathEngineHostServicesTest { null, mutableListOf(), "testFunction", - mutableListOf() + mutableListOf(), ) } assertEquals("executeFunction is not yet implemented.", exception.message)