diff --git a/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/context/MappingContextLoader.scala b/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/context/MappingContextLoader.scala index 07e5b90f..6c9fc0b2 100644 --- a/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/context/MappingContextLoader.scala +++ b/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/context/MappingContextLoader.scala @@ -62,16 +62,46 @@ class MappingContextLoader extends IMappingContextLoader { /** * Read concept mappings from the given CSV file. + * Example dataset to understand what this function does: + * Input CSV content (concept mappings), assumed to be at some file path specified: + * ----------------------------- + * source_code,target_code,display_value + * 001,A1,Foo + * 001,A2,Bar + * 002,B1,Baz + * ----------------------------- + * + * Explanation of this structure: + * - "source_code" is the key (the first column header), which will group the rows. + * - "target_code" and "display_value" are part of the data for each key grouping. + * + * Expected output of processing: + * Map( + * "001" -> Seq( + * Map("source_code" -> "001", "target_code" -> "A1", "display_value" -> "Foo"), + * Map("source_code" -> "001", "target_code" -> "A2", "display_value" -> "Bar") + * ), + * "002" -> Seq( + * Map("source_code" -> "002", "target_code" -> "B1", "display_value" -> "Baz") + * ) + * ) * * @param filePath * @return */ - private def readConceptMapContextFromCSV(filePath: String): Future[Map[String, Map[String, String]]] = { + private def readConceptMapContextFromCSV(filePath: String): Future[Map[String, Seq[Map[String, String]]]] = { readFromCSV(filePath) map { case (columns, records) => //val (firstColumnName, _) = records.head.head // Get the first element in the records list and then get the first (k,v) pair to get the name of the first column. - records.foldLeft(Map[String, Map[String, String]]()) { (conceptMap, columnMap) => - conceptMap + (columnMap(columns.head)-> columnMap) + val columnHeadKey = columns.head + records.foldLeft(Map[String, Seq[Map[String, String]]]()) { (conceptMap, columnMap) => + val key = columnMap(columnHeadKey) + // If a source code has not been encountered before, add it as the first element. + // Otherwise, append the new target values to the existing sequence. + conceptMap.updatedWith(key) { + case Some(existingValues) => Some(existingValues :+ columnMap) + case None => Some(Seq(columnMap)) + } } } } diff --git a/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/fhirPath/FhirPathMappingFunctions.scala b/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/fhirPath/FhirPathMappingFunctions.scala index c9842f1e..ae137c7c 100644 --- a/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/fhirPath/FhirPathMappingFunctions.scala +++ b/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/fhirPath/FhirPathMappingFunctions.scala @@ -132,18 +132,13 @@ class FhirPathMappingFunctions(context: FhirPathEnvironment, current: Seq[FhirPa val evaluator = new FhirPathExpressionEvaluator(context, current) // Should return the code of the concept whose mapping is requested evaluator.visit(keyExpr) match { - case Nil => - Nil + case Nil => Seq.empty case Seq(FhirPathString(conceptCode)) => conceptMapContext .concepts .get(conceptCode) - .map { - case mws:Map[String, String] if mws.size == 1 => FhirPathString(mws.values.head) - case mws => - FhirPathComplex(JObject(mws.toList.map(i => i._1 -> JString(i._2)))) - } - .toSeq + .map(mws => mws.map(res => FhirPathComplex(JObject(res.toList.map(i => i._1 -> JString(i._2)))))) + .getOrElse(Seq.empty) case _ => throw new FhirPathException(s"Invalid function call 'getConcept', given expression for keyExpr:${keyExpr.getText} for the concept code should return a string value!") } @@ -191,21 +186,18 @@ class FhirPathMappingFunctions(context: FhirPathEnvironment, current: Seq[FhirPa throw new FhirPathException(s"Invalid function call 'getConcept', given expression for keyExpr:${keyExpr.getText} for the concept code should return a string value!") } //If conceptCode returns empty, also return empty, if there is no such key or target column is null also return empty - val result = + val result: Seq[FhirPathResult] = conceptCodeResult - .headOption - .map(_.asInstanceOf[FhirPathString].s) match { - case None => Nil - case Some(conceptCode) => - conceptMapContext - .concepts - .get(conceptCode) - .flatMap(codeEntry => - codeEntry.get(targetField).filter(_ != "") - ) - .map(mappedValue => FhirPathString(mappedValue)) - .toSeq - } + .headOption + .map(_.asInstanceOf[FhirPathString].s) + .flatMap { conceptCode => + conceptMapContext.concepts.get(conceptCode).map { conceptMapEntries => + conceptMapEntries + .flatMap(_.get(targetField)) + .filter(_.nonEmpty) + .map(mappedValue => FhirPathString(mappedValue)) + } + }.getOrElse(Seq.empty) result } diff --git a/tofhir-engine/src/main/scala/io/tofhir/engine/model/FhirMappingContext.scala b/tofhir-engine/src/main/scala/io/tofhir/engine/model/FhirMappingContext.scala index ff8f3058..8773c85f 100644 --- a/tofhir-engine/src/main/scala/io/tofhir/engine/model/FhirMappingContext.scala +++ b/tofhir-engine/src/main/scala/io/tofhir/engine/model/FhirMappingContext.scala @@ -14,11 +14,15 @@ trait FhirMappingContext { * From a CSV who has a header like "source_code,source_system,source_display,unit,profile", and where * source_code is 9110-8, the concepts Map can be as in the following: * - * Map[9110-8 -> Map[(source_system -> http://loinc.org,Bleeding (cumulative)), - * (unit -> mL), - * (profile -> https://aiccelerate.eu/fhir/StructureDefinition/AIC-IntraOperativeObservation)]] + * Map[9110-8 -> Seq [ + * Map[(source_system -> http://loinc.org,Bleeding (cumulative)), + * (unit -> mL), + * (profile -> https://aiccelerate.eu/fhir/StructureDefinition/AIC-IntraOperativeObservation)] + * ]] + * + * */ -case class ConceptMapContext(concepts: Map[String, Map[String, String]]) extends FhirMappingContext { +case class ConceptMapContext(concepts: Map[String, Seq[Map[String, String]]]) extends FhirMappingContext { override def toContextObject: JObject = JObject() } diff --git a/tofhir-engine/src/test/resources/test-mappings/some-folder-1/other-observation-concept-map.csv b/tofhir-engine/src/test/resources/test-mappings/some-folder-1/other-observation-concept-map.csv index 177aeb0a..99339ce1 100644 --- a/tofhir-engine/src/test/resources/test-mappings/some-folder-1/other-observation-concept-map.csv +++ b/tofhir-engine/src/test/resources/test-mappings/some-folder-1/other-observation-concept-map.csv @@ -9,6 +9,8 @@ source_code,source_system,source_display,unit,profile 445619006,http://snomed.info/sct,NEWS score,{score},https://aiccelerate.eu/fhir/StructureDefinition/AIC-NEWSScore 445597002,http://snomed.info/sct,PEWS score,{score},https://aiccelerate.eu/fhir/StructureDefinition/AIC-PEWSScore 9269-2,http://loinc.org,Galscow Coma Scale (GCS),{score},https://aiccelerate.eu/fhir/StructureDefinition/AIC-GlascowComaScaleObservation +1234-5,http://loinc.org,Hemogoblin,g/dL,https://aiccelerate.eu/fhir/StructureDefinition/AIC-IntraOperativeObservation +1234-5,http://loinc.org,Hemogoblin X,g/cL,https://aiccelerate.eu/fhir/StructureDefinition/AIC-IntraOperativeObservation 313002,http://www.nlm.nih.gov/research/umls/rxnorm,NaCl0.9% (cumulative) given,mL,https://aiccelerate.eu/fhir/StructureDefinition/AIC-MedicationAdministration 35629,http://www.nlm.nih.gov/research/umls/rxnorm,Ringer (cumulative) given,mL,https://aiccelerate.eu/fhir/StructureDefinition/AIC-MedicationAdministration 33834,http://www.nlm.nih.gov/research/umls/rxnorm,Plasmalyte (cumulative) given,mL,https://aiccelerate.eu/fhir/StructureDefinition/AIC-MedicationAdministration diff --git a/tofhir-engine/src/test/resources/test-mappings/some-folder-3/other-observation-concept-map.csv b/tofhir-engine/src/test/resources/test-mappings/some-folder-3/other-observation-concept-map.csv index 177aeb0a..99339ce1 100644 --- a/tofhir-engine/src/test/resources/test-mappings/some-folder-3/other-observation-concept-map.csv +++ b/tofhir-engine/src/test/resources/test-mappings/some-folder-3/other-observation-concept-map.csv @@ -9,6 +9,8 @@ source_code,source_system,source_display,unit,profile 445619006,http://snomed.info/sct,NEWS score,{score},https://aiccelerate.eu/fhir/StructureDefinition/AIC-NEWSScore 445597002,http://snomed.info/sct,PEWS score,{score},https://aiccelerate.eu/fhir/StructureDefinition/AIC-PEWSScore 9269-2,http://loinc.org,Galscow Coma Scale (GCS),{score},https://aiccelerate.eu/fhir/StructureDefinition/AIC-GlascowComaScaleObservation +1234-5,http://loinc.org,Hemogoblin,g/dL,https://aiccelerate.eu/fhir/StructureDefinition/AIC-IntraOperativeObservation +1234-5,http://loinc.org,Hemogoblin X,g/cL,https://aiccelerate.eu/fhir/StructureDefinition/AIC-IntraOperativeObservation 313002,http://www.nlm.nih.gov/research/umls/rxnorm,NaCl0.9% (cumulative) given,mL,https://aiccelerate.eu/fhir/StructureDefinition/AIC-MedicationAdministration 35629,http://www.nlm.nih.gov/research/umls/rxnorm,Ringer (cumulative) given,mL,https://aiccelerate.eu/fhir/StructureDefinition/AIC-MedicationAdministration 33834,http://www.nlm.nih.gov/research/umls/rxnorm,Plasmalyte (cumulative) given,mL,https://aiccelerate.eu/fhir/StructureDefinition/AIC-MedicationAdministration diff --git a/tofhir-engine/src/test/scala/io/tofhir/test/FhirMappingFolderRepositoryTest.scala b/tofhir-engine/src/test/scala/io/tofhir/test/FhirMappingFolderRepositoryTest.scala index fce094b7..0081b74c 100644 --- a/tofhir-engine/src/test/scala/io/tofhir/test/FhirMappingFolderRepositoryTest.scala +++ b/tofhir-engine/src/test/scala/io/tofhir/test/FhirMappingFolderRepositoryTest.scala @@ -45,12 +45,21 @@ class FhirMappingFolderRepositoryTest extends AsyncFlatSpec with ToFhirTestSpec val mappingContextLoader = new MappingContextLoader mappingContextLoader.retrieveContext(contextDefinition) map { context => val conceptMapContext = context.asInstanceOf[ConceptMapContext] - conceptMapContext.concepts.size shouldBe 13 + conceptMapContext.concepts.size shouldBe 14 // source_code,source_system,source_display,unit,profile // 9187-6,http://loinc.org,Urine Output,cm3,https://aiccelerate.eu/fhir/StructureDefinition/AIC-IntraOperativeObservation - conceptMapContext.concepts("9187-6")("source_system") shouldBe "http://loinc.org" - conceptMapContext.concepts("9187-6")("unit") shouldBe "cm3" + conceptMapContext.concepts("9187-6").head("source_system") shouldBe "http://loinc.org" + conceptMapContext.concepts("9187-6").head("unit") shouldBe "cm3" + + conceptMapContext.concepts("1234-5").length shouldBe 2 + conceptMapContext.concepts("1234-5").head("source_system") shouldBe "http://loinc.org" + conceptMapContext.concepts("1234-5").head("source_display") shouldBe "Hemogoblin" + conceptMapContext.concepts("1234-5").head("unit") shouldBe "g/dL" + + conceptMapContext.concepts("1234-5")(1)("source_system") shouldBe "http://loinc.org" + conceptMapContext.concepts("1234-5")(1)("source_display") shouldBe "Hemogoblin X" + conceptMapContext.concepts("1234-5")(1)("unit") shouldBe "g/cL" } } @@ -60,12 +69,12 @@ class FhirMappingFolderRepositoryTest extends AsyncFlatSpec with ToFhirTestSpec val mappingContextLoader = new MappingContextLoader mappingContextLoader.retrieveContext(contextDefinition) map { context => val conceptMapContext = context.asInstanceOf[ConceptMapContext] - conceptMapContext.concepts.size shouldBe 13 + conceptMapContext.concepts.size shouldBe 14 // source_code,source_system,source_display,unit,profile // 9187-6,http://loinc.org,Urine Output,cm3,https://aiccelerate.eu/fhir/StructureDefinition/AIC-IntraOperativeObservation - conceptMapContext.concepts("9187-6")("source_system") shouldBe "http://loinc.org" - conceptMapContext.concepts("9187-6")("unit") shouldBe "cm3" + conceptMapContext.concepts("9187-6").head("source_system") shouldBe "http://loinc.org" + conceptMapContext.concepts("9187-6").head("unit") shouldBe "cm3" } }