diff --git a/etor/src/main/resources/rule_definitions.json b/etor/src/main/resources/rule_definitions.json index a6a381e4b..93c2ebb3e 100644 --- a/etor/src/main/resources/rule_definitions.json +++ b/etor/src/main/resources/rule_definitions.json @@ -1,6 +1,6 @@ { "rules": [ { - "name": "requiredReceiverId", + "name": "messageHasRequiredReceiverId", "description": "Message has required receiver id", "violationMessage": "Message doesn't have required receiver id", "conditions": [ ], @@ -9,7 +9,7 @@ ] }, { - "name": "requiredReceiverId", + "name": "ORMHasRequiredCardNumber", "description": "ORM has required Card Number", "violationMessage": "ORM doesn't have required Card Number", "conditions": [ diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleEngineIntegrationTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleEngineIntegrationTest.groovy index 42f0731cd..1443cd964 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleEngineIntegrationTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleEngineIntegrationTest.groovy @@ -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() @@ -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 ruleConditions, List ruleValidations) { + return new ValidationRule( + name: "Rule name", + description: "Rule description", + violationMessage: "Rule warning message", + conditions: ruleConditions, + validations: ruleValidations, + ) + } + + List 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)) } } diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirCustomEvaluationContext.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirCustomEvaluationContext.java new file mode 100644 index 000000000..509307d6b --- /dev/null +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirCustomEvaluationContext.java @@ -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); + } +} diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirImplementation.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirImplementation.java index 09ed6b310..d0ae43926 100644 --- a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirImplementation.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirImplementation.java @@ -13,7 +13,8 @@ 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() {} @@ -21,6 +22,17 @@ 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 parseResource( final String fhirResource, final Class clazz) throws FhirParseException { diff --git a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirCustomEvaluationContextTest.groovy b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirCustomEvaluationContextTest.groovy new file mode 100644 index 000000000..1e5db9af2 --- /dev/null +++ b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirCustomEvaluationContextTest.groovy @@ -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 + } +} diff --git a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirImplementationTest.groovy b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirImplementationTest.groovy index c22bc2518..d388ab047 100644 --- a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirImplementationTest.groovy +++ b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirImplementationTest.groovy @@ -75,7 +75,7 @@ class HapiFhirImplementationTest extends Specification { result == false } - def "evaluateCondition throws FhirPathExecutionException on empty string"() { + def "evaluateCondition throws Exception on empty string"() { given: def path = "" @@ -83,10 +83,10 @@ class HapiFhirImplementationTest extends Specification { 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')" @@ -94,7 +94,7 @@ class HapiFhirImplementationTest extends Specification { fhir.evaluateCondition(bundle as IBaseResource, path) then: - thrown(FhirPathExecutionException) + thrown(Exception) } def "parseResource can convert a valid string to Bundle"() {