From 4926b460342f0ac4263ec1f6d199c31ea867e24e Mon Sep 17 00:00:00 2001 From: Dylan Hall Date: Tue, 12 Nov 2024 11:38:19 -0500 Subject: [PATCH] add custom ValueSets to flexporter mapping file --- src/main/java/App.java | 2 + src/main/java/RunFlexporter.java | 5 +-- .../synthea/export/flexporter/Mapping.java | 22 ++++++++++ .../org/mitre/synthea/helpers/Utilities.java | 38 ++++++++++++++++ .../export/flexporter/ActionsTest.java | 32 +------------- .../resources/flexporter/test_mapping.yaml | 43 +++++++++++++++++++ 6 files changed, 107 insertions(+), 35 deletions(-) diff --git a/src/main/java/App.java b/src/main/java/App.java index 444e6d5fc3..7ae1be2d96 100644 --- a/src/main/java/App.java +++ b/src/main/java/App.java @@ -220,6 +220,8 @@ public static void main(String[] args) throws Exception { if (flexporterMappingFile.exists()) { Mapping mapping = Mapping.parseMapping(flexporterMappingFile); exportOptions.addFlexporterMapping(mapping); + mapping.loadValueSets(); + // disable the graalVM warning when FlexporterJavascriptContext is instantiated System.getProperties().setProperty("polyglot.engine.WarnInterpreterOnly", "false"); } else { diff --git a/src/main/java/RunFlexporter.java b/src/main/java/RunFlexporter.java index a7918e2dbb..f91103cebf 100644 --- a/src/main/java/RunFlexporter.java +++ b/src/main/java/RunFlexporter.java @@ -1,9 +1,6 @@ import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.IParser; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; - import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; @@ -14,7 +11,6 @@ import java.nio.file.StandardOpenOption; import java.util.ArrayDeque; import java.util.Arrays; -import java.util.Map; import java.util.Queue; import org.apache.commons.io.FilenameUtils; @@ -125,6 +121,7 @@ private static void convertFhir(File mappingFile, File igDirectory, File sourceF throws IOException { Mapping mapping = Mapping.parseMapping(mappingFile); + mapping.loadValueSets(); if (igDirectory != null) { loadIG(igDirectory); diff --git a/src/main/java/org/mitre/synthea/export/flexporter/Mapping.java b/src/main/java/org/mitre/synthea/export/flexporter/Mapping.java index 6920c256e7..dc5bdeeb66 100644 --- a/src/main/java/org/mitre/synthea/export/flexporter/Mapping.java +++ b/src/main/java/org/mitre/synthea/export/flexporter/Mapping.java @@ -1,5 +1,7 @@ package org.mitre.synthea.export.flexporter; +import com.fasterxml.jackson.core.JsonProcessingException; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -7,6 +9,9 @@ import java.util.List; import java.util.Map; +import org.hl7.fhir.r4.model.ValueSet; +import org.mitre.synthea.helpers.RandomCodeGenerator; +import org.mitre.synthea.helpers.Utilities; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.Constructor; @@ -15,6 +20,7 @@ public class Mapping { public String applicability; public Map variables; + public List> customValueSets; /** * Each action is a {@code Map>String,?>}. Nested fields within the YAML become ArrayLists and @@ -34,4 +40,20 @@ public static Mapping parseMapping(File mappingFile) throws FileNotFoundExceptio return yaml.loadAs(selectorInputSteam, Mapping.class); } + + /** + * Load the custom ValueSets that this mapping defines, so that the codes can be selected + * in RandomCodeGenerator. + */ + public void loadValueSets() { + try { + if (this.customValueSets != null) { + List valueSets = + Utilities.parseYamlToResources(this.customValueSets, ValueSet.class); + valueSets.forEach(vs -> RandomCodeGenerator.loadValueSet(null, vs)); + } + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } } diff --git a/src/main/java/org/mitre/synthea/helpers/Utilities.java b/src/main/java/org/mitre/synthea/helpers/Utilities.java index fe983ed8b3..3ffbe25e19 100644 --- a/src/main/java/org/mitre/synthea/helpers/Utilities.java +++ b/src/main/java/org/mitre/synthea/helpers/Utilities.java @@ -1,5 +1,9 @@ package org.mitre.synthea.helpers; +import ca.uhn.fhir.parser.IParser; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Charsets; import com.google.common.io.Resources; import com.google.gson.FieldNamingPolicy; @@ -36,9 +40,11 @@ import java.util.regex.Pattern; import org.apache.commons.lang3.Range; +import org.hl7.fhir.r4.model.Resource; import org.mitre.synthea.engine.Logic; import org.mitre.synthea.engine.Module; import org.mitre.synthea.engine.State; +import org.mitre.synthea.export.FhirR4; import org.mitre.synthea.world.concepts.HealthRecord.Code; public class Utilities { @@ -669,4 +675,36 @@ public static void enableReadingURIFromJar(URI uri) throws IOException { } } } + + /** + * Helper method to parse FHIR resources from YAML. + * This is a workaround since the FHIR model classes don't work with our YAML parser. + * + * @param Resource type contained in the YAML + * @param yaml List of pre-parsed YAML as Map<String, Object> + * @param resourceClass Specific resource class, must not be Resource + * @return List of parsed resources + * @throws JsonProcessingException (should never happen) + */ + public static List parseYamlToResources( + List> yaml, Class resourceClass) + throws JsonProcessingException { + if (yaml.isEmpty()) { + return Collections.emptyList(); + } + ObjectMapper jsonMapper = new ObjectMapper(); + IParser jsonParser = FhirR4.getContext().newJsonParser(); + List results = new ArrayList<>(); + for (Map singleYaml : yaml) { + if (!singleYaml.containsKey("resourceType")) { + // allows the YAML to be cleaner by letting the resourceType be implied + singleYaml.put("resourceType", resourceClass.getSimpleName()); + } + String resourceJson = jsonMapper.writeValueAsString(singleYaml); + @SuppressWarnings("unchecked") + T resource = (T) jsonParser.parseResource(resourceJson); + results.add(resource); + } + return results; + } } \ No newline at end of file diff --git a/src/test/java/org/mitre/synthea/export/flexporter/ActionsTest.java b/src/test/java/org/mitre/synthea/export/flexporter/ActionsTest.java index 5ac2d22bc7..1f67d1984e 100644 --- a/src/test/java/org/mitre/synthea/export/flexporter/ActionsTest.java +++ b/src/test/java/org/mitre/synthea/export/flexporter/ActionsTest.java @@ -85,6 +85,7 @@ public static void setupClass() throws FileNotFoundException { File file = new File(classLoader.getResource("flexporter/test_mapping.yaml").getFile()); testMapping = Mapping.parseMapping(file); + testMapping.loadValueSets(); } @AfterClass @@ -825,21 +826,6 @@ public void testRandomCode() { Bundle b = new Bundle(); b.setType(BundleType.COLLECTION); - ValueSet statusVs = constructValueSet( - "http://hl7.org/fhir/encounter-status", - "planned", "finished", "cancelled"); - RandomCodeGenerator.loadValueSet("http://example.org/encounterStatus", statusVs); - - ValueSet classVs = constructValueSet( - "http://terminology.hl7.org/CodeSystem/v3-ActCode", - "AMB", "EMER", "ACUTE"); - RandomCodeGenerator.loadValueSet("http://example.org/encounterClass", classVs); - - ValueSet typeVs = constructValueSet( - "http://terminology.hl7.org/CodeSystem/encounter-type", - "ADMS", "OKI"); - RandomCodeGenerator.loadValueSet("http://example.org/encounterType", typeVs); - Map action = getActionByName("testRandomCode"); Actions.applyAction(b, action, null, null); @@ -865,20 +851,4 @@ public void testRandomCode() { code = typeCoding.getCode(); assertTrue(code.equals("ADMS") || code.equals("OKI")); } - - private ValueSet constructValueSet(String system, String... codes) { - ValueSet vs = new ValueSet(); - - // populates the codes so that they can be read in RandomCodeGenerator.loadValueSet - ConceptSetComponent csc = new ConceptSetComponent(); - csc.setSystem(system); - for (String code : codes) { - csc.addConcept().setCode(code).setDisplay(code); - } - - vs.getCompose().getInclude().add(csc); - - return vs; - } - } diff --git a/src/test/resources/flexporter/test_mapping.yaml b/src/test/resources/flexporter/test_mapping.yaml index b5872de7bd..6de68a0cec 100644 --- a/src/test/resources/flexporter/test_mapping.yaml +++ b/src/test/resources/flexporter/test_mapping.yaml @@ -6,6 +6,49 @@ name: Random Testing # for now the assumption is 1 file = 1 synthea patient bundle. applicability: true +# Not a huge fan of this format, but it's better than defining yet another custom syntax +customValueSets: + - url: whats-for-dinner + compose: + include: + - system: http://snomed.info/sct + concept: + - code: 227360002 + display: Pinto beans (substance) + - code: 227319009 + display: Baked beans canned in tomato sauce with burgers (substance) + - url: http://example.org/encounterStatus + compose: + include: + - system: http://hl7.org/fhir/encounter-status + concept: + - code: planned + display: Planned + - code: finished + display: Finished + - code: cancelled + display: Cancelled + - url: http://example.org/encounterClass + compose: + include: + - system: http://terminology.hl7.org/CodeSystem/v3-ActCode + concept: + - code: AMB + display: ambulatory + - code: EMER + display: emergency + - code: ACUTE + display: inpatient acute + - url: http://example.org/encounterType + compose: + include: + - system: http://terminology.hl7.org/CodeSystem/encounter-type + concept: + - code: ADMS + display: Annual diabetes mellitus screening + - code: OKI + display: Outpatient Kenacort injection + actions: - name: Apply Profiles # v1: define specific profiles and an applicability statement on when to apply them