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

Add implementation for resolve() missing from the HapiFhir library #984

Merged
merged 17 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from 16 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
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,10 +13,22 @@ 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() {}

/**
* 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;
}
basiliskus marked this conversation as resolved.
Show resolved Hide resolved

public static HapiFhirImplementation getInstance() {
return INSTANCE;
}
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great tests!

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
Loading