Skip to content

Commit

Permalink
Merge pull request #1422 from synthetichealth/flexporter_lists
Browse files Browse the repository at this point in the history
Better handling of lists in Flexporter
  • Loading branch information
jawalonoski authored Mar 29, 2024
2 parents 94ca67d + 47fc1a9 commit 7185ded
Show file tree
Hide file tree
Showing 5 changed files with 375 additions and 13 deletions.
37 changes: 34 additions & 3 deletions src/main/java/org/mitre/synthea/export/flexporter/Actions.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -417,7 +417,8 @@ private static Map<String, Object> createFhirPathMapping(List<Map<String, Object
Bundle sourceBundle, Resource sourceResource, Person person,
FlexporterJavascriptContext fjContext) {

Map<String, Object> fhirPathMapping = new HashMap<>();
// linked hashmap to ensure lists are kept in order. could also use something like a treemap
Map<String, Object> fhirPathMapping = new LinkedHashMap<>();

for (Map<String, Object> field : fields) {
String location = (String)field.get("location");
Expand Down Expand Up @@ -458,6 +459,10 @@ private static Map<String, Object> createFhirPathMapping(List<Map<String, Object

populateFhirPathMapping(fhirPathMapping, location, valueMap);

} else if (valueDef instanceof List<?>) {
List<Object> valueList = (List<Object>) valueDef;

populateFhirPathMapping(fhirPathMapping, location, valueList);
} else {
// unexpected type here - is it even possible to get anything else?
String type = valueDef == null ? "null" : valueDef.getClass().toGenericString();
Expand All @@ -478,11 +483,37 @@ private static void populateFhirPathMapping(Map<String, Object> fhirPathMapping,

if (value instanceof String) {
fhirPathMapping.put(path, value);
} else if (value instanceof Number) {
fhirPathMapping.put(path, value.toString());
} else if (value instanceof Map<?,?>) {
populateFhirPathMapping(fhirPathMapping, path, (Map<String, Object>) value);
} else if (value instanceof List<?>) {
populateFhirPathMapping(fhirPathMapping, path, (List<Object>) value);
} else if (value != null) {
System.err
.println("Unexpected class found in populateFhirPathMapping[map]: " + value.getClass());
}
}
}

private static void populateFhirPathMapping(Map<String, Object> fhirPathMapping, String basePath,
List<Object> valueList) {
for (int i = 0; i < valueList.size(); i++) {
Object value = valueList.get(i);

String path = basePath + "[" + i + "]";

if (value instanceof String) {
fhirPathMapping.put(path, value);
} else if (value instanceof Number) {
fhirPathMapping.put(path, value.toString());
} else if (value instanceof Map<?,?>) {
populateFhirPathMapping(fhirPathMapping, path, (Map<String, Object>) value);
} else if (value instanceof List<?>) {
populateFhirPathMapping(fhirPathMapping, path, (List<Object>) value);
} else if (value != null) {
System.err
.println("Unexpected class found in populateFhirPathMapping -- " + value.getClass());
.println("Unexpected class found in populateFhirPathMapping[list]:" + value.getClass());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext;
import org.hl7.fhir.r4.model.ExpressionNode;
import org.hl7.fhir.r4.model.ExpressionNode.Kind;
import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.utils.FHIRPathEngine;
import org.mitre.synthea.export.FhirR4;
Expand Down Expand Up @@ -383,6 +384,10 @@ private void handleFunctionNode(ExpressionNode fhirPath) {
case Where:
this.handleWhereFunctionNode(fhirPath);
break;
case Item:
// eg, array indexing
this.handleIndexFunctionNode(fhirPath);
break;
// case Aggregate:
// case Alias:
// case AliasAs:
Expand Down Expand Up @@ -421,7 +426,6 @@ private void handleFunctionNode(ExpressionNode fhirPath) {
// case Intersect:
// case Is:
// case IsDistinct:
// case Item:
// case Last:
// case Length:
// case Lower:
Expand Down Expand Up @@ -461,6 +465,68 @@ private void handleFunctionNode(ExpressionNode fhirPath) {
}
}

private void handleIndexFunctionNode(ExpressionNode fhirPath) {
// treat this tier as the same as the parent,
// eg, the tier for Resource.field[0] is just the parent at Resource.field
// since that's where the child definitions are

GenerationTier parentTier = this.nodeStack.peek();
GenerationTier parentsParentTier = this.nodeStack.get(this.nodeStack.size() - 2);
GenerationTier nextTier = new GenerationTier();
// get the name of the FHIRPath for the next tier
nextTier.fhirPathName = parentTier.fhirPathName;
// get the child definition from the parent nodePefinition
nextTier.childDefinition = parentTier.childDefinition;
// create a nodeDefinition for the next tier
nextTier.nodeDefinition = parentTier.nodeDefinition;

int index = ((IntegerType)fhirPath.getParameters().get(0).getConstant()).getValue();

if (nextTier.nodeDefinition instanceof RuntimeCompositeDatatypeDefinition) {
RuntimeCompositeDatatypeDefinition compositeTarget =
(RuntimeCompositeDatatypeDefinition) nextTier.nodeDefinition;

// this could get wonky if, ex, someone sets only item 6 in an otherwise empty list,
// but we'll allow it

for (IBase nodeElement : parentsParentTier.nodes) {
List<IBase> containedNodes = nextTier.childDefinition.getAccessor().getValues(nodeElement);

while (containedNodes.size() <= index) {
ICompositeType compositeNode = compositeTarget
.newInstance(parentTier.childDefinition.getInstanceConstructorArguments());
parentTier.childDefinition.getMutator().addValue(nodeElement, compositeNode);
parentTier.nodes.add(compositeNode);
containedNodes = nextTier.childDefinition.getAccessor().getValues(nodeElement);
}

nextTier.nodes.add(containedNodes.get(index));
}
} else if (nextTier.nodeDefinition instanceof RuntimeResourceBlockDefinition) {
RuntimeResourceBlockDefinition compositeTarget =
(RuntimeResourceBlockDefinition) nextTier.nodeDefinition;

for (IBase nodeElement : parentsParentTier.nodes) {
List<IBase> containedNodes = nextTier.childDefinition.getAccessor().getValues(nodeElement);

while (containedNodes.size() <= index) {
IBase compositeNode = compositeTarget
.newInstance(nextTier.childDefinition.getInstanceConstructorArguments());
parentTier.childDefinition.getMutator().addValue(nodeElement, compositeNode);
parentTier.nodes.add(compositeNode);
containedNodes = nextTier.childDefinition.getAccessor().getValues(nodeElement);
}

nextTier.nodes.add(containedNodes.get(index));
}
}
// else if (nextTier.nodeDefinition instanceof RuntimePrimitiveDatatypeDefinition) {
// from testing this seems to not be necessary

// push the created nextTier to the nodeStack
this.nodeStack.push(nextTier);
}

/**
* Handles a function node of a `where`-function. Iterates through all params and handle where
* functions for primitive datatypes (others are not implemented and yield errors.)
Expand Down
172 changes: 171 additions & 1 deletion src/test/java/org/mitre/synthea/export/flexporter/ActionsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import ca.uhn.fhir.parser.IParser;

Expand All @@ -26,9 +27,13 @@
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.r4.model.Bundle.BundleType;
import org.hl7.fhir.r4.model.Claim;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.Enumeration;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Immunization;
Expand All @@ -37,7 +42,11 @@
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Period;
import org.hl7.fhir.r4.model.PositiveIntType;
import org.hl7.fhir.r4.model.PractitionerRole;
import org.hl7.fhir.r4.model.PractitionerRole.DaysOfWeek;
import org.hl7.fhir.r4.model.Procedure;
import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.ResourceType;
import org.hl7.fhir.r4.model.ServiceRequest;
Expand Down Expand Up @@ -294,6 +303,168 @@ public void testSetValues_object() {
assertEquals("Legally married (finding)", c.getDisplay());
}

@Test
public void testSetValues_where() {
testSetValues_encounter_common("testSetValues_where");
}

@Test
public void testSetValues_indexed() {
testSetValues_encounter_common("testSetValues_indexed");
}

@Test
public void testSetValues_list() {
testSetValues_encounter_common("testSetValues_list");
}

void testSetValues_encounter_common(String actionName) {
Map<String, Object> action = getActionByName(actionName);

Encounter e = new Encounter();
Bundle b = new Bundle();
b.addEntry().setResource(e);

Actions.applyAction(b, action, null, null);

assertNotNull(e.getServiceType());

CodeableConcept serviceType = e.getServiceType();
List<Coding> codings = serviceType.getCoding();
assertNotNull(codings);
assertEquals(2, codings.size());

boolean seen229 = false;
boolean seen355 = false;
for (Coding coding : codings) {
assertEquals("http://terminology.hl7.org/CodeSystem/service-type", coding.getSystem());
String code = coding.getCode();
if (code.equals("229")) {
if (seen229) {
fail("Code 229 seen multiple times in " + actionName);
}
seen229 = true;
} else if (code.equals("355")) {
if (seen355) {
fail("Code 355 seen multiple times in " + actionName);
}
seen355 = true;
} else {
fail("Unexpected code found in " + actionName + ": " + code);
}
}
assertTrue(seen229);
assertTrue(seen355);
}

@Test
public void testSetValues_listOfPrimitives() {
Map<String, Object> action = getActionByName("testSetValues_listOfPrimitives");

PractitionerRole pr = new PractitionerRole();
Claim c = new Claim();
Bundle b = new Bundle();
b.addEntry().setResource(pr);
b.addEntry().setResource(c);

Actions.applyAction(b, action, null, null);

assertEquals(1, pr.getAvailableTime().size());

List<Enumeration<DaysOfWeek>> daysOfWeek = pr.getAvailableTimeFirstRep().getDaysOfWeek();
assertEquals(3, daysOfWeek.size());

assertEquals(DaysOfWeek.WED, daysOfWeek.get(0).getValue());
assertEquals(DaysOfWeek.THU, daysOfWeek.get(1).getValue());
assertEquals(DaysOfWeek.FRI, daysOfWeek.get(2).getValue());

assertEquals(1, c.getItem().size());
Claim.ItemComponent item = c.getItemFirstRep();

List<PositiveIntType> diagnosisSequence = item.getDiagnosisSequence();
assertEquals(6, diagnosisSequence.size());
assertEquals(1, diagnosisSequence.get(0).getValue().intValue());
assertEquals(1, diagnosisSequence.get(1).getValue().intValue());
assertEquals(2, diagnosisSequence.get(2).getValue().intValue());
assertEquals(3, diagnosisSequence.get(3).getValue().intValue());
assertEquals(5, diagnosisSequence.get(4).getValue().intValue());
assertEquals(8, diagnosisSequence.get(5).getValue().intValue());
}

@Test
public void testSetValues_deeplyNested() {
Map<String, Object> action = getActionByName("testSetValues_deeplyNested");

Observation o = new Observation();
Bundle b = new Bundle();
b.addEntry().setResource(o);

Actions.applyAction(b, action, null, null);

CodeableConcept code = o.getCode();
assertNotNull(code);
assertEquals(1, code.getCoding().size());
Coding coding = code.getCodingFirstRep();

assertEquals("http://loinc.org", coding.getSystem());
assertEquals("85354-9", coding.getCode());
assertEquals("Blood pressure panel with all children optional", coding.getDisplay());

assertEquals("Blood pressure systolic & diastolic", code.getText());

assertEquals(2, o.getComponent().size());

Observation.ObservationComponentComponent systolic = o.getComponent().get(0);

code = systolic.getCode();
assertNotNull(code);
assertEquals(3, code.getCoding().size());

coding = code.getCoding().get(0);

assertEquals("http://loinc.org", coding.getSystem());
assertEquals("8480-6", coding.getCode());
assertEquals("Systolic blood pressure", coding.getDisplay());

coding = code.getCoding().get(1);

assertEquals("http://snomed.info/sct", coding.getSystem());
assertEquals("271649006", coding.getCode());
assertEquals("Systolic blood pressure", coding.getDisplay());

coding = code.getCoding().get(2);

assertEquals("http://acme.org/devices/clinical-codes", coding.getSystem());
assertEquals("bp-s", coding.getCode());
assertEquals("Systolic Blood pressure", coding.getDisplay());


Quantity q = systolic.getValueQuantity();

assertEquals(107, q.getValue().intValue());
assertEquals("mmHg", q.getUnit());
assertEquals("http://unitsofmeasure.org", q.getSystem());
assertEquals("mm[Hg]", q.getCode());

Observation.ObservationComponentComponent diastolic = o.getComponent().get(1);
code = diastolic.getCode();
assertNotNull(code);
assertEquals(1, code.getCoding().size());

coding = code.getCoding().get(0);

assertEquals("http://loinc.org", coding.getSystem());
assertEquals("8462-4", coding.getCode());
assertEquals("Diastolic blood pressure", coding.getDisplay());

q = diastolic.getValueQuantity();

assertEquals(60, q.getValue().intValue());
assertEquals("mmHg", q.getUnit());
assertEquals("http://unitsofmeasure.org", q.getSystem());
assertEquals("mm[Hg]", q.getCode());
}

@Test
public void testKeepResources() throws Exception {
Bundle b = loadFixtureBundle("sample_complete_patient.json");
Expand Down Expand Up @@ -358,7 +529,6 @@ public void testDateFilter() throws Exception {
Actions.applyAction(b, action, null, null);

System.out.println(b.getEntry().size());

}

@Test
Expand Down
Loading

0 comments on commit 7185ded

Please sign in to comment.