From c992bc0263b5ee6a9a09f7a318badf7b18b045a5 Mon Sep 17 00:00:00 2001 From: Basilio Bogado <541149+basiliskus@users.noreply.github.com> Date: Fri, 4 Oct 2024 10:08:01 -0700 Subject: [PATCH] RS Integration Tests in Staging (#1293) * Added new module rs-e2e for RS Integration Tests in Staging * Added initial implementation to pull files from Azure storage container * Added local file fecther, hl7 file matcher, and first test assertion on matched files Co-authored-by: Sylvie * Cleaned up and simplified logic * Clean up: remove comment * start copying rule engine stuff from etor project Co-Authored-By: Basilio Bogado <541149+basiliskus@users.noreply.github.com> * first draft of assertions Co-Authored-By: Basilio Bogado <541149+basiliskus@users.noreply.github.com> * Renamed 'transformation' to 'assertion', plus some cleanup * Some more refatoring to adapt the transformation engine to be used for HL7 assertions * Turned arrays in definitions into key value pairs * Updated the assertion definitions to use new syntax and added initial implementation for the hl7 expression evaluator Co-authored-by: Sylvie * Further implementation for the hl7 expression validator. Got a test expression working for equal comparison between hl7 field and a literal value Co-authored-by: Sylvie * Cleanup * Some refactoring and cleanup for parseAndEvaluate * Added implementation for evaluateMembership and removed Optional return value Co-authored-by: Sylvie * Added method to evaluate count operation + cleanup * Use both input and output messages, add a few more statements to test Co-Authored-By: Basilio Bogado <541149+basiliskus@users.noreply.github.com> * Start making AssertionRuleEngine work and some associated cleanup Co-Authored-By: Basilio Bogado <541149+basiliskus@users.noreply.github.com> * Start using the rules file, doing a little cleanup and troubleshooting Co-Authored-By: Basilio Bogado <541149+basiliskus@users.noreply.github.com> * Fixed loading of the definitions file and dependency injection * Fixed null pointer exception. All test assertions are now passing * Added missing assertion * Added missing catch of NumberFormatException * Trying to fix issue with sonar * Added HL7FileStream record to be able to later log the file name Co-authored-by: Sylvie * Refactored, added logging and fixed dependency injection Co-authored-by: Sylvie * Added unit tests for the asserion rules engine Co-authored-by: Sylvie * Catched missing exceptions Co-authored-by: Sylvie * Refactored to use singleton pattern and dependency injection system for consistency Co-authored-by: Sylvie * Refactored to reduce code duplication and merge rule engine framework Co-authored-by: Sylvie * Further refactoring to abstract and reduce code duplication among rule engine frameworks. Still pending to integrate the transformation and validation rule engines to use the new classes Co-authored-by: Sylvie * Further refactoring to integrate the transformation and validation rule engines to use the new classes Co-authored-by: Sylvie * Fixed tests in rs-e2e + cleanup * Refactoring to improve null handling * Added test cobverafe for HapiHL7ExpressionEvaluator and HapiHL7FileMatcher Co-authored-by: Sylvie * Cleanup and move class to appropiate location * Added some of the remaining test coverage * Added test coverage * Updated assertion definitions Co-authored-by: Sylvie * Added sample file to test UCSD transformations Co-authored-by: Sylvie * Updated message to avoid sending to ucsd Co-authored-by: Sylvie * Fixed arguments order Co-authored-by: Sylvie * Implemented a simple HL7 parser given we can't get what we need from the Hapi library * Added missing error handling * Removed unused code and fixed failing tests Co-authored-by: Sylvie * Added unit tests in rs-e2e to the allUnitTests task Co-authored-by: Sylvie * Fixed regex Co-authored-by: Sylvie * Added github workflow to run the automated test Co-authored-by: Sylvie * CHanged one assertion to fail to test the workflow Co-authored-by: Sylvie * Tests are still passing in the PR. Trying something else Co-authored-by: Sylvie * Updated MSH-10 ids for sample messages + cleanup Co-authored-by: Sylvie * Need more output from log to get information about the reason for test failure * Way too much output in the logs with --info * Reverted the change to make test fail and updated cron job to run 2 hours after the staging flow run * Removed pull_request trigger used for testing * Add additional test coverage * Added missing javadocs + cleanup * Fixed merge conflicts * Renamed 'result' to 'ORU' to avoid confusion * Moved HapiHL7FileMatcher and HL7FileStream to rs-e2e project given they are not used outside of this project * Updated to follow convention * Explicitly use UTF-8 charset * Move injections before public methods * Updated language to capture what is really the issue here * Simplified logic * Set all patterns as private static final * Missed one Pattern * Changed ArrayList for Set, which is more appropiate in this case * Moved HapiHL7ExpressionEvaluator and HapiHL7Message to rs-e2e project * Added back JavaDocs * Avoid code duplication * Need to keep inputstream open until used * Added readme files for the rs-e2e project and the sample files Co-authored-by: Sylvie * Added ADRs for the assertion engine and the automated rs integration test. Some more work to be done on the integration test one Co-authored-by: Sylvie * Added integration test section in the main readme Co-authored-by: Sylvie * Fixed merge conflicts * Added a couple more assertions for UCSD transformations * Added more to the integration test ADR. Still some more pending Co-authored-by: Sylvie * Update shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/wrappers/HealthDataTest.groovy Co-authored-by: Jorge Lopez <49923512+jorg3lopez@users.noreply.github.com> * More updates to ADR * Renamed readme.md => README.MD for consistency and changed urls to use relative path from the root of the repo * Missed on last commit * Formatting * Added a few more impact points * Capitalized file extenesion by mistake * Trying to update file extension capitalization * Trying to get git to catch the file extension capitalization * Reverting workaround to get git to catch the file extension capitalization --------- Co-authored-by: Sylvie Co-authored-by: Jorge Lopez <49923512+jorg3lopez@users.noreply.github.com> --- .../workflows/automated-staging-test-run.yml | 19 + .github/workflows/ci.yml | 2 +- README.md | 41 +- adr/024-assertion-engine.md | 53 ++ adr/025-automated-rs-integration-test.md | 73 +++ build.gradle | 2 + .../etor/EtorDomainRegistration.java | 6 +- .../etor/messages/Message.java | 4 +- .../etor/ruleengine/FhirResource.java | 11 - .../CustomFhirTransformation.java | 4 +- .../transformation/TransformationRule.java | 29 +- .../TransformationRuleEngine.java | 10 +- .../AddContactSectionToPatientResource.java | 6 +- .../custom/AddEtorProcessingTag.java | 6 +- .../custom/ConvertToOmlOrder.java | 6 +- ...opyOrcOrderProviderToObrOrderProvider.java | 6 +- .../custom/MapLocalObservationCodes.java | 6 +- .../custom/RemoveMessageTypeStructure.java | 6 +- .../custom/RemoveObservationByCode.java | 6 +- .../custom/RemoveObservationRequests.java | 6 +- .../custom/RemovePatientIdentifiers.java | 6 +- .../custom/RemovePatientNameTypeCode.java | 6 +- .../SwapPlacerOrderAndGroupNumbers.java | 6 +- .../UpdateReceivingApplicationNamespace.java | 6 +- ...acilityWithOrderingFacilityIdentifier.java | 6 +- .../UpdateSendingFacilityNamespace.java | 6 +- .../UpdateUniversalServiceIdentifier.java | 6 +- .../ruleengine/validation/ValidationRule.java | 26 +- .../validation/ValidationRuleEngine.java | 10 +- .../external/hapi/HapiMessage.java | 2 +- .../reportstream/ReportStreamOrderSender.java | 2 +- .../ReportStreamResultSender.java | 2 +- .../FhirResourceMock.groovy | 17 - .../trustedintermediary/HealthDataMock.groovy | 17 + .../cdc/trustedintermediary/OrderMock.groovy | 2 +- .../cdc/trustedintermediary/ResultMock.groovy | 2 +- .../etor/orders/OrderControllerTest.groovy | 2 +- .../etor/results/ResultControllerTest.groovy | 2 +- .../etor/ruleengine/RuleTest.groovy | 29 -- ... => TransformationRuleEngineHelper.groovy} | 4 +- ...sformationRuleEngineIntegrationTest.groovy | 12 +- .../TransformationRuleEngineTest.groovy | 14 +- .../TransformationRuleTest.groovy | 35 +- ...ContactSectionToPatientResourceTest.groovy | 6 +- ...OrderProviderToObrOrderProviderTest.groovy | 2 +- ...pyPathCustomTransformationMockClass.groovy | 6 +- .../IllegalAccessExceptionMockClass.groovy | 5 +- .../MapLocalObservationCodesTest.groovy | 2 +- .../NoSuchMethodExceptionMockClass.groovy | 5 +- .../custom/RemoveObservationByCodeTest.groovy | 2 +- .../RemoveObservationRequestsTest.groovy | 12 +- .../custom/RemovePatientIdentifierTest.groovy | 2 +- .../RemovePatientNameTypeCodeTest.groovy | 2 +- .../SwapPlacerOrderAndGroupNumbersTest.groovy | 2 +- ...yWithOrderingFacilityIdentifierTest.groovy | 2 +- .../UpdateSendingFacilityNamespaceTest.groovy | 4 +- ...pdateUniversalServiceIdentifierTest.groovy | 24 +- ...ValidationRuleEngineIntegrationTest.groovy | 4 +- .../ValidationRuleEngineTest.groovy | 14 +- .../validation/ValidationRuleTest.groovy | 57 ++- .../external/hapi/HapiOrderTest.groovy | 4 +- .../external/hapi/HapiResultTest.groovy | 2 +- ...taging_ORM_O01_short_linked_to_002_ORU.hl7 | 2 +- ...taging_ORU_O01_short_linked_to_001_ORM.hl7 | 2 +- .../003_CA_ORU_R01_CDPH_produced.hl7 | 173 +++++++ examples/Test/Automated/README.md | 17 + rs-e2e/README.md | 56 +++ rs-e2e/build.gradle | 39 ++ .../rse2e/AzureBlobFileFetcher.java | 73 +++ .../rse2e/FileFetcher.java | 11 + .../rse2e/HL7FileStream.java | 9 + .../rse2e/LocalFileFetcher.java | 50 ++ .../hapi/HapiHL7ExpressionEvaluator.java | 237 +++++++++ .../external/hapi/HapiHL7FileMatcher.java | 101 ++++ .../rse2e/external/hapi/HapiHL7Message.java | 27 + .../rse2e/ruleengine/AssertionRule.java | 63 +++ .../rse2e/ruleengine/AssertionRuleEngine.java | 78 +++ .../main/resources/assertion_definitions.json | 41 ++ .../rse2e/AutomatedTest.groovy | 67 +++ .../HapiHL7ExpressionEvaluatorTest.groovy | 464 ++++++++++++++++++ .../hapi/HapiHL7FileMatcherTest.groovy | 102 ++++ .../external/hapi/HapiHL7MessageTest.groovy | 32 ++ .../ruleengine/AssertionRuleEngineTest.groovy | 104 ++++ .../rse2e/ruleengine/AssertionRuleTest.groovy | 112 +++++ .../rse2e/ruleengine/RuleLoaderTest.groovy | 84 ++++ settings.gradle | 1 + shared/build.gradle | 8 +- .../external/hapi/HapiFhirImplementation.java | 14 +- .../external/hapi/HapiFhirResource.java | 8 +- .../trustedintermediary}/ruleengine/Rule.java | 22 +- .../ruleengine/RuleEngine.java | 6 +- .../ruleengine/RuleLoader.java | 4 +- .../ruleengine/RuleLoaderException.java | 2 +- .../wrappers/HapiFhir.java | 2 - .../wrappers/HealthData.java | 12 + .../HealthDataExpressionEvaluator.java | 7 + .../hapi/HapiFhirImplementationTest.groovy | 34 +- .../ruleengine/RuleLoaderExceptionTest.groovy | 3 +- .../ruleengine/RuleLoaderTest.groovy | 13 +- .../ruleengine/RuleTest.groovy | 90 ++++ .../wrappers/HealthDataTest.groovy | 30 ++ 101 files changed, 2589 insertions(+), 290 deletions(-) create mode 100644 .github/workflows/automated-staging-test-run.yml create mode 100644 adr/024-assertion-engine.md create mode 100644 adr/025-automated-rs-integration-test.md delete mode 100644 etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/FhirResource.java delete mode 100644 etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/FhirResourceMock.groovy create mode 100644 etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/HealthDataMock.groovy delete mode 100644 etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleTest.groovy rename etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/{RuleEngineHelper.groovy => TransformationRuleEngineHelper.groovy} (75%) create mode 100644 examples/Test/Automated/003_CA_ORU_R01_CDPH_produced.hl7 create mode 100644 examples/Test/Automated/README.md create mode 100644 rs-e2e/README.md create mode 100644 rs-e2e/build.gradle create mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/AzureBlobFileFetcher.java create mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/FileFetcher.java create mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/HL7FileStream.java create mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/LocalFileFetcher.java create mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7ExpressionEvaluator.java create mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcher.java create mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7Message.java create mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRule.java create mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRuleEngine.java create mode 100644 rs-e2e/src/main/resources/assertion_definitions.json create mode 100644 rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/AutomatedTest.groovy create mode 100644 rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7ExpressionEvaluatorTest.groovy create mode 100644 rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcherTest.groovy create mode 100644 rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7MessageTest.groovy create mode 100644 rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRuleEngineTest.groovy create mode 100644 rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRuleTest.groovy create mode 100644 rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/RuleLoaderTest.groovy rename {etor => shared}/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirResource.java (52%) rename {etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor => shared/src/main/java/gov/hhs/cdc/trustedintermediary}/ruleengine/Rule.java (73%) rename {etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor => shared/src/main/java/gov/hhs/cdc/trustedintermediary}/ruleengine/RuleEngine.java (65%) rename {etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor => shared/src/main/java/gov/hhs/cdc/trustedintermediary}/ruleengine/RuleLoader.java (96%) rename {etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor => shared/src/main/java/gov/hhs/cdc/trustedintermediary}/ruleengine/RuleLoaderException.java (79%) create mode 100644 shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/HealthData.java create mode 100644 shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/HealthDataExpressionEvaluator.java rename {etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor => shared/src/test/groovy/gov/hhs/cdc/trustedintermediary}/ruleengine/RuleLoaderExceptionTest.groovy (92%) rename {etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor => shared/src/test/groovy/gov/hhs/cdc/trustedintermediary}/ruleengine/RuleLoaderTest.groovy (79%) create mode 100644 shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/ruleengine/RuleTest.groovy create mode 100644 shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/wrappers/HealthDataTest.groovy diff --git a/.github/workflows/automated-staging-test-run.yml b/.github/workflows/automated-staging-test-run.yml new file mode 100644 index 000000000..95dfb38d1 --- /dev/null +++ b/.github/workflows/automated-staging-test-run.yml @@ -0,0 +1,19 @@ +name: Automated Staging Test - Run integration tests + +on: + schedule: + - cron: "0 2 * * 2-6" # Tuesday to Saturday at 2am UTC - two hours after `automated-staging-test-submit` runs + workflow_dispatch: + +jobs: + test_files: + runs-on: ubuntu-latest + + steps: + - name: Check out the repository + uses: actions/checkout@v4 + + - name: Run automated tests + env: + AZURE_STORAGE_CONNECTION_STRING: ${{ secrets.AUTOMATED_TEST_AZURE_STORAGE_CONNECTION_STRING }} + run: ./gradlew rs-e2e:clean rs-e2e:automatedTest diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b10195579..a14611762 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,7 @@ jobs: run: ./gradlew spotlessCheck - name: Sonar - run: ./gradlew allBuilds testCodeCoverageReport e2e:assemble sonar --info + run: ./gradlew allBuilds testCodeCoverageReport e2e:assemble rs-e2e:assemble sonar --info env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/README.md b/README.md index be580b6f7..0e63c5943 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,11 @@ This document provides instructions for setting up the environment, running the application, and performing various tasks such as compiling, testing, and contributing to the project. ## Requirements + Any distribution of the Java 17 JDK. ## Using and Running + To run the application directly, execute... ```shell @@ -17,6 +19,7 @@ This runs the web API on port 8080. The app reads/writes data to a local file (u You can view the API documentation at `/openapi`. ### Generating and using a token + 1. Run `brew install mike-engel/jwt-cli/jwt-cli` 2. Replace `PATH_TO_FILE_ON_YOUR_MACHINE` in this command with the actual path, then run it: `jwt encode --exp='+5min' --jti $(uuidgen) --alg RS256 --no-iat -S @/PATH_TO_FILE_ON_YOUR_MACHINE/trusted-intermediary/mock_credentials/organization-trusted-intermediary-private-key-local.pem` 3. Copy token from terminal and paste into your postman body with the key `client_assertion` @@ -24,7 +27,6 @@ You can view the API documentation at `/openapi`. 5. Body type should be `x-wwww-form-urlencoded` 6. You should be able to run the post call against the `v1/auth/token` endpoint to receive a bearer token [to be used in this step](#submit-request-to-reportstream) - ## Development ### Additional Requirements @@ -49,19 +51,23 @@ creates a `.env` file in the resource folder with the required configuration ```bash ./generate_env.sh ``` + 3. If you run TI using Docker rather than Gradle, update the DB and port values in the `.env` file (the alternate values are in comments) ### Using a local database + Use [docker-compose.postgres.yml](docker-compose.postgres.yml) to run your local DB. In IntelliJ, you can click the play arrow to start it ![docker-postgres.png](images/docker-postgres.png) Apply all outstanding migrations: + ```bash liquibase update --changelog-file ./etor/databaseMigrations/root.yml --url jdbc:postgresql://localhost:5433/intermediary --username intermediary --password 'changeIT!' --label-filter '!azure' ``` If running in Windows, use double quotes instead: + ```shell liquibase update --changelog-file ./etor/databaseMigrations/root.yml --url jdbc:postgresql://localhost:5433/intermediary --username intermediary --password "changeIT!" --label-filter "!azure" ``` @@ -109,6 +115,11 @@ This will start the API, wait for it to respond, run the end-to-end tests agains These tests are located under the `e2e` Gradle sub-project directory. Like any Gradle project, there are the `main` and `test` directories. The `test` directory contains the tests. The `main` directory contains our custom framework that helps us interact with the API. +#### Automated ReportStream Integration/End-to-End Test + +These tests cover the integration between ReportStream and TI. They run automatically every +weekday via Github actions. See [the rs-e2e readme](rs-e2e/README.md) for more details. + #### Load Testing Load tests are completed with [Locust.io](https://docs.locust.io/en/stable/installation.html). @@ -119,6 +130,7 @@ Run the load tests by running... ./docker-load-execute.sh ``` + Currently, we are migrating to using Azure. Local load testing is using gradle, however a docker load test is available to mimic the Azure environment settings until the azure migration is complete. This will run the API for you, so no need to run it manually. @@ -139,11 +151,12 @@ locust -f ./operations/locustfile.py The terminal will start a local web interface, and you can enter the swarm parameters for the test and the local url where the app is running -(usually http://localhost:8080). You can also set time limits for the tests under 'Advanced Settings'. +(usually `http://localhost:8080`). You can also set time limits for the tests under 'Advanced Settings'. ### Debugging #### Attached JVM Config for IntelliJ + The project comes with an attached remote jvm configuration for debuging the container. If you check your remote JVM settings, under `Run/Edit Configurations`, you will see the `Debug TI`. If you want to add a new remote JVM configuration, follow the steps below, @@ -154,6 +167,7 @@ under "**Docker Container Debugging Using Java Debug Wire Protocal**" Go into the `Dockerfile` file and change `CMD ["java", "-jar", "app.jar"]` to `CMD ["java", "-agentlib:jdwp=transport=dt_socket,address=*:6006,server=y,suspend=n", "-jar", "app.jar"]` #### Steps + 1. In Intellij, click on Run and select Edit Configurations ![img.png](images/img.png) 2. Create a new Remote JVM Debug ![img_1.png](images/img_1.png) 3. Set up the configuration for the remote JVM debug to look like this. ![img_3.png](images/img_2.png) @@ -163,7 +177,6 @@ Go into the `Dockerfile` file and change `CMD ["java", "-jar", "app.jar"]` to `C 7. Select your Docker Debug that you set up in step 3 ![img_4.png](images/img_4.png) 8. A console window will pop up that will show you that it is connected to Docker, and at that point, you can interact with your container and then step through the code at your breakpoints. ![img_5.png](images/img_5.png) - ### Deploying #### Environments @@ -177,21 +190,27 @@ deployed environment in a _non-CDC_ Azure Entra domain and subscription. See bel > **Before starting...** > -> Remember to ping the Engineering Channel to make sure someone is not already using the enviroment. +> Remember to ping the Engineering Channel to make sure someone is not already using the enviroment. To deploy to the Internal environment... + 1. Check with the team that no one is already using it. 2. [Find the `internal` branch](https://github.com/CDCgov/trusted-intermediary/branches/all?query=internal) and delete it inGitHub. 3. Delete your local `internal` branch if needed. + ```shell git branch -D internal ``` + 4. From the branch you want to test, create a new `internal` branch. + ```shell git checkout -b internal ``` + 5. Push the branch to GitHub. + ```shell git push --set-upstream origin internal ``` @@ -306,6 +325,7 @@ CDC including this GitHub page may be subject to applicable federal law, includi ### Database For database documentation: [/docs/database.md](/docs/database.md) + ### Setup with ReportStream #### CDC-TI Setup @@ -333,7 +353,7 @@ with this option enabled. 8. Run the `./reset.sh` script to reset the database 9. Run the `./load-etor-org-settings.sh` to apply the ETOR organization settings 10. Run the `./setup-local-vault.sh` script to set up the local vault secrets - - You can verify that the script created the secrets successfully by going to `http://localhost:8200/` in your browser, use the token in `prime-router/.vault/env/.env.local` to authenticate, and then go to `Secrets engines` > `secret/` to check the available secrets + - You can verify that the script created the secrets successfully by going to `http://localhost:8200/` in your browser, use the token in `prime-router/.vault/env/.env.local` to authenticate, and then go to `Secrets engines` > `secret/` to check the available secrets #### Submit request to ReportStream @@ -342,11 +362,13 @@ with this option enabled. ###### Orders To test sending from a simulated hospital: + ``` curl --header 'Content-Type: application/hl7-v2' --header 'Client: flexion.simulated-hospital' --header 'Authorization: Bearer dummy_token' --data-binary '@/path/to/orm_message.hl7' 'http://localhost:7071/api/waters' ``` To test sending from TI: + ``` curl --header 'Content-Type: application/fhir+ndjson' --header 'Client: flexion.etor-service-sender' --header 'Authorization: Bearer dummy_token' --data-binary '@/path/to/oml_message.fhir' 'http://localhost:7071/api/waters' ``` @@ -354,11 +376,13 @@ curl --header 'Content-Type: application/fhir+ndjson' --header 'Client: flexion. ###### Results To test sending from a simulated lab: + ``` curl --header 'Content-Type: application/hl7-v2' --header 'Client: flexion.simulated-lab' --header 'Authorization: Bearer dummy_token' --data-binary '@/path/to/oru_message.hl7' 'http://localhost:7071/api/waters' ``` To test sending from TI: + ``` curl --header 'Content-Type: application/fhir+ndjson' --header 'Client: flexion.etor-service-sender' --header 'Authorization: Bearer dummy_token' --data-binary '@/path/to/oru_message.fhir' 'http://localhost:7071/api/waters' ``` @@ -368,14 +392,19 @@ After one or two minutes, check that hl7 files have been dropped to `prime-repor ##### Staging In order to submit a request, you'll need to authenticate with ReportStream using JWT auth: + 1. Create a JWT for the sender (e.g. `flexion.simulated-hospital`) using the sender's private key, which should be stored in Keybase. You may use [this CLI tool](https://github.com/mike-engel/jwt-cli) to create the JWT: + ``` jwt encode --exp='+5min' --jti $(uuidgen) --alg RS256 -k -i -s -a staging.prime.cdc.gov --no-iat -S @/path/to/sender_private.pem ``` + 2. Use the generated JWT to authenticate with ReportStream and get the token, which will be in the `access_token` response + ``` curl --header 'Content-Type: application/x-www-form-urlencoded' --data 'scope=flexion.*.report' --data 'client_assertion=' --data 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' --data 'grant_type=client_credentials' 'http://localhost:7071/api/token' ``` + 3. Submit an Order or Result using the returned token in the `'Authorization: Bearer '` header ## DORA Metrics @@ -419,7 +448,7 @@ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Apache Software License for more details. You should have received a copy of the Apache Software License along with this -program. If not, see http://www.apache.org/licenses/LICENSE-2.0.html +program. If not, see The source code forked from other open source projects will inherit its license. diff --git a/adr/024-assertion-engine.md b/adr/024-assertion-engine.md new file mode 100644 index 000000000..d24368257 --- /dev/null +++ b/adr/024-assertion-engine.md @@ -0,0 +1,53 @@ +# 24. Assertion Engine + +Date: 2024-10-02 + +## Decision + +1. We decided to use the Rule Engine framework (used for the transformations and validations) to define the assertions for the ReportStream Integration Test. +2. We decided to refactor the Rule Engine framework to avoid code duplication and add HL7 handling. Before the refactor, the engine was only able to handle FHIR messages. +3. We decided to create our own simple parser for HL7 that will allow us to access segment fields using an index. +4. We decided to create our own HL7 expression syntax and validation, inspired by FHIRPath. + +## Status + +Accepted. + +## Context + +While creating the RS Integration Test framework, we needed a way to define the assertions that would be evaluated on the output files. +We decided to use the Rule Engine framework, which is already used for transformations and validations, to define these assertions. +This will allow us to reuse the existing code and make it easier to maintain. + +The reasoning behind the decisions in the previous sections is as follows: + +1. The Rule Engine framework is already in place and has been proven to work well for transformations and validations. +2. Refactoring the Rule Engine framework will allow us to avoid code duplication and make it easier to maintain. +3. While working with the Hapi library, we found some limitations that made it impossible to access values in the HL7 messages by segment index. The library's typing system doesn't allow to access HL7 fields by index, so we decided to create a very simple parser that would allow us to do that. +4. In order to create assertions, we needed to define a syntax that would allow us to access HL7 fields and compare them. Since we didn't find an existing specification for it, we decided to create our own HL7 expression syntax and validation, following the same patterns and conventions as FHIRPath so it's easier to understand for those familiar with the FHIRPath specifications. + +## Impact + +For the Assertion Engine framework, similar impact to the Transformation and Validation Engine ADRs can be assumed. + +For the HL7 parser and expression validator, this is the expected impact: + +### Positive + +- We will be able to access HL7 fields by segment index, which will gives us more flexibility and get HL7 fields programmatically. +- We will be able to define assertions for HL7 messages using a simple syntax that is easy to understand and maintain. + +### Negative + +- We will have to maintain the HL7 parser and expression validator, which could add some overhead to the project. + +### Risks + +- The HL7 parser and expression validator could introduce bugs that could affect the assertion evaluation. +- The HL7 parser and expression validator may not cover all the cases we need. + +## Related ADRs + +- [Automated RS Integration Test](025-automated-rs-integration-test.md) +- [Validation Engine](021-validation-engine.md) +- [Transformation Engine](022-transformation-engine.md) diff --git a/adr/025-automated-rs-integration-test.md b/adr/025-automated-rs-integration-test.md new file mode 100644 index 000000000..b47a6f385 --- /dev/null +++ b/adr/025-automated-rs-integration-test.md @@ -0,0 +1,73 @@ +# 25. Automated RS Integration Test + +Date: 2024-10-02 + +## Decisions + +1. We decided to create a full end-to-end test that covers the interaction of ReportStream and TI, using + existing ingestion and delivery mechanisms +2. We decided to create Github Action workflows to schedule two tasks: + - One workflow submits sample HL7 files to ReportStream in staging, with output files expected to be delivered to an Azure blob container + - Another workflow later runs the integration tests on the output and input files +3. We decided to use MSH-10 to match the input and output files, and to filter the receivers in ReportStream when MSH-6.2 not available. + +## Status + +Accepted. + +## Context + +### Decision 1 + +The RS and TI applications each have their own unit and integration tests, but we didn't have any tests +that cover the interaction between RS and TI, and we also didn't have a way to know when changes in +RS have unintended consequences that impact our workflows. + +Submitting data to RS using their existing REST endpoints and receiving it using their existing delivery +mechanisms helps make these tests realistic. + +### Decision 2 + +Since we decided to use RS's existing REST endpoints, we needed a way to submit data to them, and a way +to trigger the data flow and subsequent tests on some kind of schedule. We chose Github Actions for this +because it's easy to both schedule them based on a CRON expression and to run them manually as needed. Github +Actions also gave us a lightweight way to send the files to RS without having to add a new service. + +We are using two separate actions - the first one sends data to RS, and the second one (currently +scheduled 2 hours after the first) triggers the tests to run. The length of time it takes a file to +run through the whole workflow (from RS to TI to RS to final delivery) usually doesn't take long, but we +built in extra time in case of any issues that cause delays. + +### Decision 3 + +We're using the value in MSH-10 for two purposes: matching input and output files, and some filtering in RS. + +We chose MSH-10 to match files on because it's a value that shouldn't change and should be unique to +a particular message. We're also using it to route these test messages because in some cases, we apply +transformations that will overwrite HL7 fields used for routing (MSH-5 and MSH-6), so we can't rely on those. + +## Impact + +### Positive + +- We will have a way to test the integration between RS and TI +- We will be able to catch issues early when changes in RS break our workflows + +### Negative + +- We will run daily tests in RS' and our staging infrastructure, which will take up resources + +### Risks + +- If we forget to add additional assertions when new transformations are added, these tests may give us + a false sense of confidence +- Because we rely on MSH-10 for matching files, engineers will have to take care in setting this field + when they create additional tests in future +- If we don't maintain the filtering in RS based on MSH-6.2 and MSH-10, we may not be able to route the test messages + correctly +- Because we're using RS's existing REST endpoints and staging set up, if RS changes their endpoints or + the way they handle staging, these tests may break + +## Related ADRs + +- [Assertion Engine](024-assertion-engine.md) diff --git a/build.gradle b/build.gradle index 2789dca40..97728ba20 100644 --- a/build.gradle +++ b/build.gradle @@ -87,6 +87,7 @@ subprojects { ext.jacoco_excludes = [ '**/e2e/**', + '**/rs-e2e/**', '**/javalin/App*', '**/jackson/Jackson*', '**/slf4j/LocalLogger*', @@ -105,6 +106,7 @@ tasks.register('allUnitTests') { dependsOn 'app:test' dependsOn 'shared:test' dependsOn 'etor:test' + dependsOn 'rs-e2e:test' } tasks.register('allBuilds') { diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistration.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistration.java index 4c4b0c897..eb5c648cf 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistration.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistration.java @@ -27,13 +27,13 @@ import gov.hhs.cdc.trustedintermediary.etor.results.ResultResponse; import gov.hhs.cdc.trustedintermediary.etor.results.ResultSender; import gov.hhs.cdc.trustedintermediary.etor.results.SendResultUseCase; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleLoader; import gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.TransformationRuleEngine; import gov.hhs.cdc.trustedintermediary.etor.ruleengine.validation.ValidationRuleEngine; import gov.hhs.cdc.trustedintermediary.external.database.DatabaseMessageLinkStorage; import gov.hhs.cdc.trustedintermediary.external.database.DatabasePartnerMetadataStorage; import gov.hhs.cdc.trustedintermediary.external.database.DbDao; import gov.hhs.cdc.trustedintermediary.external.database.PostgresDao; +import gov.hhs.cdc.trustedintermediary.external.hapi.HapiFhirImplementation; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiMessageHelper; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiPartnerMetadataConverter; import gov.hhs.cdc.trustedintermediary.external.localfile.FileMessageLinkStorage; @@ -44,8 +44,10 @@ import gov.hhs.cdc.trustedintermediary.external.reportstream.ReportStreamOrderSender; import gov.hhs.cdc.trustedintermediary.external.reportstream.ReportStreamResultSender; import gov.hhs.cdc.trustedintermediary.external.reportstream.ReportStreamSenderHelper; +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoader; import gov.hhs.cdc.trustedintermediary.wrappers.FhirParseException; import gov.hhs.cdc.trustedintermediary.wrappers.HapiFhir; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthDataExpressionEvaluator; import gov.hhs.cdc.trustedintermediary.wrappers.Logger; import java.util.Map; import java.util.Optional; @@ -107,6 +109,8 @@ public Map> domainRegistra ApplicationContext.register( PartnerMetadataConverter.class, HapiPartnerMetadataConverter.getInstance()); // Validation rules + ApplicationContext.register( + HealthDataExpressionEvaluator.class, HapiFhirImplementation.getInstance()); ApplicationContext.register(RuleLoader.class, RuleLoader.getInstance()); ApplicationContext.register( ValidationRuleEngine.class, diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/messages/Message.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/messages/Message.java index bbe1da597..15b2cbe5b 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/messages/Message.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/messages/Message.java @@ -1,13 +1,13 @@ package gov.hhs.cdc.trustedintermediary.etor.messages; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; /** * Defines the structure and operations for a message. This interface allows for the retrieval of * various pieces of information related to a message, including details about the sending and * receiving applications and facilities, as well as order numbers. */ -public interface Message extends FhirResource { +public interface Message extends HealthData { String getFhirResourceId(); String getPlacerOrderNumber(); diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/FhirResource.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/FhirResource.java deleted file mode 100644 index e3fa5bda9..000000000 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/FhirResource.java +++ /dev/null @@ -1,11 +0,0 @@ -package gov.hhs.cdc.trustedintermediary.etor.ruleengine; - -/** - * This interface represents a FHIR resource. It's used as a wrapper to decouple dependency on third - * party libraries. - * - * @param the type of the underlying resource - */ -public interface FhirResource { - T getUnderlyingResource(); -} diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/CustomFhirTransformation.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/CustomFhirTransformation.java index 75a93a394..2255839e7 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/CustomFhirTransformation.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/CustomFhirTransformation.java @@ -1,6 +1,6 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import java.util.Map; /** @@ -8,5 +8,5 @@ * implemented by classes in the custom/ folder. */ public interface CustomFhirTransformation { - void transform(FhirResource resource, Map args); + void transform(HealthData resource, Map args); } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRule.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRule.java index 916f1937a..45964ae47 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRule.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRule.java @@ -1,17 +1,15 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.Rule; +import gov.hhs.cdc.trustedintermediary.ruleengine.Rule; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** - * The TransformationRule class extends the {@link - * gov.hhs.cdc.trustedintermediary.etor.ruleengine.Rule Rule} class and represents a transformation - * rule. It implements the {@link - * gov.hhs.cdc.trustedintermediary.etor.ruleengine.Rule#runRule(FhirResource) runRule} method to - * apply a transformation to the FHIR resource. + * The TransformationRule class extends the {@link Rule Rule} class and represents a transformation + * rule. It implements the {@link Rule#runRule(HealthData...) runRule} method to apply a + * transformation to the FHIR resource. */ public class TransformationRule extends Rule { @@ -35,18 +33,27 @@ public TransformationRule( } @Override - public void runRule(FhirResource resource) { + public void runRule(HealthData... resource) { + + if (resource.length != 1) { + this.logger.logError( + "Rule [" + + this.getName() + + "]: Transformation rules require exactly one resource object to be passed in."); + return; + } + for (TransformationRuleMethod transformation : this.getRules()) { try { - applyTransformation(transformation, resource); + applyTransformation(transformation, resource[0]); } catch (RuntimeException e) { - logger.logError("Error applying transformation: " + transformation.name(), e); + this.logger.logError("Error applying transformation: " + transformation.name(), e); } } } private void applyTransformation( - TransformationRuleMethod transformation, FhirResource resource) { + TransformationRuleMethod transformation, HealthData resource) { String name = transformation.name(); Map args = transformation.args(); logger.logInfo("Applying transformation: " + name); diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRuleEngine.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRuleEngine.java index 6062fa3f8..f97b5aaf1 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRuleEngine.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRuleEngine.java @@ -1,9 +1,9 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleEngine; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleLoader; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleLoaderException; +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleEngine; +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoader; +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoaderException; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import gov.hhs.cdc.trustedintermediary.wrappers.Logger; import gov.hhs.cdc.trustedintermediary.wrappers.formatter.TypeReference; import java.io.FileNotFoundException; @@ -61,7 +61,7 @@ public void ensureRulesLoaded() throws RuleLoaderException { } @Override - public void runRules(FhirResource resource) { + public void runRules(HealthData resource) { try { ensureRulesLoaded(); } catch (RuleLoaderException e) { diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/AddContactSectionToPatientResource.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/AddContactSectionToPatientResource.java index 84ea29cab..c260252ca 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/AddContactSectionToPatientResource.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/AddContactSectionToPatientResource.java @@ -2,9 +2,9 @@ import gov.hhs.cdc.trustedintermediary.context.ApplicationContext; import gov.hhs.cdc.trustedintermediary.etor.metadata.EtorMetadataStep; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; import gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.CustomFhirTransformation; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import gov.hhs.cdc.trustedintermediary.wrappers.MetricMetadata; import java.util.List; import java.util.Map; @@ -19,8 +19,8 @@ public class AddContactSectionToPatientResource implements CustomFhirTransformat ApplicationContext.getImplementation(MetricMetadata.class); @Override - public void transform(FhirResource resource, Map args) { - Bundle bundle = (Bundle) resource.getUnderlyingResource(); + public void transform(HealthData resource, Map args) { + Bundle bundle = (Bundle) resource.getUnderlyingData(); HapiHelper.resourcesInBundle(bundle, Patient.class) .forEach( diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/AddEtorProcessingTag.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/AddEtorProcessingTag.java index 404e42b22..e76ad710c 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/AddEtorProcessingTag.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/AddEtorProcessingTag.java @@ -2,9 +2,9 @@ import gov.hhs.cdc.trustedintermediary.context.ApplicationContext; import gov.hhs.cdc.trustedintermediary.etor.metadata.EtorMetadataStep; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; import gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.CustomFhirTransformation; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import gov.hhs.cdc.trustedintermediary.wrappers.MetricMetadata; import java.util.Map; import org.hl7.fhir.r4.model.Bundle; @@ -16,8 +16,8 @@ public class AddEtorProcessingTag implements CustomFhirTransformation { ApplicationContext.getImplementation(MetricMetadata.class); @Override - public void transform(FhirResource resource, Map args) { - Bundle bundle = (Bundle) resource.getUnderlyingResource(); + public void transform(HealthData resource, Map args) { + Bundle bundle = (Bundle) resource.getUnderlyingData(); var system = "http://localcodes.org/ETOR"; var code = "ETOR"; diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/ConvertToOmlOrder.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/ConvertToOmlOrder.java index 7660c6140..997e38116 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/ConvertToOmlOrder.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/ConvertToOmlOrder.java @@ -2,9 +2,9 @@ import gov.hhs.cdc.trustedintermediary.context.ApplicationContext; import gov.hhs.cdc.trustedintermediary.etor.metadata.EtorMetadataStep; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; import gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.CustomFhirTransformation; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import gov.hhs.cdc.trustedintermediary.wrappers.MetricMetadata; import java.util.Map; import org.hl7.fhir.r4.model.Bundle; @@ -16,8 +16,8 @@ public class ConvertToOmlOrder implements CustomFhirTransformation { ApplicationContext.getImplementation(MetricMetadata.class); @Override - public void transform(FhirResource resource, Map args) { - Bundle bundle = (Bundle) resource.getUnderlyingResource(); + public void transform(HealthData resource, Map args) { + Bundle bundle = (Bundle) resource.getUnderlyingData(); HapiHelper.setMSH9Coding(bundle, HapiHelper.OML_CODING); metadata.put(bundle.getId(), EtorMetadataStep.ORDER_CONVERTED_TO_OML); } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProvider.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProvider.java index b3be8c91b..1c73395a2 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProvider.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProvider.java @@ -1,8 +1,8 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.custom; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; import gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.CustomFhirTransformation; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import java.util.Map; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.DiagnosticReport; @@ -13,8 +13,8 @@ public class CopyOrcOrderProviderToObrOrderProvider implements CustomFhirTransformation { @Override - public void transform(FhirResource resource, Map args) { - Bundle bundle = (Bundle) resource.getUnderlyingResource(); + public void transform(HealthData resource, Map args) { + Bundle bundle = (Bundle) resource.getUnderlyingData(); DiagnosticReport diagnosticReport = HapiHelper.getDiagnosticReport(bundle); if (diagnosticReport == null) { return; diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/MapLocalObservationCodes.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/MapLocalObservationCodes.java index 4749f3779..ad45ff5ea 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/MapLocalObservationCodes.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/MapLocalObservationCodes.java @@ -2,9 +2,9 @@ import gov.hhs.cdc.trustedintermediary.context.ApplicationContext; import gov.hhs.cdc.trustedintermediary.etor.messages.IdentifierCode; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; import gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.CustomFhirTransformation; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import gov.hhs.cdc.trustedintermediary.wrappers.Logger; import java.util.HashMap; import java.util.Map; @@ -30,8 +30,8 @@ public MapLocalObservationCodes() { } @Override - public void transform(FhirResource resource, Map args) { - var bundle = (Bundle) resource.getUnderlyingResource(); + public void transform(HealthData resource, Map args) { + var bundle = (Bundle) resource.getUnderlyingData(); var observations = HapiHelper.resourcesInBundle(bundle, Observation.class); for (Observation obv : observations.toList()) { diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveMessageTypeStructure.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveMessageTypeStructure.java index 9f24fe246..0aa4d60c6 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveMessageTypeStructure.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveMessageTypeStructure.java @@ -1,8 +1,8 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.custom; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; import gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.CustomFhirTransformation; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -13,8 +13,8 @@ public class RemoveMessageTypeStructure implements CustomFhirTransformation { @Override - public void transform(FhirResource resource, Map args) { - Bundle bundle = (Bundle) resource.getUnderlyingResource(); + public void transform(HealthData resource, Map args) { + Bundle bundle = (Bundle) resource.getUnderlyingData(); String msh9_3 = HapiHelper.getMSH9_3Value(bundle); if (msh9_3 == null) { return; diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationByCode.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationByCode.java index c3cb989ce..cb23040bd 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationByCode.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationByCode.java @@ -1,8 +1,8 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.custom; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; import gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.CustomFhirTransformation; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -16,8 +16,8 @@ public class RemoveObservationByCode implements CustomFhirTransformation { public static final String CODING_NAME = "codingExtension"; @Override - public void transform(FhirResource resource, Map args) { - var bundle = (Bundle) resource.getUnderlyingResource(); + public void transform(HealthData resource, Map args) { + var bundle = (Bundle) resource.getUnderlyingData(); Set resourcesToRemove = new HashSet<>(); for (Bundle.BundleEntryComponent entry : bundle.getEntry()) { diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationRequests.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationRequests.java index 9cdf7d006..5b56391c5 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationRequests.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationRequests.java @@ -1,8 +1,8 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.custom; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; import gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.CustomFhirTransformation; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -22,8 +22,8 @@ public class RemoveObservationRequests implements CustomFhirTransformation { @Override - public void transform(FhirResource resource, Map args) { - Bundle bundle = (Bundle) resource.getUnderlyingResource(); + public void transform(HealthData resource, Map args) { + Bundle bundle = (Bundle) resource.getUnderlyingData(); // Let it fail if it is not a String String universalServiceIdentifier = (String) args.get("universalServiceIdentifier"); diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java index b76c8ccd4..524665d26 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java @@ -1,8 +1,8 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.custom; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; import gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.CustomFhirTransformation; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import java.util.Map; import org.hl7.fhir.r4.model.Bundle; @@ -13,8 +13,8 @@ public class RemovePatientIdentifiers implements CustomFhirTransformation { @Override - public void transform(FhirResource resource, Map args) { - Bundle bundle = (Bundle) resource.getUnderlyingResource(); + public void transform(HealthData resource, Map args) { + Bundle bundle = (Bundle) resource.getUnderlyingData(); HapiHelper.setPID3_4Value(bundle, ""); // remove PID.3-4 HapiHelper.setPID3_5Value(bundle, ""); // remove PID.3-5 } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCode.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCode.java index e694d9eab..5cf7d4ce2 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCode.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCode.java @@ -1,8 +1,8 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.custom; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; import gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.CustomFhirTransformation; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import java.util.Map; import org.hl7.fhir.r4.model.Bundle; @@ -10,8 +10,8 @@ public class RemovePatientNameTypeCode implements CustomFhirTransformation { @Override - public void transform(final FhirResource resource, final Map args) { - Bundle bundle = (Bundle) resource.getUnderlyingResource(); + public void transform(final HealthData resource, final Map args) { + Bundle bundle = (Bundle) resource.getUnderlyingData(); // Need to set the value for extension to empty instead of removing the extension, // otherwise RS will set its own value in its place HapiHelper.setPID5_7ExtensionValue(bundle, null); diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/SwapPlacerOrderAndGroupNumbers.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/SwapPlacerOrderAndGroupNumbers.java index 3576c963b..82da48409 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/SwapPlacerOrderAndGroupNumbers.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/SwapPlacerOrderAndGroupNumbers.java @@ -1,8 +1,8 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.custom; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; import gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.CustomFhirTransformation; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import java.util.Map; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.ServiceRequest; @@ -17,8 +17,8 @@ public class SwapPlacerOrderAndGroupNumbers implements CustomFhirTransformation { @Override - public void transform(FhirResource resource, Map args) { - Bundle bundle = (Bundle) resource.getUnderlyingResource(); + public void transform(HealthData resource, Map args) { + Bundle bundle = (Bundle) resource.getUnderlyingData(); var serviceRequests = HapiHelper.resourcesInBundle(bundle, ServiceRequest.class); for (ServiceRequest serviceRequest : serviceRequests.toList()) { diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateReceivingApplicationNamespace.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateReceivingApplicationNamespace.java index 12a05c937..e9d01f98b 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateReceivingApplicationNamespace.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateReceivingApplicationNamespace.java @@ -1,8 +1,8 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.custom; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; import gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.CustomFhirTransformation; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import java.util.Map; import org.hl7.fhir.r4.model.Bundle; @@ -13,8 +13,8 @@ public class UpdateReceivingApplicationNamespace implements CustomFhirTransformation { @Override - public void transform(FhirResource resource, Map args) { - Bundle bundle = (Bundle) resource.getUnderlyingResource(); + public void transform(HealthData resource, Map args) { + Bundle bundle = (Bundle) resource.getUnderlyingData(); // Let it fail if it is not a string String name = (String) args.get("name"); var receivingApplication = HapiHelper.getMSH5MessageDestinationComponent(bundle); diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateReceivingFacilityWithOrderingFacilityIdentifier.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateReceivingFacilityWithOrderingFacilityIdentifier.java index 07366f090..2a561739f 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateReceivingFacilityWithOrderingFacilityIdentifier.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateReceivingFacilityWithOrderingFacilityIdentifier.java @@ -1,8 +1,8 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.custom; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; import gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.CustomFhirTransformation; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import java.util.Map; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.DiagnosticReport; @@ -16,8 +16,8 @@ public class UpdateReceivingFacilityWithOrderingFacilityIdentifier implements CustomFhirTransformation { @Override - public void transform(FhirResource resource, Map args) { - Bundle bundle = (Bundle) resource.getUnderlyingResource(); + public void transform(HealthData resource, Map args) { + Bundle bundle = (Bundle) resource.getUnderlyingData(); DiagnosticReport diagnosticReport = HapiHelper.getDiagnosticReport(bundle); if (diagnosticReport == null) { return; diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateSendingFacilityNamespace.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateSendingFacilityNamespace.java index ab90d716d..3c5e0a796 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateSendingFacilityNamespace.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateSendingFacilityNamespace.java @@ -1,8 +1,8 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.custom; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; import gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.CustomFhirTransformation; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import java.util.Collections; import java.util.Map; import java.util.Objects; @@ -16,8 +16,8 @@ public class UpdateSendingFacilityNamespace implements CustomFhirTransformation { @Override - public void transform(FhirResource resource, Map args) { - Bundle bundle = (Bundle) resource.getUnderlyingResource(); + public void transform(HealthData resource, Map args) { + Bundle bundle = (Bundle) resource.getUnderlyingData(); Identifier namespaceIdentifier = HapiHelper.getMSH4_1Identifier(bundle); if (namespaceIdentifier == null) { return; diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateUniversalServiceIdentifier.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateUniversalServiceIdentifier.java index c73f5d1f5..148798246 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateUniversalServiceIdentifier.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateUniversalServiceIdentifier.java @@ -1,8 +1,8 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.custom; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; import gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.CustomFhirTransformation; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import java.util.List; import java.util.Map; import java.util.Objects; @@ -23,8 +23,8 @@ public class UpdateUniversalServiceIdentifier implements CustomFhirTransformatio public static final String ALTERNATE_ID_NAME = "alternateId"; @Override - public void transform(FhirResource resource, Map args) { - Bundle bundle = (Bundle) resource.getUnderlyingResource(); + public void transform(HealthData resource, Map args) { + Bundle bundle = (Bundle) resource.getUnderlyingData(); var serviceRequests = HapiHelper.resourcesInBundle(bundle, ServiceRequest.class); // Let it fail if args.get("") is not a string diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRule.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRule.java index 66bd5d2e7..146c21710 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRule.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRule.java @@ -1,14 +1,13 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.validation; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.Rule; +import gov.hhs.cdc.trustedintermediary.ruleengine.Rule; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import java.util.List; /** - * The ValidationRule class extends the {@link gov.hhs.cdc.trustedintermediary.etor.ruleengine.Rule - * Rule} class and represents a validation rule. It implements the {@link - * gov.hhs.cdc.trustedintermediary.etor.ruleengine.Rule#runRule(FhirResource) runRule} method to - * evaluate the validation and log a warning if the validation fails. + * The ValidationRule class extends the {@link Rule Rule} class and represents a validation rule. It + * implements the {@link Rule#runRule(HealthData...) runRule} method to evaluate the validation and + * log a warning if the validation fails. */ public class ValidationRule extends Rule { @@ -28,12 +27,19 @@ public ValidationRule( } @Override - public void runRule(FhirResource resource) { + public void runRule(HealthData... resource) { + + if (resource.length != 1) { + this.logger.logError( + "Rule [" + + this.getName() + + "]: Validation rules require exactly one resource object to be passed in."); + return; + } + for (String validation : this.getRules()) { try { - boolean isValid = - this.fhirEngine.evaluateCondition( - resource.getUnderlyingResource(), validation); + boolean isValid = this.evaluator.evaluateExpression(validation, resource[0]); if (!isValid) { this.logger.logWarning("Validation failed: " + this.getMessage()); } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRuleEngine.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRuleEngine.java index 496f69f0b..7d6379103 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRuleEngine.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRuleEngine.java @@ -1,9 +1,9 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.validation; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleEngine; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleLoader; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleLoaderException; +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleEngine; +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoader; +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoaderException; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import gov.hhs.cdc.trustedintermediary.wrappers.Logger; import gov.hhs.cdc.trustedintermediary.wrappers.formatter.TypeReference; import java.io.FileNotFoundException; @@ -61,7 +61,7 @@ public void ensureRulesLoaded() throws RuleLoaderException { } @Override - public void runRules(FhirResource resource) { + public void runRules(HealthData resource) { try { ensureRulesLoaded(); } catch (RuleLoaderException e) { diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiMessage.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiMessage.java index a8cea81d5..cf4cd82ae 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiMessage.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiMessage.java @@ -20,7 +20,7 @@ public HapiMessage(Bundle innerResource) { } @Override - public Bundle getUnderlyingResource() { + public Bundle getUnderlyingData() { return innerResource; } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/reportstream/ReportStreamOrderSender.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/reportstream/ReportStreamOrderSender.java index e001ab287..70b6b5cc5 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/reportstream/ReportStreamOrderSender.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/reportstream/ReportStreamOrderSender.java @@ -26,7 +26,7 @@ private ReportStreamOrderSender() {} @Override public Optional send(final Order order) throws UnableToSendMessageException { logger.logInfo("Sending the order to ReportStream"); - String json = fhir.encodeResourceToJson(order.getUnderlyingResource()); + String json = fhir.encodeResourceToJson(order.getUnderlyingData()); return sender.sendOrderToReportStream(json, order.getFhirResourceId()); } } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/reportstream/ReportStreamResultSender.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/reportstream/ReportStreamResultSender.java index 418791064..1ff64e0ba 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/reportstream/ReportStreamResultSender.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/reportstream/ReportStreamResultSender.java @@ -29,7 +29,7 @@ private ReportStreamResultSender() {} @Override public Optional send(Result result) throws UnableToSendMessageException { logger.logInfo("Sending results to ReportStream"); - String json = fhir.encodeResourceToJson(result.getUnderlyingResource()); + String json = fhir.encodeResourceToJson(result.getUnderlyingData()); return sender.sendResultToReportStream(json, result.getFhirResourceId()); } } diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/FhirResourceMock.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/FhirResourceMock.groovy deleted file mode 100644 index 9f25e65de..000000000 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/FhirResourceMock.groovy +++ /dev/null @@ -1,17 +0,0 @@ -package gov.hhs.cdc.trustedintermediary - -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource - -class FhirResourceMock implements FhirResource { - - private T innerResource - - FhirResourceMock(T innerResource) { - this.innerResource = innerResource - } - - @Override - T getUnderlyingResource() { - return innerResource - } -} diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/HealthDataMock.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/HealthDataMock.groovy new file mode 100644 index 000000000..dc0a10896 --- /dev/null +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/HealthDataMock.groovy @@ -0,0 +1,17 @@ +package gov.hhs.cdc.trustedintermediary + +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData + +class HealthDataMock implements HealthData { + + private T innerResource + + HealthDataMock(T innerResource) { + this.innerResource = innerResource + } + + @Override + T getUnderlyingData() { + return innerResource + } +} diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/OrderMock.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/OrderMock.groovy index af74e1792..1b523a3c6 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/OrderMock.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/OrderMock.groovy @@ -30,7 +30,7 @@ class OrderMock implements Order { } @Override - T getUnderlyingResource() { + T getUnderlyingData() { return this.underlyingOrders } diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/ResultMock.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/ResultMock.groovy index 46609bbc6..2a2f6f16b 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/ResultMock.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/ResultMock.groovy @@ -29,7 +29,7 @@ class ResultMock implements Result { } @Override - T getUnderlyingResource() { + T getUnderlyingData() { return this.underlyingResult } diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/orders/OrderControllerTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/orders/OrderControllerTest.groovy index f1f8c794c..7ad9ac1c8 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/orders/OrderControllerTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/orders/OrderControllerTest.groovy @@ -34,7 +34,7 @@ class OrderControllerTest extends Specification { TestApplicationContext.injectRegisteredImplementations() when: - def actualBundle = controller.parseOrders(new DomainRequest()).underlyingResource + def actualBundle = controller.parseOrders(new DomainRequest()).getUnderlyingData() then: actualBundle == expectedBundle diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/results/ResultControllerTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/results/ResultControllerTest.groovy index 1b35e32ca..23eecde39 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/results/ResultControllerTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/results/ResultControllerTest.groovy @@ -32,7 +32,7 @@ class ResultControllerTest extends Specification { TestApplicationContext.injectRegisteredImplementations() when: - def actualBundle = controller.parseResults(new DomainRequest()).underlyingResource + def actualBundle = controller.parseResults(new DomainRequest()).getUnderlyingData() then: actualBundle == expectedBundle diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleTest.groovy deleted file mode 100644 index 3f335d233..000000000 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleTest.groovy +++ /dev/null @@ -1,29 +0,0 @@ -package gov.hhs.cdc.trustedintermediary.etor.ruleengine - -import gov.hhs.cdc.trustedintermediary.FhirResourceMock -import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext -import gov.hhs.cdc.trustedintermediary.wrappers.HapiFhir -import gov.hhs.cdc.trustedintermediary.wrappers.Logger -import spock.lang.Specification - -class RuleTest extends Specification { - - def setup() { - TestApplicationContext.reset() - TestApplicationContext.init() - TestApplicationContext.register(Logger, Mock(Logger)) - TestApplicationContext.register(HapiFhir, Mock(HapiFhir)) - TestApplicationContext.injectRegisteredImplementations() - } - - def "runRule throws an UnsupportedOperationException when ran from the Rule class"() { - given: - def rule = new Rule() - - when: - rule.runRule(new FhirResourceMock("resource")) - - then: - thrown(UnsupportedOperationException) - } -} diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleEngineHelper.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/TransformationRuleEngineHelper.groovy similarity index 75% rename from etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleEngineHelper.groovy rename to etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/TransformationRuleEngineHelper.groovy index 601ab3cb1..2230dcd86 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleEngineHelper.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/TransformationRuleEngineHelper.groovy @@ -1,6 +1,8 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine -class RuleEngineHelper { +import gov.hhs.cdc.trustedintermediary.ruleengine.Rule + +class TransformationRuleEngineHelper { static T getRuleByName(List rules, String ruleName) { return rules.stream() diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRuleEngineIntegrationTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRuleEngineIntegrationTest.groovy index 0fccd8211..6b18b4af2 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRuleEngineIntegrationTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRuleEngineIntegrationTest.groovy @@ -1,16 +1,16 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleEngineHelper +import gov.hhs.cdc.trustedintermediary.etor.ruleengine.TransformationRuleEngineHelper import gov.hhs.cdc.trustedintermediary.ExamplesHelper import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleLoader +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoader import gov.hhs.cdc.trustedintermediary.external.hapi.HapiFhirHelper import gov.hhs.cdc.trustedintermediary.external.hapi.HapiFhirImplementation import gov.hhs.cdc.trustedintermediary.external.hapi.HapiFhirResource import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper import gov.hhs.cdc.trustedintermediary.external.jackson.Jackson -import gov.hhs.cdc.trustedintermediary.wrappers.HapiFhir +import gov.hhs.cdc.trustedintermediary.wrappers.HealthDataExpressionEvaluator import gov.hhs.cdc.trustedintermediary.wrappers.Logger import gov.hhs.cdc.trustedintermediary.wrappers.MetricMetadata import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter @@ -30,7 +30,7 @@ class TransformationRuleEngineIntegrationTest extends Specification { TestApplicationContext.init() TestApplicationContext.register(Formatter, Jackson.getInstance()) - TestApplicationContext.register(HapiFhir, fhir) + TestApplicationContext.register(HealthDataExpressionEvaluator, fhir) TestApplicationContext.register(TransformationRuleEngine, engine) TestApplicationContext.register(RuleLoader, RuleLoader.getInstance()) TestApplicationContext.register(Logger, mockLogger) @@ -71,7 +71,7 @@ class TransformationRuleEngineIntegrationTest extends Specification { 'addContactSectionToPatientResource' ] def fhirResource = ExamplesHelper.getExampleFhirResource(testFile) - def bundle = (Bundle) fhirResource.getUnderlyingResource() + def bundle = (Bundle) fhirResource.getUnderlyingData() expect: HapiHelper.resourceInBundle(bundle, MessageHeader).event.code == 'O01' @@ -79,7 +79,7 @@ class TransformationRuleEngineIntegrationTest extends Specification { when: transformationsToApply.each { ruleName -> - def rule = RuleEngineHelper.getRuleByName(engine.rules, ruleName) + def rule = TransformationRuleEngineHelper.getRuleByName(engine.rules, ruleName) rule.runRule(fhirResource) } def messageHeader = HapiHelper.resourceInBundle(bundle, MessageHeader) as MessageHeader diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRuleEngineTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRuleEngineTest.groovy index 4381219f2..dcae2f448 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRuleEngineTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRuleEngineTest.groovy @@ -1,10 +1,10 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleEngine -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleLoader -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleLoaderException +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleEngine +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoader +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoaderException +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData import gov.hhs.cdc.trustedintermediary.wrappers.Logger import gov.hhs.cdc.trustedintermediary.wrappers.formatter.TypeReference import spock.lang.Specification @@ -94,7 +94,7 @@ class TransformationRuleEngineTest extends Specification { } when: - ruleEngine.runRules(Mock(FhirResource)) + ruleEngine.runRules(Mock(HealthData)) then: 1 * mockLogger.logError(_ as String, exception) @@ -103,7 +103,7 @@ class TransformationRuleEngineTest extends Specification { def "runRules handles logging warning correctly"() { given: def applyingTransformationMessage = "Applying transformation" - def fhirBundle = Mock(FhirResource) + def fhirBundle = Mock(HealthData) def testRule = Mock(TransformationRule) testRule.getMessage() >> applyingTransformationMessage testRule.shouldRun(fhirBundle) >> true @@ -140,7 +140,7 @@ class TransformationRuleEngineTest extends Specification { mockRuleLoader.loadRules(_ as InputStream, _ as TypeReference) >> { throw exception } when: - ruleEngine.runRules(Mock(FhirResource)) + ruleEngine.runRules(Mock(HealthData)) then: 1 * mockLogger.logError(_ as String, exception) diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRuleTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRuleTest.groovy index 5dfe2e944..b6b14a669 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRuleTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/TransformationRuleTest.groovy @@ -1,12 +1,16 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation -import gov.hhs.cdc.trustedintermediary.FhirResourceMock +import gov.hhs.cdc.trustedintermediary.HealthDataMock import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext +import gov.hhs.cdc.trustedintermediary.etor.ruleengine.validation.ValidationRule import gov.hhs.cdc.trustedintermediary.external.hapi.HapiFhirHelper import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData +import gov.hhs.cdc.trustedintermediary.wrappers.HealthDataExpressionEvaluator import gov.hhs.cdc.trustedintermediary.wrappers.HapiFhir import gov.hhs.cdc.trustedintermediary.wrappers.Logger +import gov.hhs.cdc.trustedintermediary.external.hapi.HapiFhirImplementation import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.MessageHeader import spock.lang.Specification @@ -18,6 +22,7 @@ class TransformationRuleTest extends Specification { TestApplicationContext.reset() TestApplicationContext.init() TestApplicationContext.register(Logger, mockLogger) + TestApplicationContext.register(HealthDataExpressionEvaluator, HapiFhirImplementation.getInstance()) TestApplicationContext.injectRegisteredImplementations() } @@ -57,13 +62,13 @@ class TransformationRuleTest extends Specification { TestApplicationContext.register(HapiFhir, Mock(HapiFhir)) def rule = new TransformationRule(ruleName, ruleDescription, ruleMessage, ruleConditions, ruleActions) - def fhirResource = new FhirResourceMock(HapiFhirHelper.createMessageBundle(new HashMap())) + def fhirResource = new HealthDataMock(HapiFhirHelper.createMessageBundle(new HashMap())) when: rule.runRule(fhirResource) then: - def messageHeader = HapiHelper.resourceInBundle(fhirResource.getUnderlyingResource() as Bundle, MessageHeader.class) as MessageHeader + def messageHeader = HapiHelper.resourceInBundle(fhirResource.getUnderlyingData() as Bundle, MessageHeader.class) as MessageHeader messageHeader.getEventCoding().getCode() == "mock_code" } @@ -80,7 +85,7 @@ class TransformationRuleTest extends Specification { when: def rule = new TransformationRule(ruleName, ruleDescription, ruleMessage, ruleConditions, ruleActions) - def fhirResource = new FhirResourceMock(HapiFhirHelper.createMessageBundle(new HashMap())) + def fhirResource = new HealthDataMock(HapiFhirHelper.createMessageBundle(new HashMap())) rule.runRule(fhirResource) then: @@ -101,7 +106,7 @@ class TransformationRuleTest extends Specification { when: def rule = new TransformationRule(ruleName, ruleDescription, ruleMessage, ruleConditions, ruleActions) - def fhirResource = new FhirResourceMock(HapiFhirHelper.createMessageBundle(new HashMap())) + def fhirResource = new HealthDataMock(HapiFhirHelper.createMessageBundle(new HashMap())) rule.runRule(fhirResource) then: @@ -122,7 +127,7 @@ class TransformationRuleTest extends Specification { when: def rule = new TransformationRule(ruleName, ruleDescription, ruleMessage, ruleConditions, ruleActions) - def fhirResource = new FhirResourceMock(HapiFhirHelper.createMessageBundle(new HashMap())) + def fhirResource = new HealthDataMock(HapiFhirHelper.createMessageBundle(new HashMap())) rule.runRule(fhirResource) then: @@ -143,10 +148,26 @@ class TransformationRuleTest extends Specification { when: def rule = new TransformationRule(ruleName, ruleDescription, ruleMessage, ruleConditions, ruleActions) - def fhirResource = new FhirResourceMock(HapiFhirHelper.createMessageBundle(new HashMap())) + def fhirResource = new HealthDataMock(HapiFhirHelper.createMessageBundle(new HashMap())) rule.runRule(fhirResource) then: 1 * mockLogger.logError(_, _) } + + def "runRule logs an error if passing more than one HealthData"() { + given: + def mockFhir = Mock(HapiFhirImplementation) + TestApplicationContext.register(HealthDataExpressionEvaluator, mockFhir) + + def rule = new TransformationRule(null, null, null, ["condition"], [ + new TransformationRuleMethod("InstantiationExceptionMockClass", null) + ]) + + when: + rule.runRule(Mock(HealthData), Mock(HealthData)) + + then: + 1 * mockLogger.logError(_ as String) + } } diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/AddContactSectionToPatientResourceTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/AddContactSectionToPatientResourceTest.groovy index e34e3fa16..413ece144 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/AddContactSectionToPatientResourceTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/AddContactSectionToPatientResourceTest.groovy @@ -67,10 +67,10 @@ class AddContactSectionToPatientResourceTest extends Specification { mockOrderBundle.setEntry(entryList) when: - transformClass.transform(new HapiFhirResource(mockOrder.getUnderlyingResource()), null) + transformClass.transform(new HapiFhirResource(mockOrder.getUnderlyingData()), null) then: - def convertedPatient = HapiHelper.resourceInBundle(mockOrder.getUnderlyingResource(), Patient.class) as Patient + def convertedPatient = HapiHelper.resourceInBundle(mockOrder.getUnderlyingData(), Patient.class) as Patient def contactSection = convertedPatient.getContact()[0] contactSection != null @@ -111,7 +111,7 @@ class AddContactSectionToPatientResourceTest extends Specification { mockOrderBundle.setEntry(entryList) when: - transformClass.transform(new HapiFhirResource(mockOrder.getUnderlyingResource()), null) + transformClass.transform(new HapiFhirResource(mockOrder.getUnderlyingData()), null) then: def convertedPatient = HapiHelper.resourceInBundle(mockOrderBundle, Patient.class) as Patient diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy index bd15389b3..eb74ed5b6 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy @@ -182,7 +182,7 @@ class CopyOrcOrderProviderToObrOrderProviderTest extends Specification{ } def fhirResource = ExamplesHelper.getExampleFhirResource(fhirOruPath) - return fhirResource.getUnderlyingResource() as Bundle + return fhirResource.getUnderlyingData() as Bundle } ServiceRequest createServiceRequest(Bundle bundle) { diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/HappyPathCustomTransformationMockClass.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/HappyPathCustomTransformationMockClass.groovy index 58ca4fe7d..6108b7aa6 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/HappyPathCustomTransformationMockClass.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/HappyPathCustomTransformationMockClass.groovy @@ -1,16 +1,16 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.custom -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource import gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.CustomFhirTransformation import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.Coding class HappyPathCustomTransformationMockClass implements CustomFhirTransformation { @Override - public void transform(final FhirResource resource, final Map args) { - Bundle bundle = (Bundle) resource.getUnderlyingResource() + public void transform(final HealthData data, final Map args) { + Bundle bundle = (Bundle) data.getUnderlyingData() def system = "http://terminology.hl7.org/CodeSystem/v2-0003" def code = "mock_code" diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/IllegalAccessExceptionMockClass.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/IllegalAccessExceptionMockClass.groovy index a63173651..d9e849089 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/IllegalAccessExceptionMockClass.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/IllegalAccessExceptionMockClass.groovy @@ -1,13 +1,14 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.custom -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData + class IllegalAccessExceptionMockClass { private IllegalAccessExceptionMockClass() { } - public void transform(final FhirResource resource, final Map args) { + public void transform(final HealthData data, final Map args) { // empty for tests } } diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/MapLocalObservationCodesTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/MapLocalObservationCodesTest.groovy index 9a1e176d9..8c061582e 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/MapLocalObservationCodesTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/MapLocalObservationCodesTest.groovy @@ -172,7 +172,7 @@ class MapLocalObservationCodesTest extends Specification { given: final String FHIR_ORU_PATH = "../CA/020_CA_ORU_R01_CDPH_OBX_to_LOINC_1_hl7_translation.fhir" def fhirResource = ExamplesHelper.getExampleFhirResource(FHIR_ORU_PATH) - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle def initialObservations = HapiHelper.resourcesInBundle(bundle, Observation.class).toList() expect: diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/NoSuchMethodExceptionMockClass.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/NoSuchMethodExceptionMockClass.groovy index ed83f2f5a..6bc794c14 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/NoSuchMethodExceptionMockClass.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/NoSuchMethodExceptionMockClass.groovy @@ -1,6 +1,7 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.custom -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData + class NoSuchMethodExceptionMockClass { private String imNotNull @@ -9,7 +10,7 @@ class NoSuchMethodExceptionMockClass { imNotNull = notNullConstructor } - public void noTransform(final FhirResource resource, final Map args) { + public void noTransform(final HealthData data, final Map args) { //No such method } } diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationByCodeTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationByCodeTest.groovy index d06e9bf94..175b97c0b 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationByCodeTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationByCodeTest.groovy @@ -161,7 +161,7 @@ class RemoveObservationByCodeTest extends Specification { final String FHIR_ORU_PATH = "../CA/020_CA_ORU_R01_CDPH_OBX_to_LOINC_1_hl7_translation.fhir" def fhirResource = ExamplesHelper.getExampleFhirResource(FHIR_ORU_PATH) - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle def args = getArgs(MATCHING_CODE, MATCHING_CODING_SYSTEM_EXT, MATCHING_CODING_EXT) diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationRequestsTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationRequestsTest.groovy index 9e6a4f8ac..ff74f9c09 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationRequestsTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationRequestsTest.groovy @@ -26,7 +26,7 @@ class RemoveObservationRequestsTest extends Specification { given: // Load a FHIR resource example def fhirResource = ExamplesHelper.getExampleFhirResource("../CA/002_CA_ORU_R01_initial_translation.fhir") - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle // Prepare args with a List instead of a String to trigger null response from ternary operator def listOfIdentifiers = ["54089-8", "99717-5"] @@ -44,7 +44,7 @@ class RemoveObservationRequestsTest extends Specification { def "remove all OBRs except for the one with OBR-4.1 = '54089-8'"() { given: def fhirResource = ExamplesHelper.getExampleFhirResource("../CA/002_CA_ORU_R01_initial_translation.fhir") - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle def initialDiagnosticReports = HapiHelper.resourcesInBundle(bundle, DiagnosticReport).toList() def initialServiceRequests = HapiHelper.resourcesInBundle(bundle, ServiceRequest).toList() @@ -73,7 +73,7 @@ class RemoveObservationRequestsTest extends Specification { def "once removed all OBRs except one, attach all observations to that single OBR"() { given: def fhirResource = ExamplesHelper.getExampleFhirResource("../CA/002_CA_ORU_R01_initial_translation.fhir") - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle def initialDiagnosticReports = HapiHelper.resourcesInBundle(bundle, DiagnosticReport).toList() def initialObservations = HapiHelper.resourcesInBundle(bundle, Observation).toList() @@ -94,7 +94,7 @@ class RemoveObservationRequestsTest extends Specification { def "remove all irrelevant OBRs, with edge case of one DiagnosticReport not having a related ServiceRequest"() { given: def fhirResource = ExamplesHelper.getExampleFhirResource("../Test/Results/006_CA_ORU_R01_one_diagnostic_report_without_basedOn.fhir") - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle def initialDiagnosticReports = HapiHelper.resourcesInBundle(bundle, DiagnosticReport).toList() def initialServiceRequests = HapiHelper.resourcesInBundle(bundle, ServiceRequest).toList() @@ -118,7 +118,7 @@ class RemoveObservationRequestsTest extends Specification { def "remove all irrelevant OBRs, with edge case of all DiagnosticReports not having a related ServiceRequest"() { given: def fhirResource = ExamplesHelper.getExampleFhirResource("../Test/Results/006_CA_ORU_R01_all_diagnostic_reports_without_basedOn.fhir") - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle def initialDiagnosticReports = HapiHelper.resourcesInBundle(bundle, DiagnosticReport).toList() def initialServiceRequests = HapiHelper.resourcesInBundle(bundle, ServiceRequest).toList() @@ -142,7 +142,7 @@ class RemoveObservationRequestsTest extends Specification { def "no OBRs are removed because nothing matches the universalServiceIdentifier"() { given: def fhirResource = ExamplesHelper.getExampleFhirResource("../CA/002_CA_ORU_R01_initial_translation.fhir") - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle def initialDiagnosticReports = HapiHelper.resourcesInBundle(bundle, DiagnosticReport).toList() def initialServiceRequests = HapiHelper.resourcesInBundle(bundle, ServiceRequest).toList() diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifierTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifierTest.groovy index 85cafdb69..e1920f2c6 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifierTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifierTest.groovy @@ -23,7 +23,7 @@ class RemovePatientIdentifierTest extends Specification { def "remove PID.3-4 and PID.3-5 from Bundle"() { given: def fhirResource = ExamplesHelper.getExampleFhirResource("../MN/004_MN_ORU_R01_NBS_1_hl7_translation.fhir") - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle def pid3_4 = HapiFhirHelper.getPID3_4Value(bundle) def pid3_5 = HapiFhirHelper.getPID3_5Value(bundle) diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCodeTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCodeTest.groovy index f067bafa2..53927b19f 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCodeTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCodeTest.groovy @@ -23,7 +23,7 @@ class RemovePatientNameTypeCodeTest extends Specification { def "remove PID.5-7 from Bundle"() { given: def fhirResource = ExamplesHelper.getExampleFhirResource("../CA/002_CA_ORU_R01_initial_translation.fhir") - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle def pid5_7 = HapiFhirHelper.getPID5_7Value(bundle) expect: diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/SwapPlacerOrderAndGroupNumbersTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/SwapPlacerOrderAndGroupNumbersTest.groovy index 49559d1cb..58cb0aa11 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/SwapPlacerOrderAndGroupNumbersTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/SwapPlacerOrderAndGroupNumbersTest.groovy @@ -23,7 +23,7 @@ class SwapPlacerOrderAndGroupNumbersTest extends Specification { def "swap ORC-2.1 + OBR-2.1 with ORC-4.1 and ORC-2.2 + OBR-2.2 with ORC-4.2"() { given: def fhirResource = ExamplesHelper.getExampleFhirResource("../MN/004_MN_ORU_R01_NBS_1_hl7_translation.fhir") - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle def serviceRequest = HapiHelper.resourceInBundle(bundle, ServiceRequest) def orc2_1 = HapiHelper.getORC2_1Value(serviceRequest) diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateReceivingFacilityWithOrderingFacilityIdentifierTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateReceivingFacilityWithOrderingFacilityIdentifierTest.groovy index a179e47aa..5477b466f 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateReceivingFacilityWithOrderingFacilityIdentifierTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateReceivingFacilityWithOrderingFacilityIdentifierTest.groovy @@ -23,7 +23,7 @@ class UpdateReceivingFacilityWithOrderingFacilityIdentifierTest extends Specific def "update receiving facility with ordering facility identifier"() { given: def fhirResource = ExamplesHelper.getExampleFhirResource('../MN/004_MN_ORU_R01_NBS_1_hl7_translation.fhir') - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle def diagnosticReport = HapiHelper.getDiagnosticReport(bundle) def serviceRequest = HapiHelper.getServiceRequest(diagnosticReport) def orc21_10 = HapiHelper.getORC21Value(serviceRequest) diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateSendingFacilityNamespaceTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateSendingFacilityNamespaceTest.groovy index 205d8c854..47bfb6d2e 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateSendingFacilityNamespaceTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateSendingFacilityNamespaceTest.groovy @@ -24,7 +24,7 @@ class UpdateSendingFacilityNamespaceTest extends Specification { given: def name = (Object) "CDPH" def fhirResource = ExamplesHelper.getExampleFhirResource("../MN/004_MN_ORU_R01_NBS_1_hl7_translation.fhir") - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle expect: HapiHelper.getMSH4Organization(bundle).getIdentifier().size() > 1 @@ -54,7 +54,7 @@ class UpdateSendingFacilityNamespaceTest extends Specification { given: def name = "CDPH" def fhirResource = ExamplesHelper.getExampleFhirResource("../MN/004_MN_ORU_R01_NBS_1_hl7_translation.fhir") - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle def listOfNames = ["trusted", "intermediary"] expect: diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateUniversalServiceIdentifierTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateUniversalServiceIdentifierTest.groovy index 50e722519..4a9c69359 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateUniversalServiceIdentifierTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdateUniversalServiceIdentifierTest.groovy @@ -30,7 +30,7 @@ class UpdateUniversalServiceIdentifierTest extends Specification { def "skip transformation if the coding identifier is missing"() { given: - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle def result = getObrSections(bundle)[0] def obr4_1 = result[0] def obr4_3 = result[1] @@ -43,7 +43,7 @@ class UpdateUniversalServiceIdentifierTest extends Specification { when: transformClass.transform(fhirResource, args) - bundle = fhirResource.getUnderlyingResource() as Bundle + bundle = fhirResource.getUnderlyingData() as Bundle def transformedResult = getObrSections(bundle)[0] def transformedObr4_1 = transformedResult[0] def transformedObr4_3 = transformedResult[1] @@ -57,7 +57,7 @@ class UpdateUniversalServiceIdentifierTest extends Specification { def "skip transformation if the coding identifier is not the one we want"() { given: - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle def result = getObrSections(bundle)[1] def obr4_1 = result[0] def obr4_3 = result[1] @@ -70,7 +70,7 @@ class UpdateUniversalServiceIdentifierTest extends Specification { when: transformClass.transform(fhirResource, args) - bundle = fhirResource.getUnderlyingResource() as Bundle + bundle = fhirResource.getUnderlyingData() as Bundle def transformedResult = getObrSections(bundle)[1] def transformedObr4_1 = transformedResult[0] def transformedObr4_3 = transformedResult[1] @@ -84,7 +84,7 @@ class UpdateUniversalServiceIdentifierTest extends Specification { def "update obr4 values when the code matches"() { given: - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle def result = getObrSections(bundle)[2] def obr4_1 = result[0] def obr4_3 = result[1] @@ -97,7 +97,7 @@ class UpdateUniversalServiceIdentifierTest extends Specification { when: transformClass.transform(fhirResource, args) - bundle = fhirResource.getUnderlyingResource() as Bundle + bundle = fhirResource.getUnderlyingData() as Bundle def transformedResult = getObrSections(bundle)[2] def transformedObr4_1 = transformedResult[0] def transformedObr4_3 = transformedResult[1] @@ -112,7 +112,7 @@ class UpdateUniversalServiceIdentifierTest extends Specification { def "update only obr4-1 through obr4-3 values when the code matches and alternate id is null"() { given: - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle def result = getObrSections(bundle)[2] def obr4_1 = result[0] def obr4_3 = result[1] @@ -125,7 +125,7 @@ class UpdateUniversalServiceIdentifierTest extends Specification { when: transformClass.transform(fhirResource, argsNoAlternateId) - bundle = fhirResource.getUnderlyingResource() as Bundle + bundle = fhirResource.getUnderlyingData() as Bundle def transformedResult = getObrSections(bundle)[2] def transformedObr4_1 = transformedResult[0] def transformedObr4_3 = transformedResult[1] @@ -139,7 +139,7 @@ class UpdateUniversalServiceIdentifierTest extends Specification { def "leave obr4 values unchanged if the code matches and they're already correct"() { given: - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle def result = getObrSections(bundle)[3] def obr4_1 = result[0] def obr4_3 = result[1] @@ -152,7 +152,7 @@ class UpdateUniversalServiceIdentifierTest extends Specification { when: transformClass.transform(fhirResource, args) - bundle = fhirResource.getUnderlyingResource() as Bundle + bundle = fhirResource.getUnderlyingData() as Bundle def transformedResult = getObrSections(bundle)[3] def transformedObr4_1 = transformedResult[0] def transformedObr4_3 = transformedResult[1] @@ -166,7 +166,7 @@ class UpdateUniversalServiceIdentifierTest extends Specification { def "update values if the coding identifier is correct but the values are missing"() { given: - def bundle = fhirResource.getUnderlyingResource() as Bundle + def bundle = fhirResource.getUnderlyingData() as Bundle def result = getObrSections(bundle)[4] def obr4_1 = result[0] def obr4_3 = result[1] @@ -179,7 +179,7 @@ class UpdateUniversalServiceIdentifierTest extends Specification { when: transformClass.transform(fhirResource, args) - bundle = fhirResource.getUnderlyingResource() as Bundle + bundle = fhirResource.getUnderlyingData() as Bundle def transformedResult = getObrSections(bundle)[4] def transformedObr4_1 = transformedResult[0] def transformedObr4_3 = transformedResult[1] diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRuleEngineIntegrationTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRuleEngineIntegrationTest.groovy index a672e6968..ea7fb3769 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRuleEngineIntegrationTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRuleEngineIntegrationTest.groovy @@ -3,12 +3,13 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.validation import gov.hhs.cdc.trustedintermediary.ExamplesHelper import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleLoader +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoader import gov.hhs.cdc.trustedintermediary.external.hapi.HapiFhirHelper import gov.hhs.cdc.trustedintermediary.external.hapi.HapiFhirImplementation import gov.hhs.cdc.trustedintermediary.external.hapi.HapiFhirResource import gov.hhs.cdc.trustedintermediary.external.jackson.Jackson import gov.hhs.cdc.trustedintermediary.wrappers.HapiFhir +import gov.hhs.cdc.trustedintermediary.wrappers.HealthDataExpressionEvaluator import gov.hhs.cdc.trustedintermediary.wrappers.Logger import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter import org.hl7.fhir.r4.model.Bundle @@ -28,6 +29,7 @@ class ValidationRuleEngineIntegrationTest extends Specification { TestApplicationContext.register(HapiFhir, fhir) TestApplicationContext.register(ValidationRuleEngine, engine) TestApplicationContext.register(RuleLoader, RuleLoader.getInstance()) + TestApplicationContext.register(HealthDataExpressionEvaluator, HapiFhirImplementation.getInstance()) TestApplicationContext.register(Logger, mockLogger) TestApplicationContext.injectRegisteredImplementations() diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRuleEngineTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRuleEngineTest.groovy index 4df90cd81..a4ae3c6bb 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRuleEngineTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRuleEngineTest.groovy @@ -1,10 +1,10 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.validation import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleEngine -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleLoader -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleLoaderException +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleEngine +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoader +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoaderException +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData import gov.hhs.cdc.trustedintermediary.wrappers.Logger import gov.hhs.cdc.trustedintermediary.wrappers.formatter.TypeReference import spock.lang.Specification @@ -94,7 +94,7 @@ class ValidationRuleEngineTest extends Specification { } when: - ruleEngine.runRules(Mock(FhirResource)) + ruleEngine.runRules(Mock(HealthData)) then: 1 * mockLogger.logError(_ as String, exception) @@ -104,7 +104,7 @@ class ValidationRuleEngineTest extends Specification { given: def failedValidationMessage = "Failed validation message" def fullFailedValidationMessage = "Validation failed: " + failedValidationMessage - def fhirBundle = Mock(FhirResource) + def fhirBundle = Mock(HealthData) def invalidRule = Mock(ValidationRule) invalidRule.getMessage() >> failedValidationMessage invalidRule.shouldRun(fhirBundle) >> true @@ -141,7 +141,7 @@ class ValidationRuleEngineTest extends Specification { mockRuleLoader.loadRules(_ as InputStream, _ as TypeReference) >> { throw exception } when: - ruleEngine.runRules(Mock(FhirResource)) + ruleEngine.runRules(Mock(HealthData)) then: 1 * mockLogger.logError(_ as String, exception) diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRuleTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRuleTest.groovy index f2877557a..0143acd4f 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRuleTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/validation/ValidationRuleTest.groovy @@ -1,11 +1,11 @@ package gov.hhs.cdc.trustedintermediary.etor.ruleengine.validation -import gov.hhs.cdc.trustedintermediary.FhirResourceMock +import gov.hhs.cdc.trustedintermediary.HealthDataMock import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource import gov.hhs.cdc.trustedintermediary.external.hapi.HapiFhirImplementation -import gov.hhs.cdc.trustedintermediary.wrappers.HapiFhir +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData import gov.hhs.cdc.trustedintermediary.wrappers.Logger +import gov.hhs.cdc.trustedintermediary.wrappers.HealthDataExpressionEvaluator import spock.lang.Specification class ValidationRuleTest extends Specification { @@ -26,7 +26,8 @@ class ValidationRuleTest extends Specification { def ruleWarningMessage = "Rule Warning Message" def conditions = ["condition1", "condition2"] def validations = ["validation1", "validation2"] - TestApplicationContext.register(HapiFhir, Mock(HapiFhir)) + TestApplicationContext.register(HealthDataExpressionEvaluator, Mock(HealthDataExpressionEvaluator)) + TestApplicationContext.injectRegisteredImplementations() when: def rule = new ValidationRule(ruleName, ruleDescription, ruleWarningMessage, conditions, validations) @@ -41,9 +42,9 @@ class ValidationRuleTest extends Specification { def "shouldRun returns expected boolean depending on conditions"() { given: - def mockFhir = Mock(HapiFhir) - mockFhir.evaluateCondition(_ as Object, _ as String) >> true >> conditionResult - TestApplicationContext.register(HapiFhir, mockFhir) + def mockFhir = Mock(HapiFhirImplementation) + mockFhir.evaluateExpression(_ as String, _ as HealthData) >> true >> conditionResult + TestApplicationContext.register(HealthDataExpressionEvaluator, mockFhir) def rule = new ValidationRule(null, null, null, [ "trueCondition", @@ -51,7 +52,7 @@ class ValidationRuleTest extends Specification { ], null) expect: - rule.shouldRun(new FhirResourceMock("resource")) == applies + rule.shouldRun(Mock(HealthData)) == applies where: conditionResult | applies @@ -62,13 +63,13 @@ class ValidationRuleTest extends Specification { def "shouldRun logs an error and returns false if an exception happens when evaluating a condition"() { given: def mockFhir = Mock(HapiFhirImplementation) - mockFhir.evaluateCondition(_ as Object, "condition") >> { throw new Exception() } - TestApplicationContext.register(HapiFhir, mockFhir) + mockFhir.evaluateExpression("condition", _ as HealthData) >> { throw new Exception() } + TestApplicationContext.register(HealthDataExpressionEvaluator, mockFhir) def rule = new ValidationRule(null, null, null, ["condition"], null) when: - def applies = rule.shouldRun(Mock(FhirResource)) + def applies = rule.shouldRun(Mock(HealthData)) then: 1 * mockLogger.logError(_ as String, _ as Exception) @@ -77,8 +78,8 @@ class ValidationRuleTest extends Specification { def "runRule returns expected boolean depending on validations"() { given: - def mockFhir = Mock(HapiFhir) - TestApplicationContext.register(HapiFhir, mockFhir) + def mockFhir = Mock(HapiFhirImplementation) + TestApplicationContext.register(HealthDataExpressionEvaluator, mockFhir) def rule = new ValidationRule(null, null, null, null, [ "trueValidation", @@ -86,35 +87,49 @@ class ValidationRuleTest extends Specification { ]) when: - mockFhir.evaluateCondition(_ as Object, _ as String) >> true >> true - rule.runRule(new FhirResourceMock("resource")) + mockFhir.evaluateExpression(_ as String, _ as HealthData) >> true >> true + rule.runRule(new HealthDataMock("resource")) then: 0 * mockLogger.logWarning(_ as String) 0 * mockLogger.logError(_ as String, _ as Exception) when: - mockFhir.evaluateCondition(_ as Object, _ as String) >> true >> false - rule.runRule(new FhirResourceMock("resource")) + mockFhir.evaluateExpression(_ as String, _ as HealthData) >> true >> false + rule.runRule(new HealthDataMock("resource")) then: 1 * mockLogger.logWarning(_ as String) 0 * mockLogger.logError(_ as String, _ as Exception) } - def "runRule logs an error and returns false if an exception happens when evaluating a validation"() { + def "runRule logs an error if an exception happens when evaluating a validation"() { given: def mockFhir = Mock(HapiFhirImplementation) - mockFhir.evaluateCondition(_ as Object, "condition") >> { throw new Exception() } - TestApplicationContext.register(HapiFhir, mockFhir) + mockFhir.evaluateExpression(_ as String, _ as HealthData) >> { throw new Exception() } + TestApplicationContext.register(HealthDataExpressionEvaluator, mockFhir) def rule = new ValidationRule(null, null, null, null, ["validation"]) when: - rule.runRule(Mock(FhirResource)) + rule.runRule(Mock(HealthData)) then: 0 * mockLogger.logWarning(_ as String) 1 * mockLogger.logError(_ as String, _ as Exception) } + + def "runRule logs an error if passing more than one HealthData"() { + given: + def mockFhir = Mock(HapiFhirImplementation) + TestApplicationContext.register(HealthDataExpressionEvaluator, mockFhir) + + def rule = new ValidationRule(null, null, null, ["condition"], ["validation"]) + + when: + rule.runRule(Mock(HealthData), Mock(HealthData)) + + then: + 1 * mockLogger.logError(_ as String) + } } diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderTest.groovy index dab5cd610..b0f396763 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderTest.groovy @@ -31,13 +31,13 @@ class HapiOrderTest extends Specification { TestApplicationContext.injectRegisteredImplementations() } - def "getUnderlyingResource Works"() { + def "getUnderlyingData Works"() { given: def expectedInnerOrder = new Bundle() def order = new HapiOrder(expectedInnerOrder) when: - def actualInnerOrder = order.getUnderlyingResource() + def actualInnerOrder = order.getUnderlyingData() then: actualInnerOrder == expectedInnerOrder diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiResultTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiResultTest.groovy index 5997a313b..e63718212 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiResultTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiResultTest.groovy @@ -34,7 +34,7 @@ class HapiResultTest extends Specification { def result = new HapiResult(expectedResult) when: - def actualResult = result.getUnderlyingResource() + def actualResult = result.getUnderlyingData() then: actualResult == expectedResult diff --git a/examples/Test/Automated/001_Staging_ORM_O01_short_linked_to_002_ORU.hl7 b/examples/Test/Automated/001_Staging_ORM_O01_short_linked_to_002_ORU.hl7 index 45cbb04a6..789a99eb9 100644 --- a/examples/Test/Automated/001_Staging_ORM_O01_short_linked_to_002_ORU.hl7 +++ b/examples/Test/Automated/001_Staging_ORM_O01_short_linked_to_002_ORU.hl7 @@ -1,4 +1,4 @@ -MSH|^~\&|Sender Application^sender.test.com^DNS|Sender Facility^0.0.0.0.0.0.0.0^ISO|Receiver Application^0.0.0.0.0.0.0.0^ISO|Receiver Facility^automated-staging-test-receiver-id^DNS|20230101010000-0000||ORM^O01^ORM_O01|111111|T|2.5.1|||||||||| +MSH|^~\&|Sender Application^sender.test.com^DNS|Sender Facility^0.0.0.0.0.0.0.0^ISO|Receiver Application^0.0.0.0.0.0.0.0^ISO|Receiver Facility^automated-staging-test-receiver-id^DNS|20230101010000-0000||ORM^O01^ORM_O01|001|T|2.5.1|||||||||| PID|1||1300974^^^Baptist East^MR||ONE^TESTCASE||202402210152-0500|F^Female^HL70001||2106-3^White^HL70005|1234 GPCS WAY^^MONTGOMERY^Alabama^36117^USA^home^^Montgomery|||||||2227600015||||N^Not Hispanic or Latino^HL70189|||1||||| NK1|1|ONE^MOMFIRST|MTH^Mother^HL70063||^^^^^804^5693861||||||||||||||||||||||||||||123456789^^^Medicaid&2.16.840.1.113883.4.446&ISO^MD||||000-00-0000^^^ssn&2.16.840.1.113883.4.1&ISO^SS ORC|NW|4560411583^ORDERID||||||||||12345^^^^^^^^NPI&2.16.840.1.113883.4.6&ISO^L||||||||| diff --git a/examples/Test/Automated/002_Staging_ORU_O01_short_linked_to_001_ORM.hl7 b/examples/Test/Automated/002_Staging_ORU_O01_short_linked_to_001_ORM.hl7 index c52d356c9..18125721b 100644 --- a/examples/Test/Automated/002_Staging_ORU_O01_short_linked_to_001_ORM.hl7 +++ b/examples/Test/Automated/002_Staging_ORU_O01_short_linked_to_001_ORM.hl7 @@ -1,4 +1,4 @@ -MSH|^~\&|Sender Application^sender.test.com^DNS|Sender Facility^0.0.0.0.0.0.0.0^ISO|Receiver Application^0.0.0.0.0.0.0.0^ISO|Receiver Facility^automated-staging-test-receiver-id^DNS|20230101010000-0000||ORU^R01^ORU_R01|111111|T|2.5.1|||||||||| +MSH|^~\&|Sender Application^sender.test.com^DNS|Sender Facility^0.0.0.0.0.0.0.0^ISO|Receiver Application^0.0.0.0.0.0.0.0^ISO|Receiver Facility^automated-staging-test-receiver-id^DNS|20230101010000-0000||ORU^R01^ORU_R01|002|T|2.5.1|||||||||| PID|1||1300974^^^Baptist East^MR||ONE^TESTCASE||20230504131000|F^Female^HL70001||2106-3^White^HL70005|1234 GPCS WAY^^MONTGOMERY^Alabama^36117^USA^home^^Montgomery|||||||2227600015||||N^Not Hispanic or Latino^HL70189|||1|||||N| NK1|1|ONE^MOMFIRST|MTH^Mother^HL70063^^^^^^Mother||^^^^^804^5693861||||||||||||||||||||||||||||123456789^^^Medicaid&2.16.840.1.113883.4.446&ISO^MD||||000-00-0000^^^ssn&2.16.840.1.113883.4.1&ISO^SS||||||| ORC|RE|4560411583^ORDERID|20231561137^ALPHL|||||||||1174911127^SMITH^SAMANTHA^^^^^^NPI^L^^^NPI|||||||||BAPTIST EAST^L^^^^CMS^NPI^^^1043269798~BAPTIST EAST^L^^^^AL Public Health Lab^Submitter ID^^^739||||||||||54089-8^Newborn screening panel American Health Information Community (AHIC)^LN| diff --git a/examples/Test/Automated/003_CA_ORU_R01_CDPH_produced.hl7 b/examples/Test/Automated/003_CA_ORU_R01_CDPH_produced.hl7 new file mode 100644 index 000000000..2c4b787ac --- /dev/null +++ b/examples/Test/Automated/003_CA_ORU_R01_CDPH_produced.hl7 @@ -0,0 +1,173 @@ +MSH|^~\&|SISGDSP|SISGDSP|SISHIERECEIVER^11903029^L,M,N|^automated-staging-test-receiver-id^L,M,N|20240226034304||ORU^R01^ORU_R01|AUTOMATEDTEST-003|T|2.5.1 +PID|1||80008836^^^&NPI^MR||CDPHSIX^BOY MOMSIX^^^^^B|||M||2106-3^White||||||||||||2186-5^Not Hispanic or Latino||N|1 +NK1|1|CDPHSIX|MTH^Mother|132 ST^^SAN DIEGO^CA^92126^USA +ORC|RE|7181233072^FormNumber||189430284^HospOrdNumber||||||||^ROSEN^REBECCA|||||||||UCSD JACOBS MEDICAL CENTER^^^^^^^^^R797| 2961DR YLLUT^^SAN DIEGO^CA^99999-9999 +OBR|1|7181233072^FormNumber||54089-8^NB Screen Panel Patient AHIC|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBR|2|7181233072^FormNumber||57128-1^Newborn Screening Report summary panel|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBX|1|CE|57721-3^Reason for lab test in Dried blood spot^LN|1|LA12421-6^Initial screen^LN|||N|||F|||20240226034304 +OBX|2|CE|57718-9^Sample quality of Dried blood spot^LN|1|LA12432-3^Acceptable^LN|||N|||F|||20240226034304 +OBX|3|CE|57130-7^Newborn screening report - overall interpretation^LN|1|LA18944-1^Screen is out of range for at least one condition^LN|||N|||F|||20240226034304 +OBX|4|CE|57131-5^Newborn conditions with positive markers [Identifier] in Dried blood spot^LN|1|LA22279-6^SMA^LN|||N|||F|||20240226034304 +OBX|5|CE|57720-5^Newborn conditions with equivocal markers [Identifier] in Dried blood spot^LN|1|LA137-2^None^LN|||N|||F|||20240226034304 +OBX|6|TX|57724-7^Newborn screening short narrative summary^LN|1|ACTION REQUIRED\.br\\.br\NBS Testing Lab - COMMUNITY REG MEDICAL CENTER LAB \R\M 57752YWKP NAEBC, ETS8021 , FRESNO, CA 99999\.br\\.br\Genetic Disease Laboratory - GDL SAW 19221DVLB NOTGNIH, RICHMOND, CA 99999-9999\.br\\.br\Lab Director - Genetic Disease Laboratory, (510) 231-1790\.br\\.br\Follow-up:\.br\\.br\Amino Acid Panel: There was insufficient data to determine whether or not this newborn was at least 12 hours old when this specimen was collected. Testing of amino acids at less than 12 hours of age is not reliable for detecting certain metabolic disorder. \.br\\.br\Spinal Muscular Atrophy (SMA): An immediate referral to a CCS-approved Neuromuscular Center for evaluation and additional testing is strongly recommended due an apparent SMN1 exon 7 homozygous deletion. \.br\\.br\If you have any questions regarding these screening outcomes, please contact the Newborn Screening Staff at Rady Children's Hospital San Diego at (161) 122-8708. \.br\\.br\Methods and Limitations:\.br\\.br\Assays for ALD Tier-1, BD, CAH, CF, GAL, MS/MS Acylcarnitine and Amino Acid Panels, PCH, hemoglobinopathies, and SCID were performed at the testing laboratory specified on the front of this report. Assays for ALD Tier-2, CAH Tier-2, Pompe Disease Tier-1, MPS I Tier-1, and SMA were developed and/or optimized by the Genetic Disease Laboratory (GDL), and performed at GDL. Performance characteristics of these assays are determined by GDL. The SMA assay is designed to identify 95% of SMA patients who have homozygous deletions of the SMN1 gene on chromosome 5q. These assays have not been cleared or approved by the U.S. Food and Drug Administration (FDA). The FDA has determined that such clearance or approval is not necessary. The assays are used for clinical purposes. They should not be regarded as investigational or for research. GDL is certified under the Clinical Laboratory Improvement Amendments of 1988 (CLIA-88) to perform high complexity genetic disease screening. \.br\Attention Healthcare Provider:\.br\\.br\Due to biological variability of newborns and differences in detection rates for the various disorders in the newborn period, the Newborn Screening Program will not identify all newborns with these conditions. While a positive screening result identifies newborns at an increased risk to justify a diagnostic work-up, a negative screening result does not rule out the possibility of a disorder. Health care providers should remain watchful for any sign or symptoms of these disorders in their patients. A newborn screening result should not be considered diagnostic, and cannot replace the individualized evaluation and diagnosis of an infant by a well-trained, knowledgeable health care provider. \.br\\.br\|||N|||F|||20240226034304 +OBX|7|TX|57129-9^Full newborn screening summary report for display or printing^LN|1||||N|||F|||20240226034304 +OBX|8|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|1|LA22279-6^SMA^LN|||N|||F|||20240226034304 +OBX|9|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|2|LA25796-6^X-ALD^LN|||N|||F|||20240226034304 +OBX|10|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|3|LA14037-8^GAA^LN|||N|||F|||20240226034304 +OBX|11|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|4|LA25797-4^MPS-I^LN|||N|||F|||20240226034304 +OBX|12|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|5|LA12466-1^3-MCC^LN|||N|||F|||20240226034304 +OBX|13|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|6|LA12468-7^3MGA^LN|||N|||F|||20240226034304 +OBX|14|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|7|LA12469-5^5-OXO^LN|||N|||F|||20240226034304 +OBX|15|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|8|LA12470-3^ARG^LN|||N|||F|||20240226034304 +OBX|16|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|9|LA12482-8^CIT-I^LN|||N|||F|||20240226034304 +OBX|17|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|10|LA12483-6^CIT-II^LN|||N|||F|||20240226034304 +OBX|18|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|11|LA12485-1^CPT-Ia^LN|||N|||F|||20240226034304 +OBX|19|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|12|LA12486-9^CPT-II^LN|||N|||F|||20240226034304 +OBX|20|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|13|LA12493-5^GA-1^LN|||N|||F|||20240226034304 +OBX|21|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|14|LA12495-0^GA-2^LN|||N|||F|||20240226034304 +OBX|22|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|15|LA12497-6^HHH^LN|||N|||F|||20240226034304 +OBX|23|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|16|LA12499-2^HMG^LN|||N|||F|||20240226034304 +OBX|24|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|17|LA12505-6^IVA^LN|||N|||F|||20240226034304 +OBX|25|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|18|LA12507-2^LCHAD^LN|||N|||F|||20240226034304 +OBX|26|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|19|LA12508-0^MAL^LN|||N|||F|||20240226034304 +OBX|27|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|20|LA12509-8^MCAD^LN|||N|||F|||20240226034304 +OBX|28|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|21|LA12510-6^MCD^LN|||N|||F|||20240226034304 +OBX|29|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|22|LA12512-2^MET^LN|||N|||F|||20240226034304 +OBX|30|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|23|LA12513-0^MSUD^LN|||N|||F|||20240226034304 +OBX|31|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|24|LA12516-3^NKHG^LN|||N|||F|||20240226034304 +OBX|32|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|25|LA12520-5^PKU^LN|||N|||F|||20240226034304 +OBX|33|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|26|LA12521-3^PRO I^LN|||N|||F|||20240226034304 +OBX|34|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|27|LA12528-8^TYR-1^LN|||N|||F|||20240226034304 +OBX|35|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|28|LA12529-6^TYR-II^LN|||N|||F|||20240226034304 +OBX|36|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|29|LA12531-2^VLCAD^LN|||N|||F|||20240226034304 +OBX|37|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|30|LA12532-0^BIO^LN|||N|||F|||20240226034304 +OBX|38|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|31|LA12533-8^CAH^LN|||N|||F|||20240226034304 +OBX|39|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|32|LA12537-9^CF^LN|||N|||F|||20240226034304 +OBX|40|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|33|LA12543-7^GALT^LN|||N|||F|||20240226034304 +OBX|41|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|34|LA12566-8^SCID^LN|||N|||F|||20240226034304 +OBX|42|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|35|LA12576-7^SCAD or EMA or IBG or GA-2 (MADD)^LN|||N|||F|||20240226034304 +OBX|43|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|36|LA16207-5^Hemoglobinopathies^LN|||N|||F|||20240226034304 +OBX|44|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|37|99717-3^Hypothyroidism^L|||N|||F|||20240226034304 +OBX|45|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|38|LA12487-7^CUD^LN|||N|||F|||20240226034304 +OBX|46|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|39|LA12474-5^BKT^LN|||N|||F|||20240226034304 +OBX|47|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|40|LA12523-9^PA^LN|||N|||F|||20240226034304 +OBX|48|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|41|LA12515-5^MMA^LN|||N|||F|||20240226034304 +OBX|49|CE|57719-7^Conditions tested for in this newborn screening study [Identifier] in Dried blood spot^LN|42|LA12464-6^2M3HBA^LN|||N|||F|||20240226034304 +OBR|3|7181233072^FormNumber||57717-1^Newborn screen card data panel|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBX|1|ST|57716-3^State printed on filter paper card [Identifier] in NBS card^LN|1|CA|||N|||F|||20240226034304 +OBX|2|NM|8339-4^Birthweight^LN|1|3000|grams||N|||F|||20240226034304 +OBX|3|CE|57722-1^Birth plurality of Pregnancy^LN|1|LA12411-7^Singleton^LN|||N|||F|||20240226034304 +OBX|4|CE|57713-0^Infant NICU factors that affect newborn screening interpretation^LN|1|LA137-2^None^LN|||N|||F|||20240226034304 +OBX|5|CE|67704-7^Feeding types^LN|1|LA16914-6^Breast milk^LN|||N|||F|||20240226034304 +OBX|6|TX|^^^99717-5^Accession Number^L|1|045-89-477/21-2024-21|||N|||F|||20240226034304 +OBX|7|TX|62324-9^Post-discharge provider name^LN|1|REBECCA ROSEN|||N|||F|||20240226034304 +OBX|8|TX|62327-2^Post-discharge provider practice address^LN|1|2961DR YLLUT SAN DIEGO CA 99999-9999 USA|||N|||F|||20240226034304 +OBR|4|7181233072^FormNumber||57794-0^Newborn screening test results panel in Dried blood spot|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBR|5|7181233072^FormNumber||53261-4^Amino acid newborn screen panel|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBX|1|NM|47633-3^Glycine [Moles/volume] in Dried blood spot^LN|1|0.5|µmol/L||N|||F|||20240226034304 +OBX|2|NM|53150-9^Alanine+Beta Alanine+Sarcosine [Moles/volume] in Dried blood spot^LN|1|500|µmol/L|<1000|N|||F|||20240226034304 +OBX|3|NM|47799-2^Valine [Moles/volume] in Dried blood spot^LN|1|0.5|µmol/L||N|||F|||20240226034304 +OBX|4|NM|53151-7^Valine/Phenylalanine [Molar ratio] in Dried blood spot^LN|1|0.00645|{Ratio}|<4.3|N|||F|||20240226034304 +OBX|5|NM|53152-5^Alloisoleucine+Isoleucine+Leucine+Hydroxyproline^LN|1|125|µmol/L|<230|N|||F|||20240226034304 +OBX|6|NM|53154-1^Alloisoleucine+Isoleucine+Leucine+Hydroxyproline/Alanine [Molar ratio] in Dried blood spot^LN|1|0.65|{Ratio}|<1.35|N|||F|||20240226034304 +OBX|7|NM|29573-3^Phenylalanine [Moles/volume] in Dried blood spot^LN|1|77.5|µmol/L|<175|N|||F|||20240226034304 +OBX|8|NM|35572-7^Phenylalanine/Tyrosine [Molar ratio] in Dried blood spot^LN|1|0.75|{Ratio}|<2.6|N|||F|||20240226034304 +OBX|9|NM|35571-9^Tyrosine [Moles/volume] in Dried blood spot^LN|1|425|µmol/L|<680|N|||F|||20240226034304 +OBX|10|NM|53231-7^Succinylacetone [Moles/volume] in Dried blood spot^LN|1|2.25|µmol/L|<6.1|N|||F|||20240226034304 +OBX|11|NM|47700-0^Methionine [Moles/volume] in Dried blood spot^LN|1|27|µmol/L|6.3-100|N|||F|||20240226034304 +OBX|12|NM|42892-0^Citrulline [Moles/volume] in Dried blood spot^LN|1|16.25|µmol/L|5-49|N|||F|||20240226034304 +OBX|13|NM|54092-2^Citrulline/Arginine [Molar ratio] in Dried blood spot^LN|1|3|{Ratio}|<4.8|N|||F|||20240226034304 +OBX|14|NM|53155-8^Asparagine+Ornithine [Moles/volume] in Dried blood spot^LN|1|400|µmol/L|<800|N|||F|||20240226034304 +OBX|15|NM|75215-4^Ornithine/Citrulline [Molar ratio] in Dried blood spot^LN|1|0.5|{Ratio}||N|||F|||20240226034304 +OBX|16|NM|47562-4^Arginine [Moles/volume] in Dried blood spot^LN|1|25|µmol/L|<63|N|||F|||20240226034304 +OBX|17|NM|75214-7^Arginine/Ornithine [Molar ratio] in Dried blood spot^LN|1|0.7|{Ratio}|<1.9|N|||F|||20240226034304 +OBX|18|NM|47732-3^Proline [Moles/volume] in Dried blood spot^LN|1|750|µmol/L|<1500|N|||F|||20240226034304 +OBX|19|TX|57710-6^Amino acidemias newborn screening comment/discussion^LN|1|Collected Early. No Interpretation.|||A|||F|||20240226034304 +OBR|6|7181233072^FormNumber||58092-8^Acylcarnitine newborn screen panel|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBX|1|CE|58088-6^Acylcarnitine newborn screen interpretation^LN|1|LA18592-8^In range^LN|||N|||F|||20240226034304 +OBX|2|TX|58093-6^Acylcarnitine newborn screening comment/discussion^LN|1|Negative|||N|||F|||20240226034304 +OBR|7|7181233072^FormNumber||57084-6^Fatty acid oxidation newborn screen panel|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBX|1|NM|38481-8^Carnitine.free (C0)^LN|1|33|µmol/L|6.4-125|N|||F|||20240226034304 +OBX|2|NM|53235-8^Carnitine.free (C0)/Palmitoylcarnitine (C16)+Stearoylcarnitine (C18)^LN|1|37.5|{Ratio}|<70|N|||F|||20240226034304 +OBX|3|NM|50157-7^Acetylcarnitine (C2)^LN|1|20.38|µmol/L|10-80|N|||F|||20240226034304 +OBX|4|NM|75212-1^Malonylcarnitine (C3-DC)/Decanoylcarnitine (C10) [Molar ratio] in Dried blood spot^LN|1|0.62500|{Ratio}|<7.6|N|||F|||20240226034304 +OBX|5|NM|45211-0^Hexanoylcarnitine (C6)^LN|1|0.48|µmol/L|<0.95|N|||F|||20240226034304 +OBX|6|NM|53175-6^Octanoylcarnitine (C8)^LN|1|0.3|µmol/L|<0.45|N|||F|||20240226034304 +OBX|7|NM|53177-2^Octanoylcarnitine (C8)/Decanoylcarnitine (C10)^LN|1|0.5|{Ratio}||N|||F|||20240226034304 +OBX|8|NM|53174-9^Octenoylcarnitine (C8:1)^LN|1|0.35|µmol/L|<0.65|N|||F|||20240226034304 +OBX|9|NM|45197-1^Decanoylcarnitine (C10)^LN|1|0.32|µmol/L|<0.65|N|||F|||20240226034304 +OBX|10|NM|45198-9^Decenoylcarnitine (C10:1)^LN|1|0.22|µmol/L|<0.45|N|||F|||20240226034304 +OBX|11|NM|45199-7^Dodecanoylcarnitine (C12)^LN|1|1|µmol/L|<2|N|||F|||20240226034304 +OBX|12|NM|45200-3^Dodecenoylcarnitine (C12:1)^LN|1|0.5|µmol/L||N|||F|||20240226034304 +OBX|13|NM|53192-1^Tetradecanoylcarnitine (C14)^LN|1|0.6|µmol/L|<1.2|N|||F|||20240226034304 +OBX|14|NM|53191-3^Tetradecenoylcarnitine (C14:1)^LN|1|0.4|µmol/L|<0.9|N|||F|||20240226034304 +OBX|15|NM|53194-7^Tetradecenoylcarnitine (C14:1)/Dodecenoylcarnitine (C12:1)^LN|1|0.5|{Ratio}||N|||F|||20240226034304 +OBX|16|NM|53190-5^Tetradecadienoylcarnitine (C14:2)^LN|1|0.5|µmol/L||N|||F|||20240226034304 +OBX|17|NM|50281-5^3-Hydroxytetradecanoylcarnitine (C14-OH)^LN|1|0.1|µmol/L|<0.2|N|||F|||20240226034304 +OBX|18|NM|53199-6^Palmitoylcarnitine (C16)^LN|1|5|µmol/L|<12|N|||F|||20240226034304 +OBX|19|NM|53198-8^Palmitoleylcarnitine (C16:1)^LN|1|0.7|µmol/L|<1.4|N|||F|||20240226034304 +OBX|20|NM|50125-4^3-Hydroxypalmitoylcarnitine (C16-OH)^LN|1|0.05|µmol/L|<0.1|N|||F|||20240226034304 +OBX|21|NM|53201-0^3-Hydroxypalmitoylcarnitine (C16-OH)/Palmitoylcarnitine (C16)^LN|1|0.01000|{Ratio}|<0.07|N|||F|||20240226034304 +OBX|22|NM|53241-6^Stearoylcarnitine (C18)^LN|1|2|µmol/L|<3.5|N|||F|||20240226034304 +OBX|23|NM|53202-8^Oleoylcarnitine (C18:1)^LN|1|3.5|µmol/L|<7|N|||F|||20240226034304 +OBX|24|NM|45217-7^Linoleoylcarnitine (C18:2)^LN|1|0.5|µmol/L||N|||F|||20240226034304 +OBX|25|NM|50132-0^3-Hydroxystearoylcarnitine (C18-OH)^LN|1|0.05|µmol/L|<0.1|N|||F|||20240226034304 +OBX|26|NM|50113-0^3-Hydroxyoleoylcarnitine (C18:1-OH)^LN|1|0.05|µmol/L|<0.1|N|||F|||20240226034304 +OBR|8|7181233072^FormNumber||57085-3^Organic acid newborn screen panel|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBX|1|NM|53160-8^Propionylcarnitine (C3)^LN|1|3.15|µmol/L|<7.9|N|||F|||20240226034304 +OBX|2|NM|53163-2^Propionylcarnitine (C3)/Acetylcarnitine (C2)^LN|1|0.15|{Ratio}|<0.42|N|||F|||20240226034304 +OBX|3|NM|67708-8^Malonylcarnitine (C3-DC)+3-Hydroxybutyrylcarnitine (C4-OH)^LN|1|0.2|µmol/L|<0.48|N|||F|||20240226034304 +OBX|4|NM|53166-5^Butyrylcarnitine+Isobutyrylcarnitine (C4)^LN|1|0.85|µmol/L|<1.7|N|||F|||20240226034304 +OBX|5|NM|45216-9^Isovalerylcarnitine+Methylbutyrylcarnitine (C5)^LN|1|0.5|µmol/L|<0.95|N|||F|||20240226034304 +OBX|6|NM|53240-8^Isovalerylcarnitine+Methylbutyrylcarnitine (C5)/Propionylcarnitine (C3)^LN|1|0.15873|{Ratio}|<0.38|N|||F|||20240226034304 +OBX|7|NM|53170-7^Tiglylcarnitine (C5:1)^LN|1|0.3|µmol/L|<0.5|N|||F|||20240226034304 +OBX|8|NM|50106-4^3-Hydroxyisovalerylcarnitine (C5-OH)^LN|1|0.45|µmol/L|<1.15|N|||F|||20240226034304 +OBX|9|NM|67710-4^Glutarylcarnitine (C5-DC)+3-Hydroxyhexanoylcarnitine (C6-OH)^LN|1|0.3|µmol/L|<0.38|N|||F|||20240226034304 +OBX|10|NM|75216-2^Glutarylcarnitine (C5-DC)/Malonylcarnitine (C3-DC) [Molar ratio] in Dried blood spot^LN|1|1.50000|{Ratio}|>0.1|N|||F|||20240226034304 +OBR|9|7181233072^FormNumber||54078-1^Cystic fibrosis newborn screening panel|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBX|1|NM|48633-2^Trypsinogen I.free^LN|1|31.00|ng/mL|<71|N|||F|||20240226034304 +OBX|2|CE|46769-6^Cystic fibrosis newborn screen interpretation^LN|1|LA18592-8^In range^LN|||N|||F|||20240226034304 +OBX|3|TX|57707-2^Cystic fibrosis newborn screening comment/discussion^LN|1|Negative|||N|||F|||20240226034304 +OBR|10|7181233072^FormNumber||57086-1^Congenital adrenal hyperplasia newborn screening panel|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBX|1|NM|38473-5^17-Hydroxyprogesterone^LN|1|35|nmol/L|<85|N|||F|||20240226034304 +OBX|2|CE|46758-9^Congenital adrenal hyperplasia newborn screen interpretation^LN|1|LA18592-8^In range^LN|||N|||F|||20240226034304 +OBX|3|TX|57706-4^Congenital adrenal hyperplasia newborn screening comment-discussion^LN|1|Negative|||N|||F|||20240226034304 +OBR|11|7181233072^FormNumber||54090-6^Thyroid newborn screening panel|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBX|1|NM|29575-8^Thyrotropin^LN|1|14.50|mIU/L|<29|N|||F|||20240226034304 +OBX|2|CE|46762-1^Congenital hypothyroidism newborn screen interpretation^LN|1|LA18592-8^In range^LN|||N|||F|||20240226034304 +OBX|3|TX|57705-6^Congenital hypothyroidism newborn screening comment-discussion^LN|1|Negative|||N|||F|||20240226034304 +OBR|12|7181233072^FormNumber||54079-9^Galactosemia newborn screening panel|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBX|1|NM|42906-8^Galactose 1 phosphate uridyl transferase^LN|1|55.00|enzyme units|>50|N|||F|||20240226034304 +OBX|2|CE|46737-3^Galactosemias newborn screen interpretation^LN|1|LA18592-8^In range^LN|||N|||F|||20240226034304 +OBX|3|TX|57704-9^Galactosemias newborn screening comment-discussion^LN|1|Negative|||N|||F|||20240226034304 +OBR|13|7181233072^FormNumber||54081-5^Hemoglobinopathies newborn screening panel|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBX|1|TX|54104-5^Hemoglobin pattern^LN|1|FA||||||F|||20240226034304 +OBX|2|TX|57703-1^Hemoglobin disorders newborn screening comment/discussion^LN|1|Usual hemoglobin pattern.†These results assume no transfusion prior to testing and do not rule out the possibility of a thalassemia trait or rare hemoglobin variants.||||||F|||20240226034304 +OBR|14|7181233072^FormNumber||57087-9^Biotinidase newborn screening panel|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBX|1|NM|75217-0^Biotinidase [Enzymatic activity/volume] in Dried blood spot^LN|1|15.00|ERU|>10|N|||F|||20240226034304 +OBX|2|CE|46761-3^Biotinidase deficiency newborn screen interpretation^LN|1|LA18592-8^In range^LN|||N|||F|||20240226034304 +OBX|3|TX|57699-1^Biotinidase deficiency newborn screening comment-discussion^LN|1|Negative|||N|||F|||20240226034304 +OBR|15|7181233072^FormNumber||62333-0^Severe combined immunodeficiency (SCID) newborn screening panel|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBX|1|NM|62320-7^T-cell receptor excision circle [#/volume] in Dried blood spot by Probe and target amplification method^LN|1|33|copies/µL|>18|N|||F|||20240226034304 +OBX|2|CE|62321-5^Severe combined immunodeficiency newborn screen interpretation^LN|1|LA18592-8^In range^LN|||N|||F|||20240226034304 +OBX|3|TX|62322-3^Severe combined immunodeficiency newborn screening comment-discussion^LN|1|Negative|||N|||F|||20240226034304 +OBR|16|7181233072^FormNumber||63414-7^Pompe Disease newborn screening panel|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBX|1|NM|55827-0^Acid alpha glucosidase [Enzymatic activity/volume] in DBS^LN|1|12.923|µmol/L/h|>=2.079|N|||F|||20240226034304 +OBX|2|CE|63415-4^Pompe Disease deficiency newborn screen interpretation^LN|1|LA18592-8^In range^LN|||N|||F|||20240226034304 +OBX|3|TX|63416-2^Pompe Disease deficiency newborn screening comments-discussion^LN|1|Negative|||N|||F|||20240226034304 +OBX|4|TX|63416-2^Pompe Disease deficiency newborn screening comments-discussion^LN|2|Interpretation Comments: The acid alpha-glucosidase Enzyme activity level is above the 18% of the daily patient median and suggests it is screen negative for Pompe disease.|||N|||F|||20240226034304 +OBR|17|7181233072^FormNumber||79563-3^Mucopolysaccharidosis type I newborn screening panel|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBX|1|NM|55909-6^Alpha-L-iduronidase [Enzymatic activity/volume] in DBS^LN|1|3.117|µmol/L/h|>=1.2204|N|||F|||20240226034304 +OBX|2|CE|79564-1^Mucopolysaccharidosis type I newborn screen interpretation^LN|1|LA18592-8^In range^LN|||N|||F|||20240226034304 +OBX|3|TX|79565-8^Mucopolysaccharidosis type I newborn screening comment-discussion^LN|1|Negative|||N|||F|||20240226034304 +OBX|4|TX|79565-8^Mucopolysaccharidosis type I newborn screening comment-discussion^LN|2|Interpretation Comments: The alpha-L-iduronidase Enzyme activity is above the 18% of the daily patient median and suggests it is screen negative for Mucopolysaccharidosis I (MPS I) disease.|||N|||F|||20240226034304 +OBR|18|7181233072^FormNumber||92005-8^Spinal muscular atrophy newborn screening panel|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBX|1|TX|^^^99717-60^SMN1 Homozygous Deletion Analysis^L|1|Exon 7 Absent||Exon 7 Present|A|||F|||20240226034304 +OBX|2|CE|92004-1^Spinal muscular atrophy newborn screen interpretation^LN|1|LA18593-6^Out of range^LN|||A|||F|||20240226034304 +OBX|3|TX|92003-3^Spinal muscular atrophy newborn screening comment-discussion^LN|1|SMA Positive|||A|||F|||20240226034304 +OBX|4|TX|92003-3^Spinal muscular atrophy newborn screening comment-discussion^LN|2|Interpretation Comments: qPCR detected possible homozygoues deletion in exon 7 of the SMN1 gene. Confirmatory test for SMN1 and SMN2 copy determination are required.|||A|||F|||20240226034304 +OBR|19|7181233072^FormNumber||^^^99717-28^Adrenoleukodystrophy newborn screening panel^L|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBX|1|CE|^^^99717-32^Adrenoleukodystrophy deficiency newborn screening interpretation^L|1|LA18592-8^In range^LN|||N|||F|||20240226034304 +OBX|2|TX|^^^99717-33^Adrenoleukodystrophy deficiency newborn screening comments-discussion^L|1|Negative|||N|||F|||20240226034304 +OBR|20|7181233072^FormNumber||^^^99717-29^Adrenoleukodystrophy Tier-1 newborn screening panel^L|||202402131546|||||||||^JOHNSON^KATHRYN||||||20240226034304|||F +OBX|1|NM|79321-6^Lysophosphatidylcholine(26:0) [Moles/volume] in Dried blood spot^LN|1|0.2|µmol/L|<0.49|N|||F|||20240226034304 diff --git a/examples/Test/Automated/README.md b/examples/Test/Automated/README.md new file mode 100644 index 000000000..41086b82a --- /dev/null +++ b/examples/Test/Automated/README.md @@ -0,0 +1,17 @@ +This folder contains the HL7 sample files that are used in the automated ReportStream +integration tests. The [automated-staging-test-submit.yml](/.github/workflows/automated-staging-test-submit.yml) +Github workflow that runs daily will grab these files and send them to ReportStream in staging. +The files are expected to go through the whole flow and to be delivered to an Azure blob +container that will later be used by the +[automated-staging-test-run.yml](/.github/workflows/automated-staging-test-run.yml) workflow to run tests on them. + +## Requirements for the HL7 files + +The files are required to: + +- Be a valid HL7 file +- Be a supported HL7 message type: `ORM`, `OML` or `ORU` +- Have `automated-staging-test-receiver-id` in `MSH-6.2` in order to be routed correctly + - If it's a sample file for UCSD, which has transformation that will overwrite `MSH-6.2`, the HL7 is required to have the prefix `AUTOMATEDTEST-` in `MSH-10` as a workaround - there is special routing in ReportStream to handle this +- Each file must have a unique value in `MSH-10`. We use this value to match the input and output files, so if it's not unique, we won't be able to match the files correctly + - We format `MSH-10` based on the file index, like `001` (or `AUTOMATEDTEST-001` for UCSD) diff --git a/rs-e2e/README.md b/rs-e2e/README.md new file mode 100644 index 000000000..242909bd0 --- /dev/null +++ b/rs-e2e/README.md @@ -0,0 +1,56 @@ +# ReportStream Integration Test + +The ReportStream Integration Test is a framework meant to add test coverage for the integration between the +Intermediary and ReportStream. It's scheduled to run daily using the +[automated-staging-test-run.yml](/.github/workflows/automated-staging-test-run.yml) workflow + +Information on how to set up the sample files evaluated by the tests can be found [here](/examples/Test/Automated/README.md) + +## Running the tests + +- Automatically - these are scheduled to run every weekday +- Manually via Github + - Run the [automated-staging-test-submit](/.github/workflows/automated-staging-test-submit.yml) action + - Wait for RS and TI to finish processing files + - Run the [automated-staging-test-run](/.github/workflows/automated-staging-test-run.yml) action +- Locally + - Set the `AZURE_STORAGE_CONNECTION_STRING` environment variable to the [value in Keybase](keybase://team/cdc_ti/service_keys/TI/staging/azure-storage-connection-string-for-automated-rs-e2e-tests.txt) + - Run the tests with `./gradlew rs-e2e:clean rs-e2e:automatedTest` + +## Assertions Definition + +The assertions for the integration tests are defined in the +[assertion_definitions.json](/rs-e2e/src/main/resources/assertion_definitions.json) file, which uses +the same rules engine framework as the transformations and validations in the [etor](/etor) project + +### File Structure + +The file contains a list of definitions which each contain: + +- `name`: a descriptive name for the assertions group +- `conditions`: a list of conditions to be met. These determine whether this set of + assertions apply to the file being evaluated. When no conditions are included, the definition + applies to all files. If conditions are included, all of them must be satisfied for the + definition to apply. Conditions are structured the same way as rules +- `rules`: a list of assertions to evaluate + +#### Rules + +The rules are the assertions for the integration test. The assertions are HL7 expressions inspired +by `FHIRPath`. The current assertions we allow are: equality, non-equality, membership, and +segment count. We can evaluate strings and/or values in HL7 fields. An HL7 field in a rule +can be in either the input file or the output file. If no file is specified, we assume it's the output. +Each rule is contained in double quotes and any string values are contained in single quotes + +Examples: + +- Equality between an HL7 field in the output and input + - `"MSH-10 = input.MSH-10"` - `MSH-10` has the same value in the output and input files + - `"output.MSH-10 = input.MSH-10"` - same as above +- Equality between an HL7 field and a string + - `"MSH-4 = 'CDPH'"` - the value of `MSH-4` in the output file equals `CDPH` + - `"MSH-4 != ''"` - the value of `MSH-4` in the output file doesn't equal an empty string +- Membership + - `"MSH-6 in ('R797', 'R508')"` - the value of `MSH-6` in the output file is either `R797` or `R508` +- Segment count + - `"OBR.count() = 1"` - there is only one `OBR` segment in the file diff --git a/rs-e2e/build.gradle b/rs-e2e/build.gradle new file mode 100644 index 000000000..d45b723fd --- /dev/null +++ b/rs-e2e/build.gradle @@ -0,0 +1,39 @@ +plugins { + id 'java' + id 'groovy' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +dependencies { + implementation project(':shared') + testImplementation testFixtures(project(':shared')) + + //jackson + implementation 'com.fasterxml.jackson.core:jackson-core:2.17.2' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.2' + + // azure + implementation 'com.azure:azure-storage-blob:12.27.1' + + // hapi hl7 + implementation 'ca.uhn.hapi:hapi-base:2.5.1' + implementation 'ca.uhn.hapi:hapi-structures-v251:2.5.1' + + testImplementation 'org.apache.groovy:groovy:4.0.22' + testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0' +} + +tasks.named('test') { + useJUnitPlatform() + exclude '**/AutomatedTest.*' +} + +task automatedTest(type: Test) { + useJUnitPlatform() + include '**/AutomatedTest.*' +} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/AzureBlobFileFetcher.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/AzureBlobFileFetcher.java new file mode 100644 index 000000000..3245bb6d1 --- /dev/null +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/AzureBlobFileFetcher.java @@ -0,0 +1,73 @@ +package gov.hhs.cdc.trustedintermediary.rse2e; + +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobContainerClientBuilder; +import com.azure.storage.blob.models.BlobItem; +import com.azure.storage.blob.models.BlobProperties; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.List; + +/** + * The AzureBlobFileFetcher class implements the {@link FileFetcher FileFetcher} interface and + * fetches files from an Azure Blob Storage container. + */ +public class AzureBlobFileFetcher implements FileFetcher { + + private static final FileFetcher INSTANCE = new AzureBlobFileFetcher(); + + private final BlobContainerClient blobContainerClient; + + private AzureBlobFileFetcher() { + String azureStorageConnectionName = "automated"; + String azureStorageConnectionString = System.getenv("AZURE_STORAGE_CONNECTION_STRING"); + + if (azureStorageConnectionString == null || azureStorageConnectionString.isEmpty()) { + throw new IllegalArgumentException( + "Environment variable AZURE_STORAGE_CONNECTION_STRING is not set"); + } + this.blobContainerClient = + new BlobContainerClientBuilder() + .connectionString(azureStorageConnectionString) + .containerName(azureStorageConnectionName) + .buildClient(); + } + + public static FileFetcher getInstance() { + return INSTANCE; + } + + @Override + public List fetchFiles() { + List recentFiles = new ArrayList<>(); + LocalDate mostRecentDay = null; + + for (BlobItem blobItem : blobContainerClient.listBlobs()) { + BlobClient blobClient = blobContainerClient.getBlobClient(blobItem.getName()); + BlobProperties properties = blobClient.getProperties(); + + // Currently we're doing everything in UTC. If we start uploading files manually and + // running + // this test manually, we may want to revisit this logic and/or the file structure + // because midnight UTC is 5pm PDT on the previous day + LocalDate blobCreationDate = + properties.getLastModified().toInstant().atZone(ZoneOffset.UTC).toLocalDate(); + + if (mostRecentDay != null && blobCreationDate.isBefore(mostRecentDay)) { + continue; + } + + if (mostRecentDay == null || blobCreationDate.isAfter(mostRecentDay)) { + mostRecentDay = blobCreationDate; + recentFiles.clear(); + } + + recentFiles.add( + new HL7FileStream(blobClient.getBlobName(), blobClient.openInputStream())); + } + + return recentFiles; + } +} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/FileFetcher.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/FileFetcher.java new file mode 100644 index 000000000..dcd998148 --- /dev/null +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/FileFetcher.java @@ -0,0 +1,11 @@ +package gov.hhs.cdc.trustedintermediary.rse2e; + +import java.util.List; + +/** + * The FileFetcher interface represents a component responsible for fetching files. Implementations + * of this interface should provide a way to retrieve a list of HL7FileStream objects. + */ +public interface FileFetcher { + List fetchFiles(); +} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/HL7FileStream.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/HL7FileStream.java new file mode 100644 index 000000000..6064c9d13 --- /dev/null +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/HL7FileStream.java @@ -0,0 +1,9 @@ +package gov.hhs.cdc.trustedintermediary.rse2e; + +import java.io.InputStream; + +/** + * The HL7FileStream class represents a file stream that contains HL7 data and the corresponding + * file name. + */ +public record HL7FileStream(String fileName, InputStream inputStream) {} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/LocalFileFetcher.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/LocalFileFetcher.java new file mode 100644 index 000000000..e8cb9cada --- /dev/null +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/LocalFileFetcher.java @@ -0,0 +1,50 @@ +package gov.hhs.cdc.trustedintermediary.rse2e; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * The LocalFileFetcher class implements the {@link FileFetcher FileFetcher} interface and + * represents a file fetcher that fetches files from the local file system. + */ +public class LocalFileFetcher implements FileFetcher { + + private static final String FILES_PATH = "../examples/Test/Automated/"; + private static final String EXTENSION = "hl7"; + private static final FileFetcher INSTANCE = new LocalFileFetcher(); + + private LocalFileFetcher() {} + + public static FileFetcher getInstance() { + return INSTANCE; + } + + @Override + public List fetchFiles() { + try (Stream stream = Files.walk(Paths.get(FILES_PATH))) { + return stream.filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith(EXTENSION)) + .map( + p -> { + try { + // Need to keep the input stream open until the test is done + // Must make sure to close the input stream after use + InputStream inputStream = Files.newInputStream(p); + return new HL7FileStream( + p.getFileName().toString(), inputStream); + } catch (IOException e) { + throw new RuntimeException("Error opening file: " + p, e); + } + }) + .collect(Collectors.toList()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7ExpressionEvaluator.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7ExpressionEvaluator.java new file mode 100644 index 000000000..563a76823 --- /dev/null +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7ExpressionEvaluator.java @@ -0,0 +1,237 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.external.hapi; + +import ca.uhn.hl7v2.HL7Exception; +import ca.uhn.hl7v2.model.Message; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthDataExpressionEvaluator; +import java.util.Arrays; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * The HapiHL7ExpressionEvaluator class is responsible for evaluating expressions on HL7 messages. + * The expressions can be used to compare fields, count segments, and check for membership. The + * expressions are in the form of: `field = value`, `field != value`, `field in (value1, value2)`, + * `field.count() = value`, etc. The field can be a literal value (e.g. 'EPIC') or a field reference + * (e.g. MSH-5.1, input.MSH-5.1). + */ +public class HapiHL7ExpressionEvaluator implements HealthDataExpressionEvaluator { + + private static final HapiHL7ExpressionEvaluator INSTANCE = new HapiHL7ExpressionEvaluator(); + + private static final String NEWLINE_REGEX = "\\r?\\n|\\r"; + private static final Pattern OPERATION_PATTERN = + Pattern.compile("^(\\S+)\\s*(=|!=|in)\\s*(.+)$"); + private static final Pattern HL7_COUNT_PATTERN = Pattern.compile("(\\S+)\\.count\\(\\)"); + private static final Pattern LITERAL_VALUE_PATTERN = Pattern.compile("'(.*)'"); + private static final Pattern LITERAL_VALUE_COLLECTION_PATTERN = + Pattern.compile("\\(([^)]+)\\)"); + private static final Pattern MESSAGE_SOURCE_PATTERN = + Pattern.compile("(input|output)?\\.?(\\S+)"); + private static final Pattern HL7_FIELD_NAME_PATTERN = Pattern.compile("(\\w+)(?:-(\\S+))?"); + + private HapiHL7ExpressionEvaluator() {} + + public static HapiHL7ExpressionEvaluator getInstance() { + return INSTANCE; + } + + @Override + public final boolean evaluateExpression(String expression, HealthData... data) { + if (data.length > 2) { + throw new IllegalArgumentException( + "Expected two messages, but received: " + data.length); + } + + Matcher matcher = OPERATION_PATTERN.matcher(expression); + + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid statement format."); + } + + String leftOperand = matcher.group(1); // e.g. MSH-5.1, input.MSH-5.1, 'EPIC', OBR.count() + String operator = matcher.group(2); // `=`, `!=`, or `in` + String rightOperand = + matcher.group(3); // e.g. MSH-5.1, input.MSH-5.1, 'EPIC', ('EPIC', 'CERNER'), 2 + + Message outputMessage = (Message) data[0].getUnderlyingData(); + Message inputMessage = (data.length > 1) ? (Message) data[1].getUnderlyingData() : null; + + // matches a count operation (e.g. OBR.count()) + Matcher hl7CountMatcher = HL7_COUNT_PATTERN.matcher(leftOperand); + if (hl7CountMatcher.matches()) { + return evaluateCollectionCount( + outputMessage, hl7CountMatcher.group(1), rightOperand, operator); + } + + // matches either a literal value (e.g. 'EPIC') or a field reference (e.g. MSH-5.1, + // input.MSH-5.1) + String leftValue = getLiteralOrFieldValue(outputMessage, inputMessage, leftOperand); + + // matches membership operator (e.g. MSH-5.1 in ('EPIC', 'CERNER')) + if (operator.equals("in")) { + return evaluateMembership(leftValue, rightOperand); + } + + // matches either a literal value (e.g. 'EPIC') or a field reference (e.g. MSH-5.1, + // input.MSH-5.1) + String rightValue = getLiteralOrFieldValue(outputMessage, inputMessage, rightOperand); + + // matches equality operators (e.g. MSH-5.1 = 'EPIC', MSH-5.1 != 'EPIC') + return evaluateEquality(leftValue, rightValue, operator); + } + + protected > boolean evaluateEquality( + T leftValue, T rightValue, String operator) { + if (operator.equals("=")) { + return leftValue.equals(rightValue); + } else if (operator.equals("!=")) { + return !leftValue.equals(rightValue); + } + throw new IllegalArgumentException("Unknown operator: " + operator); + } + + protected boolean evaluateMembership(String leftValue, String rightOperand) { + Matcher literalValueCollectionMatcher = + LITERAL_VALUE_COLLECTION_PATTERN.matcher(rightOperand); + if (!literalValueCollectionMatcher.matches()) { + throw new IllegalArgumentException("Invalid collection format: " + rightOperand); + } + String arrayString = literalValueCollectionMatcher.group(1); + Set values = + Arrays.stream(arrayString.split(",")) + .map(s -> s.trim().replace("'", "")) + .collect(Collectors.toSet()); + return values.contains(leftValue); + } + + protected boolean evaluateCollectionCount( + Message message, String segmentName, String rightOperand, String operator) { + try { + int count = countSegments(message.encode(), segmentName); + int rightValue = Integer.parseInt(rightOperand); + return evaluateEquality(count, rightValue, operator); + } catch (HL7Exception | NumberFormatException e) { + throw new IllegalArgumentException( + "Error evaluating collection count. Segment: " + + segmentName + + ", count: " + + rightOperand, + e); + } + } + + protected String getLiteralOrFieldValue( + Message outputMessage, Message inputMessage, String operand) { + Matcher literalValueMatcher = LITERAL_VALUE_PATTERN.matcher(operand); + if (literalValueMatcher.matches()) { + return literalValueMatcher.group(1); + } + return getFieldValue(outputMessage, inputMessage, operand); + } + + protected String getFieldValue(Message outputMessage, Message inputMessage, String fieldName) { + Matcher messageSourceMatcher = MESSAGE_SOURCE_PATTERN.matcher(fieldName); + if (!messageSourceMatcher.matches()) { + throw new IllegalArgumentException("Invalid field name format: " + fieldName); + } + + String fileSource = messageSourceMatcher.group(1); + String fieldNameWithoutFileSource = messageSourceMatcher.group(2); + Message message = getMessageBySource(fileSource, inputMessage, outputMessage); + + try { + String messageString = message.encode(); + char fieldSeparator = message.getFieldSeparatorValue(); + String encodingCharacters = message.getEncodingCharactersValue(); + return getSegmentFieldValue( + messageString, fieldNameWithoutFileSource, fieldSeparator, encodingCharacters); + } catch (HL7Exception | NumberFormatException e) { + throw new IllegalArgumentException( + "Failed to extract field value for: " + fieldName, e); + } + } + + // We decided to implement our own simple HL7 parser as the Hapi library was not adequate for + // our needs. + protected static String getSegmentFieldValue( + String hl7Message, String fieldName, char fieldSeparator, String encodingCharacters) { + Matcher hl7FieldNameMatcher = HL7_FIELD_NAME_PATTERN.matcher(fieldName); + if (!hl7FieldNameMatcher.matches()) { + throw new IllegalArgumentException("Invalid HL7 field format: " + fieldName); + } + + String segmentName = hl7FieldNameMatcher.group(1); + String segmentFieldIndex = hl7FieldNameMatcher.group(2); + + String[] lines = hl7Message.split(NEWLINE_REGEX); + for (String line : lines) { + if (!line.startsWith(segmentName)) { + continue; + } + + if (segmentFieldIndex == null) { + return line; + } + + String[] fields = line.split(Pattern.quote(String.valueOf(fieldSeparator))); + String[] indexParts = segmentFieldIndex.split("\\."); + + try { + int fieldPos = Integer.parseInt(indexParts[0]); + + if (segmentName.equals("MSH")) { + fieldPos--; + } + + if (fieldPos < 0 || fieldPos >= fields.length) { + throw new IllegalArgumentException( + "Invalid field index (out of bounds): " + segmentFieldIndex); + } + + String field = fields[fieldPos]; + + if (indexParts.length == 1 || field.isEmpty()) { + return field; + } + + int subFieldEncodingCharactersIndex = indexParts.length - 2; + if (subFieldEncodingCharactersIndex >= encodingCharacters.length()) { + throw new IllegalArgumentException( + "Invalid subfield index (out of bounds): " + segmentFieldIndex); + } + char subfieldSeparator = encodingCharacters.charAt(subFieldEncodingCharactersIndex); + String[] subfields = field.split(Pattern.quote(String.valueOf(subfieldSeparator))); + int subFieldPos = Integer.parseInt(indexParts[1]) - 1; + return subFieldPos >= 0 && subFieldPos < subfields.length + ? subfields[subFieldPos] + : ""; + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Invalid field index formatting: " + segmentFieldIndex, e); + } + } + + return null; + } + + protected static int countSegments(String hl7Message, String segmentName) { + return (int) + Arrays.stream(hl7Message.split(NEWLINE_REGEX)) + .filter(line -> line.startsWith(segmentName)) + .count(); + } + + protected Message getMessageBySource( + String source, Message inputMessage, Message outputMessage) { + if ("input".equals(source)) { + if (inputMessage == null) { + throw new IllegalArgumentException("Input message is null for: " + source); + } + return inputMessage; + } + return outputMessage; + } +} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcher.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcher.java new file mode 100644 index 000000000..b9151a1e4 --- /dev/null +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcher.java @@ -0,0 +1,101 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.external.hapi; + +import ca.uhn.hl7v2.DefaultHapiContext; +import ca.uhn.hl7v2.HL7Exception; +import ca.uhn.hl7v2.HapiContext; +import ca.uhn.hl7v2.model.Message; +import ca.uhn.hl7v2.model.v251.segment.MSH; +import ca.uhn.hl7v2.parser.Parser; +import gov.hhs.cdc.trustedintermediary.rse2e.HL7FileStream; +import gov.hhs.cdc.trustedintermediary.wrappers.Logger; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.inject.Inject; + +/** + * The HapiHL7FileMatcher class is responsible for matching input and output HL7 files based on the + * control ID (MSH-10). + */ +public class HapiHL7FileMatcher { + + private static final HapiHL7FileMatcher INSTANCE = new HapiHL7FileMatcher(); + + @Inject Logger logger; + + private HapiHL7FileMatcher() {} + + public static HapiHL7FileMatcher getInstance() { + return INSTANCE; + } + + public Map matchFiles( + List outputFiles, List inputFiles) { + // We pair up output and input files based on the control ID, which is in MSH-10 + // Any files (either input or output) that don't have a match are logged + Map inputMap = mapMessageByControlId(inputFiles); + Map outputMap = mapMessageByControlId(outputFiles); + + Set unmatchedInputKeys = new HashSet<>(inputMap.keySet()); + unmatchedInputKeys.removeAll(outputMap.keySet()); + + Set unmatchedOutputKeys = new HashSet<>(outputMap.keySet()); + unmatchedOutputKeys.removeAll(inputMap.keySet()); + + Set unmatchedKeys = new HashSet<>(); + unmatchedKeys.addAll(unmatchedInputKeys); + unmatchedKeys.addAll(unmatchedOutputKeys); + + if (!unmatchedKeys.isEmpty()) { + logger.logError( + "Found no match for the following messages with MSH-10: " + unmatchedKeys); + } + + Map messageMap = new HashMap<>(); + inputMap.keySet().retainAll(outputMap.keySet()); + inputMap.forEach( + (key, inputMessage) -> { + Message outputMessage = outputMap.get(key); + messageMap.put(inputMessage, outputMessage); + }); + + return messageMap; + } + + public Map mapMessageByControlId(List files) { + + Map messageMap = new HashMap<>(); + + try (HapiContext context = new DefaultHapiContext()) { + Parser parser = context.getPipeParser(); + + for (HL7FileStream hl7FileStream : files) { + try (InputStream inputStream = hl7FileStream.inputStream()) { + String content = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + Message message = parser.parse(content); + MSH mshSegment = (MSH) message.get("MSH"); + String msh10 = mshSegment.getMessageControlID().getValue(); + if (msh10 == null || msh10.isEmpty()) { + logger.logError("MSH-10 is empty for : " + hl7FileStream.fileName()); + continue; + } + messageMap.put(msh10, message); + } catch (IOException | HL7Exception e) { + logger.logError( + "An error occurred while parsing the message: " + + hl7FileStream.fileName(), + e); + } + } + } catch (IOException e) { + logger.logError("An error occurred while constructing the DefaultHapiContext", e); + } + + return messageMap; + } +} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7Message.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7Message.java new file mode 100644 index 000000000..81da8afff --- /dev/null +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7Message.java @@ -0,0 +1,27 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.external.hapi; + +import ca.uhn.hl7v2.model.Message; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; + +/** + * Represents a HAPI HL7 message that implements the HealthData interface. This class provides a + * wrapper around the HAPI Message object. + */ +public class HapiHL7Message implements HealthData { + + protected final Message underlyingData; + + public HapiHL7Message(Message innerResource) { + this.underlyingData = innerResource; + } + + @Override + public Message getUnderlyingData() { + return underlyingData; + } + + @Override + public String getName() { + return underlyingData.getName(); + } +} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRule.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRule.java new file mode 100644 index 000000000..c53b0729a --- /dev/null +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRule.java @@ -0,0 +1,63 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.ruleengine; + +import gov.hhs.cdc.trustedintermediary.ruleengine.Rule; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; +import java.util.List; + +/** + * The AssertionRule class extends the {@link Rule Rule} class and represents an assertion rule. It + * implements the {@link Rule#runRule(HealthData...) runRule} method to apply an assertion to the + * HL7 message. + */ +public class AssertionRule extends Rule { + + /** + * Do not delete this constructor! It is used for JSON deserialization when loading rules from a + * file. + */ + public AssertionRule() {} + + public AssertionRule(String ruleName, List ruleConditions, List ruleActions) { + super(ruleName, null, null, ruleConditions, ruleActions); + } + + @Override + public final void runRule(HealthData... data) { + + if (data.length != 2) { + this.logger.logError( + "Rule [" + + this.getName() + + "]: Assertion rules require exactly two data objects to be passed in."); + return; + } + + HealthData outputData = data[0]; + HealthData inputData = data[1]; + + for (String assertion : this.getRules()) { + try { + boolean isValid = + this.evaluator.evaluateExpression(assertion, outputData, inputData); + if (!isValid) { + this.logger.logWarning( + "Assertion failed for '" + + this.getName() + + "': " + + assertion + + " (" + + outputData.getName() + + ")"); + } + } catch (Exception e) { + this.logger.logError( + "Rule [" + + this.getName() + + "]: " + + "An error occurred while evaluating the assertion: " + + assertion, + e); + } + } + } +} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRuleEngine.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRuleEngine.java new file mode 100644 index 000000000..a040e67f9 --- /dev/null +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRuleEngine.java @@ -0,0 +1,78 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.ruleengine; + +import ca.uhn.hl7v2.model.Message; +import gov.hhs.cdc.trustedintermediary.rse2e.external.hapi.HapiHL7Message; +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoader; +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoaderException; +import gov.hhs.cdc.trustedintermediary.wrappers.Logger; +import gov.hhs.cdc.trustedintermediary.wrappers.formatter.TypeReference; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import javax.inject.Inject; + +/** + * The AssertionRuleEngine is responsible for loading and running assertion rules against HL7 + * messages. + */ +public class AssertionRuleEngine { + private static final AssertionRuleEngine INSTANCE = new AssertionRuleEngine(); + final List assertionRules = new ArrayList<>(); + volatile boolean rulesLoaded = false; + + @Inject Logger logger; + @Inject RuleLoader ruleLoader; + + public static AssertionRuleEngine getInstance() { + return INSTANCE; + } + + public AssertionRuleEngine() {} + + public void unloadRules() { + assertionRules.clear(); + rulesLoaded = false; + } + + public void ensureRulesLoaded() throws RuleLoaderException { + if (!rulesLoaded) { + synchronized (assertionRules) { + if (!rulesLoaded) { + String ruleDefinitionsFileName = "assertion_definitions.json"; + try (InputStream stream = + getClass() + .getClassLoader() + .getResourceAsStream(ruleDefinitionsFileName)) { + List parsedRules = + ruleLoader.loadRules(stream, new TypeReference<>() {}); + assertionRules.addAll(parsedRules); + rulesLoaded = true; + + } catch (IOException | NullPointerException e) { + throw new RuleLoaderException( + "File not found: " + ruleDefinitionsFileName, e); + } + } + } + } + } + + public void runRules(Message outputMessage, Message inputMessage) { + try { + ensureRulesLoaded(); + } catch (RuleLoaderException e) { + logger.logError("Failed to load rules definitions", e); + return; + } + + HapiHL7Message outputHapiMessage = new HapiHL7Message(outputMessage); + HapiHL7Message inputHapiMessage = new HapiHL7Message(inputMessage); + + for (AssertionRule rule : assertionRules) { + if (rule.shouldRun(outputHapiMessage)) { + rule.runRule(outputHapiMessage, inputHapiMessage); + } + } + } +} diff --git a/rs-e2e/src/main/resources/assertion_definitions.json b/rs-e2e/src/main/resources/assertion_definitions.json new file mode 100644 index 000000000..ff38c7cbe --- /dev/null +++ b/rs-e2e/src/main/resources/assertion_definitions.json @@ -0,0 +1,41 @@ +{ + "definitions": [ + { + "name": "General requirements", + "conditions": [], + "rules": [ + "MSH-2 = input.MSH-2", + "MSH-7 = input.MSH-7", + "MSH-10 = input.MSH-10", + "MSH-11 = input.MSH-11", + "MSH-12 = input.MSH-12" + ] + }, + { + "name": "UCSD ORU requirements", + "conditions": [ + "MSH-9.2 = 'R01'", + "MSH-6 in ('R797', 'R508')" + ], + "rules": [ + "MSH-4 = 'CDPH'", + "MSH-5 = 'EPIC'", + "MSH-9 = 'ORU^R01'", + "PID-3.4 = ''", + "PID-3.5 = ''", + "PID-5.7 = ''", + "OBR-4.1 = '54089-8'", + "OBR-4.3 = 'CDPHGSPEAP'", + "ORC-2.1 = input.ORC-4.1", + "ORC-2.2 = input.ORC-4.2", + "ORC-4.1 = input.ORC-2.1", + "ORC-4.2 = input.ORC-2.2", + "ORC-21.10 = MSH-6", + "OBR-2.1 = ORC-2.1", + "OBR-2.2 = ORC-2.2", + "OBR-16 = ORC-12", + "OBR.count() = 1" + ] + } + ] +} diff --git a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/AutomatedTest.groovy b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/AutomatedTest.groovy new file mode 100644 index 000000000..5cb1cd713 --- /dev/null +++ b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/AutomatedTest.groovy @@ -0,0 +1,67 @@ +package gov.hhs.cdc.trustedintermediary.rse2e + +import ca.uhn.hl7v2.model.Message +import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext +import gov.hhs.cdc.trustedintermediary.rse2e.external.hapi.HapiHL7FileMatcher +import gov.hhs.cdc.trustedintermediary.rse2e.external.hapi.HapiHL7ExpressionEvaluator +import gov.hhs.cdc.trustedintermediary.external.jackson.Jackson +import gov.hhs.cdc.trustedintermediary.wrappers.HealthDataExpressionEvaluator +import gov.hhs.cdc.trustedintermediary.wrappers.Logger +import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoader +import gov.hhs.cdc.trustedintermediary.rse2e.ruleengine.AssertionRuleEngine +import spock.lang.Specification + +class AutomatedTest extends Specification { + + List recentAzureFiles + List recentLocalFiles + AssertionRuleEngine engine + HapiHL7FileMatcher fileMatcher + def mockLogger = Mock(Logger) + + def setup() { + FileFetcher azureFileFetcher = AzureBlobFileFetcher.getInstance() + recentAzureFiles = azureFileFetcher.fetchFiles() + + FileFetcher localFileFetcher = LocalFileFetcher.getInstance() + recentLocalFiles = localFileFetcher.fetchFiles() + + engine = AssertionRuleEngine.getInstance() + fileMatcher = HapiHL7FileMatcher.getInstance() + + TestApplicationContext.reset() + TestApplicationContext.init() + TestApplicationContext.register(AssertionRuleEngine, engine) + TestApplicationContext.register(RuleLoader, RuleLoader.getInstance()) + TestApplicationContext.register(Logger, mockLogger) + TestApplicationContext.register(Formatter, Jackson.getInstance()) + TestApplicationContext.register(HapiHL7FileMatcher, fileMatcher) + TestApplicationContext.register(HealthDataExpressionEvaluator, HapiHL7ExpressionEvaluator.getInstance()) + TestApplicationContext.register(AzureBlobFileFetcher, azureFileFetcher) + TestApplicationContext.register(LocalFileFetcher, LocalFileFetcher.getInstance()) + TestApplicationContext.injectRegisteredImplementations() + } + + def cleanup() { + for (HL7FileStream fileStream : recentLocalFiles + recentAzureFiles) { + fileStream.inputStream().close() + } + } + + def "test defined assertions on relevant messages"() { + given: + def matchedFiles = fileMatcher.matchFiles(recentAzureFiles, recentLocalFiles) + + when: + for (messagePair in matchedFiles) { + Message inputMessage = messagePair.getKey() as Message + Message outputMessage = messagePair.getValue() as Message + engine.runRules(outputMessage, inputMessage) + } + + then: + 0 * mockLogger.logError(_ as String, _ as Exception) + 0 * mockLogger.logWarning(_ as String) + } +} diff --git a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7ExpressionEvaluatorTest.groovy b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7ExpressionEvaluatorTest.groovy new file mode 100644 index 000000000..aaba6b9d7 --- /dev/null +++ b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7ExpressionEvaluatorTest.groovy @@ -0,0 +1,464 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.external.hapi + +import ca.uhn.hl7v2.model.Message +import ca.uhn.hl7v2.model.Segment +import ca.uhn.hl7v2.parser.PipeParser +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData +import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext +import spock.lang.Specification + +class HapiHL7ExpressionEvaluatorTest extends Specification { + + def evaluator = HapiHL7ExpressionEvaluator.getInstance() + + char hl7FieldSeparator = '|' + String hl7FieldEncodingCharacters = "^~\\&" + Message mshMessage + Segment mshSegment + String mshSegmentText + + def setup() { + TestApplicationContext.reset() + TestApplicationContext.init() + TestApplicationContext.register(HapiHL7ExpressionEvaluator, evaluator) + + mshSegmentText = "MSH|^~\\&|Sender Application^sender.test.com^DNS|Sender Facility^0.0.0.0.0.0.0.0^ISO|Receiver Application^0.0.0.0.0.0.0.0^ISO|Receiver Facility^simulated-lab-id^DNS|20230101010000-0000||ORM^O01^ORM_O01|111111|T|2.5.1" + def pipeParser = new PipeParser() + mshMessage = pipeParser.parse(mshSegmentText) + mshSegment = (Segment) mshMessage.get("MSH") + + TestApplicationContext.injectRegisteredImplementations() + } + + def "evaluateExpression returns boolean when evaluating valid assertions"() { + given: + def spyEvaluator = Spy(HapiHL7ExpressionEvaluator.getInstance()) + spyEvaluator.getLiteralOrFieldValue(_ as Message, _ as Message, _ as String) >> "mockedValue" + spyEvaluator.evaluateEquality(_ as String, _ as String, _ as String) >> true + spyEvaluator.evaluateMembership(_ as String, _ as String) >> true + spyEvaluator.evaluateCollectionCount(_ as Message, _ as String, _ as String, _ as String) >> true + + def healthData = Mock(HealthData) { + getUnderlyingData() >> Mock(Message) + } + + expect: + spyEvaluator.evaluateExpression(assertion, healthData, healthData) + + where: + assertion | _ + "input.MSH-1 = MSH-1" | _ + "input.MSH-1 = input.MSH-1" | _ + "MSH-1 = input.MSH-1" | _ + "output.MSH-1 = MSH-1" | _ + "output.MSH-1 = output.MSH-1" | _ + "MSH-1 = output.MSH-1" | _ + "input.MSH-1 = output.MSH-1" | _ + "output.MSH-1 = input.MSH-1" | _ + "MSH-9.1 = 'R01'" | _ + "MSH-9.1 = 'R01'" | _ + "MSH-6 in ('R797', 'R508')" | _ + "OBR.count() = 1" | _ + } + + def "evaluateExpression allows null input message when no assertions use input"() { + given: + def spyEvaluator = Spy(HapiHL7ExpressionEvaluator.getInstance()) + spyEvaluator.getLiteralOrFieldValue(_ as Message, null, _ as String) >> "mockedValue" + spyEvaluator.evaluateEquality(_ as String, _ as String, _ as String) >> true + spyEvaluator.evaluateMembership(_ as String, _ as String) >> true + spyEvaluator.evaluateCollectionCount(_ as Message, _ as String, _ as String, _ as String) >> true + + def healthData = Mock(HealthData) { + getUnderlyingData() >> Mock(Message) + } + + expect: + spyEvaluator.evaluateExpression(assertion, healthData) + + where: + assertion | _ + "MSH-1 = MSH-1" | _ + "output.MSH-1 = MSH-1" | _ + "MSH-9.1 = 'R01'" | _ + "MSH-6 in ('R797', 'R508')" | _ + "OBR.count() = 1" | _ + } + + def "evaluateExpression should throw exception for invalid expression format"() { + when: + evaluator.evaluateExpression("invalid format", Mock(HealthData)) + + then: + def e = thrown(IllegalArgumentException) + e.getMessage().contains("Invalid statement format") + } + + def "evaluateExpression should throw exception for more than two messages"() { + when: + evaluator.evaluateExpression("'EPIC' = 'EPIC'", Mock(HealthData), Mock(HealthData), Mock(HealthData)) + + then: + def e = thrown(IllegalArgumentException) + e.getMessage().contains("Expected two messages") + } + + def "evaluateExpression should throw exception when there is no operator"() { + given: + def condition = "input.MSH-3" + + when: + evaluator.evaluateExpression(condition, Mock(HealthData)) + + then: + def e = thrown(IllegalArgumentException) + e.getMessage().contains("Invalid statement format") + } + + def "evaluateEquality should evaluate values in evaluateEquality correctly"() { + given: + boolean result + + when: + result = evaluator.evaluateEquality("'EPIC'", "'EPIC'", "=") + + then: + result + + when: + result = evaluator.evaluateEquality("'EPIC'", "'othervalue'", "=") + + then: + !result + + when: + result = evaluator.evaluateEquality("'EPIC'", "'CERNER'", "!=") + + then: + result + + when: + result = evaluator.evaluateEquality("'EPIC'", "'EPIC'", "!=") + + then: + !result + } + + def "evaluateEquality should throw exception when the operator in evaluateEquality is not valid"() { + given: + def unknownOperator = "<>" + + when: + evaluator.evaluateEquality("left", "right", unknownOperator) + + then: + def e = thrown(IllegalArgumentException) + e.getMessage().contains("Unknown operator") + } + + def "evaluateMembership returns boolean when evaluating valid assertions"() { + given: + def expectedValue = "expectedValue" + + when: + def expectedValueInSet = "('expectedValue', 'value2')" + def expectedValueInSetResult = evaluator.evaluateMembership(expectedValue, expectedValueInSet) + + then: + expectedValueInSetResult + + when: + def expectedValueNotInSet = "('value1', 'value2')" + def expectedValueNotInSetResult = evaluator.evaluateMembership(expectedValue, expectedValueNotInSet) + + then: + !expectedValueNotInSetResult + } + + def "evaluateMembership throws exception when the set is not valid"() { + given: + def invalidSet = "invalidSet" + + when: + evaluator.evaluateMembership("value", invalidSet) + + then: + def e = thrown(IllegalArgumentException) + e.getMessage().contains("Invalid collection format") + } + + def "evaluateCollectionCount returns true when segment count matches desired count"() { + given: + def rightOperand = "1" + def segmentName = "MSH" + def operator = "=" + + when: + def result = evaluator.evaluateCollectionCount(mshMessage, segmentName, rightOperand, operator) + + then: + result + } + + def "evaluateCollectionCount returns false when segment count does not match desired count"() { + given: + def rightOperand = "3" + def segmentName = "MSH" + def operator = "=" + + when: + def result = evaluator.evaluateCollectionCount(mshMessage, segmentName, rightOperand, operator) + + then: + !result + } + + def "evaluateCollectionCount throws exception when segment count is not numeric"() { + given: + def rightOperand = "three" + def segmentName = "MSH" + def operator = "=" + + when: + evaluator.evaluateCollectionCount(mshMessage, segmentName, rightOperand, operator) + + then: + def e = thrown(IllegalArgumentException) + e.getCause().getClass() == NumberFormatException + } + + def "evaluateCollectionCount evaluates correctly when specified segment is not in message"() { + given: + def rightOperand = "3" + def segmentName = "OBX" + def operator = "=" + + when: + def result = evaluator.evaluateCollectionCount(mshMessage, segmentName, rightOperand, operator) + + then: + !result + } + + def "getLiteralOrFieldValue returns literal value when literal is specified"() { + given: + def operand = "'Epic'" + def inputMessage = Mock(Message) + def outputMessage = Mock(Message) + + when: + def result = evaluator.getLiteralOrFieldValue(outputMessage, inputMessage, operand) + + then: + result == "Epic" + } + + def "getLiteralOrFieldValue returns field value when field is specified"() { + given: + def operand = "MSH-3" + def inputMessage = Mock(Message) + def msh3 = "Sender Application^sender.test.com^DNS" + + when: + def result = evaluator.getLiteralOrFieldValue(mshMessage, inputMessage, operand) + + then: + result == msh3 + } + + def "getFieldValue returns specified field value"() { + given: + def fieldName = "MSH-3" + def msh3 = "Sender Application^sender.test.com^DNS" + def inputMessage = Mock(Message) + + when: + def result = evaluator.getFieldValue(mshMessage, inputMessage, fieldName) + + then: + result == msh3 + } + + def "getFieldValue throws exception for non numeric field index"() { + given: + def fieldName = "MSH-three" + def inputMessage = Mock(Message) + + when: + evaluator.getFieldValue(mshMessage, inputMessage, fieldName) + + then: + def e = thrown(IllegalArgumentException) + e.getCause().getClass() == NumberFormatException + } + + def "getFieldValue throws exception for empty field name"() { + given: + def fieldName = "" + def inputMessage = Mock(Message) + + when: + evaluator.getFieldValue(mshMessage, inputMessage, fieldName) + + then: + def e = thrown(IllegalArgumentException) + e.getMessage().contains("Invalid field name format") + } + + def "getSegmentFieldValue should return segment when field components indicate segment"() { + given: + def fieldName = "MSH" + + when: + def result = evaluator.getSegmentFieldValue(mshSegmentText, fieldName, hl7FieldSeparator, hl7FieldEncodingCharacters) + + then: + result == mshSegmentText + } + + def "getSegmentFieldValue should return field when field components indicate field"() { + given: + def fieldName = "MSH-3" + def msh3 = "Sender Application^sender.test.com^DNS" + + when: + def result = evaluator.getSegmentFieldValue(mshSegmentText, fieldName, hl7FieldSeparator, hl7FieldEncodingCharacters) + + then: + result == msh3 + } + + def "getSegmentFieldValue should return subfield when field components indicate subfield"() { + given: + def fieldName = "MSH-3.2" + def msh32 = "sender.test.com" + + when: + def result = evaluator.getSegmentFieldValue(mshSegmentText, fieldName, hl7FieldSeparator, hl7FieldEncodingCharacters) + + then: + result == msh32 + } + + def "getSegmentFieldValue should return empty string when field components indicate subfield but subfield not present"() { + given: + def fieldName = "MSH-3.4" + + when: + def result = evaluator.getSegmentFieldValue(mshSegmentText, fieldName, hl7FieldSeparator, hl7FieldEncodingCharacters) + + then: + result == "" + } + + def "getSegmentFieldValue returns null when looking for segment that isn't in message"() { + given: + def fieldName = "OBX" + + when: + def result = evaluator.getSegmentFieldValue(mshSegmentText, fieldName, hl7FieldSeparator, hl7FieldEncodingCharacters) + + then: + result == null + } + + def "getSegmentFieldValue throws exception when field name is invalid"() { + given: + def fieldName = "MSH-" + + when: + evaluator.getSegmentFieldValue(mshSegmentText, fieldName, hl7FieldSeparator, hl7FieldEncodingCharacters) + + then: + def e = thrown(IllegalArgumentException) + e.getMessage().contains("Invalid HL7 field format: ") + } + + def "getSegmentFieldValue throws exception when field index is out of bounds"() { + given: + def fieldName = "MSH-99" + + when: + evaluator.getSegmentFieldValue(mshSegmentText, fieldName, hl7FieldSeparator, hl7FieldEncodingCharacters) + + then: + def e = thrown(IllegalArgumentException) + e.getMessage().contains("Invalid field index (out of bounds)") + } + + def "getSegmentFieldValue throws exception when there are too many subfield levels"() { + given: + def fieldName = "MSH-3.3.3.3.3.3" + + when: + evaluator.getSegmentFieldValue(mshSegmentText, fieldName, hl7FieldSeparator, hl7FieldEncodingCharacters) + + then: + def e = thrown(IllegalArgumentException) + e.getMessage().contains("Invalid subfield index (out of bounds)") + } + + def "getSegmentFieldValue returns empty string when sub-field index is out of bounds"() { + given: + def fieldName = "MSH-3.99" + + when: + def result = evaluator.getSegmentFieldValue(mshSegmentText, fieldName, hl7FieldSeparator, hl7FieldEncodingCharacters) + + then: + result == "" + } + + def "getMessageBySource should return input message when source is input"() { + given: + def source = "input" + def inputMessage = Mock(Message) + def outputMessage = Mock(Message) + + when: + def result = evaluator.getMessageBySource(source, inputMessage, outputMessage) + + then: + result == inputMessage + } + + def "getMessageBySource should return output message when source is not input"() { + given: + def source = "output" + def inputMessage = Mock(Message) + def outputMessage = Mock(Message) + + when: + def result = evaluator.getMessageBySource(source, inputMessage, outputMessage) + + then: + result == outputMessage + } + + def "getMessageBySource should return output message when source is empty"() { + given: + def source = "" + def inputMessage = Mock(Message) + def outputMessage = Mock(Message) + + when: + def result = evaluator.getMessageBySource(source, inputMessage, outputMessage) + + then: + result == outputMessage + } + + def "getMessageBySource should throw exception when source is input and input message is null"() { + given: + def source = "input" + def inputMessage = null + def outputMessage = Mock(Message) + + when: + evaluator.getMessageBySource(source, inputMessage, outputMessage) + + then: + def e = thrown(IllegalArgumentException) + e.getMessage().contains("Input message is null for") + } +} diff --git a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcherTest.groovy b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcherTest.groovy new file mode 100644 index 000000000..889038a18 --- /dev/null +++ b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcherTest.groovy @@ -0,0 +1,102 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.external.hapi + +import ca.uhn.hl7v2.model.Message +import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext +import gov.hhs.cdc.trustedintermediary.rse2e.HL7FileStream +import gov.hhs.cdc.trustedintermediary.wrappers.Logger +import spock.lang.Specification + +class HapiHL7FileMatcherTest extends Specification { + + def mockLogger = Mock(Logger) + def fileMatcher = HapiHL7FileMatcher.getInstance() + + def setup() { + TestApplicationContext.reset() + TestApplicationContext.init() + TestApplicationContext.register(Logger, mockLogger) + TestApplicationContext.register(HapiHL7FileMatcher, fileMatcher) + + TestApplicationContext.injectRegisteredImplementations() + } + + def "should correctly match input and output files and log unmatched files"() { + given: + def spyFileMatcher = Spy(HapiHL7FileMatcher.getInstance()) + def fileStream1 = new HL7FileStream("file1", Mock(InputStream)) + def fileStream2 = new HL7FileStream("file2", Mock(InputStream)) + def fileStream3 = new HL7FileStream("file3", Mock(InputStream)) + def mockInputFiles = [fileStream1, fileStream2] + def mockOutputFiles = [fileStream2, fileStream3] + def mockInputMessage2 = Mock(Message) + def mockOutputMessage2 = Mock(Message) + spyFileMatcher.mapMessageByControlId(mockInputFiles) >> [ "1": Mock(Message), "2": mockInputMessage2 ] + spyFileMatcher.mapMessageByControlId(mockOutputFiles) >> [ "2": mockOutputMessage2, "3": Mock(Message) ] + + when: + def result = spyFileMatcher.matchFiles(mockOutputFiles, mockInputFiles) + + then: + result.size() == 1 + result == Map.of(mockInputMessage2, mockOutputMessage2) + 1 * mockLogger.logError({ it.contains("Found no match") && it.contains("1") && it.contains("3") }) + } + + def "should map message by control ID"() { + given: + def msh1to9 = "MSH|^~\\&|Sender Application^sender.test.com^DNS|Sender Facility^0.0.0.0.0.0.0.0^ISO|Receiver Application^0.0.0.0.0.0.0.0^ISO|Receiver Facility^simulated-lab-id^DNS|20230101010000-0000||ORM^O01^ORM_O01|" + def msh11to12 = "|T|2.5.1" + def file1Msh10 = "1111111" + String file1MshSegment = msh1to9 + file1Msh10 + msh11to12 + def file1InputStream = new ByteArrayInputStream(file1MshSegment.bytes) + def file1Hl7FileStream = new HL7FileStream("file1", file1InputStream) + def file2Msh10 = "2222222" + String file2MshSegment = msh1to9 + file2Msh10 + msh11to12 + def file2InputStream = new ByteArrayInputStream(file2MshSegment.bytes) + def file2Hl7FileStream = new HL7FileStream("file2", file2InputStream) + def mockFiles = [ + file1Hl7FileStream, + file2Hl7FileStream + ] + + when: + def result = fileMatcher.mapMessageByControlId(mockFiles) + + then: + result.size() == 2 + result[file1Msh10] != null + file1MshSegment == result[file1Msh10].encode().trim() + result[file2Msh10] != null + file2MshSegment == result[file2Msh10].encode().trim() + } + + def "should log an error and continue when MSH-10 is empty"() { + given: + def msh1to9 = "MSH|^~\\&|Sender Application^sender.test.com^DNS|Sender Facility^0.0.0.0.0.0.0.0^ISO|Receiver Application^0.0.0.0.0.0.0.0^ISO|Receiver Facility^simulated-lab-id^DNS|20230101010000-0000||ORM^O01^ORM_O01|" + def msh11to12 = "|T|2.5.1" + def emptyMsh10 = "" + String mshSegment = msh1to9 + emptyMsh10 + msh11to12 + def inputStream = new ByteArrayInputStream(mshSegment.bytes) + def hl7FileStream = new HL7FileStream("file1", inputStream) + + when: + def result = fileMatcher.mapMessageByControlId([hl7FileStream]) + + then: + result.size() == 0 + 1 * mockLogger.logError({ it.contains("MSH-10 is empty") }) + } + + def "should log an error when not able to parse the file as HL7 message"() { + given: + def inputStream = new ByteArrayInputStream("".bytes) + def hl7FileStream = new HL7FileStream("badFile", inputStream) + + when: + def result = fileMatcher.mapMessageByControlId([hl7FileStream]) + + then: + result.size() == 0 + 1 * mockLogger.logError({ it.contains("An error occurred while parsing the message") }, _ as Exception) + } +} diff --git a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7MessageTest.groovy b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7MessageTest.groovy new file mode 100644 index 000000000..18fb3cd1a --- /dev/null +++ b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7MessageTest.groovy @@ -0,0 +1,32 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.external.hapi + +import ca.uhn.hl7v2.model.Message +import spock.lang.Specification + +class HapiHL7MessageTest extends Specification { + + def "should correctly initialize and return underlying data"() { + given: + def mockMessage = Mock(Message) + def hl7Message = new HapiHL7Message(mockMessage) + + expect: + hl7Message.getUnderlyingData() == mockMessage + } + + def "should return the name of the underlying message"() { + given: + def expectedName = "TestMessage" + def message = Mock(Message) + message.getName() >> expectedName + + and: + def hl7Message = new HapiHL7Message(message) + + when: + def name = hl7Message.getName() + + then: + name == expectedName + } +} diff --git a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRuleEngineTest.groovy b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRuleEngineTest.groovy new file mode 100644 index 000000000..ce7250ca8 --- /dev/null +++ b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRuleEngineTest.groovy @@ -0,0 +1,104 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.ruleengine + +import ca.uhn.hl7v2.model.Message +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoader +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoaderException +import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext +import gov.hhs.cdc.trustedintermediary.wrappers.HealthDataExpressionEvaluator +import gov.hhs.cdc.trustedintermediary.wrappers.Logger +import gov.hhs.cdc.trustedintermediary.wrappers.formatter.TypeReference +import spock.lang.Specification + +class AssertionRuleEngineTest extends Specification { + + def ruleEngine = AssertionRuleEngine.getInstance() + def mockRuleLoader = Mock(RuleLoader) + def mockLogger = Mock(Logger) + def mockRule = Mock(AssertionRule) + + def setup() { + ruleEngine.unloadRules() + + TestApplicationContext.reset() + TestApplicationContext.init() + TestApplicationContext.register(RuleLoader, mockRuleLoader) + TestApplicationContext.register(Logger, mockLogger) + TestApplicationContext.register(AssertionRuleEngine, ruleEngine) + TestApplicationContext.register(HealthDataExpressionEvaluator, Mock(HealthDataExpressionEvaluator)) + TestApplicationContext.injectRegisteredImplementations() + } + + def "ensureRulesLoaded happy path"() { + given: + mockRuleLoader.loadRules(_ as InputStream, _ as TypeReference) >> [mockRule] + + when: + ruleEngine.ensureRulesLoaded() + + then: + 1 * mockRuleLoader.loadRules(_ as InputStream, _ as TypeReference) >> [mockRule] + ruleEngine.assertionRules.size() == 1 + } + + def "ensureRulesLoaded loads rules only once by default"() { + given: + mockRuleLoader.loadRules(_ as InputStream, _ as TypeReference) >> [mockRule] + + when: + ruleEngine.ensureRulesLoaded() + ruleEngine.ensureRulesLoaded() // Call twice to test if rules are loaded only once + + then: + 1 * mockRuleLoader.loadRules(_ as InputStream, _ as TypeReference) >> [mockRule] + ruleEngine.assertionRules.size() == 1 + } + + def "ensureRulesLoaded loads rules only once on multiple threads"() { + given: + def threadsNum = 10 + def iterations = 4 + + when: + mockRuleLoader.loadRules(_ as InputStream, _ as TypeReference) >> [mockRule] + List threads = [] + (1..threadsNum).each { threadId -> + threads.add(new Thread({ + for (int i = 0; i < iterations; i++) { + ruleEngine.ensureRulesLoaded() + } + })) + } + threads*.start() + threads*.join() + + then: + 1 * mockRuleLoader.loadRules(_ as InputStream, _ as TypeReference) + } + + def "ensureRulesLoaded logs an error if there is an exception loading the rules"() { + given: + def exception = new RuleLoaderException("Error loading rules", new Exception()) + mockRuleLoader.loadRules(_ as InputStream, _ as TypeReference) >> { + mockLogger.logError("Error loading rules", exception) + return [] + } + + when: + ruleEngine.runRules(Mock(Message), Mock(Message)) + + then: + 1 * mockLogger.logError(_ as String, exception) + } + + def "runRules logs an error and doesn't run any rules when there's a RuleLoaderException"() { + given: + def exception = new RuleLoaderException("Error loading rules", new Exception()) + mockRuleLoader.loadRules(_ as InputStream, _ as TypeReference) >> { throw exception } + + when: + ruleEngine.runRules(Mock(Message), Mock(Message)) + + then: + 1 * mockLogger.logError(_ as String, exception) + } +} diff --git a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRuleTest.groovy b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRuleTest.groovy new file mode 100644 index 000000000..92282a3f2 --- /dev/null +++ b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRuleTest.groovy @@ -0,0 +1,112 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.ruleengine + +import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData +import gov.hhs.cdc.trustedintermediary.wrappers.Logger +import gov.hhs.cdc.trustedintermediary.wrappers.HealthDataExpressionEvaluator +import spock.lang.Specification + +class AssertionRuleTest extends Specification { + + def mockLogger = Mock(Logger) + def mockEvaluator = Mock(HealthDataExpressionEvaluator) + + def setup() { + TestApplicationContext.reset() + TestApplicationContext.init() + TestApplicationContext.register(Logger, mockLogger) + TestApplicationContext.register(HealthDataExpressionEvaluator, mockEvaluator) + TestApplicationContext.injectRegisteredImplementations() + } + + def "AssertionRule's properties are set and get correctly"() { + given: + def ruleName = "Rule name" + def conditions = ["condition1", "condition2"] + def assertions = ["assertion1", "assertion2"] + + when: + def rule = new AssertionRule(ruleName, conditions, assertions) + + then: + rule.getName() == ruleName + rule.getConditions() == conditions + rule.getRules() == assertions + } + + def "shouldRun returns expected boolean depending on conditions"() { + given: + def mockData = Mock(HealthData) + mockEvaluator.evaluateExpression(_ as String, mockData) >> true >> conditionResult + + def rule = new AssertionRule(null, [ + "trueCondition", + "secondCondition" + ], null) + + expect: + rule.shouldRun(mockData) == applies + + where: + conditionResult | applies + true | true + false | false + } + + def "shouldRun logs an error and returns false if an exception happens when evaluating a condition"() { + given: + def mockData = Mock(HealthData) + mockEvaluator.evaluateExpression(_ as String, mockData) >> { throw new Exception() } + + def rule = new AssertionRule(null, ["condition"], null) + + when: + def applies = rule.shouldRun(mockData) + + then: + 1 * mockLogger.logError(_ as String, _ as Exception) + !applies + } + + def "runRule returns expected boolean depending on assertions"() { + given: + def mockData = Mock(HealthData) + + def rule = new AssertionRule(null, null, [ + "trueValidation", + "secondValidation" + ]) + + when: + mockEvaluator.evaluateExpression(_ as String, mockData, _ as HealthData) >> true >> true + rule.runRule(mockData, Mock(HealthData)) + + then: + 0 * mockLogger.logWarning(_ as String) + 0 * mockLogger.logError(_ as String, _ as Exception) + + when: + mockEvaluator.evaluateExpression(_ as String, mockData, _ as HealthData) >> true >> false + rule.runRule(mockData, Mock(HealthData)) + + then: + 1 * mockLogger.logWarning(_ as String) + 0 * mockLogger.logError(_ as String, _ as Exception) + } + + def "runRule logs an error and returns false if an exception happens when evaluating an assertion"() { + given: + + def mockData = Mock(HealthData) + mockEvaluator.evaluateExpression(_ as String, mockData, _ as HealthData) >> { throw new Exception() } + + def rule = new AssertionRule(null, null, ["validation"]) + + when: + rule.runRule(mockData, Mock(HealthData)) + + then: + 0 * mockLogger.logWarning(_ as String) + 1 * mockLogger.logError(_ as String, _ as Exception) + } +} diff --git a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/RuleLoaderTest.groovy b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/RuleLoaderTest.groovy new file mode 100644 index 000000000..ecd65257d --- /dev/null +++ b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/RuleLoaderTest.groovy @@ -0,0 +1,84 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.ruleengine + +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoader +import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoaderException +import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext +import gov.hhs.cdc.trustedintermediary.external.jackson.Jackson +import gov.hhs.cdc.trustedintermediary.rse2e.external.hapi.HapiHL7ExpressionEvaluator +import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter +import gov.hhs.cdc.trustedintermediary.wrappers.formatter.TypeReference +import gov.hhs.cdc.trustedintermediary.wrappers.HealthDataExpressionEvaluator +import spock.lang.Specification + +import java.nio.file.Files +import java.nio.file.Path + +class RuleLoaderTest extends Specification { + + String fileContents + Path tempFile + + def setup() { + TestApplicationContext.reset() + TestApplicationContext.init() + TestApplicationContext.register(RuleLoader, RuleLoader.getInstance()) + TestApplicationContext.register(Formatter, Jackson.getInstance()) + TestApplicationContext.register(HealthDataExpressionEvaluator, HapiHL7ExpressionEvaluator.getInstance()) + TestApplicationContext.injectRegisteredImplementations() + + tempFile = Files.createTempFile("test_validation_definition", ".json") + } + + def cleanup(){ + Files.deleteIfExists(tempFile) + } + + def "load rules from file"() { + given: + fileContents = """ + { + "definitions": [ + { + "name": "Example result requirements", + "conditions": [ + "MSH-9.1 = 'R01'", + "MSH-6 in ('R797', 'R508')" + ], + "rules": [ + "MSH-4 = 'CDPH'", + "OBR.count() = 1" + ] + } + ] + } + """ + Files.writeString(tempFile, fileContents) + + when: + List rules = RuleLoader.getInstance().loadRules(Files.newInputStream(tempFile), new TypeReference>>() {}) + + then: + rules.size() == 1 + AssertionRule rule = rules.get(0) as AssertionRule + rule.getName() == "Example result requirements" + rule.getConditions() == [ + "MSH-9.1 = 'R01'", + "MSH-6 in ('R797', 'R508')" + ] + rule.getRules() == [ + "MSH-4 = 'CDPH'", + "OBR.count() = 1" + ] + } + + def "handle FormatterProcessingException when loading rules from a non existent file"() { + given: + Files.writeString(tempFile, "!K@WJ#8uhy") + + when: + RuleLoader.getInstance().loadRules(Files.newInputStream(tempFile), new TypeReference>>() {}) + + then: + thrown(RuleLoaderException) + } +} diff --git a/settings.gradle b/settings.gradle index c6317d444..f9cf9d5ec 100644 --- a/settings.gradle +++ b/settings.gradle @@ -12,3 +12,4 @@ include('app') include('shared') include('etor') include('e2e') +include('rs-e2e') diff --git a/shared/build.gradle b/shared/build.gradle index debaa4805..68ca9da71 100644 --- a/shared/build.gradle +++ b/shared/build.gradle @@ -16,7 +16,7 @@ dependencies { api 'javax.inject:javax.inject:1' implementation 'javax.annotation:javax.annotation-api:1.3.2' - //logging + // logging implementation 'org.slf4j:slf4j-api:2.0.16' implementation 'ch.qos.logback:logback-classic:1.5.8' implementation 'net.logstash.logback:logstash-logback-encoder:8.0' @@ -27,13 +27,17 @@ dependencies { implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.18.0' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.0' - //fhir + // hapi fhir api 'ca.uhn.hapi.fhir:hapi-fhir-base:7.4.3' api 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:7.4.3' implementation 'ca.uhn.hapi.fhir:hapi-fhir-caching-caffeine:7.4.3' implementation 'ca.uhn.hapi.fhir:hapi-fhir-validation-resources-r4:7.4.3' api 'org.fhir:ucum:1.0.8' + // hapi hl7 + implementation 'ca.uhn.hapi:hapi-base:2.5.1' + implementation 'ca.uhn.hapi:hapi-structures-v251:2.5.1' + // Apache Client implementation 'org.apache.httpcomponents.client5:httpclient5:5.4' implementation 'org.apache.httpcomponents.client5:httpclient5-fluent:5.4' 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 e37c2a108..705d3b486 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 @@ -5,12 +5,14 @@ import ca.uhn.fhir.parser.IParser; import gov.hhs.cdc.trustedintermediary.wrappers.FhirParseException; import gov.hhs.cdc.trustedintermediary.wrappers.HapiFhir; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthDataExpressionEvaluator; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Base; import org.hl7.fhir.r4.model.BooleanType; /** Concrete implementation that calls the Hapi FHIR library. */ -public class HapiFhirImplementation implements HapiFhir { +public class HapiFhirImplementation implements HapiFhir, HealthDataExpressionEvaluator { private static final HapiFhirImplementation INSTANCE = new HapiFhirImplementation(); private static final FhirContext CONTEXT = FhirContext.forR4(); @@ -68,9 +70,15 @@ public String encodeResourceToJson(Object resource) { * @return True if the expression has at least one match for the given root, else false. */ @Override - public Boolean evaluateCondition(Object resource, String expression) { + public boolean evaluateExpression(String expression, HealthData... data) { + if (data.length != 1) { + throw new IllegalArgumentException( + "Expected one resource, but received: " + data.length); + } + var result = - PATH_ENGINE.evaluateFirst((IBaseResource) resource, expression, BooleanType.class); + PATH_ENGINE.evaluateFirst( + (IBaseResource) data[0].getUnderlyingData(), expression, BooleanType.class); return result.map(BooleanType::booleanValue).orElse(false); } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirResource.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirResource.java similarity index 52% rename from etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirResource.java rename to shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirResource.java index a0f41783e..45fdb874e 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirResource.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiFhirResource.java @@ -1,10 +1,10 @@ package gov.hhs.cdc.trustedintermediary.external.hapi; -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import org.hl7.fhir.instance.model.api.IBaseResource; -/** An implementation of {@link FhirResource} to use as a wrapper around HAPI FHIR IBaseResource */ -public class HapiFhirResource implements FhirResource { +/** An implementation of {@link HealthData} to use as a wrapper around HAPI FHIR IBaseResource */ +public class HapiFhirResource implements HealthData { private final IBaseResource innerResource; @@ -13,7 +13,7 @@ public HapiFhirResource(IBaseResource innerResource) { } @Override - public IBaseResource getUnderlyingResource() { + public IBaseResource getUnderlyingData() { return innerResource; } } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/Rule.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/ruleengine/Rule.java similarity index 73% rename from etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/Rule.java rename to shared/src/main/java/gov/hhs/cdc/trustedintermediary/ruleengine/Rule.java index 58453bb64..cd424527e 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/Rule.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/ruleengine/Rule.java @@ -1,19 +1,22 @@ -package gov.hhs.cdc.trustedintermediary.etor.ruleengine; +package gov.hhs.cdc.trustedintermediary.ruleengine; import gov.hhs.cdc.trustedintermediary.context.ApplicationContext; -import gov.hhs.cdc.trustedintermediary.wrappers.HapiFhir; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthDataExpressionEvaluator; import gov.hhs.cdc.trustedintermediary.wrappers.Logger; import java.util.List; /** - * Represents a rule that can be run on a FHIR resource. Each rule has a name, description, logging - * message, conditions to determine if the rule should run, and actions to run in case the condition - * is met. + * Represents a rule that can be run on HealthData objects. Each rule has a name, description, + * message for logging, conditions to determine if the rule should run, and actions to run in case + * the condition is met. */ public class Rule { protected final Logger logger = ApplicationContext.getImplementation(Logger.class); - protected final HapiFhir fhirEngine = ApplicationContext.getImplementation(HapiFhir.class); + protected final HealthDataExpressionEvaluator evaluator = + ApplicationContext.getImplementation(HealthDataExpressionEvaluator.class); + private String name; private String description; private String message; @@ -59,13 +62,12 @@ public List getRules() { return rules; } - public boolean shouldRun(FhirResource resource) { + public boolean shouldRun(HealthData data) { return conditions.stream() .allMatch( condition -> { try { - return fhirEngine.evaluateCondition( - resource.getUnderlyingResource(), condition); + return evaluator.evaluateExpression(condition, data); } catch (Exception e) { logger.logError( "Rule [" @@ -79,7 +81,7 @@ public boolean shouldRun(FhirResource resource) { }); } - public void runRule(FhirResource resource) { + public void runRule(HealthData... data) { throw new UnsupportedOperationException("This method must be implemented by subclasses."); } } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleEngine.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/ruleengine/RuleEngine.java similarity index 65% rename from etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleEngine.java rename to shared/src/main/java/gov/hhs/cdc/trustedintermediary/ruleengine/RuleEngine.java index 6357598d8..ebfe85224 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleEngine.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/ruleengine/RuleEngine.java @@ -1,4 +1,6 @@ -package gov.hhs.cdc.trustedintermediary.etor.ruleengine; +package gov.hhs.cdc.trustedintermediary.ruleengine; + +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; /** * The RuleEngine interface defines the structure for a rule engine. Each rule engine has methods to @@ -9,5 +11,5 @@ public interface RuleEngine { void ensureRulesLoaded() throws RuleLoaderException; - void runRules(FhirResource resource); + void runRules(HealthData resource); } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleLoader.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/ruleengine/RuleLoader.java similarity index 96% rename from etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleLoader.java rename to shared/src/main/java/gov/hhs/cdc/trustedintermediary/ruleengine/RuleLoader.java index 1fad40553..8d3d002b6 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleLoader.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/ruleengine/RuleLoader.java @@ -1,4 +1,4 @@ -package gov.hhs.cdc.trustedintermediary.etor.ruleengine; +package gov.hhs.cdc.trustedintermediary.ruleengine; import gov.hhs.cdc.trustedintermediary.wrappers.Logger; import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter; @@ -14,7 +14,9 @@ /** Manages the loading of rules from a definitions file. */ public class RuleLoader { + private static final RuleLoader INSTANCE = new RuleLoader(); + @Inject Formatter formatter; @Inject Logger logger; diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleLoaderException.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/ruleengine/RuleLoaderException.java similarity index 79% rename from etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleLoaderException.java rename to shared/src/main/java/gov/hhs/cdc/trustedintermediary/ruleengine/RuleLoaderException.java index c644f17bf..81cba7be4 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleLoaderException.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/ruleengine/RuleLoaderException.java @@ -1,4 +1,4 @@ -package gov.hhs.cdc.trustedintermediary.etor.ruleengine; +package gov.hhs.cdc.trustedintermediary.ruleengine; /** Custom exception class use to catch RuleLoader exceptions */ public class RuleLoaderException extends Exception { diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/HapiFhir.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/HapiFhir.java index bc09b2cbf..374332f19 100644 --- a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/HapiFhir.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/HapiFhir.java @@ -14,7 +14,5 @@ T parseResource(String fhirResource, Class clazz) String encodeResourceToJson(Object resource); - Boolean evaluateCondition(Object resource, String expression); - String getStringFromFhirPath(Object resource, String expression); } diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/HealthData.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/HealthData.java new file mode 100644 index 000000000..5273da407 --- /dev/null +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/HealthData.java @@ -0,0 +1,12 @@ +package gov.hhs.cdc.trustedintermediary.wrappers; + +/** + * Represents a generic health data object. The data object could be a HL7 message or FHIR resource + */ +public interface HealthData { + T getUnderlyingData(); + + default String getName() { + return ""; + } +} diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/HealthDataExpressionEvaluator.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/HealthDataExpressionEvaluator.java new file mode 100644 index 000000000..74f0f4d5f --- /dev/null +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/HealthDataExpressionEvaluator.java @@ -0,0 +1,7 @@ +package gov.hhs.cdc.trustedintermediary.wrappers; + +/** Represents an interface for evaluating expressions on health data objects. */ +public interface HealthDataExpressionEvaluator { + boolean evaluateExpression(String expression, HealthData... data) + throws IllegalArgumentException; +} 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 17951ef7e..b2ddfa177 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 @@ -42,61 +42,69 @@ class HapiFhirImplementationTest extends Specification { bundle.addEntry(entry2) } - def "evaluateCondition returns true on finding existing value"() { + def "evaluateExpression returns true on finding existing value"() { given: def path = "Bundle.id.exists()" when: - def result = fhir.evaluateCondition(bundle as IBaseResource, path) + def result = fhir.evaluateExpression(path, new HapiFhirResource(bundle)) then: - result == true + result } - def "evaluateCondition returns false on not finding non-existing value"() { + def "evaluateExpression returns false on not finding non-existing value"() { given: def path = "Bundle.timestamp.exists()" when: - def result = fhir.evaluateCondition(bundle as IBaseResource, path) + def result = fhir.evaluateExpression(path, new HapiFhirResource(bundle)) then: - result == false + !result } - def "evaluateCondition returns false on not finding matching extension"() { + def "evaluateExpression returns false on not finding matching extension"() { given: def path = "Bundle.entry[0].resource.extension('blah')" when: - def result = fhir.evaluateCondition(bundle as IBaseResource, path) + def result = fhir.evaluateExpression(path, new HapiFhirResource(bundle)) then: - result == false + !result } - def "evaluateCondition throws Exception on empty string"() { + def "evaluateExpression throws Exception on empty string"() { given: def path = "" when: - fhir.evaluateCondition(bundle as IBaseResource, path) + fhir.evaluateExpression(path, new HapiFhirResource(bundle)) then: thrown(Exception) } - def "evaluateCondition throws Exception on fake method"() { + def "evaluateExpression throws Exception on fake method"() { given: def path = "Bundle.entry[0].resource.BadMethod('blah')" when: - fhir.evaluateCondition(bundle as IBaseResource, path) + fhir.evaluateExpression(path, new HapiFhirResource(bundle)) then: thrown(Exception) } + def "evaluateExpression throws IllegalArgumentException when passing more than one HealthData"() { + when: + fhir.evaluateExpression("fhirpath", new HapiFhirResource(bundle), new HapiFhirResource(bundle)) + + then: + thrown(IllegalArgumentException) + } + def "getStringFromFhirPath returns correct string value for existing path"() { given: def path = "Bundle.entry[0].resource.id" diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleLoaderExceptionTest.groovy b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/ruleengine/RuleLoaderExceptionTest.groovy similarity index 92% rename from etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleLoaderExceptionTest.groovy rename to shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/ruleengine/RuleLoaderExceptionTest.groovy index a2ff9b6cc..f187ff311 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleLoaderExceptionTest.groovy +++ b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/ruleengine/RuleLoaderExceptionTest.groovy @@ -1,9 +1,10 @@ -package gov.hhs.cdc.trustedintermediary.etor.ruleengine +package gov.hhs.cdc.trustedintermediary.ruleengine import gov.hhs.cdc.trustedintermediary.wrappers.HttpClientException import spock.lang.Specification class RuleLoaderExceptionTest extends Specification { + def "constructor works"() { given: def message = "rules loaded wrong!" diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleLoaderTest.groovy b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/ruleengine/RuleLoaderTest.groovy similarity index 79% rename from etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleLoaderTest.groovy rename to shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/ruleengine/RuleLoaderTest.groovy index cd66dc08d..d8a16585a 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleLoaderTest.groovy +++ b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/ruleengine/RuleLoaderTest.groovy @@ -1,9 +1,8 @@ -package gov.hhs.cdc.trustedintermediary.etor.ruleengine +package gov.hhs.cdc.trustedintermediary.ruleengine import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext -import gov.hhs.cdc.trustedintermediary.etor.ruleengine.validation.ValidationRule import gov.hhs.cdc.trustedintermediary.external.jackson.Jackson -import gov.hhs.cdc.trustedintermediary.wrappers.HapiFhir +import gov.hhs.cdc.trustedintermediary.wrappers.HealthDataExpressionEvaluator import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter import gov.hhs.cdc.trustedintermediary.wrappers.formatter.TypeReference import spock.lang.Specification @@ -19,9 +18,9 @@ class RuleLoaderTest extends Specification { def setup() { TestApplicationContext.reset() TestApplicationContext.init() + TestApplicationContext.register(HealthDataExpressionEvaluator, Mock(HealthDataExpressionEvaluator)) TestApplicationContext.register(RuleLoader, RuleLoader.getInstance()) TestApplicationContext.register(Formatter, Jackson.getInstance()) - TestApplicationContext.register(HapiFhir, Mock(HapiFhir)) TestApplicationContext.injectRegisteredImplementations() tempFile = Files.createTempFile("test_validation_definition", ".json") @@ -49,11 +48,11 @@ class RuleLoaderTest extends Specification { Files.writeString(tempFile, fileContents) when: - List rules = RuleLoader.getInstance().loadRules(Files.newInputStream(tempFile), new TypeReference>>() {}) + List rules = RuleLoader.getInstance().loadRules(Files.newInputStream(tempFile), new TypeReference>>() {}) then: rules.size() == 1 - ValidationRule rule = rules.get(0) as ValidationRule + Rule rule = rules.get(0) as Rule rule.getName() == "patientName" rule.getDescription() == "a test rule" rule.getMessage() == "testing the message" @@ -68,7 +67,7 @@ class RuleLoaderTest extends Specification { Files.writeString(tempFile, "!K@WJ#8uhy") when: - RuleLoader.getInstance().loadRules(Files.newInputStream(tempFile), new TypeReference>>() {}) + RuleLoader.getInstance().loadRules(Files.newInputStream(tempFile), new TypeReference>>() {}) then: thrown(RuleLoaderException) diff --git a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/ruleengine/RuleTest.groovy b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/ruleengine/RuleTest.groovy new file mode 100644 index 000000000..d032c0afa --- /dev/null +++ b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/ruleengine/RuleTest.groovy @@ -0,0 +1,90 @@ +package gov.hhs.cdc.trustedintermediary.ruleengine + +import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData +import gov.hhs.cdc.trustedintermediary.wrappers.HealthDataExpressionEvaluator +import gov.hhs.cdc.trustedintermediary.wrappers.Logger +import spock.lang.Specification + +class RuleTest extends Specification { + + def mockLogger = Mock(Logger) + + def setup() { + TestApplicationContext.reset() + TestApplicationContext.init() + TestApplicationContext.register(Logger, mockLogger) + TestApplicationContext.register(HealthDataExpressionEvaluator, Mock(HealthDataExpressionEvaluator)) + TestApplicationContext.injectRegisteredImplementations() + } + + def "Rule's properties are set and get correctly"() { + given: + def ruleName = "Rule name" + def ruleDescription = "Rule Description" + def ruleWarningMessage = "Rule Warning Message" + def conditions = ["condition1", "condition2"] + def validations = ["validation1", "validation2"] + + when: + def rule = new Rule(ruleName, ruleDescription, ruleWarningMessage, conditions, validations) + + then: + rule.getName() == ruleName + rule.getDescription() == ruleDescription + rule.getMessage() == ruleWarningMessage + rule.getConditions() == conditions + rule.getRules() == validations + } + + def "shouldRun returns expected boolean depending on conditions"() { + given: + def mockHealthData = Mock(HealthData) + def mockEvaluator = Mock(HealthDataExpressionEvaluator) + mockEvaluator.evaluateExpression(_ as String, mockHealthData) >> true >> conditionResult + TestApplicationContext.register(HealthDataExpressionEvaluator, mockEvaluator) + TestApplicationContext.injectRegisteredImplementations() + + def rule = new Rule(null, null, null, [ + "trueCondition", + "secondCondition" + ], null) + + expect: + rule.shouldRun(mockHealthData) == applies + + where: + conditionResult | applies + true | true + false | false + } + + def "shouldRun logs an error and returns false if an exception happens when evaluating a condition"() { + given: + def mockHealthData = Mock(HealthData) + def mockEvaluator = Mock(HealthDataExpressionEvaluator) + mockEvaluator.evaluateExpression(_ as String, mockHealthData) >> { throw new Exception() } + TestApplicationContext.register(HealthDataExpressionEvaluator, mockEvaluator) + TestApplicationContext.injectRegisteredImplementations() + + def rule = new Rule(null, null, null, ["condition"], null) + + when: + def applies = rule.shouldRun(mockHealthData) + + then: + 1 * mockLogger.logError(_ as String, _ as Exception) + !applies + } + + def "runRule throws an UnsupportedOperationException when ran from the Rule class"() { + given: + def rule = new Rule() + + when: + rule.runRule(Mock(HealthData)) + + then: + thrown(UnsupportedOperationException) + } +} diff --git a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/wrappers/HealthDataTest.groovy b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/wrappers/HealthDataTest.groovy new file mode 100644 index 000000000..7e8da084d --- /dev/null +++ b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/wrappers/HealthDataTest.groovy @@ -0,0 +1,30 @@ +package gov.hhs.cdc.trustedintermediary.wrappers + +import spock.lang.Specification + +class HealthDataTest extends Specification { + def "default getName returns empty string"() { + setup: + def healthData = new IntegerHealthData(1) + + when: + def actual = healthData.getName() + + then: + actual == "" + } +} + +// Simple implementation of HealthData for testing +class IntegerHealthData implements HealthData { + private final Integer data + + IntegerHealthData(Integer data) { + this.data = data + } + + @Override + Integer getUnderlyingData() { + return data + } +}