Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flexporter updates - October 2024 #1525

Merged
merged 4 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 41 additions & 17 deletions src/main/java/org/mitre/synthea/export/flexporter/Actions.java
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,12 @@ private static Map<String, Object> createFhirPathMapping(List<Map<String, Object
String location = (String)field.get("location");
Object valueDef = field.get("value");
String transform = (String)field.get("transform");
String ifDef = (String)field.get("if");

if (ifDef != null && !FhirPathUtils.appliesToResource(sourceResource, ifDef)) {
// The "if" condition returned falsy, so don't evaluate this value/set this field
continue;
}

if (valueDef == null) {
// do nothing, leave it null
Expand Down Expand Up @@ -457,12 +463,15 @@ private static Map<String, Object> createFhirPathMapping(List<Map<String, Object
} else if (valueDef instanceof Map<?,?>) {
Map<String,Object> valueMap = (Map<String, Object>) valueDef;

populateFhirPathMapping(fhirPathMapping, location, valueMap);
populateFhirPathMapping(fhirPathMapping, location, valueMap, sourceBundle, sourceResource,
person, fjContext);

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

populateFhirPathMapping(fhirPathMapping, location, valueList);
populateFhirPathMapping(fhirPathMapping, location, valueList, sourceBundle, sourceResource,
person, fjContext);

} else {
// unexpected type here - is it even possible to get anything else?
String type = valueDef == null ? "null" : valueDef.getClass().toGenericString();
Expand All @@ -474,21 +483,32 @@ private static Map<String, Object> createFhirPathMapping(List<Map<String, Object
}

private static void populateFhirPathMapping(Map<String, Object> fhirPathMapping, String basePath,
Map<String, Object> valueMap) {
Map<String, Object> valueMap, Bundle sourceBundle, Resource sourceResource, Person person,
FlexporterJavascriptContext fjContext) {
for (Map.Entry<String,Object> entry : valueMap.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();

String path = basePath + "." + key;

if (value instanceof String) {
String valueString = (String)value;

if (valueString.startsWith("$")) {
value = getValue(sourceBundle, valueString, sourceResource, person, fjContext);
}
}

if (value instanceof String || value instanceof Base) {
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);
populateFhirPathMapping(fhirPathMapping, path, (Map<String, Object>) value, sourceBundle,
sourceResource, person, fjContext);
} else if (value instanceof List<?>) {
populateFhirPathMapping(fhirPathMapping, path, (List<Object>) value);
populateFhirPathMapping(fhirPathMapping, path, (List<Object>) value, sourceBundle,
sourceResource, person, fjContext);
} else if (value != null) {
System.err
.println("Unexpected class found in populateFhirPathMapping[map]: " + value.getClass());
Expand All @@ -497,20 +517,23 @@ private static void populateFhirPathMapping(Map<String, Object> fhirPathMapping,
}

private static void populateFhirPathMapping(Map<String, Object> fhirPathMapping, String basePath,
List<Object> valueList) {
List<Object> valueList, Bundle sourceBundle, Resource sourceResource, Person person,
FlexporterJavascriptContext fjContext) {
for (int i = 0; i < valueList.size(); i++) {
Object value = valueList.get(i);

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

if (value instanceof String) {
if (value instanceof String || value instanceof Base) {
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);
populateFhirPathMapping(fhirPathMapping, path, (Map<String, Object>) value, sourceBundle,
sourceResource, person, fjContext);
} else if (value instanceof List<?>) {
populateFhirPathMapping(fhirPathMapping, path, (List<Object>) value);
populateFhirPathMapping(fhirPathMapping, path, (List<Object>) value, sourceBundle,
sourceResource, person, fjContext);
} else if (value != null) {
System.err
.println("Unexpected class found in populateFhirPathMapping[list]:" + value.getClass());
Expand Down Expand Up @@ -649,13 +672,9 @@ private static void dateFilter(Bundle bundle, String minDateStr, String maxDateS
* Cascade (current), Delete reference field but leave object, Do nothing
*
* @param bundle FHIR Bundle to filter
* @param list List of resource types to delete, other types not listed will be kept
* @param list List of resource types or FHIRPath to delete, other types not listed will be kept
*/
public static void deleteResources(Bundle bundle, List<String> list) {
// TODO: make this FHIRPath instead of just straight resource types

Set<String> resourceTypesToDelete = new HashSet<>(list);

Set<String> deletedResourceIDs = new HashSet<>();

Iterator<BundleEntryComponent> itr = bundle.getEntry().iterator();
Expand All @@ -665,9 +684,14 @@ public static void deleteResources(Bundle bundle, List<String> list) {

Resource resource = entry.getResource();
String resourceType = resource.getResourceType().toString();
if (resourceTypesToDelete.contains(resourceType)) {
deletedResourceIDs.add(resource.getId());
itr.remove();

for (String applicability : list) {
if (applicability.equals(resourceType)
|| FhirPathUtils.appliesToResource(resource, applicability)) {
deletedResourceIDs.add(resource.getId());
itr.remove();
break;
}
}
}

Expand Down
61 changes: 51 additions & 10 deletions src/test/java/org/mitre/synthea/export/flexporter/ActionsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import ca.uhn.fhir.parser.IParser;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
Expand All @@ -30,6 +28,7 @@
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.Condition;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.Encounter;
Expand All @@ -46,6 +45,7 @@
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.Provenance;
import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.ResourceType;
Expand All @@ -60,6 +60,8 @@
import org.mitre.synthea.export.FhirR4;
import org.mitre.synthea.world.agents.Person;

import ca.uhn.fhir.parser.IParser;

public class ActionsTest {

private static Map<String, String> buildApplyProfileAction(String profile, String applicability) {
Expand Down Expand Up @@ -486,21 +488,34 @@ public void testKeepResources() throws Exception {

@Test
public void testDeleteResources() throws Exception {
Bundle b = loadFixtureBundle("sample_complete_patient.json");
Bundle b = new Bundle();
b.addEntry().setResource(new Provenance());

long countProvenance = b.getEntry().stream()
.filter(bec -> bec.getResource().getResourceType() == ResourceType.Provenance).count();
Condition c1 = new Condition();
c1.setCode(new CodeableConcept(new Coding("http://snomed.info/sct", "15777000", "Prediabetes")));
b.addEntry().setResource(c1);

Condition c2 = new Condition();
c2.setCode(new CodeableConcept(new Coding("http://snomed.info/sct", "444814009", "Viral sinusitis")));
b.addEntry().setResource(c2);

Condition c3 = new Condition();
c3.setCode(new CodeableConcept(new Coding("http://snomed.info/sct", "49727002", "Cough (finding)")));
b.addEntry().setResource(c3);

// this is just making sure the fixture actually contains the thing we want to delete
assertEquals(1, countProvenance);
Map<String, Object> action = getActionByName("testDeleteResources");

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

countProvenance = b.getEntry().stream()
.filter(bec -> bec.getResource().getResourceType() == ResourceType.Provenance).count();
// provenance, c1, and c2 should be deleted by the rule. c3 should stay
assertEquals(1, b.getEntry().size());

assertEquals(0, countProvenance);
Resource r = b.getEntryFirstRep().getResource();
// this would work as of today but not guaranteed
// assertTrue(r == c3);
assertTrue(r instanceof Condition);
Condition cOut = (Condition)r;
assertEquals("49727002", cOut.getCode().getCodingFirstRep().getCode());
}

@Test
Expand Down Expand Up @@ -752,6 +767,32 @@ public void testCreateResources_createBasedOnState() throws Exception {
assertEquals(120_000L, mr.getAuthoredOn().toInstant().toEpochMilli());
}

@Test
public void testCreateResources_if() throws Exception {
Bundle b = new Bundle();
b.setType(BundleType.COLLECTION);

Procedure p1 = new Procedure();
p1.setPerformed(new Period().setStart(new Date(0)));
b.addEntry().setResource(p1);

Procedure p2 = new Procedure();
p2.setPerformed(new DateTimeType("2024-10-01T12:34:56"));
b.addEntry().setResource(p2);

Map<String, Object> action = getActionByName("testCreateResources_if");

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

assertEquals(4, b.getEntry().size());

ServiceRequest sr1 = (ServiceRequest)b.getEntry().get(2).getResource();
assertTrue(sr1.getAuthoredOn().getTime() == 0);

ServiceRequest sr2 = (ServiceRequest)b.getEntry().get(3).getResource();
DateTimeType sr2AuthoredOn = sr2.getAuthoredOnElement();
assertEquals("2024-10-01T12:34:56", sr2AuthoredOn.getValueAsString());
}

@Test
public void testGetAttribute() throws Exception {
Expand Down
15 changes: 15 additions & 0 deletions src/test/resources/flexporter/test_mapping.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,20 @@ actions:
- location: Patient.name.family
value: $getAttribute([last_name])

- name: testCreateResources_if
create_resource:
- resourceType: ServiceRequest
based_on:
resource: Procedure
fields:
- if: Procedure.performed.ofType(Period)
location: ServiceRequest.authoredOn
value: $getField([Procedure.performed.start]) # period choice type
- if: Procedure.performed.ofType(dateTime)
location: ServiceRequest.authoredOn
value: $getField([Procedure.performed]) # datetime choice type


- name: testExecuteScript
execute_script:
- apply_to: bundle
Expand Down Expand Up @@ -300,6 +314,7 @@ actions:
- name: testDeleteResources
delete_resources:
- Provenance
- Condition.code.coding.where($this.code in ('15777000' | '444814009'))

- name: testDeleteResourcesCascade
delete_resources:
Expand Down
Loading