Skip to content

Commit

Permalink
Add implementation for resolve() missing from the HapiFhir library (#…
Browse files Browse the repository at this point in the history
…984)

* Update HapiFhirImplementation.java

Testing different engine implementation to support resolve

* Create HapiFhirCustomEvaluationContext.java

adding dummy file

* Added resolveReference implementation

* Fixed tests

* Adding docs

* Added tests and method that goes through all fhir files in examples/test

* Fixed rules names

* Added integration tests

* Added comment

* Updated comment

* Create HapiFhirCustomEvaluationContextTest.groovy

* Update HapiFhirCustomEvaluationContext.java

Handle reference resolving if the input is provided but not a reference

* Moved method based on feedback

---------

Co-authored-by: Basilio Bogado <[email protected]>
  • Loading branch information
luis-pabon-tf and basiliskus authored Mar 29, 2024
1 parent 0880620 commit d1b2375
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 13 deletions.
4 changes: 2 additions & 2 deletions etor/src/main/resources/rule_definitions.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"rules": [ {
"name": "requiredReceiverId",
"name": "messageHasRequiredReceiverId",
"description": "Message has required receiver id",
"violationMessage": "Message doesn't have required receiver id",
"conditions": [ ],
Expand All @@ -9,7 +9,7 @@
]
},
{
"name": "requiredReceiverId",
"name": "ORMHasRequiredCardNumber",
"description": "ORM has required Card Number",
"violationMessage": "ORM doesn't have required Card Number",
"conditions": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@ import java.nio.file.Files
import java.nio.file.Path

class RuleEngineIntegrationTest extends Specification {
def testExampleFilesPath = "../examples/Test"
def fhir = HapiFhirImplementation.getInstance()
def engine = RuleEngine.getInstance()
def mockLogger = Mock(Logger)

String fhirBody

def setup() {
TestApplicationContext.reset()
TestApplicationContext.init()
Expand All @@ -33,15 +32,91 @@ class RuleEngineIntegrationTest extends Specification {
TestApplicationContext.injectRegisteredImplementations()
}

def "validation logs a warning when a validation fails"() {
def "validation logs a warning when at least one validation fails"() {
given:
fhirBody = Files.readString(Path.of("../examples/Test/Orders/001_OML_O21_short.fhir"))
def bundle = fhir.parseResource(fhirBody, Bundle)
def bundle = new Bundle()

when:
engine.validate(new HapiFhirResource(bundle))

then:
1 * mockLogger.logWarning(_ as String)
(1.._) * mockLogger.logWarning(_ as String)
}

def "validation doesn't break for any of the sample test messages"() {
given:
def exampleFhirResources = getExampleFhirResources("Orders")

when:
exampleFhirResources.each { resource ->
engine.validate(resource)
}

then:
noExceptionThrown()
}

def "validation rule with resolve() works as expected"() {
given:
def fhirResource = getExampleFhirResource("Orders/001_OML_O21_short.fhir")
def validation = "Bundle.entry.resource.ofType(MessageHeader).focus.resolve().category.exists()"
def rule = createValidationRule([], [validation])
when:
def applies = rule.isValid(fhirResource)
then:
applies
}
def "validation rules pass for test files"() {
given:
def fhirResource = getExampleFhirResource(testFile)
def rule = createValidationRule([], [validation])
expect:
rule.isValid(fhirResource)
where:
testFile | validation
"Orders/001_OML_O21_short.fhir" | "Bundle.entry.resource.ofType(MessageHeader).focus.resolve().category.exists()"
"Orders/003_AL_ORM_O01_NBS_Fully_Populated_1_hl7_translation.fhir" | "Bundle.entry.resource.ofType(MessageHeader).destination.receiver.resolve().identifier.value.exists()"
// Once we fix the mapping for ORM from story #900 and update the FHIR files in /examples/Test/Orders, we can uncomment the below line
// "Orders/003_AL_ORM_O01_NBS_Fully_Populated_1_hl7_translation.fhir" | "Bundle.entry.resource.ofType(Observation).where(code.coding.code = '57723-9').value.coding.code.exists()"
}
def "validation rules fail for test files"() {
given:
def fhirResource = getExampleFhirResource(testFile)
def rule = createValidationRule([], [validation])
expect:
!rule.isValid(fhirResource)
where:
testFile | validation
"Orders/001_OML_O21_short.fhir" | "Bundle.entry.resource.ofType(MessageHeader).destination.receiver.resolve().identifier.value.exists()"
"Orders/001_OML_O21_short.fhir" | "Bundle.entry.resource.ofType(Observation).where(code.coding.code = '57723-9').value.coding.code.exists()"
}
Rule createValidationRule(List<String> ruleConditions, List<String> ruleValidations) {
return new ValidationRule(
name: "Rule name",
description: "Rule description",
violationMessage: "Rule warning message",
conditions: ruleConditions,
validations: ruleValidations,
)
}
List<HapiFhirResource> getExampleFhirResources(String messageType = "") {
return Files.walk(Path.of(testExampleFilesPath, messageType))
.filter { it.toString().endsWith(".fhir") }
.map { new HapiFhirResource(fhir.parseResource(Files.readString(it), Bundle)) }
.collect()
}
HapiFhirResource getExampleFhirResource(String relativeFilePath) {
return new HapiFhirResource(fhir.parseResource(Files.readString(Path.of(testExampleFilesPath, relativeFilePath)), Bundle))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package gov.hhs.cdc.trustedintermediary.external.hapi;

import ca.uhn.fhir.fhirpath.IFhirPathEvaluationContext;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Reference;

public class HapiFhirCustomEvaluationContext implements IFhirPathEvaluationContext {

/**
* When a FHIR path includes the "resolve()" method, this function is called to parse that into
* a Resource.
*
* @param theReference Id-based reference to the resource we're attempting to resolve.
* @param theContext Internally converted resource version of theReference.
* @return Reference resource if available, else null.
*/
@Override
public IBase resolveReference(@Nonnull IIdType theReference, @Nullable IBase theContext) {
if (theContext != null) {
if (theContext.getClass() == Reference.class) {
return ((Reference) theContext).getResource();
}
return theContext;
}
return IFhirPathEvaluationContext.super.resolveReference(theReference, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,26 @@ public class HapiFhirImplementation implements HapiFhir {

private static final HapiFhirImplementation INSTANCE = new HapiFhirImplementation();
private static final FhirContext CONTEXT = FhirContext.forR4();
private static final IFhirPath PATH_ENGINE = CONTEXT.newFhirPath();

private static final IFhirPath PATH_ENGINE = createEngine();

private HapiFhirImplementation() {}

public static HapiFhirImplementation getInstance() {
return INSTANCE;
}

/**
* Creates FHIRPath engine with a custom evaluation context.
*
* @return Configured engine.
*/
private static IFhirPath createEngine() {
var engine = CONTEXT.newFhirPath();
engine.setEvaluationContext(new HapiFhirCustomEvaluationContext());
return engine;
}

@Override
public <T extends IBaseResource> T parseResource(
final String fhirResource, final Class<T> clazz) throws FhirParseException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package gov.hhs.cdc.trustedintermediary.external.hapi

import ca.uhn.fhir.model.primitive.IdDt
import org.hl7.fhir.r4.model.Organization
import org.hl7.fhir.r4.model.Reference
import spock.lang.Specification

class HapiFhirCustomEvaluationContextTest extends Specification {
def context = new HapiFhirCustomEvaluationContext()

def "resolveReference returns null if the context is NOT provided"() {
when:
def result = context.resolveReference(new IdDt(), null)

then:
result == null
}

def "resolveReference attempts to get a resource if the context is provided and is a reference"() {
given:
def refId = new IdDt()
def org = new Organization()
def theContext = new Reference(org)

when:
def result = context.resolveReference(refId, theContext)

then:
result == org
}

def "resolveReference returns the given context if it's NOT a reference"() {
given:
def refId = new IdDt()
def org = new Organization()

when:
def result = context.resolveReference(refId, org)

then:
result == org
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,26 +75,26 @@ class HapiFhirImplementationTest extends Specification {
result == false
}

def "evaluateCondition throws FhirPathExecutionException on empty string"() {
def "evaluateCondition throws Exception on empty string"() {
given:
def path = ""

when:
fhir.evaluateCondition(bundle as IBaseResource, path)

then:
thrown(FhirPathExecutionException)
thrown(Exception)
}

def "evaluateCondition throws FhirPathExecutionException on fake method"() {
def "evaluateCondition throws Exception on fake method"() {
given:
def path = "Bundle.entry[0].resource.BadMethod('blah')"

when:
fhir.evaluateCondition(bundle as IBaseResource, path)

then:
thrown(FhirPathExecutionException)
thrown(Exception)
}

def "parseResource can convert a valid string to Bundle"() {
Expand Down

0 comments on commit d1b2375

Please sign in to comment.