Skip to content

Commit

Permalink
CqlFhirParametersConverter EmptyList and Null support (#597)
Browse files Browse the repository at this point in the history
* Handle Empty Lists and Null EvaluationResults in CqlFhirParametersConverter

* Ran spotless

* Make types version independent

* Fix author ext being added to item instead of responseItem

* Perf fixes

* Remove uneeded cast

* Remove duplicate code

* Debug CI failure

* Debug

* Sorted paths so behavior is consistent between machines

---------

Co-authored-by: Jonathan Percival <jonathan.i.percival@gmail.com>
Co-authored-by: Brenin Rhodes <brenin@alphora.com>
Co-authored-by: JP <jonathan.percival@smilecdr.com>
  • Loading branch information
4 people authored Dec 3, 2024
1 parent 3916f70 commit fb1b126
Show file tree
Hide file tree
Showing 10 changed files with 335 additions and 184 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static java.util.Objects.requireNonNull;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
Expand All @@ -11,8 +12,10 @@
import java.util.Optional;
import java.util.stream.Collectors;
import org.hl7.fhir.instance.model.api.IBaseBackboneElement;
import org.hl7.fhir.instance.model.api.IBaseBooleanDatatype;
import org.hl7.fhir.instance.model.api.IBaseDatatype;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
Expand All @@ -36,6 +39,9 @@ public class CqlFhirParametersConverter {
// private IFhirPath fhirPath;
private ModelResolver modelResolver;

/*
* Converts both CQL parameters and CQL Evaluation Results into Fhir Parameters Resources
*/
public CqlFhirParametersConverter(
FhirContext fhirContext, IAdapterFactory adapterFactory, FhirTypeConverter fhirTypeConverter) {
this.fhirContext = requireNonNull(fhirContext);
Expand All @@ -47,6 +53,42 @@ public CqlFhirParametersConverter(
// this.fhirPath = FhirPathCache.cachedForContext(fhirContext);
}

// This is basically a copy and paste from R4FhirTypeConverter, but it's not exposed.
static final String EMPTY_LIST_EXT_URL = "http://hl7.org/fhir/StructureDefinition/cqf-isEmptyList";
static final String DATA_ABSENT_REASON_EXT_URL = "http://hl7.org/fhir/StructureDefinition/data-absent-reason";
static final String DATA_ABSENT_REASON_UNKNOWN_CODE = "unknown";

private static IBaseBooleanDatatype booleanType(FhirContext context, Boolean value) {
try {
return (IBaseBooleanDatatype) context.getElementDefinition("Boolean")
.getImplementingClass()
.getDeclaredConstructor(Boolean.class)
.newInstance(value);
} catch (Exception e) {
throw new InternalErrorException("error creating BooleanType", e);
}
}

private static IBaseDatatype codeType(FhirContext context, String value) {
try {
return (IBaseDatatype) context.getElementDefinition("Code")
.getImplementingClass()
.getDeclaredConstructor(String.class)
.newInstance(value);
} catch (Exception e) {
throw new InternalErrorException("error creating CodeType", e);
}
}

private static IBaseBooleanDatatype emptyBooleanWithExtension(
FhirContext context, String url, IBaseDatatype value) {
var result = booleanType(context, null);
var ext = ((IBaseHasExtensions) result).addExtension();
ext.setUrl(url);
ext.setValue(value);
return result;
}

public IBaseParameters toFhirParameters(EvaluationResult evaluationResult) {
IBaseParameters params = null;
try {
Expand All @@ -67,11 +109,23 @@ public IBaseParameters toFhirParameters(EvaluationResult evaluationResult) {
Object value = entry.getValue().value();

if (value == null) {
this.addPart(pa, name);
// Null value, add a single empty value with an extension indicating the reason
var dataAbsentValue = emptyBooleanWithExtension(
fhirContext,
DATA_ABSENT_REASON_EXT_URL,
codeType(fhirContext, DATA_ABSENT_REASON_UNKNOWN_CODE));
addPart(pa, name, dataAbsentValue);
continue;
}

if (value instanceof Iterable) {
var iterable = (Iterable<?>) value;
if (!iterable.iterator().hasNext()) {
// Empty list
var emptyListValue =
emptyBooleanWithExtension(fhirContext, EMPTY_LIST_EXT_URL, booleanType(fhirContext, true));
addPart(pa, name, emptyListValue);
}
Iterable<?> values = (Iterable<?>) value;
for (Object o : values) {
this.addPart(pa, name, o);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.ParameterDefinition;
Expand Down Expand Up @@ -59,6 +61,41 @@ void TestEvaluationResultToParameters() {
assertTrue(expected.equalsDeep(actual));
}

@Test
void TestEvaluationResultToEmptyListParameters() {
Parameters expected = new Parameters();
expected.addParameter().setName("Patient").setResource(new Patient());
BooleanType nullBooleanValue = new BooleanType((String) null);
nullBooleanValue.addExtension("http://hl7.org/fhir/StructureDefinition/cqf-isEmptyList", new BooleanType(true));
expected.addParameter().setName("Encounters").setValue(nullBooleanValue);

EvaluationResult testData = new EvaluationResult();
testData.expressionResults.put("Patient", new ExpressionResult(new Patient(), null));
testData.expressionResults.put("Encounters", new ExpressionResult(Collections.emptyList(), null));

Parameters actual = (Parameters) cqlFhirParametersConverter.toFhirParameters(testData);

assertTrue(expected.equalsDeep(actual));
}

@Test
void TestEvaluationResultNullParameters() {
Parameters expected = new Parameters();
expected.addParameter().setName("Patient").setResource(new Patient());
BooleanType nullBooleanValue = new BooleanType((String) null);
nullBooleanValue.addExtension(
"http://hl7.org/fhir/StructureDefinition/data-absent-reason", new CodeType("unknown"));
expected.addParameter().setName("Null").setValue(nullBooleanValue);

EvaluationResult testData = new EvaluationResult();
testData.expressionResults.put("Patient", new ExpressionResult(new Patient(), null));
testData.expressionResults.put("Null", new ExpressionResult(null, null));

Parameters actual = (Parameters) cqlFhirParametersConverter.toFhirParameters(testData);

assertTrue(expected.equalsDeep(actual));
}

@Test
void TestFhirParametersToCqlParameters() {
Map<String, Object> expected = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public ProcessItem(ExpressionProcessor expressionProcessor) {

public IBaseBackboneElement processItem(PopulateRequest request, IBaseBackboneElement item) {
final var responseItem = createResponseItem(request.getFhirVersion(), item);
populateAnswer(request, responseItem, getInitialValue(request, item));
populateAnswer(request, responseItem, getInitialValue(request, item, responseItem));
return responseItem;
}

Expand All @@ -44,15 +44,16 @@ protected void populateAnswer(PopulateRequest request, IBaseBackboneElement resp
request.getModelResolver().setValue(responseItem, "answer", answers);
}

protected List<IBase> getInitialValue(PopulateRequest request, IBaseBackboneElement item) {
protected List<IBase> getInitialValue(
PopulateRequest request, IBaseBackboneElement item, IBaseBackboneElement responseItem) {
List<IBase> results;
var expression = expressionProcessor.getItemInitialExpression(request, item);
if (expression != null) {
var itemLinkId = request.getItemLinkId(item);
try {
results = expressionProcessor.getExpressionResultForItem(request, expression, itemLinkId);
if (results != null && !results.isEmpty()) {
addAuthorExtension(request, item);
addAuthorExtension(request, responseItem);
}
} catch (Exception e) {
var message = String.format(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ IBaseBackboneElement createResponseContextItem(
if (extension != null) {
// pass the context resource(s) as a parameter to the evaluation
request.addContextParameter("%" + contextName, context);
populateAnswer(request, responseItem, getInitialValue(request, item));
populateAnswer(request, responseItem, getInitialValue(request, item, responseItem));
}
}
return responseItem;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ void evaluateDstu3() {
.libraryId("OutpatientPriorAuthorizationPrepopulation")
.subjectId("OPA-Patient1")
.thenEvaluate()
.hasResults(47);
.hasResults(48);
}

@Test
Expand All @@ -137,7 +137,7 @@ void evaluateR4() {
.libraryId("OutpatientPriorAuthorizationPrepopulation")
.subjectId("OPA-Patient1")
.thenEvaluate()
.hasResults(50);
.hasResults(51);
}

@Test
Expand All @@ -147,7 +147,7 @@ void evaluateR5() {
.libraryId("OutpatientPriorAuthorizationPrepopulation")
.subjectId("OPA-Patient1")
.thenEvaluate()
.hasResults(48);
.hasResults(49);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.hl7.fhir.instance.model.api.IBaseBackboneElement;
import org.hl7.fhir.r4.model.Questionnaire;
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent;
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -132,11 +133,14 @@ void getExpressionResultsShouldReturnEmptyListIfInitialExpressionIsNull() {
final PopulateRequest prePopulateRequest =
newPopulateRequestForVersion(FhirVersionEnum.R4, libraryEngine, questionnaire);
final QuestionnaireItemComponent questionnaireItemComponent = new QuestionnaireItemComponent();
final QuestionnaireResponseItemComponent questionnaireResponseItemComponent =
new QuestionnaireResponseItemComponent();
doReturn(null)
.when(expressionProcessor)
.getItemInitialExpression(prePopulateRequest, questionnaireItemComponent);
// execute
final List<IBase> actual = processItem.getInitialValue(prePopulateRequest, questionnaireItemComponent);
final List<IBase> actual = processItem.getInitialValue(
prePopulateRequest, questionnaireItemComponent, questionnaireResponseItemComponent);
// validate
assertTrue(actual.isEmpty());
verify(expressionProcessor).getItemInitialExpression(prePopulateRequest, questionnaireItemComponent);
Expand All @@ -153,6 +157,8 @@ void getExpressionResultsShouldReturnListOfResourcesIfInitialExpressionIsNotNull
newPopulateRequestForVersion(FhirVersionEnum.R4, libraryEngine, questionnaire);
final QuestionnaireItemComponent questionnaireItemComponent = new QuestionnaireItemComponent();
questionnaireItemComponent.setLinkId("linkId");
final QuestionnaireResponseItemComponent questionnaireResponseItemComponent =
new QuestionnaireResponseItemComponent();
final CqfExpression expression = withExpression();
doReturn(expression)
.when(expressionProcessor)
Expand All @@ -161,7 +167,8 @@ void getExpressionResultsShouldReturnListOfResourcesIfInitialExpressionIsNotNull
.when(expressionProcessor)
.getExpressionResultForItem(prePopulateRequest, expression, "linkId");
// execute
final List<IBase> actual = processItem.getInitialValue(prePopulateRequest, questionnaireItemComponent);
final List<IBase> actual = processItem.getInitialValue(
prePopulateRequest, questionnaireItemComponent, questionnaireResponseItemComponent);
// validate
assertEquals(expected, actual);
verify(expressionProcessor).getItemInitialExpression(prePopulateRequest, questionnaireItemComponent);
Expand Down
Loading

0 comments on commit fb1b126

Please sign in to comment.