Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rules engine refactoring #1037

Merged
merged 36 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f5e94fe
Refactored validation rule engine in preparation for tranformation en…
basiliskus Apr 23, 2024
28505a5
Fixed instantiation
basiliskus Apr 23, 2024
2c3f69d
Merge branch 'main' into story/1024/transformation-engine
basiliskus Apr 24, 2024
bdd568a
Refactored to make Rule interface more generic
basiliskus Apr 25, 2024
f65cede
Fixed tests
basiliskus Apr 25, 2024
0cf5640
Update transformation_definitions.json
luis-pabon-tf Apr 25, 2024
a08c687
Merge branch 'main' into story/1024/transformation-engine
basiliskus Apr 25, 2024
8631334
Added missing override tags
basiliskus Apr 25, 2024
6214e84
Fixed arg naming
basiliskus Apr 25, 2024
4756364
Use generics to deserialize correct type of Rule implementation
basiliskus Apr 25, 2024
1de28ef
Converted RuleEngine back into a singleton and added parameters
basiliskus Apr 26, 2024
5c8073b
Refactored to have a separate implementation for ValidationRuleEngine
basiliskus Apr 26, 2024
45a3bca
Refactored to pass TypeReference and fix mapping issue
basiliskus Apr 26, 2024
d5490e8
Added missing override tags
basiliskus Apr 26, 2024
8e7398c
Fixed tests in ValidationRuleEngineIntegrationTest
basiliskus Apr 26, 2024
dbd8043
Fixed more tests
basiliskus Apr 26, 2024
d399054
Removed transformation engine implementation to move into new PR
basiliskus Apr 26, 2024
76b7d82
Update ValidationRuleTest.groovy
luis-pabon-tf Apr 29, 2024
27ea75d
Update ValidationRuleEngineTest.groovy
luis-pabon-tf Apr 29, 2024
0f1b275
Update ValidationRuleEngine.java
luis-pabon-tf Apr 29, 2024
5acab7a
Update ValidationRuleEngineTest.groovy
luis-pabon-tf Apr 29, 2024
28b0d7a
Update ValidationRuleEngineTest.groovy
luis-pabon-tf Apr 29, 2024
1f57bb1
Update Rule loading to handle Path for flexibility
saquino0827 Apr 29, 2024
5d4dd9e
Merge branch 'story/1024/transformation-engine' of https://github.com…
saquino0827 Apr 29, 2024
34a2bf9
Fixed remaining test
basiliskus Apr 29, 2024
e137c1d
Fixed actual remaining test
basiliskus Apr 29, 2024
0fc78c5
Changed naming
basiliskus Apr 29, 2024
95d4b2d
Update ValidationRuleEngineTest.groovy
luis-pabon-tf Apr 29, 2024
a27a876
Merge branch 'story/1024/transformation-engine' of https://github.com…
luis-pabon-tf Apr 29, 2024
c0f0093
Added test coverage
basiliskus Apr 29, 2024
2a5f7ab
Updated javadocs
basiliskus Apr 29, 2024
4a5068d
Created trasnformation package and moved transformation engine files
basiliskus Apr 29, 2024
2bd21c8
Changed implementation for rules implementation to inherit from Rule …
basiliskus Apr 29, 2024
6ba1943
Clean up
basiliskus Apr 29, 2024
a03dd0b
Added missing override tag
basiliskus Apr 29, 2024
e9290aa
Create RuleTest.groovy
luis-pabon-tf Apr 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
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.RuleEngine;
import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleLoader;
import gov.hhs.cdc.trustedintermediary.etor.ruleengine.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;
Expand Down Expand Up @@ -133,7 +133,7 @@ public Map<HttpEndpoint, Function<DomainRequest, DomainResponse>> domainRegistra
PartnerMetadataConverter.class, HapiPartnerMetadataConverter.getInstance());
// Validation rules
ApplicationContext.register(RuleLoader.class, RuleLoader.getInstance());
ApplicationContext.register(RuleEngine.class, RuleEngine.getInstance());
ApplicationContext.register(ValidationRuleEngine.class, ValidationRuleEngine.getInstance());

ApplicationContext.register(SendMessageHelper.class, SendMessageHelper.getInstance());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import gov.hhs.cdc.trustedintermediary.domainconnector.DomainRequest;
import gov.hhs.cdc.trustedintermediary.etor.metadata.EtorMetadataStep;
import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleEngine;
import gov.hhs.cdc.trustedintermediary.etor.ruleengine.ValidationRuleEngine;
import gov.hhs.cdc.trustedintermediary.external.hapi.HapiFhirResource;
import gov.hhs.cdc.trustedintermediary.external.hapi.HapiOrder;
import gov.hhs.cdc.trustedintermediary.wrappers.FhirParseException;
Expand All @@ -20,7 +20,7 @@ public class OrderController {
@Inject HapiFhir fhir;
@Inject Logger logger;
@Inject MetricMetadata metadata;
@Inject RuleEngine ruleEngine;
@Inject ValidationRuleEngine validationEngine;
jcrichlake marked this conversation as resolved.
Show resolved Hide resolved

private OrderController() {}

Expand All @@ -31,7 +31,7 @@ public static OrderController getInstance() {
public Order<?> parseOrders(DomainRequest request) throws FhirParseException {
logger.logInfo("Parsing orders");
var fhirBundle = fhir.parseResource(request.getBody(), Bundle.class);
ruleEngine.validate(new HapiFhirResource(fhirBundle));
validationEngine.runRules(new HapiFhirResource(fhirBundle));
metadata.put(fhirBundle.getId(), EtorMetadataStep.RECEIVED_FROM_REPORT_STREAM);
return new HapiOrder(fhirBundle);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,9 @@ public interface Rule {

String getDescription();

/**
* Descriptive message when there's a rule violation Note: When implementing this method, make
* sure that no PII or PHI is included in the message!
*/
String getViolationMessage();

List<String> getConditions();

List<String> getValidations();

boolean isValid(FhirResource<?> resource);
boolean shouldRun(FhirResource<?> resource);
luis-pabon-tf marked this conversation as resolved.
Show resolved Hide resolved

boolean appliesTo(FhirResource<?> resource);
void runRule(FhirResource<?> resource);
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,17 @@
package gov.hhs.cdc.trustedintermediary.etor.ruleengine;

import gov.hhs.cdc.trustedintermediary.wrappers.Logger;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;

/** Manages the application of rules loaded from a definitions file using the RuleLoader. */
public class RuleEngine {

private static final RuleEngine INSTANCE = new RuleEngine();

final List<Rule> rules = new ArrayList<>();
RuleLoader ruleLoader;
String ruleDefinitionsFileName;

@Inject Logger logger;
@Inject RuleLoader ruleLoader;

private RuleEngine() {}

public static RuleEngine getInstance() {
return INSTANCE;
RuleEngine(RuleLoader ruleLoader, String ruleDefinitionsFileName) {
this.ruleLoader = ruleLoader;
this.ruleDefinitionsFileName = ruleDefinitionsFileName;
}

public void unloadRules() {
Expand All @@ -32,31 +22,22 @@ public void ensureRulesLoaded() {
if (rules.isEmpty()) {
synchronized (this) {
if (rules.isEmpty()) {
loadRules();
var parsedRules = ruleLoader.loadRules(ruleDefinitionsFileName);
loadRules(parsedRules);
}
}
}
}

private synchronized void loadRules() {
String fileName = "rule_definitions.json";
try (InputStream ruleDefinitionStream =
getClass().getClassLoader().getResourceAsStream(fileName)) {
assert ruleDefinitionStream != null;
var ruleStream =
new String(ruleDefinitionStream.readAllBytes(), StandardCharsets.UTF_8);
rules.addAll(ruleLoader.loadRules(ruleStream));
} catch (IOException | RuleLoaderException e) {
logger.logError("Failed to load rules definitions from: " + fileName, e);
}
private synchronized void loadRules(List<Rule> rules) {
this.rules.addAll(rules);
}

public void validate(FhirResource<?> resource) {
logger.logDebug("Validating FHIR resource");
public void runRules(FhirResource<?> resource) {
Fixed Show fixed Hide fixed
ensureRulesLoaded();
for (Rule rule : rules) {
if (rule.appliesTo(resource) && !rule.isValid(resource)) {
logger.logWarning("Rule violation: " + rule.getViolationMessage());
if (rule.shouldRun(resource)) {
rule.runRule(resource);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package gov.hhs.cdc.trustedintermediary.etor.ruleengine;

import gov.hhs.cdc.trustedintermediary.wrappers.Logger;
import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter;
import gov.hhs.cdc.trustedintermediary.wrappers.formatter.FormatterProcessingException;
import gov.hhs.cdc.trustedintermediary.wrappers.formatter.TypeReference;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
Expand All @@ -12,21 +16,26 @@
public class RuleLoader {
private static final RuleLoader INSTANCE = new RuleLoader();
@Inject Formatter formatter;
@Inject Logger logger;

private RuleLoader() {}

public static RuleLoader getInstance() {
return INSTANCE;
}

public List<ValidationRule> loadRules(String ruleStream) throws RuleLoaderException {
try {
Map<String, List<ValidationRule>> jsonObj =
formatter.convertJsonToObject(ruleStream, new TypeReference<>() {});
public List<Rule> loadRules(String fileName) {
try (InputStream ruleDefinitionStream =
getClass().getClassLoader().getResourceAsStream(fileName)) {
assert ruleDefinitionStream != null;
var rulesString =
new String(ruleDefinitionStream.readAllBytes(), StandardCharsets.UTF_8);
Map<String, List<Rule>> jsonObj =
formatter.convertJsonToObject(rulesString, new TypeReference<>() {});
return jsonObj.getOrDefault("rules", Collections.emptyList());
} catch (FormatterProcessingException e) {
throw new RuleLoaderException(
"Failed to load rules definitions for provided stream", e);
} catch (IOException | FormatterProcessingException e) {
logger.logError("Failed to load rules definitions from: " + fileName, e);
return Collections.emptyList();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package gov.hhs.cdc.trustedintermediary.etor.ruleengine;

import java.util.List;

public class TransformationRule implements Rule {
@Override
public String getName() {
return "";
}

@Override
public String getDescription() {
return "";
}

@Override
public List<String> getConditions() {
return List.of();
}

@Override
public boolean shouldRun(FhirResource<?> resource) {
return false;
}

@Override
public void runRule(FhirResource<?> resource) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ public String getDescription() {
return description;
}

@Override
public String getViolationMessage() {
return violationMessage;
}
Expand All @@ -59,34 +58,12 @@ public List<String> getConditions() {
return conditions;
}

@Override
public List<String> getValidations() {
return validations;
}

@Override
public boolean isValid(FhirResource<?> resource) {
return validations.stream()
.allMatch(
validation -> {
try {
return fhirEngine.evaluateCondition(
resource.getUnderlyingResource(), validation);
} catch (Exception e) {
logger.logError(
"Rule ["
+ name
+ "]: "
+ "An error occurred while evaluating the validation: "
+ validation,
e);
return false;
}
});
}

@Override
public boolean appliesTo(FhirResource<?> resource) {
public boolean shouldRun(FhirResource<?> resource) {
return conditions.stream()
.allMatch(
condition -> {
Expand All @@ -105,4 +82,25 @@ public boolean appliesTo(FhirResource<?> resource) {
}
});
}

@Override
public void runRule(FhirResource<?> resource) {
for (String validation : validations) {
try {
boolean isValid =
fhirEngine.evaluateCondition(resource.getUnderlyingResource(), validation);
if (!isValid) {
logger.logWarning("Rule violation: " + violationMessage);
}
} catch (Exception e) {
logger.logError(
"Rule ["
+ name
+ "]: "
+ "An error occurred while evaluating the validation: "
+ validation,
e);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package gov.hhs.cdc.trustedintermediary.etor.ruleengine;

import gov.hhs.cdc.trustedintermediary.wrappers.Logger;
import javax.inject.Inject;

public class ValidationRuleEngine extends RuleEngine {
@Inject Logger logger;

private static final ValidationRuleEngine INSTANCE = new ValidationRuleEngine();

private ValidationRuleEngine() {
super(RuleLoader.getInstance(), "validation_definitions.json");
}

public static ValidationRuleEngine getInstance() {
return INSTANCE;
}
}
9 changes: 9 additions & 0 deletions etor/src/main/resources/transformation_definitions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"rules": [ {
"name": "",
"description": "",
"conditions": [ ],
"transformations": [ ]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class OrderControllerTest extends Specification {

then:
actualBundle == expectedBundle
(1.._) * ruleEngine.validate(_)
(1.._) * ruleEngine.runRules(_)
}

def "parseOrders registers a metadata step"() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class RuleEngineIntegrationTest extends Specification {
def bundle = new Bundle()

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

then:
(1.._) * mockLogger.logWarning(_ as String)
Expand All @@ -53,7 +53,7 @@ class RuleEngineIntegrationTest extends Specification {

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

then:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ class RuleEngineTest extends Specification {
mockRuleLoader.loadRules(_ as String) >> { throw exception }

when:
ruleEngine.validate(Mock(FhirResource))
ruleEngine.runRules(Mock(FhirResource))

then:
1 * mockLogger.logError(_ as String, exception)
}

def "validate handles logging warning correctly"() {
def 'runRules handles logging warning correctly'() {
given:
def ruleViolationMessage = "Rule violation message"
def fullRuleViolationMessage = "Rule violation: " + ruleViolationMessage
Expand All @@ -82,25 +82,25 @@ class RuleEngineTest extends Specification {
mockRuleLoader.loadRules(_ as String) >> [invalidRule]

when:
invalidRule.appliesTo(fhirBundle) >> true
invalidRule.shouldRun(fhirBundle) >> true
invalidRule.isValid(fhirBundle) >> false
ruleEngine.validate(fhirBundle)
ruleEngine.runRules(fhirBundle)

then:
1 * mockLogger.logWarning(fullRuleViolationMessage)

when:
invalidRule.appliesTo(fhirBundle) >> true
invalidRule.shouldRun(fhirBundle) >> true
invalidRule.isValid(fhirBundle) >> true
ruleEngine.validate(fhirBundle)
ruleEngine.runRules(fhirBundle)

then:
0 * mockLogger.logWarning(fullRuleViolationMessage)

when:
invalidRule.appliesTo(fhirBundle) >> false
invalidRule.shouldRun(fhirBundle) >> false
invalidRule.isValid(fhirBundle) >> false
ruleEngine.validate(fhirBundle)
ruleEngine.runRules(fhirBundle)

then:
0 * mockLogger.logWarning(fullRuleViolationMessage)
Expand Down
Loading
Loading