Skip to content

Commit

Permalink
Feature/issue127 golden rule exception handling (deliveredtechnologie…
Browse files Browse the repository at this point in the history
…s#135)

* added ERROR_ON_FAILURE RuleChainActionType

* updated GoldenRule to throw errors when RuleChainActionType is ERROR_ON_FAILURE

* added RuleChainActionType actionType parameter to Rule annotation for POJO rules

* updated RuleAdapter to allow errors to be thrown on ERROR_ON_FAILURE

* updated corresponding tests

* updated tests for RuleBookRunner

* updated JavaDoc comments for builder that inclued ERROR_ON_FAILURE
  • Loading branch information
Clayton7510 authored May 9, 2018
1 parent 518a4c1 commit c802521
Show file tree
Hide file tree
Showing 25 changed files with 237 additions and 132 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.deliveredtechnologies.rulebook.annotation;

import com.deliveredtechnologies.rulebook.Decision;
import com.deliveredtechnologies.rulebook.model.RuleChainActionType;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static com.deliveredtechnologies.rulebook.model.RuleChainActionType.CONTINUE_ON_FAILURE;

/**
* Rule defines a class to be injected into a {@link Decision}.
Expand All @@ -26,4 +28,10 @@
* Two rules in the same package with the same order will execute in a non-specified order.
*/
int order() default 1;

/**
* This specifies the impact that a Rule failure should have on the rule chain.
*/
RuleChainActionType ruleChainAction() default CONTINUE_ON_FAILURE;
}

Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ public class RuleBuilder<T, U> implements TerminatingRuleBuilder<T, U> {
* Returns a new RuleBuilder for the specified Rule class.
* @param ruleClass the class of Rule to build
* @param actionType if STOP_ON_FAILURE, stops the rule chain if RuleState is BREAK only if the rule fails
* (for supported rule classes); default is CONTINUE_ON_FAILURE
* (for supported rule classes); if ERROR_ON_FAILURE, allows exceptions to be thrown from rule;
* default is CONTINUE_ON_FAILURE
* @return a new RuleBuilder
*/
public static RuleBuilder<Object, Object> create(Class<? extends Rule> ruleClass,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@

import static com.deliveredtechnologies.rulebook.model.RuleChainActionType.CONTINUE_ON_FAILURE;
import static com.deliveredtechnologies.rulebook.model.RuleChainActionType.STOP_ON_FAILURE;
import static com.deliveredtechnologies.rulebook.model.RuleChainActionType.ERROR_ON_FAILURE;


/**
* A standard implementation of {@link Rule}.
Expand Down Expand Up @@ -181,6 +183,9 @@ public boolean invoke(NameValueReferableMap facts) {
}
} catch (IllegalAccessException | InvocationTargetException err) {
LOGGER.error("Error invoking action on " + action.getClass(), err);
if (_actionType.equals(ERROR_ON_FAILURE)) {
throw new RuleException(err);
}
}
});
facts.putAll(usingFacts);
Expand All @@ -190,12 +195,14 @@ public boolean invoke(NameValueReferableMap facts) {
if (_actionType.equals(STOP_ON_FAILURE)) {
this._ruleState = RuleState.NEXT;
}

return true;
}
} catch (Exception ex) {
//catch errors in case the 'when' condition fails; in that case, log the error and just move on
//catch errors from the 'when' condition; if it fails, just continue along unless ERROR_ON_FAILURE is set
LOGGER.error("Error occurred when trying to evaluate rule!", ex);
if (_actionType.equals(ERROR_ON_FAILURE)) {
throw new RuleException(ex);
}
}

return _actionType.equals(STOP_ON_FAILURE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
*/
public enum RuleChainActionType {
STOP_ON_FAILURE,
ERROR_ON_FAILURE,
CONTINUE_ON_FAILURE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.deliveredtechnologies.rulebook.model;

/**
* Runtime exception to wrap Rule exceptions.
* This exception exists for two reasons:
* <ol>
* <li>It denotes that the exception happened in a Rule.</li>
* <li>It allows exceptions to be thrown from a Rule without changing the interface.</li>
* </ol>
*/
public class RuleException extends RuntimeException {

/**
* Create a RuleException with an exception message.
* @param message exception message
*/
public RuleException(String message) {
super(message);
}

/**
* Create a RuleException with an exception message and a cause (the originating Throwable exception).
* @param message exception message
* @param cause the originating Throwable exception
*/
public RuleException(String message, Throwable cause) {
super(message, cause);
}

/**
* Create a RuleException using the originating Throwable exception.
* @param cause the originating Throwable exception
*/
public RuleException(Throwable cause) {
super(cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import com.deliveredtechnologies.rulebook.annotation.When;
import com.deliveredtechnologies.rulebook.model.GoldenRule;
import com.deliveredtechnologies.rulebook.model.Rule;
import com.deliveredtechnologies.rulebook.model.RuleChainActionType;
import com.deliveredtechnologies.rulebook.model.RuleException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -46,6 +48,7 @@ public class RuleAdapter implements Rule {

private Rule _rule;
private Object _pojoRule;
private RuleChainActionType _actionType;

/**
* Adapts a POJO to a Rule given a POJO and a Rule.
Expand All @@ -54,10 +57,15 @@ public class RuleAdapter implements Rule {
* @throws InvalidClassException if the @Rule annotation is missing from the POJO
*/
public RuleAdapter(Object pojoRule, Rule rule) throws InvalidClassException {
if (getAnnotation(com.deliveredtechnologies.rulebook.annotation.Rule.class, pojoRule.getClass()) == null) {
com.deliveredtechnologies.rulebook.annotation.Rule ruleAnnotation =
getAnnotation(com.deliveredtechnologies.rulebook.annotation.Rule.class, pojoRule.getClass());

if (ruleAnnotation == null) {
throw new InvalidClassException(pojoRule.getClass() + " is not a Rule; missing @Rule annotation");
}
_rule = rule;

_actionType = ruleAnnotation.ruleChainAction();
_rule = rule == null ? new GoldenRule(Object.class, _actionType) : rule;
_pojoRule = pojoRule;
}

Expand All @@ -68,7 +76,7 @@ public RuleAdapter(Object pojoRule, Rule rule) throws InvalidClassException {
*/
@SuppressWarnings("unchecked")
public RuleAdapter(Object pojoRule) throws InvalidClassException {
this(pojoRule, new GoldenRule(Object.class));
this(pojoRule, null);
}

@Override
Expand Down Expand Up @@ -139,8 +147,11 @@ public Predicate<NameValueReferableMap> getCondition() {
try {
return (Boolean) method.invoke(_pojoRule);
} catch (InvocationTargetException | IllegalAccessException ex) {
if (_actionType == RuleChainActionType.ERROR_ON_FAILURE) {
throw new RuleException(ex);
}
LOGGER.error(
"Unable to validate condition due to an exception. Condition will be interpreted as false", ex);
"Unable to validate condition due to an exception. It will be evaluated as false", ex);
return false;
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public void setConditionShouldSetTheCondition() {
}

@Test
@SuppressWarnings("unchecked")
public void setRuleStateShouldSetTheRuleState() {
Rule rule = new GoldenRule(Object.class);
rule.setRuleState(RuleState.BREAK);
Expand Down Expand Up @@ -91,7 +92,7 @@ public void addingActionsAddsActionsToTheActionList() {
public void settingTheResultSetsTheResult() {
Rule<String, String> rule = new GoldenRule<>(String.class);
Assert.assertFalse(rule.getResult().isPresent());
rule.setResult(new Result<String>("My Result"));
rule.setResult(new Result<>("My Result"));
Assert.assertEquals("My Result", rule.getResult().get().getValue());
}

Expand Down Expand Up @@ -121,4 +122,19 @@ public void addingDuplicateActionsFindsOnlyOneActionAdded() {
Mockito.verify(biConsumer, Mockito.times(1))
.accept(Mockito.any(NameValueReferableTypeConvertibleMap.class), Mockito.any(Result.class));
}

@Test(expected = RuleException.class)
public void rulesSetToErrorOnFailureThrowExceptionsInWhen() {
Rule<String, String> rule = new GoldenRule<>(String.class, RuleChainActionType.ERROR_ON_FAILURE);
rule.setCondition(facts -> facts.getValue("some fact").equals("nothing"));
rule.invoke(new FactMap<String>());
}

@Test(expected = RuleException.class)
public void rulesToErrorOnFailureThrowExceptionsInActions() {
Rule<String, String> rule = new GoldenRule<>(String.class, RuleChainActionType.ERROR_ON_FAILURE);
rule.setCondition(facts -> true);
rule.addAction(facts -> System.out.println(facts.getValue("some fact").toLowerCase()));
rule.invoke(new FactMap<String>());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,10 @@ public void ruleBookAuditorCanAuditRules() {
@Test
@SuppressWarnings("unchecked")
public void rulesAreStillExecutedWithNullFacts() {
Rule rule = new GoldenRule(Object.class);
BiConsumer<NameValueReferableMap, Result> action = (facts, result)
Rule<Object, Object> rule = new GoldenRule(Object.class);
rule.addAction((facts, result)
-> result.setValue("Rule was triggered with status=" + facts.get("status")
+ " and object=" + facts.get("object"));
rule.addAction(action);
+ " and object=" + facts.get("object")));

AuditableRule auditableRule = new AuditableRule(rule, "SimpleRule");

Expand All @@ -61,11 +60,52 @@ public void rulesAreStillExecutedWithNullFacts() {

RuleBookAuditor auditor = new RuleBookAuditor(ruleBook);
auditor.addRule(auditableRule);
ruleBook.run(facts);
auditor.run(new FactMap());

Assert.assertEquals(RuleStatus.EXECUTED, auditor.getRuleStatus("SimpleRule"));
}

/**
* Test to ensure that rules that error on a failure are shown in a PENDING status.
*/
@Test
@SuppressWarnings("unchecked")
public void errorOnFailureRulesResultInPendingStatus() {
Rule<Object, Object> rule1 = new GoldenRule(Object.class);
rule1.addAction((facts, result)
-> result.setValue("Rule was triggered with status=" + facts.get("status")
+ " and object=" + facts.get("object")));

Rule<Object, Object> rule2 = new GoldenRule(Object.class, RuleChainActionType.ERROR_ON_FAILURE);
rule2.setCondition(stuff -> stuff.getValue("invalid").equals("something"));

AuditableRule auditableRule1 = new AuditableRule(rule1, "SimpleRule");
AuditableRule auditableRule2 = new AuditableRule(rule2, "ErrorRule");

FactMap facts = new FactMap();
facts.setValue("status", 1);
facts.setValue("object", null);

RuleBook ruleBook = new CoRRuleBook();

RuleBookAuditor auditor = new RuleBookAuditor(ruleBook);
auditor.addRule(auditableRule1);
auditor.addRule(auditableRule2);

try {
ruleBook.run(facts);
} catch (Exception e) {
Assert.assertEquals(RuleStatus.EXECUTED, auditor.getRuleStatus("SimpleRule"));
Assert.assertEquals(RuleStatus.PENDING, auditor.getRuleStatus("ErrorRule"));
Assert.assertTrue(e instanceof RuleException);
return;
}
Assert.fail();
}

/**
* Test to confirm audited named rules built by RuleBook's builder.
*/
@Test
@SuppressWarnings("unchecked")
public void namedRulesInRuleBookAuditorBuiltByRuleBookBuilderAreAudited() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
import com.deliveredtechnologies.rulebook.Result;
import com.deliveredtechnologies.rulebook.lang.RuleBookBuilder;
import com.deliveredtechnologies.rulebook.model.RuleBook;
import com.deliveredtechnologies.rulebook.model.RuleException;
import com.deliveredtechnologies.rulebook.model.Rule;
import com.deliveredtechnologies.rulebook.model.GoldenRule;
import com.deliveredtechnologies.rulebook.model.RuleChainActionType;
import net.jodah.concurrentunit.Waiter;
import org.junit.Assert;
import org.junit.Test;
Expand Down Expand Up @@ -107,4 +111,14 @@ public void coRRuleBookDoesntAcceptNullRules() {

Assert.assertFalse(ruleBook.hasRules());
}

@Test(expected = RuleException.class)
public void rulesSetToErrorOnFailureThrowExceptionsInRuleBook() {
Rule<String, String> rule = new GoldenRule<>(String.class, RuleChainActionType.ERROR_ON_FAILURE);
rule.setCondition(facts -> facts.getValue("some fact").equals("nothing"));

RuleBook ruleBook = new CoRRuleBook();
ruleBook.addRule(rule);
ruleBook.run(new FactMap<String>());
}
}

This file was deleted.

This file was deleted.

Loading

0 comments on commit c802521

Please sign in to comment.