Skip to content

Commit 34f00e2

Browse files
authored
Merge pull request #1527 from synthetichealth/randomcode_updates
Flexporter - Update randomCode fn to take output type as an optional second param
2 parents b69833d + 4926b46 commit 34f00e2

File tree

9 files changed

+190
-14
lines changed

9 files changed

+190
-14
lines changed

src/main/java/App.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ public static void main(String[] args) throws Exception {
220220
if (flexporterMappingFile.exists()) {
221221
Mapping mapping = Mapping.parseMapping(flexporterMappingFile);
222222
exportOptions.addFlexporterMapping(mapping);
223+
mapping.loadValueSets();
224+
223225
// disable the graalVM warning when FlexporterJavascriptContext is instantiated
224226
System.getProperties().setProperty("polyglot.engine.WarnInterpreterOnly", "false");
225227
} else {

src/main/java/RunFlexporter.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import ca.uhn.fhir.parser.DataFormatException;
22
import ca.uhn.fhir.parser.IParser;
33

4-
import com.fasterxml.jackson.core.type.TypeReference;
5-
import com.fasterxml.jackson.databind.ObjectMapper;
6-
74
import java.io.File;
85
import java.io.FileNotFoundException;
96
import java.io.IOException;
@@ -14,7 +11,6 @@
1411
import java.nio.file.StandardOpenOption;
1512
import java.util.ArrayDeque;
1613
import java.util.Arrays;
17-
import java.util.Map;
1814
import java.util.Queue;
1915

2016
import org.apache.commons.io.FilenameUtils;
@@ -125,6 +121,7 @@ private static void convertFhir(File mappingFile, File igDirectory, File sourceF
125121
throws IOException {
126122

127123
Mapping mapping = Mapping.parseMapping(mappingFile);
124+
mapping.loadValueSets();
128125

129126
if (igDirectory != null) {
130127
loadIG(igDirectory);

src/main/java/org/mitre/synthea/export/FhirR4.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3385,7 +3385,7 @@ private static Type convertFhirDateTime(long datetime, boolean time) {
33853385
* @param system The system identifier, such as a URI. Optional; may be null.
33863386
* @return The converted CodeableConcept
33873387
*/
3388-
private static CodeableConcept mapCodeToCodeableConcept(Code from, String system) {
3388+
public static CodeableConcept mapCodeToCodeableConcept(Code from, String system) {
33893389
CodeableConcept to = new CodeableConcept();
33903390
system = system == null ? null : ExportHelper.getSystemURI(system);
33913391
from.system = ExportHelper.getSystemURI(from.system);

src/main/java/org/mitre/synthea/export/flexporter/Actions.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.hl7.fhir.r4.model.Bundle.BundleEntryRequestComponent;
2727
import org.hl7.fhir.r4.model.Bundle.BundleType;
2828
import org.hl7.fhir.r4.model.Bundle.HTTPVerb;
29+
import org.hl7.fhir.r4.model.Coding;
2930
import org.hl7.fhir.r4.model.DateTimeType;
3031
import org.hl7.fhir.r4.model.Encounter;
3132
import org.hl7.fhir.r4.model.Meta;
@@ -822,7 +823,7 @@ private static Object getValue(Bundle bundle, String valueDef, Resource currentR
822823
} else if (flag.equals("getAttribute")) {
823824
return getAttribute(person, flagValues);
824825
} else if (flag.equals("randomCode")) {
825-
return randomCode(flagValues[0]);
826+
return randomCode(flagValues);
826827
}
827828

828829
return null;
@@ -900,13 +901,22 @@ private static Base findValue(Bundle bundle, String... args) {
900901
return fieldValues.get(0);
901902
}
902903

903-
private static Map<String, String> randomCode(String valueSetUrl) {
904+
private static Object randomCode(String... args) {
905+
String valueSetUrl = args[0];
906+
String outputType = (args.length > 1) ? args[1] : "Coding";
904907
Code code = RandomCodeGenerator.getCode(valueSetUrl,
905908
(int) (Math.random() * Integer.MAX_VALUE));
906-
Map<String, String> codeAsMap = Map.of(
907-
"system", code.system,
908-
"code", code.code,
909-
"display", code.display == null ? "" : code.display);
910-
return codeAsMap;
909+
910+
if (outputType.equalsIgnoreCase("code")) {
911+
return code.code;
912+
} else if (outputType.equalsIgnoreCase("Coding")) {
913+
return new Coding(code.system, code.code, code.display);
914+
} else if (outputType.equalsIgnoreCase("CodeableConcept")) {
915+
return FhirR4.mapCodeToCodeableConcept(code, null);
916+
} else {
917+
throw new IllegalArgumentException("Unexpected output type for randomCode: " + outputType
918+
+ ". Valid values are: code, Coding, CodeableConcept");
919+
}
920+
911921
}
912922
}

src/main/java/org/mitre/synthea/export/flexporter/Mapping.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package org.mitre.synthea.export.flexporter;
22

3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
35
import java.io.File;
46
import java.io.FileInputStream;
57
import java.io.FileNotFoundException;
68
import java.io.InputStream;
79
import java.util.List;
810
import java.util.Map;
911

12+
import org.hl7.fhir.r4.model.ValueSet;
13+
import org.mitre.synthea.helpers.RandomCodeGenerator;
14+
import org.mitre.synthea.helpers.Utilities;
1015
import org.yaml.snakeyaml.Yaml;
1116
import org.yaml.snakeyaml.constructor.Constructor;
1217

@@ -15,6 +20,7 @@ public class Mapping {
1520
public String applicability;
1621

1722
public Map<String, Object> variables;
23+
public List<Map<String, Object>> customValueSets;
1824

1925
/**
2026
* 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
3440

3541
return yaml.loadAs(selectorInputSteam, Mapping.class);
3642
}
43+
44+
/**
45+
* Load the custom ValueSets that this mapping defines, so that the codes can be selected
46+
* in RandomCodeGenerator.
47+
*/
48+
public void loadValueSets() {
49+
try {
50+
if (this.customValueSets != null) {
51+
List<ValueSet> valueSets =
52+
Utilities.parseYamlToResources(this.customValueSets, ValueSet.class);
53+
valueSets.forEach(vs -> RandomCodeGenerator.loadValueSet(null, vs));
54+
}
55+
} catch (JsonProcessingException e) {
56+
throw new RuntimeException(e);
57+
}
58+
}
3759
}

src/main/java/org/mitre/synthea/helpers/RandomCodeGenerator.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import okhttp3.ResponseBody;
1919
import org.apache.commons.lang3.StringUtils;
2020
import org.apache.commons.validator.routines.UrlValidator;
21+
import org.hl7.fhir.r4.model.OperationOutcome;
22+
import org.hl7.fhir.r4.model.Resource;
2123
import org.hl7.fhir.r4.model.ValueSet;
2224
import org.hl7.fhir.r4.model.ValueSet.ConceptReferenceComponent;
2325
import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent;
@@ -117,8 +119,25 @@ private static synchronized void expandValueSet(String valueSetUri) {
117119
ResponseBody body = response.body();
118120
if (body != null) {
119121
IParser parser = FhirR4.getContext().newJsonParser();
120-
ValueSet valueSet = (ValueSet) parser.parseResource(body.charStream());
121-
loadValueSet(valueSetUri, valueSet);
122+
Resource resource = (Resource) parser.parseResource(body.charStream());
123+
if (resource instanceof ValueSet) {
124+
loadValueSet(valueSetUri, (ValueSet)resource);
125+
} else if (resource instanceof OperationOutcome) {
126+
OperationOutcome oo = (OperationOutcome)resource;
127+
parser.setPrettyPrint(true);
128+
System.err.println(parser.encodeResourceToString(oo));
129+
String details = oo.getIssueFirstRep().getDetails().getText();
130+
131+
throw new RuntimeException(
132+
"Received OperationOutcome in ValueSet expand response. Detail: "
133+
+ details + ". See log for full resource");
134+
} else {
135+
parser.setPrettyPrint(true);
136+
System.err.println(parser.encodeResourceToString(resource));
137+
throw new RuntimeException(
138+
"Unexpected resourceType received in expand ValueSet response: "
139+
+ resource.getResourceType() + ". See log for full resource");
140+
}
122141
} else {
123142
throw new RuntimeException("Value Set Expansion contained no body");
124143
}

src/main/java/org/mitre/synthea/helpers/Utilities.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package org.mitre.synthea.helpers;
22

3+
import ca.uhn.fhir.parser.IParser;
4+
5+
import com.fasterxml.jackson.core.JsonProcessingException;
6+
import com.fasterxml.jackson.databind.ObjectMapper;
37
import com.google.common.base.Charsets;
48
import com.google.common.io.Resources;
59
import com.google.gson.FieldNamingPolicy;
@@ -36,9 +40,11 @@
3640
import java.util.regex.Pattern;
3741

3842
import org.apache.commons.lang3.Range;
43+
import org.hl7.fhir.r4.model.Resource;
3944
import org.mitre.synthea.engine.Logic;
4045
import org.mitre.synthea.engine.Module;
4146
import org.mitre.synthea.engine.State;
47+
import org.mitre.synthea.export.FhirR4;
4248
import org.mitre.synthea.world.concepts.HealthRecord.Code;
4349

4450
public class Utilities {
@@ -669,4 +675,36 @@ public static void enableReadingURIFromJar(URI uri) throws IOException {
669675
}
670676
}
671677
}
678+
679+
/**
680+
* Helper method to parse FHIR resources from YAML.
681+
* This is a workaround since the FHIR model classes don't work with our YAML parser.
682+
*
683+
* @param <T> Resource type contained in the YAML
684+
* @param yaml List of pre-parsed YAML as Map&lt;String, Object&gt;
685+
* @param resourceClass Specific resource class, must not be Resource
686+
* @return List of parsed resources
687+
* @throws JsonProcessingException (should never happen)
688+
*/
689+
public static <T extends Resource> List<T> parseYamlToResources(
690+
List<Map<String, Object>> yaml, Class<T> resourceClass)
691+
throws JsonProcessingException {
692+
if (yaml.isEmpty()) {
693+
return Collections.emptyList();
694+
}
695+
ObjectMapper jsonMapper = new ObjectMapper();
696+
IParser jsonParser = FhirR4.getContext().newJsonParser();
697+
List<T> results = new ArrayList<>();
698+
for (Map<String, Object> singleYaml : yaml) {
699+
if (!singleYaml.containsKey("resourceType")) {
700+
// allows the YAML to be cleaner by letting the resourceType be implied
701+
singleYaml.put("resourceType", resourceClass.getSimpleName());
702+
}
703+
String resourceJson = jsonMapper.writeValueAsString(singleYaml);
704+
@SuppressWarnings("unchecked")
705+
T resource = (T) jsonParser.parseResource(resourceJson);
706+
results.add(resource);
707+
}
708+
return results;
709+
}
672710
}

src/test/java/org/mitre/synthea/export/flexporter/ActionsTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,15 @@
5454
import org.hl7.fhir.r4.model.ServiceRequest;
5555
import org.hl7.fhir.r4.model.TimeType;
5656
import org.hl7.fhir.r4.model.Type;
57+
import org.hl7.fhir.r4.model.ValueSet;
58+
import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent;
5759
import org.junit.AfterClass;
5860
import org.junit.BeforeClass;
5961
import org.junit.Test;
6062
import org.mitre.synthea.engine.Module;
6163
import org.mitre.synthea.engine.State;
6264
import org.mitre.synthea.export.FhirR4;
65+
import org.mitre.synthea.helpers.RandomCodeGenerator;
6366
import org.mitre.synthea.world.agents.Person;
6467

6568
public class ActionsTest {
@@ -82,6 +85,7 @@ public static void setupClass() throws FileNotFoundException {
8285
File file = new File(classLoader.getResource("flexporter/test_mapping.yaml").getFile());
8386

8487
testMapping = Mapping.parseMapping(file);
88+
testMapping.loadValueSets();
8589
}
8690

8791
@AfterClass
@@ -817,4 +821,34 @@ public void testGetAttribute() throws Exception {
817821
assertEquals("Robert Rainbow", name.getText());
818822
}
819823

824+
@Test
825+
public void testRandomCode() {
826+
Bundle b = new Bundle();
827+
b.setType(BundleType.COLLECTION);
828+
829+
Map<String, Object> action = getActionByName("testRandomCode");
830+
Actions.applyAction(b, action, null, null);
831+
832+
Encounter e = (Encounter) b.getEntryFirstRep().getResource();
833+
834+
Encounter.EncounterStatus status = e.getStatus();
835+
assertNotNull(status);
836+
assertTrue(status == Encounter.EncounterStatus.PLANNED
837+
|| status == Encounter.EncounterStatus.FINISHED
838+
|| status == Encounter.EncounterStatus.CANCELLED);
839+
840+
Coding encClass = e.getClass_();
841+
assertNotNull(encClass);
842+
assertEquals("http://terminology.hl7.org/CodeSystem/v3-ActCode", encClass.getSystem());
843+
String code = encClass.getCode();
844+
assertTrue(code.equals("AMB") || code.equals("EMER") || code.equals("ACUTE"));
845+
846+
CodeableConcept type = e.getTypeFirstRep();
847+
assertNotNull(type);
848+
Coding typeCoding = type.getCodingFirstRep();
849+
assertNotNull(typeCoding);
850+
assertEquals("http://terminology.hl7.org/CodeSystem/encounter-type", typeCoding.getSystem());
851+
code = typeCoding.getCode();
852+
assertTrue(code.equals("ADMS") || code.equals("OKI"));
853+
}
820854
}

src/test/resources/flexporter/test_mapping.yaml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,49 @@ name: Random Testing
66
# for now the assumption is 1 file = 1 synthea patient bundle.
77
applicability: true
88

9+
# Not a huge fan of this format, but it's better than defining yet another custom syntax
10+
customValueSets:
11+
- url: whats-for-dinner
12+
compose:
13+
include:
14+
- system: http://snomed.info/sct
15+
concept:
16+
- code: 227360002
17+
display: Pinto beans (substance)
18+
- code: 227319009
19+
display: Baked beans canned in tomato sauce with burgers (substance)
20+
- url: http://example.org/encounterStatus
21+
compose:
22+
include:
23+
- system: http://hl7.org/fhir/encounter-status
24+
concept:
25+
- code: planned
26+
display: Planned
27+
- code: finished
28+
display: Finished
29+
- code: cancelled
30+
display: Cancelled
31+
- url: http://example.org/encounterClass
32+
compose:
33+
include:
34+
- system: http://terminology.hl7.org/CodeSystem/v3-ActCode
35+
concept:
36+
- code: AMB
37+
display: ambulatory
38+
- code: EMER
39+
display: emergency
40+
- code: ACUTE
41+
display: inpatient acute
42+
- url: http://example.org/encounterType
43+
compose:
44+
include:
45+
- system: http://terminology.hl7.org/CodeSystem/encounter-type
46+
concept:
47+
- code: ADMS
48+
display: Annual diabetes mellitus screening
49+
- code: OKI
50+
display: Outpatient Kenacort injection
51+
952
actions:
1053
- name: Apply Profiles
1154
# v1: define specific profiles and an applicability statement on when to apply them
@@ -280,6 +323,17 @@ actions:
280323
location: ServiceRequest.authoredOn
281324
value: $getField([Procedure.performed]) # datetime choice type
282325

326+
- name: testRandomCode
327+
create_resource:
328+
- resourceType: Encounter
329+
fields:
330+
- location: Encounter.status
331+
value: $randomCode([http://example.org/encounterStatus,code])
332+
- location: Encounter.class
333+
value: $randomCode([http://example.org/encounterClass])
334+
- location: Encounter.type
335+
value: $randomCode([http://example.org/encounterType,CodeableConcept])
336+
283337

284338
- name: testExecuteScript
285339
execute_script:

0 commit comments

Comments
 (0)