regola

Maven CentralCI

regola is a rule evaluator written in Java.

Disclaimer: This library is in development mode and there could be breaking changes as new versions are released.

Goals

  • be fast
  • be reusable and extensible
  • be well documented
  • have high test coverage
  • allow for efficient evaluation against data retrieved from external data sources
  • have Json rules conversion builtin in the library
  • run on Java 11+

Basic usage

  1. Add to your maven dependencies:
<dependency>
<groupId>com.adobe.abp</groupId>
<artifactId>regola</artifactId>
<version>0.0.24</version>
</dependency>
  1. Write a rule
varrule = newStringRule();
rule.setKey("MARKET_SEGMENT");
rule.setOperator(Operator.EQUALS);
rule.setValue("COM");
rule.setDescription("The market segment should be COM");

You could also use fluent setters:

varrule = newStringRule()
.setValue("COM")
.setOperator(Operator.EQUALS)
.setKey("MARKET_SEGMENT")
.setDescription("The market segment should be COM");

For some rules, you could also pass some parameters directly via the constructor for conciseness:

varrule = newStringRule("MARKET_SEGMENT", Operator.EQUALS, "COM");
rule.setDescription("The market segment should be COM");
  1. Define how data for the "MARKET_SEGMENT" key must be retrieved
varfactsResolver = newSimpleFactsResolver<>();
factsResolver.addFact(newFact<>("MARKET_SEGMENT", data -> "COM"));
  1. Evaluate
varevaluationResult = newEvaluator().evaluate(rule, factsResolver);
// The evaluation is an asynchronous process, so the associated CompletableFuture must be executed to get a result.// The following line returns the result value when complete, or throws an (unchecked) exception if completed exceptionally.evaluationResult.status().join(); varresult = evaluationResult.snapshot();

The result object will contain information on whether the evaluation was valid or not, plus any relevant information about the rule run.

  1. If we were to print the result as json
{"result" : "VALID","type" : "STRING","operator" : "EQUALS","key" : "MARKET_SEGMENT","description": "The market segment should be COM","expectedValue" : "COM","actualValue": "COM"}

Rules Vocabulary

Boolean Rules

Boolean rules are used to combine rules together.

And Rule

The "And Rule" is used to combine multiple rules together, where all the rules must evaluate to VALID for it to evaluate to VALID.

{"type" : "AND","rules" : [// list of other rules]}
ABA && B
VALIDVALIDVALID
VALIDINVALIDINVALID
VALIDMAYBEMAYBE
VALIDFAILEDFAILED
VALIDOPERATION_NOT_SUPPORTEDOPERATION_NOT_SUPPORTED

The AND rule is commutative: A && B = B && A.

An empty AND evaluates to VALID. This follows the identity of conjunction: with no subrules, there is nothing to invalidate the expression.

Or Rule

The "Or Rule" is used to combine multiple rules together, where at least one rule must evaluate to VALID for it to evaluate to VALID.

{"type" : "OR","rules" : [// list of other rules]}
ABA || B
VALIDanyVALID
INVALIDINVALIDINVALID

The order of precedence for non-VALID results is: FAILED, OPERATION_NOT_SUPPORTED, INVALID, MAYBE. So, for example: FAILED || INVALID == FAILED, while MAYBE || INVALID == INVALID and so on.

The OR rule is commutative: A || B = B || A.

An empty OR evaluates to INVALID. This follows the identity of disjunction: with no subrules, there is nothing that can make the expression valid.

Not Rule

The "Not Rule" is used to negate the result of another rule.

{"type" : "NOT","rule" : {// rule to negate}}
A!A
VALIDINVALID
INVALIDVALID
MAYBEMAYBE
FAILEDFAILED
OPERATION_NOT_SUPPORTEDOPERATION_NOT_SUPPORTED

A NOT rule must contain a non-null rule. If the operand is missing, the evaluation is treated as malformed configuration and fails.

Fact-only Rules

Exists Rule

The "Exists Rule" is used to check whether a fact exists or not.

{"type": "EXISTS","key": "foo"}
Some examples
KeyFactResult
"foo"{ "foo": "bar" }VALID
"foo"{ "foo": null }INVALID
"foo"{ "not_foo": "bar" }INVALID

Fixed Rules

CONSTANT Rule

The "Constant Rule" is used to always return the same result, regardless of the fact.

{"type": "CONSTANT","result": "VALID"// INVALID, MAYBE, FAILED, OPERATION_NOT_SUPPORTED}

Value-based Rules

These rules evaluate facts against a value set in the rule. When creating a value-based rule, you must also set an operator (e.g., EQUALS, GREATER_THAN, IN, etc...).

The relationship between facts, values and operators is: fact OPERATOR value.

So, for example a rule having value Cat, operator EQUALS, and evaluated against the fact Dog reads as: Dog EQUALS Cat (false). A rule having value Cat, operator CONTAINS, and evaluated against the fact [Dog, Bird, Cat] reads as: [Dog, Bird, Cat] CONTAINS Cat (true).

Number Rule

The "Number Rule" is used to evaluate facts against a number. The number can be an integer or a double.

{"type": "NUMBER","operator": "GREATER_THAN","key": "foo","value": 7// you can also have 7.0}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS, DIVISIBLE_BY

Integer-Double comparisons between the rule's value and the data provided by the fact work for all operators except CONTAINS.

The DIVISIBLE_BY operator is valid only for integer values since divisibility is not well-defined for floating-point numbers.

Some examples
Rule ValueOperatorFactResult
7EQUALS7VALID
7GREATER_THAN7INVALID
7GREATER_THAN8VALID
7GREATER_THAN_EQUAL7VALID
7GREATER_THAN_EQUAL7.5VALID
7.4GREATER_THAN7.5VALID
7.5GREATER_THAN7.5INVALID
7CONTAINS[ 6, 7, 8]VALID
7CONTAINS[ 6, 8]INVALID
7CONTAINS[ 6, 7.0, 8]INVALID
7.0CONTAINS[ 6, 7.0, 8]VALID
7DIVISIBLE_BY7VALID
7DIVISIBLE_BY8INVALID
0DIVISIBLE_BY8FAILED
7DIVISIBLE_BY0VALID
any numbersupported operatornullINVALID
nullsupported operatorany numberINVALID

When using the CONTAINS operator, the Fact must be a Set of numbers.

String Rule

{"type": "STRING","operator": "EQUALS","key": "foo","value": "bar"}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS

Comparisons are case-sensitive.

Some examples
Rule ValueOperatorFactResult
"bar"EQUALS"bar"VALID
"bar"EQUALS"BAR"INVALID
"bar"EQUALS"baz"INVALID
"bar"GREATER_THAN"car"VALID
"bar"GREATER_THAN_EQUAL"bar"VALID
"bar"GREATER_THAN"are"INVALID
"bar"STARTS_WITH"bar"VALID
"bar"STARTS_WITH"barfoo"VALID
"bar"STARTS_WITH"foobar"INVALID
"bar"ENDS_WITH"bar"VALID
"bar"ENDS_WITH"foobar"VALID
"bar"ENDS_WITH"barfoo"INVALID
"bar"CONTAINS["are", "bar", "baz"]VALID
"bar"CONTAINS["are", "baz"]INVALID
any stringsupported operatornullINVALID
nullsupported operatorany stringINVALID

Set Rule

A "Set rule" is a rule that evaluates a fact against a set of values. The set rule has two operators: IN and INTERSECTS.

IN: evaluates to VALID if the fact is a subset of the rule's value. INTERSECTS: evaluates to VALID if the fact and the rule's value have at least one item in common.

{"type": "SET","operator": "IN","key": "foo","values": ["bar","baz"]}

Supported operators: IN, INTERSECTS

String comparisons are case-sensitive.

Some examples
Rule ValueOperatorFactResult
["bar", "baz"]IN"bar"VALID
["bar", "baz"]IN"waz"INVALID
[1, 2, 3]IN2VALID
[1, 2, 3]IN4INVALID
["bar", "baz"]IN[]INVALID
[]IN[]VALID
[]IN["bar"]INVALID
["bar", "baz"]IN["bar"]VALID
["bar", "baz"]IN["waz"]INVALID
["bar", "baz"]IN["bar", "waz"]INVALID
["bar", "baz"]INTERSECTS"bar"VALID
["bar", "baz"]INTERSECTS"waz"INVALID
["bar", "baz"]INTERSECTS["bar", "waz"]VALID
["bar", "baz"]INTERSECTS["wiz", "waz"]INVALID
["bar", "baz"]INTERSECTS[]INVALID
[]INTERSECTS[]INVALID
any setany operatornullINVALID
Comparing complex objects

The Set rule can be used on more complex objects too.

SetRule<Patient> setRule = newSetRule<>();
setRule.setKey("PATIENT");
setRule.setOperator(Operator.IN);
setRule.setValues(bob, alice);

Where bob and alice are instances of Patient. You must also make sure that the Patient class overrides the equals and hashCode methods. Regola expects the equals method to perform a commutative comparison between objects.

Also note that regola does not support JSON serialization/deserialization for SET rules with complex objects.

Date Rule

The "Date Rule" is a rule that evaluates a fact against a date value.

{"type" : "DATE","operator" : "GREATER_THAN","key" : "foo","value" : "2021-07-07T12:30:00Z"}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS

Dates must be parsable to an OffsetDateTime:

A date-time with an offset from UTC/Greenwich in the ISO-8601 calendar system

Some examples
Rule ValueOperatorFactResult
"2021-07-07T12:30:00Z"EQUALS"2021-07-07T12:30:00Z"VALID
"2021-07-07T12:30:00Z"GREATER_THAN"2022-07-07T12:30:00Z"VALID
"2021-07-07T12:30:00Z"GREATER_THAN"2020-07-07T12:30:00Z"INVALID
"2021-07-07T12:30:00Z"LESS_THAN"2020-07-07T12:30:00Z"VALID
any datesupported operatornullINVALID
nullsupported operatorany dateINVALID

Null Rule

A "Null Rule" is a rule that evaluates a fact against a null value.

{"type": "NULL","key": "foo"}
Some examples
KeyFactResult
"foo"nullVALID
"foo""foo"INVALID

Combining Rules

Rules can be combined using the boolean rules: AND, OR, NOT.

{"type" : "AND","rules" : [{"type" : "STRING","operator" : "EQUALS","key" : "foo","value" : "bar"},{"type" : "OR","rules" : [{"type" : "EXISTS","key" : "waz"},{"type" : "NUMBER","operator" : "EQUALS","key" : "foobar","value" : 21}]}]}

Ignoring results

Rules can be set to be ignored, so that the evaluation of AND/OR/NOT rules does not take them into account.

This matters for empty or effectively-empty boolean rules too:

  • AND with no subrules evaluates to VALID.
  • OR with no subrules evaluates to INVALID.
  • OR where every subrule is present but marked as ignored evaluates to VALID, because all configured subrules are excluded from the final decision.
  • NOT with an ignored subrule evaluates to VALID.
  • NOT with no subrule fails.

Example of a rule marked as ignored:

{"type" : "STRING","operator" : "EQUALS","key" : "foo","value" : "bar","ignored" : true}

This is useful when you want to run a rule but not have it affect the evaluation of the tree.

For example, the following tree evaluated to VALID even thought one of the subrules of AND was INVALID:

{"result": "VALID","type": "AND","description": "Example of a Composite Rule with subrules ignored",// Optional, can be added to any rule in the tree"ignored": false,"rules": [{"result": "INVALID","type": "STRING","ignored": true,"operator": "EQUALS","key": "foo","expectedValue": "bar","actualValue": "not_bar"},{"result": "VALID","type": "EXISTS","ignored": false,"key": "foo","expectedValue": "<any>","actualValue": "not_bar"}]}

Json Deserialization with Jackson

You can use Jackson to deserialize rules from regola. These are the dependencies you will need:

<!-- pom.xml -->
<properties>
<jackson.version>2.13.1</jackson.version>
</properties>
<depdendencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
</depdendencies>

And this is the minimum setup for your ObjectMapper.

ObjectMappermapper = newObjectMapper()
.registerModule(newJavaTimeModule())
.registerModule(newRuleModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

Then you can deserialize a rule as such:

StringjsonRule = readRuleFromDataSource(); // This method will vary depending on your applicationRulerule = mapper.readValue(jsonRule, Rule.class);

New rules

  1. Create a new Rule by extending Rule (or one of the provided abstract classes)
publicclassYourRuleextendsRule {
publicYourRule() {
super("YOUR_TYPE"); // You should make sure this does not conflict with the type of any existing rule
}
@OverridepublicEvaluationResultevaluate(FactsResolverfactsResolver) {
returnnewEvaluationResult() {
privateResultresult = Result.MAYBE;
@OverridepublicRuleResultsnapshot() {
// Build and return a RuleResult
}
@OverridepublicCompletableFuture<Result> status() {
returnfacts.resolveFact(getKey())
.thenCompose(fact -> CompletableFuture.supplyAsync(() -> {
result = ... // perform the relevant checks for this rule and update the resultreturnresult;
}))
.exceptionally(throwable -> {
result = Result.FAILED;
returnresult;
});
}
};
}
}
  1. (Optional) If you need to parse rules from Json, then you must extend the RuleModule as such:
mapper.registerModule(newRuleModule()
.addRule("YOUR_TYPE", YourRule.class));
  1. Start using your new rule!

Programmatic Rule Creation

While transforming rules from Json is convenient, sometimes you may want to create rules programmatically.

Here is an example of how to do that:

SetRule<String> stringSetRule = newSetRule<>();
stringSetRule.setKey("MARKET_SEGMENT");
stringSetRule.setOperator(Operator.IN);
stringSetRule.setValues(Set.of("COM", "EDU"));
NumberRule<Integer> numberRule = newNumberRule<>();
numberRule.setKey("capacity");
numberRule.setOperator(Operator.EQUALS);
numberRule.setValue(3);
OrRuleorRule = newOrRule();
orRule.setRules(List.of(numberRule, stringSetRule));

Facts

Now that we have got some rules, we want to do something with it.

We do that by creating facts and supplying those to the evaluator which will check whether they satisfy our rule or not.

An example of a fact for the foo data point is:

varfact = newFact<>("foo", data -> "bar");

In regola, we can also write facts that use custom data fetchers to retrieve additional data:

Fact<Offer> fact = newFact<>("segment", CustomDataSources.OFFER, Offer::getSegment);

The FactsResolver

Defining facts, if using a custom data source, is not enough. We must tell our evaluator how to get those facts when a rule is run. This is done using a FactsResolver as shown below:

Map<DataSource, DataFetcher<?, YourContext>> dataFetchers = Map.of(
CustomDataSource.OFFER, offerDataFetcher,
...
);
FactsResolverfactsResolver = newSimpleFactsResolver<>(yourContext, dataFetchers);
factsResolver.addFact(newFact<>("segment", CustomDataSources.OFFER, Offer::getSegment));

Writing a Data Fetcher

Example of a data fetcher getting data over HTTP:

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
privatefinalOfferHttpConnectorofferHttpConnector;
publicOfferDataFetcher(OfferHttpConnectorofferHttpConnector) {
this.offerHttpConnector = offerHttpConnector;
}
@OverridepublicCompletableFuture<FetchResponse<Offer>> fetchResponse(YourContextcontext) {
GetOffersRequestrequest = buildRequest(context);
returnCompletableFuture.supplyAsync(() -> {
finalvarresponse = newFetchResponse<>();
response.setData(offerHttpConnector.getOffers(request)
.stream()
.findFirst());
returnresponse;
});
}
// Do not override this method if you do not want to cache results from this data fetcher.@OverridepublicStringcalculateRequestKey(YourContextcontext) {
GetOffersRequestrequest = buildRequest(context);
returnrequest.URL().toString();
}
privateRequestbuildRequest(YourContextcontext) {
returnnewGetOffersRequest(Set.of(requestContext.getOfferId()), Set.of());
}
}
publicclassYourContextimplementsContext {
privateStringofferId;
publicStringgetOfferId() {
returnofferId;
}
publicvoidsetOfferId(StringofferId) {
this.offerId = offerId;
}
}

The initialization of a data fetcher can be expensive, depending on your implementation, so it is recommended that data fetchers are re-used across multiple evaluations.

Caching in the Data Fetcher

The abstract DataFetcher uses caffeine to cache the results of the fetch results.

The default cache is setup with an expiry policy of 1 minute and max size of 1_000 entries, but a custom configuration can be setup by passing a DataFetcherConfiguration to the DataFetcher's constructor.

Custom Cache

If the default caffeine-based cache does not satisfy your requirements, you can provide your own implementation.

First, create a class for your custom cache:

classYourCustomCache<V> implementsDataFetcherCache<V> {
publicYourCustomCache(DataCacheConfigurationconfiguration) {
// (optional) construct your cache using the given configuration
}
@OverridepublicCompletableFuture<V> get(Stringkey, Function<String, CompletableFuture<V>> mappingFunction) {
// implement your "if cached, return; otherwise create, cache and return" cache function here
}
}

Then pass an instance of your custom cache to your data fetchers:

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
privatefinalOfferHttpConnectorofferHttpConnector;
publicOfferDataFetcher(OfferHttpConnectorofferHttpConnector, YourCustomCache<Offer> cache) {
super(cache);
this.offerHttpConnector = offerHttpConnector;
}
// rest of this data fetcher implementation
}

SLA handling within the Data Fetcher

By default, your custom data fetcher will not have any SLA failure handling. However, if needed you can specify an SLA on the fetch requestTime and override the whenFailingSlaFetchTime to handle any SLA failures.

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
publicOfferDataFetcher(/* other params */, longslaFetchTime) {
super(newDataFetcherConfiguration().setSlaFetchTime(slaFetchTime));
// any other initialization
}
@OverridepublicvoidwhenFailingSlaFetchTime(StringrequestKey, longslaFetchTime, doublerequestTime) {
// This method gets called whenever "requestTime > slaFetchTime"
}
}

Actions

Actions are used to define operations we want to perform after a rule is evaluated.

Basic usage

varaction = newAction()
.setDescription("Print 'Hello' if VALID")
.setOnCompletion((result, throwable, ruleResult) -> {
if (result == Result.VALID) {
System.out.println("Hello");
}
});
rule.setAction(action);

In this particular example, this action will be executed when the rule is evaluated and the result is VALID.

Chaining

It is possible to chain actions using the andThen method on the TriConsumer:

TriConsumer<Result, Throwable, RuleResult> actionConsumer = (result, throwable, ruleResult) -> {
if (result == Result.VALID) {
System.out.println("Hello");
}
};
// Chain the action to always print "World"actionConsumer = actionConsumer.andThen((result, throwable, ruleResult) -> System.out.println("World"));
varaction = newAction()
.setDescription("Print 'Hello' if VALID and the word 'World' irrespective of result")
.setOnCompletion(actionConsumer);
rule.setAction(action);

Understanding the Results of a Rule Evaluation

The following is an example of a result (pretty printed in json) returned upon evaluating a tree of rules:

{"result": "VALID","type": "AND","description": "Example of a Composite Rule",// Optional, can be added to any rule in the tree"ignored": false,"rules": [{"result": "VALID","type": "STRING","ignored": false,"operator": "EQUALS","key": "foo","expectedValue": "bar","actualValue": "bar"},{"result": "VALID","type": "OR","ignored": false,"rules": [{"result": "VALID","type": "EXISTS","ignored": false,"key": "waz","expectedValue": "<any>",// <any> is a special keyword matching any actual value for the EXISTS rule"actualValue": "wazab"},{"result": "MAYBE","type": "NUMBER","ignored": false,"operator": "EQUALS","key": "foobar","expectedValue": 21,// no actual value for MAYBE, since the rule was not evaluated due to short-circuiting}]}]}

The top-level "result" is the overall result of the rule.

  • If VALID, the subresults must be all VALID or MAYBE (i.e., rule did not need to be evaluated due to short-circuiting).
  • If not VALID, one or more of the subresults are: INVALID, OPERATION_NOT_SUPPORTED or FAILED.

Oh, by the way, what does "regola" mean?

regola is the italian word for rule.

About

A rule evaluator in Java

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

8 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

regola

Maven CentralCI

regola is a rule evaluator written in Java.

Disclaimer: This library is in development mode and there could be breaking changes as new versions are released.

Goals

  • be fast
  • be reusable and extensible
  • be well documented
  • have high test coverage
  • allow for efficient evaluation against data retrieved from external data sources
  • have Json rules conversion builtin in the library
  • run on Java 11+

Basic usage

  1. Add to your maven dependencies:
<dependency>
<groupId>com.adobe.abp</groupId>
<artifactId>regola</artifactId>
<version>0.0.24</version>
</dependency>
  1. Write a rule
varrule = newStringRule();
rule.setKey("MARKET_SEGMENT");
rule.setOperator(Operator.EQUALS);
rule.setValue("COM");
rule.setDescription("The market segment should be COM");

You could also use fluent setters:

varrule = newStringRule()
.setValue("COM")
.setOperator(Operator.EQUALS)
.setKey("MARKET_SEGMENT")
.setDescription("The market segment should be COM");

For some rules, you could also pass some parameters directly via the constructor for conciseness:

varrule = newStringRule("MARKET_SEGMENT", Operator.EQUALS, "COM");
rule.setDescription("The market segment should be COM");
  1. Define how data for the "MARKET_SEGMENT" key must be retrieved
varfactsResolver = newSimpleFactsResolver<>();
factsResolver.addFact(newFact<>("MARKET_SEGMENT", data -> "COM"));
  1. Evaluate
varevaluationResult = newEvaluator().evaluate(rule, factsResolver);
// The evaluation is an asynchronous process, so the associated CompletableFuture must be executed to get a result.// The following line returns the result value when complete, or throws an (unchecked) exception if completed exceptionally.evaluationResult.status().join(); varresult = evaluationResult.snapshot();

The result object will contain information on whether the evaluation was valid or not, plus any relevant information about the rule run.

  1. If we were to print the result as json
{"result" : "VALID","type" : "STRING","operator" : "EQUALS","key" : "MARKET_SEGMENT","description": "The market segment should be COM","expectedValue" : "COM","actualValue": "COM"}

Rules Vocabulary

Boolean Rules

Boolean rules are used to combine rules together.

And Rule

The "And Rule" is used to combine multiple rules together, where all the rules must evaluate to VALID for it to evaluate to VALID.

{"type" : "AND","rules" : [// list of other rules]}
ABA && B
VALIDVALIDVALID
VALIDINVALIDINVALID
VALIDMAYBEMAYBE
VALIDFAILEDFAILED
VALIDOPERATION_NOT_SUPPORTEDOPERATION_NOT_SUPPORTED

The AND rule is commutative: A && B = B && A.

An empty AND evaluates to VALID. This follows the identity of conjunction: with no subrules, there is nothing to invalidate the expression.

Or Rule

The "Or Rule" is used to combine multiple rules together, where at least one rule must evaluate to VALID for it to evaluate to VALID.

{"type" : "OR","rules" : [// list of other rules]}
ABA || B
VALIDanyVALID
INVALIDINVALIDINVALID

The order of precedence for non-VALID results is: FAILED, OPERATION_NOT_SUPPORTED, INVALID, MAYBE. So, for example: FAILED || INVALID == FAILED, while MAYBE || INVALID == INVALID and so on.

The OR rule is commutative: A || B = B || A.

An empty OR evaluates to INVALID. This follows the identity of disjunction: with no subrules, there is nothing that can make the expression valid.

Not Rule

The "Not Rule" is used to negate the result of another rule.

{"type" : "NOT","rule" : {// rule to negate}}
A!A
VALIDINVALID
INVALIDVALID
MAYBEMAYBE
FAILEDFAILED
OPERATION_NOT_SUPPORTEDOPERATION_NOT_SUPPORTED

A NOT rule must contain a non-null rule. If the operand is missing, the evaluation is treated as malformed configuration and fails.

Fact-only Rules

Exists Rule

The "Exists Rule" is used to check whether a fact exists or not.

{"type": "EXISTS","key": "foo"}
Some examples
KeyFactResult
"foo"{ "foo": "bar" }VALID
"foo"{ "foo": null }INVALID
"foo"{ "not_foo": "bar" }INVALID

Fixed Rules

CONSTANT Rule

The "Constant Rule" is used to always return the same result, regardless of the fact.

{"type": "CONSTANT","result": "VALID"// INVALID, MAYBE, FAILED, OPERATION_NOT_SUPPORTED}

Value-based Rules

These rules evaluate facts against a value set in the rule. When creating a value-based rule, you must also set an operator (e.g., EQUALS, GREATER_THAN, IN, etc...).

The relationship between facts, values and operators is: fact OPERATOR value.

So, for example a rule having value Cat, operator EQUALS, and evaluated against the fact Dog reads as: Dog EQUALS Cat (false). A rule having value Cat, operator CONTAINS, and evaluated against the fact [Dog, Bird, Cat] reads as: [Dog, Bird, Cat] CONTAINS Cat (true).

Number Rule

The "Number Rule" is used to evaluate facts against a number. The number can be an integer or a double.

{"type": "NUMBER","operator": "GREATER_THAN","key": "foo","value": 7// you can also have 7.0}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS, DIVISIBLE_BY

Integer-Double comparisons between the rule's value and the data provided by the fact work for all operators except CONTAINS.

The DIVISIBLE_BY operator is valid only for integer values since divisibility is not well-defined for floating-point numbers.

Some examples
Rule ValueOperatorFactResult
7EQUALS7VALID
7GREATER_THAN7INVALID
7GREATER_THAN8VALID
7GREATER_THAN_EQUAL7VALID
7GREATER_THAN_EQUAL7.5VALID
7.4GREATER_THAN7.5VALID
7.5GREATER_THAN7.5INVALID
7CONTAINS[ 6, 7, 8]VALID
7CONTAINS[ 6, 8]INVALID
7CONTAINS[ 6, 7.0, 8]INVALID
7.0CONTAINS[ 6, 7.0, 8]VALID
7DIVISIBLE_BY7VALID
7DIVISIBLE_BY8INVALID
0DIVISIBLE_BY8FAILED
7DIVISIBLE_BY0VALID
any numbersupported operatornullINVALID
nullsupported operatorany numberINVALID

When using the CONTAINS operator, the Fact must be a Set of numbers.

String Rule

{"type": "STRING","operator": "EQUALS","key": "foo","value": "bar"}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS

Comparisons are case-sensitive.

Some examples
Rule ValueOperatorFactResult
"bar"EQUALS"bar"VALID
"bar"EQUALS"BAR"INVALID
"bar"EQUALS"baz"INVALID
"bar"GREATER_THAN"car"VALID
"bar"GREATER_THAN_EQUAL"bar"VALID
"bar"GREATER_THAN"are"INVALID
"bar"STARTS_WITH"bar"VALID
"bar"STARTS_WITH"barfoo"VALID
"bar"STARTS_WITH"foobar"INVALID
"bar"ENDS_WITH"bar"VALID
"bar"ENDS_WITH"foobar"VALID
"bar"ENDS_WITH"barfoo"INVALID
"bar"CONTAINS["are", "bar", "baz"]VALID
"bar"CONTAINS["are", "baz"]INVALID
any stringsupported operatornullINVALID
nullsupported operatorany stringINVALID

Set Rule

A "Set rule" is a rule that evaluates a fact against a set of values. The set rule has two operators: IN and INTERSECTS.

IN: evaluates to VALID if the fact is a subset of the rule's value. INTERSECTS: evaluates to VALID if the fact and the rule's value have at least one item in common.

{"type": "SET","operator": "IN","key": "foo","values": ["bar","baz"]}

Supported operators: IN, INTERSECTS

String comparisons are case-sensitive.

Some examples
Rule ValueOperatorFactResult
["bar", "baz"]IN"bar"VALID
["bar", "baz"]IN"waz"INVALID
[1, 2, 3]IN2VALID
[1, 2, 3]IN4INVALID
["bar", "baz"]IN[]INVALID
[]IN[]VALID
[]IN["bar"]INVALID
["bar", "baz"]IN["bar"]VALID
["bar", "baz"]IN["waz"]INVALID
["bar", "baz"]IN["bar", "waz"]INVALID
["bar", "baz"]INTERSECTS"bar"VALID
["bar", "baz"]INTERSECTS"waz"INVALID
["bar", "baz"]INTERSECTS["bar", "waz"]VALID
["bar", "baz"]INTERSECTS["wiz", "waz"]INVALID
["bar", "baz"]INTERSECTS[]INVALID
[]INTERSECTS[]INVALID
any setany operatornullINVALID
Comparing complex objects

The Set rule can be used on more complex objects too.

SetRule<Patient> setRule = newSetRule<>();
setRule.setKey("PATIENT");
setRule.setOperator(Operator.IN);
setRule.setValues(bob, alice);

Where bob and alice are instances of Patient. You must also make sure that the Patient class overrides the equals and hashCode methods. Regola expects the equals method to perform a commutative comparison between objects.

Also note that regola does not support JSON serialization/deserialization for SET rules with complex objects.

Date Rule

The "Date Rule" is a rule that evaluates a fact against a date value.

{"type" : "DATE","operator" : "GREATER_THAN","key" : "foo","value" : "2021-07-07T12:30:00Z"}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS

Dates must be parsable to an OffsetDateTime:

A date-time with an offset from UTC/Greenwich in the ISO-8601 calendar system

Some examples
Rule ValueOperatorFactResult
"2021-07-07T12:30:00Z"EQUALS"2021-07-07T12:30:00Z"VALID
"2021-07-07T12:30:00Z"GREATER_THAN"2022-07-07T12:30:00Z"VALID
"2021-07-07T12:30:00Z"GREATER_THAN"2020-07-07T12:30:00Z"INVALID
"2021-07-07T12:30:00Z"LESS_THAN"2020-07-07T12:30:00Z"VALID
any datesupported operatornullINVALID
nullsupported operatorany dateINVALID

Null Rule

A "Null Rule" is a rule that evaluates a fact against a null value.

{"type": "NULL","key": "foo"}
Some examples
KeyFactResult
"foo"nullVALID
"foo""foo"INVALID

Combining Rules

Rules can be combined using the boolean rules: AND, OR, NOT.

{"type" : "AND","rules" : [{"type" : "STRING","operator" : "EQUALS","key" : "foo","value" : "bar"},{"type" : "OR","rules" : [{"type" : "EXISTS","key" : "waz"},{"type" : "NUMBER","operator" : "EQUALS","key" : "foobar","value" : 21}]}]}

Ignoring results

Rules can be set to be ignored, so that the evaluation of AND/OR/NOT rules does not take them into account.

This matters for empty or effectively-empty boolean rules too:

  • AND with no subrules evaluates to VALID.
  • OR with no subrules evaluates to INVALID.
  • OR where every subrule is present but marked as ignored evaluates to VALID, because all configured subrules are excluded from the final decision.
  • NOT with an ignored subrule evaluates to VALID.
  • NOT with no subrule fails.

Example of a rule marked as ignored:

{"type" : "STRING","operator" : "EQUALS","key" : "foo","value" : "bar","ignored" : true}

This is useful when you want to run a rule but not have it affect the evaluation of the tree.

For example, the following tree evaluated to VALID even thought one of the subrules of AND was INVALID:

{"result": "VALID","type": "AND","description": "Example of a Composite Rule with subrules ignored",// Optional, can be added to any rule in the tree"ignored": false,"rules": [{"result": "INVALID","type": "STRING","ignored": true,"operator": "EQUALS","key": "foo","expectedValue": "bar","actualValue": "not_bar"},{"result": "VALID","type": "EXISTS","ignored": false,"key": "foo","expectedValue": "<any>","actualValue": "not_bar"}]}

Json Deserialization with Jackson

You can use Jackson to deserialize rules from regola. These are the dependencies you will need:

<!-- pom.xml -->
<properties>
<jackson.version>2.13.1</jackson.version>
</properties>
<depdendencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
</depdendencies>

And this is the minimum setup for your ObjectMapper.

ObjectMappermapper = newObjectMapper()
.registerModule(newJavaTimeModule())
.registerModule(newRuleModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

Then you can deserialize a rule as such:

StringjsonRule = readRuleFromDataSource(); // This method will vary depending on your applicationRulerule = mapper.readValue(jsonRule, Rule.class);

New rules

  1. Create a new Rule by extending Rule (or one of the provided abstract classes)
publicclassYourRuleextendsRule {
publicYourRule() {
super("YOUR_TYPE"); // You should make sure this does not conflict with the type of any existing rule
}
@OverridepublicEvaluationResultevaluate(FactsResolverfactsResolver) {
returnnewEvaluationResult() {
privateResultresult = Result.MAYBE;
@OverridepublicRuleResultsnapshot() {
// Build and return a RuleResult
}
@OverridepublicCompletableFuture<Result> status() {
returnfacts.resolveFact(getKey())
.thenCompose(fact -> CompletableFuture.supplyAsync(() -> {
result = ... // perform the relevant checks for this rule and update the resultreturnresult;
}))
.exceptionally(throwable -> {
result = Result.FAILED;
returnresult;
});
}
};
}
}
  1. (Optional) If you need to parse rules from Json, then you must extend the RuleModule as such:
mapper.registerModule(newRuleModule()
.addRule("YOUR_TYPE", YourRule.class));
  1. Start using your new rule!

Programmatic Rule Creation

While transforming rules from Json is convenient, sometimes you may want to create rules programmatically.

Here is an example of how to do that:

SetRule<String> stringSetRule = newSetRule<>();
stringSetRule.setKey("MARKET_SEGMENT");
stringSetRule.setOperator(Operator.IN);
stringSetRule.setValues(Set.of("COM", "EDU"));
NumberRule<Integer> numberRule = newNumberRule<>();
numberRule.setKey("capacity");
numberRule.setOperator(Operator.EQUALS);
numberRule.setValue(3);
OrRuleorRule = newOrRule();
orRule.setRules(List.of(numberRule, stringSetRule));

Facts

Now that we have got some rules, we want to do something with it.

We do that by creating facts and supplying those to the evaluator which will check whether they satisfy our rule or not.

An example of a fact for the foo data point is:

varfact = newFact<>("foo", data -> "bar");

In regola, we can also write facts that use custom data fetchers to retrieve additional data:

Fact<Offer> fact = newFact<>("segment", CustomDataSources.OFFER, Offer::getSegment);

The FactsResolver

Defining facts, if using a custom data source, is not enough. We must tell our evaluator how to get those facts when a rule is run. This is done using a FactsResolver as shown below:

Map<DataSource, DataFetcher<?, YourContext>> dataFetchers = Map.of(
CustomDataSource.OFFER, offerDataFetcher,
...
);
FactsResolverfactsResolver = newSimpleFactsResolver<>(yourContext, dataFetchers);
factsResolver.addFact(newFact<>("segment", CustomDataSources.OFFER, Offer::getSegment));

Writing a Data Fetcher

Example of a data fetcher getting data over HTTP:

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
privatefinalOfferHttpConnectorofferHttpConnector;
publicOfferDataFetcher(OfferHttpConnectorofferHttpConnector) {
this.offerHttpConnector = offerHttpConnector;
}
@OverridepublicCompletableFuture<FetchResponse<Offer>> fetchResponse(YourContextcontext) {
GetOffersRequestrequest = buildRequest(context);
returnCompletableFuture.supplyAsync(() -> {
finalvarresponse = newFetchResponse<>();
response.setData(offerHttpConnector.getOffers(request)
.stream()
.findFirst());
returnresponse;
});
}
// Do not override this method if you do not want to cache results from this data fetcher.@OverridepublicStringcalculateRequestKey(YourContextcontext) {
GetOffersRequestrequest = buildRequest(context);
returnrequest.URL().toString();
}
privateRequestbuildRequest(YourContextcontext) {
returnnewGetOffersRequest(Set.of(requestContext.getOfferId()), Set.of());
}
}
publicclassYourContextimplementsContext {
privateStringofferId;
publicStringgetOfferId() {
returnofferId;
}
publicvoidsetOfferId(StringofferId) {
this.offerId = offerId;
}
}

The initialization of a data fetcher can be expensive, depending on your implementation, so it is recommended that data fetchers are re-used across multiple evaluations.

Caching in the Data Fetcher

The abstract DataFetcher uses caffeine to cache the results of the fetch results.

The default cache is setup with an expiry policy of 1 minute and max size of 1_000 entries, but a custom configuration can be setup by passing a DataFetcherConfiguration to the DataFetcher's constructor.

Custom Cache

If the default caffeine-based cache does not satisfy your requirements, you can provide your own implementation.

First, create a class for your custom cache:

classYourCustomCache<V> implementsDataFetcherCache<V> {
publicYourCustomCache(DataCacheConfigurationconfiguration) {
// (optional) construct your cache using the given configuration
}
@OverridepublicCompletableFuture<V> get(Stringkey, Function<String, CompletableFuture<V>> mappingFunction) {
// implement your "if cached, return; otherwise create, cache and return" cache function here
}
}

Then pass an instance of your custom cache to your data fetchers:

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
privatefinalOfferHttpConnectorofferHttpConnector;
publicOfferDataFetcher(OfferHttpConnectorofferHttpConnector, YourCustomCache<Offer> cache) {
super(cache);
this.offerHttpConnector = offerHttpConnector;
}
// rest of this data fetcher implementation
}

SLA handling within the Data Fetcher

By default, your custom data fetcher will not have any SLA failure handling. However, if needed you can specify an SLA on the fetch requestTime and override the whenFailingSlaFetchTime to handle any SLA failures.

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
publicOfferDataFetcher(/* other params */, longslaFetchTime) {
super(newDataFetcherConfiguration().setSlaFetchTime(slaFetchTime));
// any other initialization
}
@OverridepublicvoidwhenFailingSlaFetchTime(StringrequestKey, longslaFetchTime, doublerequestTime) {
// This method gets called whenever "requestTime > slaFetchTime"
}
}

Actions

Actions are used to define operations we want to perform after a rule is evaluated.

Basic usage

varaction = newAction()
.setDescription("Print 'Hello' if VALID")
.setOnCompletion((result, throwable, ruleResult) -> {
if (result == Result.VALID) {
System.out.println("Hello");
}
});
rule.setAction(action);

In this particular example, this action will be executed when the rule is evaluated and the result is VALID.

Chaining

It is possible to chain actions using the andThen method on the TriConsumer:

TriConsumer<Result, Throwable, RuleResult> actionConsumer = (result, throwable, ruleResult) -> {
if (result == Result.VALID) {
System.out.println("Hello");
}
};
// Chain the action to always print "World"actionConsumer = actionConsumer.andThen((result, throwable, ruleResult) -> System.out.println("World"));
varaction = newAction()
.setDescription("Print 'Hello' if VALID and the word 'World' irrespective of result")
.setOnCompletion(actionConsumer);
rule.setAction(action);

Understanding the Results of a Rule Evaluation

The following is an example of a result (pretty printed in json) returned upon evaluating a tree of rules:

{"result": "VALID","type": "AND","description": "Example of a Composite Rule",// Optional, can be added to any rule in the tree"ignored": false,"rules": [{"result": "VALID","type": "STRING","ignored": false,"operator": "EQUALS","key": "foo","expectedValue": "bar","actualValue": "bar"},{"result": "VALID","type": "OR","ignored": false,"rules": [{"result": "VALID","type": "EXISTS","ignored": false,"key": "waz","expectedValue": "<any>",// <any> is a special keyword matching any actual value for the EXISTS rule"actualValue": "wazab"},{"result": "MAYBE","type": "NUMBER","ignored": false,"operator": "EQUALS","key": "foobar","expectedValue": 21,// no actual value for MAYBE, since the rule was not evaluated due to short-circuiting}]}]}

The top-level "result" is the overall result of the rule.

  • If VALID, the subresults must be all VALID or MAYBE (i.e., rule did not need to be evaluated due to short-circuiting).
  • If not VALID, one or more of the subresults are: INVALID, OPERATION_NOT_SUPPORTED or FAILED.

Oh, by the way, what does "regola" mean?

regola is the italian word for rule.

About

A rule evaluator in Java

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

8 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

regola

Maven CentralCI

regola is a rule evaluator written in Java.

Disclaimer: This library is in development mode and there could be breaking changes as new versions are released.

Goals

  • be fast
  • be reusable and extensible
  • be well documented
  • have high test coverage
  • allow for efficient evaluation against data retrieved from external data sources
  • have Json rules conversion builtin in the library
  • run on Java 11+

Basic usage

  1. Add to your maven dependencies:
<dependency>
<groupId>com.adobe.abp</groupId>
<artifactId>regola</artifactId>
<version>0.0.24</version>
</dependency>
  1. Write a rule
varrule = newStringRule();
rule.setKey("MARKET_SEGMENT");
rule.setOperator(Operator.EQUALS);
rule.setValue("COM");
rule.setDescription("The market segment should be COM");

You could also use fluent setters:

varrule = newStringRule()
.setValue("COM")
.setOperator(Operator.EQUALS)
.setKey("MARKET_SEGMENT")
.setDescription("The market segment should be COM");

For some rules, you could also pass some parameters directly via the constructor for conciseness:

varrule = newStringRule("MARKET_SEGMENT", Operator.EQUALS, "COM");
rule.setDescription("The market segment should be COM");
  1. Define how data for the "MARKET_SEGMENT" key must be retrieved
varfactsResolver = newSimpleFactsResolver<>();
factsResolver.addFact(newFact<>("MARKET_SEGMENT", data -> "COM"));
  1. Evaluate
varevaluationResult = newEvaluator().evaluate(rule, factsResolver);
// The evaluation is an asynchronous process, so the associated CompletableFuture must be executed to get a result.// The following line returns the result value when complete, or throws an (unchecked) exception if completed exceptionally.evaluationResult.status().join(); varresult = evaluationResult.snapshot();

The result object will contain information on whether the evaluation was valid or not, plus any relevant information about the rule run.

  1. If we were to print the result as json
{"result" : "VALID","type" : "STRING","operator" : "EQUALS","key" : "MARKET_SEGMENT","description": "The market segment should be COM","expectedValue" : "COM","actualValue": "COM"}

Rules Vocabulary

Boolean Rules

Boolean rules are used to combine rules together.

And Rule

The "And Rule" is used to combine multiple rules together, where all the rules must evaluate to VALID for it to evaluate to VALID.

{"type" : "AND","rules" : [// list of other rules]}
ABA && B
VALIDVALIDVALID
VALIDINVALIDINVALID
VALIDMAYBEMAYBE
VALIDFAILEDFAILED
VALIDOPERATION_NOT_SUPPORTEDOPERATION_NOT_SUPPORTED

The AND rule is commutative: A && B = B && A.

An empty AND evaluates to VALID. This follows the identity of conjunction: with no subrules, there is nothing to invalidate the expression.

Or Rule

The "Or Rule" is used to combine multiple rules together, where at least one rule must evaluate to VALID for it to evaluate to VALID.

{"type" : "OR","rules" : [// list of other rules]}
ABA || B
VALIDanyVALID
INVALIDINVALIDINVALID

The order of precedence for non-VALID results is: FAILED, OPERATION_NOT_SUPPORTED, INVALID, MAYBE. So, for example: FAILED || INVALID == FAILED, while MAYBE || INVALID == INVALID and so on.

The OR rule is commutative: A || B = B || A.

An empty OR evaluates to INVALID. This follows the identity of disjunction: with no subrules, there is nothing that can make the expression valid.

Not Rule

The "Not Rule" is used to negate the result of another rule.

{"type" : "NOT","rule" : {// rule to negate}}
A!A
VALIDINVALID
INVALIDVALID
MAYBEMAYBE
FAILEDFAILED
OPERATION_NOT_SUPPORTEDOPERATION_NOT_SUPPORTED

A NOT rule must contain a non-null rule. If the operand is missing, the evaluation is treated as malformed configuration and fails.

Fact-only Rules

Exists Rule

The "Exists Rule" is used to check whether a fact exists or not.

{"type": "EXISTS","key": "foo"}
Some examples
KeyFactResult
"foo"{ "foo": "bar" }VALID
"foo"{ "foo": null }INVALID
"foo"{ "not_foo": "bar" }INVALID

Fixed Rules

CONSTANT Rule

The "Constant Rule" is used to always return the same result, regardless of the fact.

{"type": "CONSTANT","result": "VALID"// INVALID, MAYBE, FAILED, OPERATION_NOT_SUPPORTED}

Value-based Rules

These rules evaluate facts against a value set in the rule. When creating a value-based rule, you must also set an operator (e.g., EQUALS, GREATER_THAN, IN, etc...).

The relationship between facts, values and operators is: fact OPERATOR value.

So, for example a rule having value Cat, operator EQUALS, and evaluated against the fact Dog reads as: Dog EQUALS Cat (false). A rule having value Cat, operator CONTAINS, and evaluated against the fact [Dog, Bird, Cat] reads as: [Dog, Bird, Cat] CONTAINS Cat (true).

Number Rule

The "Number Rule" is used to evaluate facts against a number. The number can be an integer or a double.

{"type": "NUMBER","operator": "GREATER_THAN","key": "foo","value": 7// you can also have 7.0}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS, DIVISIBLE_BY

Integer-Double comparisons between the rule's value and the data provided by the fact work for all operators except CONTAINS.

The DIVISIBLE_BY operator is valid only for integer values since divisibility is not well-defined for floating-point numbers.

Some examples
Rule ValueOperatorFactResult
7EQUALS7VALID
7GREATER_THAN7INVALID
7GREATER_THAN8VALID
7GREATER_THAN_EQUAL7VALID
7GREATER_THAN_EQUAL7.5VALID
7.4GREATER_THAN7.5VALID
7.5GREATER_THAN7.5INVALID
7CONTAINS[ 6, 7, 8]VALID
7CONTAINS[ 6, 8]INVALID
7CONTAINS[ 6, 7.0, 8]INVALID
7.0CONTAINS[ 6, 7.0, 8]VALID
7DIVISIBLE_BY7VALID
7DIVISIBLE_BY8INVALID
0DIVISIBLE_BY8FAILED
7DIVISIBLE_BY0VALID
any numbersupported operatornullINVALID
nullsupported operatorany numberINVALID

When using the CONTAINS operator, the Fact must be a Set of numbers.

String Rule

{"type": "STRING","operator": "EQUALS","key": "foo","value": "bar"}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS

Comparisons are case-sensitive.

Some examples
Rule ValueOperatorFactResult
"bar"EQUALS"bar"VALID
"bar"EQUALS"BAR"INVALID
"bar"EQUALS"baz"INVALID
"bar"GREATER_THAN"car"VALID
"bar"GREATER_THAN_EQUAL"bar"VALID
"bar"GREATER_THAN"are"INVALID
"bar"STARTS_WITH"bar"VALID
"bar"STARTS_WITH"barfoo"VALID
"bar"STARTS_WITH"foobar"INVALID
"bar"ENDS_WITH"bar"VALID
"bar"ENDS_WITH"foobar"VALID
"bar"ENDS_WITH"barfoo"INVALID
"bar"CONTAINS["are", "bar", "baz"]VALID
"bar"CONTAINS["are", "baz"]INVALID
any stringsupported operatornullINVALID
nullsupported operatorany stringINVALID

Set Rule

A "Set rule" is a rule that evaluates a fact against a set of values. The set rule has two operators: IN and INTERSECTS.

IN: evaluates to VALID if the fact is a subset of the rule's value. INTERSECTS: evaluates to VALID if the fact and the rule's value have at least one item in common.

{"type": "SET","operator": "IN","key": "foo","values": ["bar","baz"]}

Supported operators: IN, INTERSECTS

String comparisons are case-sensitive.

Some examples
Rule ValueOperatorFactResult
["bar", "baz"]IN"bar"VALID
["bar", "baz"]IN"waz"INVALID
[1, 2, 3]IN2VALID
[1, 2, 3]IN4INVALID
["bar", "baz"]IN[]INVALID
[]IN[]VALID
[]IN["bar"]INVALID
["bar", "baz"]IN["bar"]VALID
["bar", "baz"]IN["waz"]INVALID
["bar", "baz"]IN["bar", "waz"]INVALID
["bar", "baz"]INTERSECTS"bar"VALID
["bar", "baz"]INTERSECTS"waz"INVALID
["bar", "baz"]INTERSECTS["bar", "waz"]VALID
["bar", "baz"]INTERSECTS["wiz", "waz"]INVALID
["bar", "baz"]INTERSECTS[]INVALID
[]INTERSECTS[]INVALID
any setany operatornullINVALID
Comparing complex objects

The Set rule can be used on more complex objects too.

SetRule<Patient> setRule = newSetRule<>();
setRule.setKey("PATIENT");
setRule.setOperator(Operator.IN);
setRule.setValues(bob, alice);

Where bob and alice are instances of Patient. You must also make sure that the Patient class overrides the equals and hashCode methods. Regola expects the equals method to perform a commutative comparison between objects.

Also note that regola does not support JSON serialization/deserialization for SET rules with complex objects.

Date Rule

The "Date Rule" is a rule that evaluates a fact against a date value.

{"type" : "DATE","operator" : "GREATER_THAN","key" : "foo","value" : "2021-07-07T12:30:00Z"}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS

Dates must be parsable to an OffsetDateTime:

A date-time with an offset from UTC/Greenwich in the ISO-8601 calendar system

Some examples
Rule ValueOperatorFactResult
"2021-07-07T12:30:00Z"EQUALS"2021-07-07T12:30:00Z"VALID
"2021-07-07T12:30:00Z"GREATER_THAN"2022-07-07T12:30:00Z"VALID
"2021-07-07T12:30:00Z"GREATER_THAN"2020-07-07T12:30:00Z"INVALID
"2021-07-07T12:30:00Z"LESS_THAN"2020-07-07T12:30:00Z"VALID
any datesupported operatornullINVALID
nullsupported operatorany dateINVALID

Null Rule

A "Null Rule" is a rule that evaluates a fact against a null value.

{"type": "NULL","key": "foo"}
Some examples
KeyFactResult
"foo"nullVALID
"foo""foo"INVALID

Combining Rules

Rules can be combined using the boolean rules: AND, OR, NOT.

{"type" : "AND","rules" : [{"type" : "STRING","operator" : "EQUALS","key" : "foo","value" : "bar"},{"type" : "OR","rules" : [{"type" : "EXISTS","key" : "waz"},{"type" : "NUMBER","operator" : "EQUALS","key" : "foobar","value" : 21}]}]}

Ignoring results

Rules can be set to be ignored, so that the evaluation of AND/OR/NOT rules does not take them into account.

This matters for empty or effectively-empty boolean rules too:

  • AND with no subrules evaluates to VALID.
  • OR with no subrules evaluates to INVALID.
  • OR where every subrule is present but marked as ignored evaluates to VALID, because all configured subrules are excluded from the final decision.
  • NOT with an ignored subrule evaluates to VALID.
  • NOT with no subrule fails.

Example of a rule marked as ignored:

{"type" : "STRING","operator" : "EQUALS","key" : "foo","value" : "bar","ignored" : true}

This is useful when you want to run a rule but not have it affect the evaluation of the tree.

For example, the following tree evaluated to VALID even thought one of the subrules of AND was INVALID:

{"result": "VALID","type": "AND","description": "Example of a Composite Rule with subrules ignored",// Optional, can be added to any rule in the tree"ignored": false,"rules": [{"result": "INVALID","type": "STRING","ignored": true,"operator": "EQUALS","key": "foo","expectedValue": "bar","actualValue": "not_bar"},{"result": "VALID","type": "EXISTS","ignored": false,"key": "foo","expectedValue": "<any>","actualValue": "not_bar"}]}

Json Deserialization with Jackson

You can use Jackson to deserialize rules from regola. These are the dependencies you will need:

<!-- pom.xml -->
<properties>
<jackson.version>2.13.1</jackson.version>
</properties>
<depdendencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
</depdendencies>

And this is the minimum setup for your ObjectMapper.

ObjectMappermapper = newObjectMapper()
.registerModule(newJavaTimeModule())
.registerModule(newRuleModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

Then you can deserialize a rule as such:

StringjsonRule = readRuleFromDataSource(); // This method will vary depending on your applicationRulerule = mapper.readValue(jsonRule, Rule.class);

New rules

  1. Create a new Rule by extending Rule (or one of the provided abstract classes)
publicclassYourRuleextendsRule {
publicYourRule() {
super("YOUR_TYPE"); // You should make sure this does not conflict with the type of any existing rule
}
@OverridepublicEvaluationResultevaluate(FactsResolverfactsResolver) {
returnnewEvaluationResult() {
privateResultresult = Result.MAYBE;
@OverridepublicRuleResultsnapshot() {
// Build and return a RuleResult
}
@OverridepublicCompletableFuture<Result> status() {
returnfacts.resolveFact(getKey())
.thenCompose(fact -> CompletableFuture.supplyAsync(() -> {
result = ... // perform the relevant checks for this rule and update the resultreturnresult;
}))
.exceptionally(throwable -> {
result = Result.FAILED;
returnresult;
});
}
};
}
}
  1. (Optional) If you need to parse rules from Json, then you must extend the RuleModule as such:
mapper.registerModule(newRuleModule()
.addRule("YOUR_TYPE", YourRule.class));
  1. Start using your new rule!

Programmatic Rule Creation

While transforming rules from Json is convenient, sometimes you may want to create rules programmatically.

Here is an example of how to do that:

SetRule<String> stringSetRule = newSetRule<>();
stringSetRule.setKey("MARKET_SEGMENT");
stringSetRule.setOperator(Operator.IN);
stringSetRule.setValues(Set.of("COM", "EDU"));
NumberRule<Integer> numberRule = newNumberRule<>();
numberRule.setKey("capacity");
numberRule.setOperator(Operator.EQUALS);
numberRule.setValue(3);
OrRuleorRule = newOrRule();
orRule.setRules(List.of(numberRule, stringSetRule));

Facts

Now that we have got some rules, we want to do something with it.

We do that by creating facts and supplying those to the evaluator which will check whether they satisfy our rule or not.

An example of a fact for the foo data point is:

varfact = newFact<>("foo", data -> "bar");

In regola, we can also write facts that use custom data fetchers to retrieve additional data:

Fact<Offer> fact = newFact<>("segment", CustomDataSources.OFFER, Offer::getSegment);

The FactsResolver

Defining facts, if using a custom data source, is not enough. We must tell our evaluator how to get those facts when a rule is run. This is done using a FactsResolver as shown below:

Map<DataSource, DataFetcher<?, YourContext>> dataFetchers = Map.of(
CustomDataSource.OFFER, offerDataFetcher,
...
);
FactsResolverfactsResolver = newSimpleFactsResolver<>(yourContext, dataFetchers);
factsResolver.addFact(newFact<>("segment", CustomDataSources.OFFER, Offer::getSegment));

Writing a Data Fetcher

Example of a data fetcher getting data over HTTP:

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
privatefinalOfferHttpConnectorofferHttpConnector;
publicOfferDataFetcher(OfferHttpConnectorofferHttpConnector) {
this.offerHttpConnector = offerHttpConnector;
}
@OverridepublicCompletableFuture<FetchResponse<Offer>> fetchResponse(YourContextcontext) {
GetOffersRequestrequest = buildRequest(context);
returnCompletableFuture.supplyAsync(() -> {
finalvarresponse = newFetchResponse<>();
response.setData(offerHttpConnector.getOffers(request)
.stream()
.findFirst());
returnresponse;
});
}
// Do not override this method if you do not want to cache results from this data fetcher.@OverridepublicStringcalculateRequestKey(YourContextcontext) {
GetOffersRequestrequest = buildRequest(context);
returnrequest.URL().toString();
}
privateRequestbuildRequest(YourContextcontext) {
returnnewGetOffersRequest(Set.of(requestContext.getOfferId()), Set.of());
}
}
publicclassYourContextimplementsContext {
privateStringofferId;
publicStringgetOfferId() {
returnofferId;
}
publicvoidsetOfferId(StringofferId) {
this.offerId = offerId;
}
}

The initialization of a data fetcher can be expensive, depending on your implementation, so it is recommended that data fetchers are re-used across multiple evaluations.

Caching in the Data Fetcher

The abstract DataFetcher uses caffeine to cache the results of the fetch results.

The default cache is setup with an expiry policy of 1 minute and max size of 1_000 entries, but a custom configuration can be setup by passing a DataFetcherConfiguration to the DataFetcher's constructor.

Custom Cache

If the default caffeine-based cache does not satisfy your requirements, you can provide your own implementation.

First, create a class for your custom cache:

classYourCustomCache<V> implementsDataFetcherCache<V> {
publicYourCustomCache(DataCacheConfigurationconfiguration) {
// (optional) construct your cache using the given configuration
}
@OverridepublicCompletableFuture<V> get(Stringkey, Function<String, CompletableFuture<V>> mappingFunction) {
// implement your "if cached, return; otherwise create, cache and return" cache function here
}
}

Then pass an instance of your custom cache to your data fetchers:

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
privatefinalOfferHttpConnectorofferHttpConnector;
publicOfferDataFetcher(OfferHttpConnectorofferHttpConnector, YourCustomCache<Offer> cache) {
super(cache);
this.offerHttpConnector = offerHttpConnector;
}
// rest of this data fetcher implementation
}

SLA handling within the Data Fetcher

By default, your custom data fetcher will not have any SLA failure handling. However, if needed you can specify an SLA on the fetch requestTime and override the whenFailingSlaFetchTime to handle any SLA failures.

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
publicOfferDataFetcher(/* other params */, longslaFetchTime) {
super(newDataFetcherConfiguration().setSlaFetchTime(slaFetchTime));
// any other initialization
}
@OverridepublicvoidwhenFailingSlaFetchTime(StringrequestKey, longslaFetchTime, doublerequestTime) {
// This method gets called whenever "requestTime > slaFetchTime"
}
}

Actions

Actions are used to define operations we want to perform after a rule is evaluated.

Basic usage

varaction = newAction()
.setDescription("Print 'Hello' if VALID")
.setOnCompletion((result, throwable, ruleResult) -> {
if (result == Result.VALID) {
System.out.println("Hello");
}
});
rule.setAction(action);

In this particular example, this action will be executed when the rule is evaluated and the result is VALID.

Chaining

It is possible to chain actions using the andThen method on the TriConsumer:

TriConsumer<Result, Throwable, RuleResult> actionConsumer = (result, throwable, ruleResult) -> {
if (result == Result.VALID) {
System.out.println("Hello");
}
};
// Chain the action to always print "World"actionConsumer = actionConsumer.andThen((result, throwable, ruleResult) -> System.out.println("World"));
varaction = newAction()
.setDescription("Print 'Hello' if VALID and the word 'World' irrespective of result")
.setOnCompletion(actionConsumer);
rule.setAction(action);

Understanding the Results of a Rule Evaluation

The following is an example of a result (pretty printed in json) returned upon evaluating a tree of rules:

{"result": "VALID","type": "AND","description": "Example of a Composite Rule",// Optional, can be added to any rule in the tree"ignored": false,"rules": [{"result": "VALID","type": "STRING","ignored": false,"operator": "EQUALS","key": "foo","expectedValue": "bar","actualValue": "bar"},{"result": "VALID","type": "OR","ignored": false,"rules": [{"result": "VALID","type": "EXISTS","ignored": false,"key": "waz","expectedValue": "<any>",// <any> is a special keyword matching any actual value for the EXISTS rule"actualValue": "wazab"},{"result": "MAYBE","type": "NUMBER","ignored": false,"operator": "EQUALS","key": "foobar","expectedValue": 21,// no actual value for MAYBE, since the rule was not evaluated due to short-circuiting}]}]}

The top-level "result" is the overall result of the rule.

  • If VALID, the subresults must be all VALID or MAYBE (i.e., rule did not need to be evaluated due to short-circuiting).
  • If not VALID, one or more of the subresults are: INVALID, OPERATION_NOT_SUPPORTED or FAILED.

Oh, by the way, what does "regola" mean?

regola is the italian word for rule.

About

A rule evaluator in Java

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

8 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

regola

Maven CentralCI

regola is a rule evaluator written in Java.

Disclaimer: This library is in development mode and there could be breaking changes as new versions are released.

Goals

  • be fast
  • be reusable and extensible
  • be well documented
  • have high test coverage
  • allow for efficient evaluation against data retrieved from external data sources
  • have Json rules conversion builtin in the library
  • run on Java 11+

Basic usage

  1. Add to your maven dependencies:
<dependency>
<groupId>com.adobe.abp</groupId>
<artifactId>regola</artifactId>
<version>0.0.24</version>
</dependency>
  1. Write a rule
varrule = newStringRule();
rule.setKey("MARKET_SEGMENT");
rule.setOperator(Operator.EQUALS);
rule.setValue("COM");
rule.setDescription("The market segment should be COM");

You could also use fluent setters:

varrule = newStringRule()
.setValue("COM")
.setOperator(Operator.EQUALS)
.setKey("MARKET_SEGMENT")
.setDescription("The market segment should be COM");

For some rules, you could also pass some parameters directly via the constructor for conciseness:

varrule = newStringRule("MARKET_SEGMENT", Operator.EQUALS, "COM");
rule.setDescription("The market segment should be COM");
  1. Define how data for the "MARKET_SEGMENT" key must be retrieved
varfactsResolver = newSimpleFactsResolver<>();
factsResolver.addFact(newFact<>("MARKET_SEGMENT", data -> "COM"));
  1. Evaluate
varevaluationResult = newEvaluator().evaluate(rule, factsResolver);
// The evaluation is an asynchronous process, so the associated CompletableFuture must be executed to get a result.// The following line returns the result value when complete, or throws an (unchecked) exception if completed exceptionally.evaluationResult.status().join(); varresult = evaluationResult.snapshot();

The result object will contain information on whether the evaluation was valid or not, plus any relevant information about the rule run.

  1. If we were to print the result as json
{"result" : "VALID","type" : "STRING","operator" : "EQUALS","key" : "MARKET_SEGMENT","description": "The market segment should be COM","expectedValue" : "COM","actualValue": "COM"}

Rules Vocabulary

Boolean Rules

Boolean rules are used to combine rules together.

And Rule

The "And Rule" is used to combine multiple rules together, where all the rules must evaluate to VALID for it to evaluate to VALID.

{"type" : "AND","rules" : [// list of other rules]}
ABA && B
VALIDVALIDVALID
VALIDINVALIDINVALID
VALIDMAYBEMAYBE
VALIDFAILEDFAILED
VALIDOPERATION_NOT_SUPPORTEDOPERATION_NOT_SUPPORTED

The AND rule is commutative: A && B = B && A.

An empty AND evaluates to VALID. This follows the identity of conjunction: with no subrules, there is nothing to invalidate the expression.

Or Rule

The "Or Rule" is used to combine multiple rules together, where at least one rule must evaluate to VALID for it to evaluate to VALID.

{"type" : "OR","rules" : [// list of other rules]}
ABA || B
VALIDanyVALID
INVALIDINVALIDINVALID

The order of precedence for non-VALID results is: FAILED, OPERATION_NOT_SUPPORTED, INVALID, MAYBE. So, for example: FAILED || INVALID == FAILED, while MAYBE || INVALID == INVALID and so on.

The OR rule is commutative: A || B = B || A.

An empty OR evaluates to INVALID. This follows the identity of disjunction: with no subrules, there is nothing that can make the expression valid.

Not Rule

The "Not Rule" is used to negate the result of another rule.

{"type" : "NOT","rule" : {// rule to negate}}
A!A
VALIDINVALID
INVALIDVALID
MAYBEMAYBE
FAILEDFAILED
OPERATION_NOT_SUPPORTEDOPERATION_NOT_SUPPORTED

A NOT rule must contain a non-null rule. If the operand is missing, the evaluation is treated as malformed configuration and fails.

Fact-only Rules

Exists Rule

The "Exists Rule" is used to check whether a fact exists or not.

{"type": "EXISTS","key": "foo"}
Some examples
KeyFactResult
"foo"{ "foo": "bar" }VALID
"foo"{ "foo": null }INVALID
"foo"{ "not_foo": "bar" }INVALID

Fixed Rules

CONSTANT Rule

The "Constant Rule" is used to always return the same result, regardless of the fact.

{"type": "CONSTANT","result": "VALID"// INVALID, MAYBE, FAILED, OPERATION_NOT_SUPPORTED}

Value-based Rules

These rules evaluate facts against a value set in the rule. When creating a value-based rule, you must also set an operator (e.g., EQUALS, GREATER_THAN, IN, etc...).

The relationship between facts, values and operators is: fact OPERATOR value.

So, for example a rule having value Cat, operator EQUALS, and evaluated against the fact Dog reads as: Dog EQUALS Cat (false). A rule having value Cat, operator CONTAINS, and evaluated against the fact [Dog, Bird, Cat] reads as: [Dog, Bird, Cat] CONTAINS Cat (true).

Number Rule

The "Number Rule" is used to evaluate facts against a number. The number can be an integer or a double.

{"type": "NUMBER","operator": "GREATER_THAN","key": "foo","value": 7// you can also have 7.0}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS, DIVISIBLE_BY

Integer-Double comparisons between the rule's value and the data provided by the fact work for all operators except CONTAINS.

The DIVISIBLE_BY operator is valid only for integer values since divisibility is not well-defined for floating-point numbers.

Some examples
Rule ValueOperatorFactResult
7EQUALS7VALID
7GREATER_THAN7INVALID
7GREATER_THAN8VALID
7GREATER_THAN_EQUAL7VALID
7GREATER_THAN_EQUAL7.5VALID
7.4GREATER_THAN7.5VALID
7.5GREATER_THAN7.5INVALID
7CONTAINS[ 6, 7, 8]VALID
7CONTAINS[ 6, 8]INVALID
7CONTAINS[ 6, 7.0, 8]INVALID
7.0CONTAINS[ 6, 7.0, 8]VALID
7DIVISIBLE_BY7VALID
7DIVISIBLE_BY8INVALID
0DIVISIBLE_BY8FAILED
7DIVISIBLE_BY0VALID
any numbersupported operatornullINVALID
nullsupported operatorany numberINVALID

When using the CONTAINS operator, the Fact must be a Set of numbers.

String Rule

{"type": "STRING","operator": "EQUALS","key": "foo","value": "bar"}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS

Comparisons are case-sensitive.

Some examples
Rule ValueOperatorFactResult
"bar"EQUALS"bar"VALID
"bar"EQUALS"BAR"INVALID
"bar"EQUALS"baz"INVALID
"bar"GREATER_THAN"car"VALID
"bar"GREATER_THAN_EQUAL"bar"VALID
"bar"GREATER_THAN"are"INVALID
"bar"STARTS_WITH"bar"VALID
"bar"STARTS_WITH"barfoo"VALID
"bar"STARTS_WITH"foobar"INVALID
"bar"ENDS_WITH"bar"VALID
"bar"ENDS_WITH"foobar"VALID
"bar"ENDS_WITH"barfoo"INVALID
"bar"CONTAINS["are", "bar", "baz"]VALID
"bar"CONTAINS["are", "baz"]INVALID
any stringsupported operatornullINVALID
nullsupported operatorany stringINVALID

Set Rule

A "Set rule" is a rule that evaluates a fact against a set of values. The set rule has two operators: IN and INTERSECTS.

IN: evaluates to VALID if the fact is a subset of the rule's value. INTERSECTS: evaluates to VALID if the fact and the rule's value have at least one item in common.

{"type": "SET","operator": "IN","key": "foo","values": ["bar","baz"]}

Supported operators: IN, INTERSECTS

String comparisons are case-sensitive.

Some examples
Rule ValueOperatorFactResult
["bar", "baz"]IN"bar"VALID
["bar", "baz"]IN"waz"INVALID
[1, 2, 3]IN2VALID
[1, 2, 3]IN4INVALID
["bar", "baz"]IN[]INVALID
[]IN[]VALID
[]IN["bar"]INVALID
["bar", "baz"]IN["bar"]VALID
["bar", "baz"]IN["waz"]INVALID
["bar", "baz"]IN["bar", "waz"]INVALID
["bar", "baz"]INTERSECTS"bar"VALID
["bar", "baz"]INTERSECTS"waz"INVALID
["bar", "baz"]INTERSECTS["bar", "waz"]VALID
["bar", "baz"]INTERSECTS["wiz", "waz"]INVALID
["bar", "baz"]INTERSECTS[]INVALID
[]INTERSECTS[]INVALID
any setany operatornullINVALID
Comparing complex objects

The Set rule can be used on more complex objects too.

SetRule<Patient> setRule = newSetRule<>();
setRule.setKey("PATIENT");
setRule.setOperator(Operator.IN);
setRule.setValues(bob, alice);

Where bob and alice are instances of Patient. You must also make sure that the Patient class overrides the equals and hashCode methods. Regola expects the equals method to perform a commutative comparison between objects.

Also note that regola does not support JSON serialization/deserialization for SET rules with complex objects.

Date Rule

The "Date Rule" is a rule that evaluates a fact against a date value.

{"type" : "DATE","operator" : "GREATER_THAN","key" : "foo","value" : "2021-07-07T12:30:00Z"}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS

Dates must be parsable to an OffsetDateTime:

A date-time with an offset from UTC/Greenwich in the ISO-8601 calendar system

Some examples
Rule ValueOperatorFactResult
"2021-07-07T12:30:00Z"EQUALS"2021-07-07T12:30:00Z"VALID
"2021-07-07T12:30:00Z"GREATER_THAN"2022-07-07T12:30:00Z"VALID
"2021-07-07T12:30:00Z"GREATER_THAN"2020-07-07T12:30:00Z"INVALID
"2021-07-07T12:30:00Z"LESS_THAN"2020-07-07T12:30:00Z"VALID
any datesupported operatornullINVALID
nullsupported operatorany dateINVALID

Null Rule

A "Null Rule" is a rule that evaluates a fact against a null value.

{"type": "NULL","key": "foo"}
Some examples
KeyFactResult
"foo"nullVALID
"foo""foo"INVALID

Combining Rules

Rules can be combined using the boolean rules: AND, OR, NOT.

{"type" : "AND","rules" : [{"type" : "STRING","operator" : "EQUALS","key" : "foo","value" : "bar"},{"type" : "OR","rules" : [{"type" : "EXISTS","key" : "waz"},{"type" : "NUMBER","operator" : "EQUALS","key" : "foobar","value" : 21}]}]}

Ignoring results

Rules can be set to be ignored, so that the evaluation of AND/OR/NOT rules does not take them into account.

This matters for empty or effectively-empty boolean rules too:

  • AND with no subrules evaluates to VALID.
  • OR with no subrules evaluates to INVALID.
  • OR where every subrule is present but marked as ignored evaluates to VALID, because all configured subrules are excluded from the final decision.
  • NOT with an ignored subrule evaluates to VALID.
  • NOT with no subrule fails.

Example of a rule marked as ignored:

{"type" : "STRING","operator" : "EQUALS","key" : "foo","value" : "bar","ignored" : true}

This is useful when you want to run a rule but not have it affect the evaluation of the tree.

For example, the following tree evaluated to VALID even thought one of the subrules of AND was INVALID:

{"result": "VALID","type": "AND","description": "Example of a Composite Rule with subrules ignored",// Optional, can be added to any rule in the tree"ignored": false,"rules": [{"result": "INVALID","type": "STRING","ignored": true,"operator": "EQUALS","key": "foo","expectedValue": "bar","actualValue": "not_bar"},{"result": "VALID","type": "EXISTS","ignored": false,"key": "foo","expectedValue": "<any>","actualValue": "not_bar"}]}

Json Deserialization with Jackson

You can use Jackson to deserialize rules from regola. These are the dependencies you will need:

<!-- pom.xml -->
<properties>
<jackson.version>2.13.1</jackson.version>
</properties>
<depdendencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
</depdendencies>

And this is the minimum setup for your ObjectMapper.

ObjectMappermapper = newObjectMapper()
.registerModule(newJavaTimeModule())
.registerModule(newRuleModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

Then you can deserialize a rule as such:

StringjsonRule = readRuleFromDataSource(); // This method will vary depending on your applicationRulerule = mapper.readValue(jsonRule, Rule.class);

New rules

  1. Create a new Rule by extending Rule (or one of the provided abstract classes)
publicclassYourRuleextendsRule {
publicYourRule() {
super("YOUR_TYPE"); // You should make sure this does not conflict with the type of any existing rule
}
@OverridepublicEvaluationResultevaluate(FactsResolverfactsResolver) {
returnnewEvaluationResult() {
privateResultresult = Result.MAYBE;
@OverridepublicRuleResultsnapshot() {
// Build and return a RuleResult
}
@OverridepublicCompletableFuture<Result> status() {
returnfacts.resolveFact(getKey())
.thenCompose(fact -> CompletableFuture.supplyAsync(() -> {
result = ... // perform the relevant checks for this rule and update the resultreturnresult;
}))
.exceptionally(throwable -> {
result = Result.FAILED;
returnresult;
});
}
};
}
}
  1. (Optional) If you need to parse rules from Json, then you must extend the RuleModule as such:
mapper.registerModule(newRuleModule()
.addRule("YOUR_TYPE", YourRule.class));
  1. Start using your new rule!

Programmatic Rule Creation

While transforming rules from Json is convenient, sometimes you may want to create rules programmatically.

Here is an example of how to do that:

SetRule<String> stringSetRule = newSetRule<>();
stringSetRule.setKey("MARKET_SEGMENT");
stringSetRule.setOperator(Operator.IN);
stringSetRule.setValues(Set.of("COM", "EDU"));
NumberRule<Integer> numberRule = newNumberRule<>();
numberRule.setKey("capacity");
numberRule.setOperator(Operator.EQUALS);
numberRule.setValue(3);
OrRuleorRule = newOrRule();
orRule.setRules(List.of(numberRule, stringSetRule));

Facts

Now that we have got some rules, we want to do something with it.

We do that by creating facts and supplying those to the evaluator which will check whether they satisfy our rule or not.

An example of a fact for the foo data point is:

varfact = newFact<>("foo", data -> "bar");

In regola, we can also write facts that use custom data fetchers to retrieve additional data:

Fact<Offer> fact = newFact<>("segment", CustomDataSources.OFFER, Offer::getSegment);

The FactsResolver

Defining facts, if using a custom data source, is not enough. We must tell our evaluator how to get those facts when a rule is run. This is done using a FactsResolver as shown below:

Map<DataSource, DataFetcher<?, YourContext>> dataFetchers = Map.of(
CustomDataSource.OFFER, offerDataFetcher,
...
);
FactsResolverfactsResolver = newSimpleFactsResolver<>(yourContext, dataFetchers);
factsResolver.addFact(newFact<>("segment", CustomDataSources.OFFER, Offer::getSegment));

Writing a Data Fetcher

Example of a data fetcher getting data over HTTP:

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
privatefinalOfferHttpConnectorofferHttpConnector;
publicOfferDataFetcher(OfferHttpConnectorofferHttpConnector) {
this.offerHttpConnector = offerHttpConnector;
}
@OverridepublicCompletableFuture<FetchResponse<Offer>> fetchResponse(YourContextcontext) {
GetOffersRequestrequest = buildRequest(context);
returnCompletableFuture.supplyAsync(() -> {
finalvarresponse = newFetchResponse<>();
response.setData(offerHttpConnector.getOffers(request)
.stream()
.findFirst());
returnresponse;
});
}
// Do not override this method if you do not want to cache results from this data fetcher.@OverridepublicStringcalculateRequestKey(YourContextcontext) {
GetOffersRequestrequest = buildRequest(context);
returnrequest.URL().toString();
}
privateRequestbuildRequest(YourContextcontext) {
returnnewGetOffersRequest(Set.of(requestContext.getOfferId()), Set.of());
}
}
publicclassYourContextimplementsContext {
privateStringofferId;
publicStringgetOfferId() {
returnofferId;
}
publicvoidsetOfferId(StringofferId) {
this.offerId = offerId;
}
}

The initialization of a data fetcher can be expensive, depending on your implementation, so it is recommended that data fetchers are re-used across multiple evaluations.

Caching in the Data Fetcher

The abstract DataFetcher uses caffeine to cache the results of the fetch results.

The default cache is setup with an expiry policy of 1 minute and max size of 1_000 entries, but a custom configuration can be setup by passing a DataFetcherConfiguration to the DataFetcher's constructor.

Custom Cache

If the default caffeine-based cache does not satisfy your requirements, you can provide your own implementation.

First, create a class for your custom cache:

classYourCustomCache<V> implementsDataFetcherCache<V> {
publicYourCustomCache(DataCacheConfigurationconfiguration) {
// (optional) construct your cache using the given configuration
}
@OverridepublicCompletableFuture<V> get(Stringkey, Function<String, CompletableFuture<V>> mappingFunction) {
// implement your "if cached, return; otherwise create, cache and return" cache function here
}
}

Then pass an instance of your custom cache to your data fetchers:

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
privatefinalOfferHttpConnectorofferHttpConnector;
publicOfferDataFetcher(OfferHttpConnectorofferHttpConnector, YourCustomCache<Offer> cache) {
super(cache);
this.offerHttpConnector = offerHttpConnector;
}
// rest of this data fetcher implementation
}

SLA handling within the Data Fetcher

By default, your custom data fetcher will not have any SLA failure handling. However, if needed you can specify an SLA on the fetch requestTime and override the whenFailingSlaFetchTime to handle any SLA failures.

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
publicOfferDataFetcher(/* other params */, longslaFetchTime) {
super(newDataFetcherConfiguration().setSlaFetchTime(slaFetchTime));
// any other initialization
}
@OverridepublicvoidwhenFailingSlaFetchTime(StringrequestKey, longslaFetchTime, doublerequestTime) {
// This method gets called whenever "requestTime > slaFetchTime"
}
}

Actions

Actions are used to define operations we want to perform after a rule is evaluated.

Basic usage

varaction = newAction()
.setDescription("Print 'Hello' if VALID")
.setOnCompletion((result, throwable, ruleResult) -> {
if (result == Result.VALID) {
System.out.println("Hello");
}
});
rule.setAction(action);

In this particular example, this action will be executed when the rule is evaluated and the result is VALID.

Chaining

It is possible to chain actions using the andThen method on the TriConsumer:

TriConsumer<Result, Throwable, RuleResult> actionConsumer = (result, throwable, ruleResult) -> {
if (result == Result.VALID) {
System.out.println("Hello");
}
};
// Chain the action to always print "World"actionConsumer = actionConsumer.andThen((result, throwable, ruleResult) -> System.out.println("World"));
varaction = newAction()
.setDescription("Print 'Hello' if VALID and the word 'World' irrespective of result")
.setOnCompletion(actionConsumer);
rule.setAction(action);

Understanding the Results of a Rule Evaluation

The following is an example of a result (pretty printed in json) returned upon evaluating a tree of rules:

{"result": "VALID","type": "AND","description": "Example of a Composite Rule",// Optional, can be added to any rule in the tree"ignored": false,"rules": [{"result": "VALID","type": "STRING","ignored": false,"operator": "EQUALS","key": "foo","expectedValue": "bar","actualValue": "bar"},{"result": "VALID","type": "OR","ignored": false,"rules": [{"result": "VALID","type": "EXISTS","ignored": false,"key": "waz","expectedValue": "<any>",// <any> is a special keyword matching any actual value for the EXISTS rule"actualValue": "wazab"},{"result": "MAYBE","type": "NUMBER","ignored": false,"operator": "EQUALS","key": "foobar","expectedValue": 21,// no actual value for MAYBE, since the rule was not evaluated due to short-circuiting}]}]}

The top-level "result" is the overall result of the rule.

  • If VALID, the subresults must be all VALID or MAYBE (i.e., rule did not need to be evaluated due to short-circuiting).
  • If not VALID, one or more of the subresults are: INVALID, OPERATION_NOT_SUPPORTED or FAILED.

Oh, by the way, what does "regola" mean?

regola is the italian word for rule.

About

A rule evaluator in Java

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

8 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

regola

Maven CentralCI

regola is a rule evaluator written in Java.

Disclaimer: This library is in development mode and there could be breaking changes as new versions are released.

Goals

  • be fast
  • be reusable and extensible
  • be well documented
  • have high test coverage
  • allow for efficient evaluation against data retrieved from external data sources
  • have Json rules conversion builtin in the library
  • run on Java 11+

Basic usage

  1. Add to your maven dependencies:
<dependency>
<groupId>com.adobe.abp</groupId>
<artifactId>regola</artifactId>
<version>0.0.24</version>
</dependency>
  1. Write a rule
varrule = newStringRule();
rule.setKey("MARKET_SEGMENT");
rule.setOperator(Operator.EQUALS);
rule.setValue("COM");
rule.setDescription("The market segment should be COM");

You could also use fluent setters:

varrule = newStringRule()
.setValue("COM")
.setOperator(Operator.EQUALS)
.setKey("MARKET_SEGMENT")
.setDescription("The market segment should be COM");

For some rules, you could also pass some parameters directly via the constructor for conciseness:

varrule = newStringRule("MARKET_SEGMENT", Operator.EQUALS, "COM");
rule.setDescription("The market segment should be COM");
  1. Define how data for the "MARKET_SEGMENT" key must be retrieved
varfactsResolver = newSimpleFactsResolver<>();
factsResolver.addFact(newFact<>("MARKET_SEGMENT", data -> "COM"));
  1. Evaluate
varevaluationResult = newEvaluator().evaluate(rule, factsResolver);
// The evaluation is an asynchronous process, so the associated CompletableFuture must be executed to get a result.// The following line returns the result value when complete, or throws an (unchecked) exception if completed exceptionally.evaluationResult.status().join(); varresult = evaluationResult.snapshot();

The result object will contain information on whether the evaluation was valid or not, plus any relevant information about the rule run.

  1. If we were to print the result as json
{"result" : "VALID","type" : "STRING","operator" : "EQUALS","key" : "MARKET_SEGMENT","description": "The market segment should be COM","expectedValue" : "COM","actualValue": "COM"}

Rules Vocabulary

Boolean Rules

Boolean rules are used to combine rules together.

And Rule

The "And Rule" is used to combine multiple rules together, where all the rules must evaluate to VALID for it to evaluate to VALID.

{"type" : "AND","rules" : [// list of other rules]}
ABA && B
VALIDVALIDVALID
VALIDINVALIDINVALID
VALIDMAYBEMAYBE
VALIDFAILEDFAILED
VALIDOPERATION_NOT_SUPPORTEDOPERATION_NOT_SUPPORTED

The AND rule is commutative: A && B = B && A.

An empty AND evaluates to VALID. This follows the identity of conjunction: with no subrules, there is nothing to invalidate the expression.

Or Rule

The "Or Rule" is used to combine multiple rules together, where at least one rule must evaluate to VALID for it to evaluate to VALID.

{"type" : "OR","rules" : [// list of other rules]}
ABA || B
VALIDanyVALID
INVALIDINVALIDINVALID

The order of precedence for non-VALID results is: FAILED, OPERATION_NOT_SUPPORTED, INVALID, MAYBE. So, for example: FAILED || INVALID == FAILED, while MAYBE || INVALID == INVALID and so on.

The OR rule is commutative: A || B = B || A.

An empty OR evaluates to INVALID. This follows the identity of disjunction: with no subrules, there is nothing that can make the expression valid.

Not Rule

The "Not Rule" is used to negate the result of another rule.

{"type" : "NOT","rule" : {// rule to negate}}
A!A
VALIDINVALID
INVALIDVALID
MAYBEMAYBE
FAILEDFAILED
OPERATION_NOT_SUPPORTEDOPERATION_NOT_SUPPORTED

A NOT rule must contain a non-null rule. If the operand is missing, the evaluation is treated as malformed configuration and fails.

Fact-only Rules

Exists Rule

The "Exists Rule" is used to check whether a fact exists or not.

{"type": "EXISTS","key": "foo"}
Some examples
KeyFactResult
"foo"{ "foo": "bar" }VALID
"foo"{ "foo": null }INVALID
"foo"{ "not_foo": "bar" }INVALID

Fixed Rules

CONSTANT Rule

The "Constant Rule" is used to always return the same result, regardless of the fact.

{"type": "CONSTANT","result": "VALID"// INVALID, MAYBE, FAILED, OPERATION_NOT_SUPPORTED}

Value-based Rules

These rules evaluate facts against a value set in the rule. When creating a value-based rule, you must also set an operator (e.g., EQUALS, GREATER_THAN, IN, etc...).

The relationship between facts, values and operators is: fact OPERATOR value.

So, for example a rule having value Cat, operator EQUALS, and evaluated against the fact Dog reads as: Dog EQUALS Cat (false). A rule having value Cat, operator CONTAINS, and evaluated against the fact [Dog, Bird, Cat] reads as: [Dog, Bird, Cat] CONTAINS Cat (true).

Number Rule

The "Number Rule" is used to evaluate facts against a number. The number can be an integer or a double.

{"type": "NUMBER","operator": "GREATER_THAN","key": "foo","value": 7// you can also have 7.0}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS, DIVISIBLE_BY

Integer-Double comparisons between the rule's value and the data provided by the fact work for all operators except CONTAINS.

The DIVISIBLE_BY operator is valid only for integer values since divisibility is not well-defined for floating-point numbers.

Some examples
Rule ValueOperatorFactResult
7EQUALS7VALID
7GREATER_THAN7INVALID
7GREATER_THAN8VALID
7GREATER_THAN_EQUAL7VALID
7GREATER_THAN_EQUAL7.5VALID
7.4GREATER_THAN7.5VALID
7.5GREATER_THAN7.5INVALID
7CONTAINS[ 6, 7, 8]VALID
7CONTAINS[ 6, 8]INVALID
7CONTAINS[ 6, 7.0, 8]INVALID
7.0CONTAINS[ 6, 7.0, 8]VALID
7DIVISIBLE_BY7VALID
7DIVISIBLE_BY8INVALID
0DIVISIBLE_BY8FAILED
7DIVISIBLE_BY0VALID
any numbersupported operatornullINVALID
nullsupported operatorany numberINVALID

When using the CONTAINS operator, the Fact must be a Set of numbers.

String Rule

{"type": "STRING","operator": "EQUALS","key": "foo","value": "bar"}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS

Comparisons are case-sensitive.

Some examples
Rule ValueOperatorFactResult
"bar"EQUALS"bar"VALID
"bar"EQUALS"BAR"INVALID
"bar"EQUALS"baz"INVALID
"bar"GREATER_THAN"car"VALID
"bar"GREATER_THAN_EQUAL"bar"VALID
"bar"GREATER_THAN"are"INVALID
"bar"STARTS_WITH"bar"VALID
"bar"STARTS_WITH"barfoo"VALID
"bar"STARTS_WITH"foobar"INVALID
"bar"ENDS_WITH"bar"VALID
"bar"ENDS_WITH"foobar"VALID
"bar"ENDS_WITH"barfoo"INVALID
"bar"CONTAINS["are", "bar", "baz"]VALID
"bar"CONTAINS["are", "baz"]INVALID
any stringsupported operatornullINVALID
nullsupported operatorany stringINVALID

Set Rule

A "Set rule" is a rule that evaluates a fact against a set of values. The set rule has two operators: IN and INTERSECTS.

IN: evaluates to VALID if the fact is a subset of the rule's value. INTERSECTS: evaluates to VALID if the fact and the rule's value have at least one item in common.

{"type": "SET","operator": "IN","key": "foo","values": ["bar","baz"]}

Supported operators: IN, INTERSECTS

String comparisons are case-sensitive.

Some examples
Rule ValueOperatorFactResult
["bar", "baz"]IN"bar"VALID
["bar", "baz"]IN"waz"INVALID
[1, 2, 3]IN2VALID
[1, 2, 3]IN4INVALID
["bar", "baz"]IN[]INVALID
[]IN[]VALID
[]IN["bar"]INVALID
["bar", "baz"]IN["bar"]VALID
["bar", "baz"]IN["waz"]INVALID
["bar", "baz"]IN["bar", "waz"]INVALID
["bar", "baz"]INTERSECTS"bar"VALID
["bar", "baz"]INTERSECTS"waz"INVALID
["bar", "baz"]INTERSECTS["bar", "waz"]VALID
["bar", "baz"]INTERSECTS["wiz", "waz"]INVALID
["bar", "baz"]INTERSECTS[]INVALID
[]INTERSECTS[]INVALID
any setany operatornullINVALID
Comparing complex objects

The Set rule can be used on more complex objects too.

SetRule<Patient> setRule = newSetRule<>();
setRule.setKey("PATIENT");
setRule.setOperator(Operator.IN);
setRule.setValues(bob, alice);

Where bob and alice are instances of Patient. You must also make sure that the Patient class overrides the equals and hashCode methods. Regola expects the equals method to perform a commutative comparison between objects.

Also note that regola does not support JSON serialization/deserialization for SET rules with complex objects.

Date Rule

The "Date Rule" is a rule that evaluates a fact against a date value.

{"type" : "DATE","operator" : "GREATER_THAN","key" : "foo","value" : "2021-07-07T12:30:00Z"}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS

Dates must be parsable to an OffsetDateTime:

A date-time with an offset from UTC/Greenwich in the ISO-8601 calendar system

Some examples
Rule ValueOperatorFactResult
"2021-07-07T12:30:00Z"EQUALS"2021-07-07T12:30:00Z"VALID
"2021-07-07T12:30:00Z"GREATER_THAN"2022-07-07T12:30:00Z"VALID
"2021-07-07T12:30:00Z"GREATER_THAN"2020-07-07T12:30:00Z"INVALID
"2021-07-07T12:30:00Z"LESS_THAN"2020-07-07T12:30:00Z"VALID
any datesupported operatornullINVALID
nullsupported operatorany dateINVALID

Null Rule

A "Null Rule" is a rule that evaluates a fact against a null value.

{"type": "NULL","key": "foo"}
Some examples
KeyFactResult
"foo"nullVALID
"foo""foo"INVALID

Combining Rules

Rules can be combined using the boolean rules: AND, OR, NOT.

{"type" : "AND","rules" : [{"type" : "STRING","operator" : "EQUALS","key" : "foo","value" : "bar"},{"type" : "OR","rules" : [{"type" : "EXISTS","key" : "waz"},{"type" : "NUMBER","operator" : "EQUALS","key" : "foobar","value" : 21}]}]}

Ignoring results

Rules can be set to be ignored, so that the evaluation of AND/OR/NOT rules does not take them into account.

This matters for empty or effectively-empty boolean rules too:

  • AND with no subrules evaluates to VALID.
  • OR with no subrules evaluates to INVALID.
  • OR where every subrule is present but marked as ignored evaluates to VALID, because all configured subrules are excluded from the final decision.
  • NOT with an ignored subrule evaluates to VALID.
  • NOT with no subrule fails.

Example of a rule marked as ignored:

{"type" : "STRING","operator" : "EQUALS","key" : "foo","value" : "bar","ignored" : true}

This is useful when you want to run a rule but not have it affect the evaluation of the tree.

For example, the following tree evaluated to VALID even thought one of the subrules of AND was INVALID:

{"result": "VALID","type": "AND","description": "Example of a Composite Rule with subrules ignored",// Optional, can be added to any rule in the tree"ignored": false,"rules": [{"result": "INVALID","type": "STRING","ignored": true,"operator": "EQUALS","key": "foo","expectedValue": "bar","actualValue": "not_bar"},{"result": "VALID","type": "EXISTS","ignored": false,"key": "foo","expectedValue": "<any>","actualValue": "not_bar"}]}

Json Deserialization with Jackson

You can use Jackson to deserialize rules from regola. These are the dependencies you will need:

<!-- pom.xml -->
<properties>
<jackson.version>2.13.1</jackson.version>
</properties>
<depdendencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
</depdendencies>

And this is the minimum setup for your ObjectMapper.

ObjectMappermapper = newObjectMapper()
.registerModule(newJavaTimeModule())
.registerModule(newRuleModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

Then you can deserialize a rule as such:

StringjsonRule = readRuleFromDataSource(); // This method will vary depending on your applicationRulerule = mapper.readValue(jsonRule, Rule.class);

New rules

  1. Create a new Rule by extending Rule (or one of the provided abstract classes)
publicclassYourRuleextendsRule {
publicYourRule() {
super("YOUR_TYPE"); // You should make sure this does not conflict with the type of any existing rule
}
@OverridepublicEvaluationResultevaluate(FactsResolverfactsResolver) {
returnnewEvaluationResult() {
privateResultresult = Result.MAYBE;
@OverridepublicRuleResultsnapshot() {
// Build and return a RuleResult
}
@OverridepublicCompletableFuture<Result> status() {
returnfacts.resolveFact(getKey())
.thenCompose(fact -> CompletableFuture.supplyAsync(() -> {
result = ... // perform the relevant checks for this rule and update the resultreturnresult;
}))
.exceptionally(throwable -> {
result = Result.FAILED;
returnresult;
});
}
};
}
}
  1. (Optional) If you need to parse rules from Json, then you must extend the RuleModule as such:
mapper.registerModule(newRuleModule()
.addRule("YOUR_TYPE", YourRule.class));
  1. Start using your new rule!

Programmatic Rule Creation

While transforming rules from Json is convenient, sometimes you may want to create rules programmatically.

Here is an example of how to do that:

SetRule<String> stringSetRule = newSetRule<>();
stringSetRule.setKey("MARKET_SEGMENT");
stringSetRule.setOperator(Operator.IN);
stringSetRule.setValues(Set.of("COM", "EDU"));
NumberRule<Integer> numberRule = newNumberRule<>();
numberRule.setKey("capacity");
numberRule.setOperator(Operator.EQUALS);
numberRule.setValue(3);
OrRuleorRule = newOrRule();
orRule.setRules(List.of(numberRule, stringSetRule));

Facts

Now that we have got some rules, we want to do something with it.

We do that by creating facts and supplying those to the evaluator which will check whether they satisfy our rule or not.

An example of a fact for the foo data point is:

varfact = newFact<>("foo", data -> "bar");

In regola, we can also write facts that use custom data fetchers to retrieve additional data:

Fact<Offer> fact = newFact<>("segment", CustomDataSources.OFFER, Offer::getSegment);

The FactsResolver

Defining facts, if using a custom data source, is not enough. We must tell our evaluator how to get those facts when a rule is run. This is done using a FactsResolver as shown below:

Map<DataSource, DataFetcher<?, YourContext>> dataFetchers = Map.of(
CustomDataSource.OFFER, offerDataFetcher,
...
);
FactsResolverfactsResolver = newSimpleFactsResolver<>(yourContext, dataFetchers);
factsResolver.addFact(newFact<>("segment", CustomDataSources.OFFER, Offer::getSegment));

Writing a Data Fetcher

Example of a data fetcher getting data over HTTP:

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
privatefinalOfferHttpConnectorofferHttpConnector;
publicOfferDataFetcher(OfferHttpConnectorofferHttpConnector) {
this.offerHttpConnector = offerHttpConnector;
}
@OverridepublicCompletableFuture<FetchResponse<Offer>> fetchResponse(YourContextcontext) {
GetOffersRequestrequest = buildRequest(context);
returnCompletableFuture.supplyAsync(() -> {
finalvarresponse = newFetchResponse<>();
response.setData(offerHttpConnector.getOffers(request)
.stream()
.findFirst());
returnresponse;
});
}
// Do not override this method if you do not want to cache results from this data fetcher.@OverridepublicStringcalculateRequestKey(YourContextcontext) {
GetOffersRequestrequest = buildRequest(context);
returnrequest.URL().toString();
}
privateRequestbuildRequest(YourContextcontext) {
returnnewGetOffersRequest(Set.of(requestContext.getOfferId()), Set.of());
}
}
publicclassYourContextimplementsContext {
privateStringofferId;
publicStringgetOfferId() {
returnofferId;
}
publicvoidsetOfferId(StringofferId) {
this.offerId = offerId;
}
}

The initialization of a data fetcher can be expensive, depending on your implementation, so it is recommended that data fetchers are re-used across multiple evaluations.

Caching in the Data Fetcher

The abstract DataFetcher uses caffeine to cache the results of the fetch results.

The default cache is setup with an expiry policy of 1 minute and max size of 1_000 entries, but a custom configuration can be setup by passing a DataFetcherConfiguration to the DataFetcher's constructor.

Custom Cache

If the default caffeine-based cache does not satisfy your requirements, you can provide your own implementation.

First, create a class for your custom cache:

classYourCustomCache<V> implementsDataFetcherCache<V> {
publicYourCustomCache(DataCacheConfigurationconfiguration) {
// (optional) construct your cache using the given configuration
}
@OverridepublicCompletableFuture<V> get(Stringkey, Function<String, CompletableFuture<V>> mappingFunction) {
// implement your "if cached, return; otherwise create, cache and return" cache function here
}
}

Then pass an instance of your custom cache to your data fetchers:

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
privatefinalOfferHttpConnectorofferHttpConnector;
publicOfferDataFetcher(OfferHttpConnectorofferHttpConnector, YourCustomCache<Offer> cache) {
super(cache);
this.offerHttpConnector = offerHttpConnector;
}
// rest of this data fetcher implementation
}

SLA handling within the Data Fetcher

By default, your custom data fetcher will not have any SLA failure handling. However, if needed you can specify an SLA on the fetch requestTime and override the whenFailingSlaFetchTime to handle any SLA failures.

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
publicOfferDataFetcher(/* other params */, longslaFetchTime) {
super(newDataFetcherConfiguration().setSlaFetchTime(slaFetchTime));
// any other initialization
}
@OverridepublicvoidwhenFailingSlaFetchTime(StringrequestKey, longslaFetchTime, doublerequestTime) {
// This method gets called whenever "requestTime > slaFetchTime"
}
}

Actions

Actions are used to define operations we want to perform after a rule is evaluated.

Basic usage

varaction = newAction()
.setDescription("Print 'Hello' if VALID")
.setOnCompletion((result, throwable, ruleResult) -> {
if (result == Result.VALID) {
System.out.println("Hello");
}
});
rule.setAction(action);

In this particular example, this action will be executed when the rule is evaluated and the result is VALID.

Chaining

It is possible to chain actions using the andThen method on the TriConsumer:

TriConsumer<Result, Throwable, RuleResult> actionConsumer = (result, throwable, ruleResult) -> {
if (result == Result.VALID) {
System.out.println("Hello");
}
};
// Chain the action to always print "World"actionConsumer = actionConsumer.andThen((result, throwable, ruleResult) -> System.out.println("World"));
varaction = newAction()
.setDescription("Print 'Hello' if VALID and the word 'World' irrespective of result")
.setOnCompletion(actionConsumer);
rule.setAction(action);

Understanding the Results of a Rule Evaluation

The following is an example of a result (pretty printed in json) returned upon evaluating a tree of rules:

{"result": "VALID","type": "AND","description": "Example of a Composite Rule",// Optional, can be added to any rule in the tree"ignored": false,"rules": [{"result": "VALID","type": "STRING","ignored": false,"operator": "EQUALS","key": "foo","expectedValue": "bar","actualValue": "bar"},{"result": "VALID","type": "OR","ignored": false,"rules": [{"result": "VALID","type": "EXISTS","ignored": false,"key": "waz","expectedValue": "<any>",// <any> is a special keyword matching any actual value for the EXISTS rule"actualValue": "wazab"},{"result": "MAYBE","type": "NUMBER","ignored": false,"operator": "EQUALS","key": "foobar","expectedValue": 21,// no actual value for MAYBE, since the rule was not evaluated due to short-circuiting}]}]}

The top-level "result" is the overall result of the rule.

  • If VALID, the subresults must be all VALID or MAYBE (i.e., rule did not need to be evaluated due to short-circuiting).
  • If not VALID, one or more of the subresults are: INVALID, OPERATION_NOT_SUPPORTED or FAILED.

Oh, by the way, what does "regola" mean?

regola is the italian word for rule.

About

A rule evaluator in Java

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

8 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

regola

Maven CentralCI

regola is a rule evaluator written in Java.

Disclaimer: This library is in development mode and there could be breaking changes as new versions are released.

Goals

  • be fast
  • be reusable and extensible
  • be well documented
  • have high test coverage
  • allow for efficient evaluation against data retrieved from external data sources
  • have Json rules conversion builtin in the library
  • run on Java 11+

Basic usage

  1. Add to your maven dependencies:
<dependency>
<groupId>com.adobe.abp</groupId>
<artifactId>regola</artifactId>
<version>0.0.24</version>
</dependency>
  1. Write a rule
varrule = newStringRule();
rule.setKey("MARKET_SEGMENT");
rule.setOperator(Operator.EQUALS);
rule.setValue("COM");
rule.setDescription("The market segment should be COM");

You could also use fluent setters:

varrule = newStringRule()
.setValue("COM")
.setOperator(Operator.EQUALS)
.setKey("MARKET_SEGMENT")
.setDescription("The market segment should be COM");

For some rules, you could also pass some parameters directly via the constructor for conciseness:

varrule = newStringRule("MARKET_SEGMENT", Operator.EQUALS, "COM");
rule.setDescription("The market segment should be COM");
  1. Define how data for the "MARKET_SEGMENT" key must be retrieved
varfactsResolver = newSimpleFactsResolver<>();
factsResolver.addFact(newFact<>("MARKET_SEGMENT", data -> "COM"));
  1. Evaluate
varevaluationResult = newEvaluator().evaluate(rule, factsResolver);
// The evaluation is an asynchronous process, so the associated CompletableFuture must be executed to get a result.// The following line returns the result value when complete, or throws an (unchecked) exception if completed exceptionally.evaluationResult.status().join(); varresult = evaluationResult.snapshot();

The result object will contain information on whether the evaluation was valid or not, plus any relevant information about the rule run.

  1. If we were to print the result as json
{"result" : "VALID","type" : "STRING","operator" : "EQUALS","key" : "MARKET_SEGMENT","description": "The market segment should be COM","expectedValue" : "COM","actualValue": "COM"}

Rules Vocabulary

Boolean Rules

Boolean rules are used to combine rules together.

And Rule

The "And Rule" is used to combine multiple rules together, where all the rules must evaluate to VALID for it to evaluate to VALID.

{"type" : "AND","rules" : [// list of other rules]}
ABA && B
VALIDVALIDVALID
VALIDINVALIDINVALID
VALIDMAYBEMAYBE
VALIDFAILEDFAILED
VALIDOPERATION_NOT_SUPPORTEDOPERATION_NOT_SUPPORTED

The AND rule is commutative: A && B = B && A.

An empty AND evaluates to VALID. This follows the identity of conjunction: with no subrules, there is nothing to invalidate the expression.

Or Rule

The "Or Rule" is used to combine multiple rules together, where at least one rule must evaluate to VALID for it to evaluate to VALID.

{"type" : "OR","rules" : [// list of other rules]}
ABA || B
VALIDanyVALID
INVALIDINVALIDINVALID

The order of precedence for non-VALID results is: FAILED, OPERATION_NOT_SUPPORTED, INVALID, MAYBE. So, for example: FAILED || INVALID == FAILED, while MAYBE || INVALID == INVALID and so on.

The OR rule is commutative: A || B = B || A.

An empty OR evaluates to INVALID. This follows the identity of disjunction: with no subrules, there is nothing that can make the expression valid.

Not Rule

The "Not Rule" is used to negate the result of another rule.

{"type" : "NOT","rule" : {// rule to negate}}
A!A
VALIDINVALID
INVALIDVALID
MAYBEMAYBE
FAILEDFAILED
OPERATION_NOT_SUPPORTEDOPERATION_NOT_SUPPORTED

A NOT rule must contain a non-null rule. If the operand is missing, the evaluation is treated as malformed configuration and fails.

Fact-only Rules

Exists Rule

The "Exists Rule" is used to check whether a fact exists or not.

{"type": "EXISTS","key": "foo"}
Some examples
KeyFactResult
"foo"{ "foo": "bar" }VALID
"foo"{ "foo": null }INVALID
"foo"{ "not_foo": "bar" }INVALID

Fixed Rules

CONSTANT Rule

The "Constant Rule" is used to always return the same result, regardless of the fact.

{"type": "CONSTANT","result": "VALID"// INVALID, MAYBE, FAILED, OPERATION_NOT_SUPPORTED}

Value-based Rules

These rules evaluate facts against a value set in the rule. When creating a value-based rule, you must also set an operator (e.g., EQUALS, GREATER_THAN, IN, etc...).

The relationship between facts, values and operators is: fact OPERATOR value.

So, for example a rule having value Cat, operator EQUALS, and evaluated against the fact Dog reads as: Dog EQUALS Cat (false). A rule having value Cat, operator CONTAINS, and evaluated against the fact [Dog, Bird, Cat] reads as: [Dog, Bird, Cat] CONTAINS Cat (true).

Number Rule

The "Number Rule" is used to evaluate facts against a number. The number can be an integer or a double.

{"type": "NUMBER","operator": "GREATER_THAN","key": "foo","value": 7// you can also have 7.0}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS, DIVISIBLE_BY

Integer-Double comparisons between the rule's value and the data provided by the fact work for all operators except CONTAINS.

The DIVISIBLE_BY operator is valid only for integer values since divisibility is not well-defined for floating-point numbers.

Some examples
Rule ValueOperatorFactResult
7EQUALS7VALID
7GREATER_THAN7INVALID
7GREATER_THAN8VALID
7GREATER_THAN_EQUAL7VALID
7GREATER_THAN_EQUAL7.5VALID
7.4GREATER_THAN7.5VALID
7.5GREATER_THAN7.5INVALID
7CONTAINS[ 6, 7, 8]VALID
7CONTAINS[ 6, 8]INVALID
7CONTAINS[ 6, 7.0, 8]INVALID
7.0CONTAINS[ 6, 7.0, 8]VALID
7DIVISIBLE_BY7VALID
7DIVISIBLE_BY8INVALID
0DIVISIBLE_BY8FAILED
7DIVISIBLE_BY0VALID
any numbersupported operatornullINVALID
nullsupported operatorany numberINVALID

When using the CONTAINS operator, the Fact must be a Set of numbers.

String Rule

{"type": "STRING","operator": "EQUALS","key": "foo","value": "bar"}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS

Comparisons are case-sensitive.

Some examples
Rule ValueOperatorFactResult
"bar"EQUALS"bar"VALID
"bar"EQUALS"BAR"INVALID
"bar"EQUALS"baz"INVALID
"bar"GREATER_THAN"car"VALID
"bar"GREATER_THAN_EQUAL"bar"VALID
"bar"GREATER_THAN"are"INVALID
"bar"STARTS_WITH"bar"VALID
"bar"STARTS_WITH"barfoo"VALID
"bar"STARTS_WITH"foobar"INVALID
"bar"ENDS_WITH"bar"VALID
"bar"ENDS_WITH"foobar"VALID
"bar"ENDS_WITH"barfoo"INVALID
"bar"CONTAINS["are", "bar", "baz"]VALID
"bar"CONTAINS["are", "baz"]INVALID
any stringsupported operatornullINVALID
nullsupported operatorany stringINVALID

Set Rule

A "Set rule" is a rule that evaluates a fact against a set of values. The set rule has two operators: IN and INTERSECTS.

IN: evaluates to VALID if the fact is a subset of the rule's value. INTERSECTS: evaluates to VALID if the fact and the rule's value have at least one item in common.

{"type": "SET","operator": "IN","key": "foo","values": ["bar","baz"]}

Supported operators: IN, INTERSECTS

String comparisons are case-sensitive.

Some examples
Rule ValueOperatorFactResult
["bar", "baz"]IN"bar"VALID
["bar", "baz"]IN"waz"INVALID
[1, 2, 3]IN2VALID
[1, 2, 3]IN4INVALID
["bar", "baz"]IN[]INVALID
[]IN[]VALID
[]IN["bar"]INVALID
["bar", "baz"]IN["bar"]VALID
["bar", "baz"]IN["waz"]INVALID
["bar", "baz"]IN["bar", "waz"]INVALID
["bar", "baz"]INTERSECTS"bar"VALID
["bar", "baz"]INTERSECTS"waz"INVALID
["bar", "baz"]INTERSECTS["bar", "waz"]VALID
["bar", "baz"]INTERSECTS["wiz", "waz"]INVALID
["bar", "baz"]INTERSECTS[]INVALID
[]INTERSECTS[]INVALID
any setany operatornullINVALID
Comparing complex objects

The Set rule can be used on more complex objects too.

SetRule<Patient> setRule = newSetRule<>();
setRule.setKey("PATIENT");
setRule.setOperator(Operator.IN);
setRule.setValues(bob, alice);

Where bob and alice are instances of Patient. You must also make sure that the Patient class overrides the equals and hashCode methods. Regola expects the equals method to perform a commutative comparison between objects.

Also note that regola does not support JSON serialization/deserialization for SET rules with complex objects.

Date Rule

The "Date Rule" is a rule that evaluates a fact against a date value.

{"type" : "DATE","operator" : "GREATER_THAN","key" : "foo","value" : "2021-07-07T12:30:00Z"}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS

Dates must be parsable to an OffsetDateTime:

A date-time with an offset from UTC/Greenwich in the ISO-8601 calendar system

Some examples
Rule ValueOperatorFactResult
"2021-07-07T12:30:00Z"EQUALS"2021-07-07T12:30:00Z"VALID
"2021-07-07T12:30:00Z"GREATER_THAN"2022-07-07T12:30:00Z"VALID
"2021-07-07T12:30:00Z"GREATER_THAN"2020-07-07T12:30:00Z"INVALID
"2021-07-07T12:30:00Z"LESS_THAN"2020-07-07T12:30:00Z"VALID
any datesupported operatornullINVALID
nullsupported operatorany dateINVALID

Null Rule

A "Null Rule" is a rule that evaluates a fact against a null value.

{"type": "NULL","key": "foo"}
Some examples
KeyFactResult
"foo"nullVALID
"foo""foo"INVALID

Combining Rules

Rules can be combined using the boolean rules: AND, OR, NOT.

{"type" : "AND","rules" : [{"type" : "STRING","operator" : "EQUALS","key" : "foo","value" : "bar"},{"type" : "OR","rules" : [{"type" : "EXISTS","key" : "waz"},{"type" : "NUMBER","operator" : "EQUALS","key" : "foobar","value" : 21}]}]}

Ignoring results

Rules can be set to be ignored, so that the evaluation of AND/OR/NOT rules does not take them into account.

This matters for empty or effectively-empty boolean rules too:

  • AND with no subrules evaluates to VALID.
  • OR with no subrules evaluates to INVALID.
  • OR where every subrule is present but marked as ignored evaluates to VALID, because all configured subrules are excluded from the final decision.
  • NOT with an ignored subrule evaluates to VALID.
  • NOT with no subrule fails.

Example of a rule marked as ignored:

{"type" : "STRING","operator" : "EQUALS","key" : "foo","value" : "bar","ignored" : true}

This is useful when you want to run a rule but not have it affect the evaluation of the tree.

For example, the following tree evaluated to VALID even thought one of the subrules of AND was INVALID:

{"result": "VALID","type": "AND","description": "Example of a Composite Rule with subrules ignored",// Optional, can be added to any rule in the tree"ignored": false,"rules": [{"result": "INVALID","type": "STRING","ignored": true,"operator": "EQUALS","key": "foo","expectedValue": "bar","actualValue": "not_bar"},{"result": "VALID","type": "EXISTS","ignored": false,"key": "foo","expectedValue": "<any>","actualValue": "not_bar"}]}

Json Deserialization with Jackson

You can use Jackson to deserialize rules from regola. These are the dependencies you will need:

<!-- pom.xml -->
<properties>
<jackson.version>2.13.1</jackson.version>
</properties>
<depdendencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
</depdendencies>

And this is the minimum setup for your ObjectMapper.

ObjectMappermapper = newObjectMapper()
.registerModule(newJavaTimeModule())
.registerModule(newRuleModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

Then you can deserialize a rule as such:

StringjsonRule = readRuleFromDataSource(); // This method will vary depending on your applicationRulerule = mapper.readValue(jsonRule, Rule.class);

New rules

  1. Create a new Rule by extending Rule (or one of the provided abstract classes)
publicclassYourRuleextendsRule {
publicYourRule() {
super("YOUR_TYPE"); // You should make sure this does not conflict with the type of any existing rule
}
@OverridepublicEvaluationResultevaluate(FactsResolverfactsResolver) {
returnnewEvaluationResult() {
privateResultresult = Result.MAYBE;
@OverridepublicRuleResultsnapshot() {
// Build and return a RuleResult
}
@OverridepublicCompletableFuture<Result> status() {
returnfacts.resolveFact(getKey())
.thenCompose(fact -> CompletableFuture.supplyAsync(() -> {
result = ... // perform the relevant checks for this rule and update the resultreturnresult;
}))
.exceptionally(throwable -> {
result = Result.FAILED;
returnresult;
});
}
};
}
}
  1. (Optional) If you need to parse rules from Json, then you must extend the RuleModule as such:
mapper.registerModule(newRuleModule()
.addRule("YOUR_TYPE", YourRule.class));
  1. Start using your new rule!

Programmatic Rule Creation

While transforming rules from Json is convenient, sometimes you may want to create rules programmatically.

Here is an example of how to do that:

SetRule<String> stringSetRule = newSetRule<>();
stringSetRule.setKey("MARKET_SEGMENT");
stringSetRule.setOperator(Operator.IN);
stringSetRule.setValues(Set.of("COM", "EDU"));
NumberRule<Integer> numberRule = newNumberRule<>();
numberRule.setKey("capacity");
numberRule.setOperator(Operator.EQUALS);
numberRule.setValue(3);
OrRuleorRule = newOrRule();
orRule.setRules(List.of(numberRule, stringSetRule));

Facts

Now that we have got some rules, we want to do something with it.

We do that by creating facts and supplying those to the evaluator which will check whether they satisfy our rule or not.

An example of a fact for the foo data point is:

varfact = newFact<>("foo", data -> "bar");

In regola, we can also write facts that use custom data fetchers to retrieve additional data:

Fact<Offer> fact = newFact<>("segment", CustomDataSources.OFFER, Offer::getSegment);

The FactsResolver

Defining facts, if using a custom data source, is not enough. We must tell our evaluator how to get those facts when a rule is run. This is done using a FactsResolver as shown below:

Map<DataSource, DataFetcher<?, YourContext>> dataFetchers = Map.of(
CustomDataSource.OFFER, offerDataFetcher,
...
);
FactsResolverfactsResolver = newSimpleFactsResolver<>(yourContext, dataFetchers);
factsResolver.addFact(newFact<>("segment", CustomDataSources.OFFER, Offer::getSegment));

Writing a Data Fetcher

Example of a data fetcher getting data over HTTP:

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
privatefinalOfferHttpConnectorofferHttpConnector;
publicOfferDataFetcher(OfferHttpConnectorofferHttpConnector) {
this.offerHttpConnector = offerHttpConnector;
}
@OverridepublicCompletableFuture<FetchResponse<Offer>> fetchResponse(YourContextcontext) {
GetOffersRequestrequest = buildRequest(context);
returnCompletableFuture.supplyAsync(() -> {
finalvarresponse = newFetchResponse<>();
response.setData(offerHttpConnector.getOffers(request)
.stream()
.findFirst());
returnresponse;
});
}
// Do not override this method if you do not want to cache results from this data fetcher.@OverridepublicStringcalculateRequestKey(YourContextcontext) {
GetOffersRequestrequest = buildRequest(context);
returnrequest.URL().toString();
}
privateRequestbuildRequest(YourContextcontext) {
returnnewGetOffersRequest(Set.of(requestContext.getOfferId()), Set.of());
}
}
publicclassYourContextimplementsContext {
privateStringofferId;
publicStringgetOfferId() {
returnofferId;
}
publicvoidsetOfferId(StringofferId) {
this.offerId = offerId;
}
}

The initialization of a data fetcher can be expensive, depending on your implementation, so it is recommended that data fetchers are re-used across multiple evaluations.

Caching in the Data Fetcher

The abstract DataFetcher uses caffeine to cache the results of the fetch results.

The default cache is setup with an expiry policy of 1 minute and max size of 1_000 entries, but a custom configuration can be setup by passing a DataFetcherConfiguration to the DataFetcher's constructor.

Custom Cache

If the default caffeine-based cache does not satisfy your requirements, you can provide your own implementation.

First, create a class for your custom cache:

classYourCustomCache<V> implementsDataFetcherCache<V> {
publicYourCustomCache(DataCacheConfigurationconfiguration) {
// (optional) construct your cache using the given configuration
}
@OverridepublicCompletableFuture<V> get(Stringkey, Function<String, CompletableFuture<V>> mappingFunction) {
// implement your "if cached, return; otherwise create, cache and return" cache function here
}
}

Then pass an instance of your custom cache to your data fetchers:

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
privatefinalOfferHttpConnectorofferHttpConnector;
publicOfferDataFetcher(OfferHttpConnectorofferHttpConnector, YourCustomCache<Offer> cache) {
super(cache);
this.offerHttpConnector = offerHttpConnector;
}
// rest of this data fetcher implementation
}

SLA handling within the Data Fetcher

By default, your custom data fetcher will not have any SLA failure handling. However, if needed you can specify an SLA on the fetch requestTime and override the whenFailingSlaFetchTime to handle any SLA failures.

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
publicOfferDataFetcher(/* other params */, longslaFetchTime) {
super(newDataFetcherConfiguration().setSlaFetchTime(slaFetchTime));
// any other initialization
}
@OverridepublicvoidwhenFailingSlaFetchTime(StringrequestKey, longslaFetchTime, doublerequestTime) {
// This method gets called whenever "requestTime > slaFetchTime"
}
}

Actions

Actions are used to define operations we want to perform after a rule is evaluated.

Basic usage

varaction = newAction()
.setDescription("Print 'Hello' if VALID")
.setOnCompletion((result, throwable, ruleResult) -> {
if (result == Result.VALID) {
System.out.println("Hello");
}
});
rule.setAction(action);

In this particular example, this action will be executed when the rule is evaluated and the result is VALID.

Chaining

It is possible to chain actions using the andThen method on the TriConsumer:

TriConsumer<Result, Throwable, RuleResult> actionConsumer = (result, throwable, ruleResult) -> {
if (result == Result.VALID) {
System.out.println("Hello");
}
};
// Chain the action to always print "World"actionConsumer = actionConsumer.andThen((result, throwable, ruleResult) -> System.out.println("World"));
varaction = newAction()
.setDescription("Print 'Hello' if VALID and the word 'World' irrespective of result")
.setOnCompletion(actionConsumer);
rule.setAction(action);

Understanding the Results of a Rule Evaluation

The following is an example of a result (pretty printed in json) returned upon evaluating a tree of rules:

{"result": "VALID","type": "AND","description": "Example of a Composite Rule",// Optional, can be added to any rule in the tree"ignored": false,"rules": [{"result": "VALID","type": "STRING","ignored": false,"operator": "EQUALS","key": "foo","expectedValue": "bar","actualValue": "bar"},{"result": "VALID","type": "OR","ignored": false,"rules": [{"result": "VALID","type": "EXISTS","ignored": false,"key": "waz","expectedValue": "<any>",// <any> is a special keyword matching any actual value for the EXISTS rule"actualValue": "wazab"},{"result": "MAYBE","type": "NUMBER","ignored": false,"operator": "EQUALS","key": "foobar","expectedValue": 21,// no actual value for MAYBE, since the rule was not evaluated due to short-circuiting}]}]}

The top-level "result" is the overall result of the rule.

  • If VALID, the subresults must be all VALID or MAYBE (i.e., rule did not need to be evaluated due to short-circuiting).
  • If not VALID, one or more of the subresults are: INVALID, OPERATION_NOT_SUPPORTED or FAILED.

Oh, by the way, what does "regola" mean?

regola is the italian word for rule.

About

A rule evaluator in Java

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

8 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

regola

Maven CentralCI

regola is a rule evaluator written in Java.

Disclaimer: This library is in development mode and there could be breaking changes as new versions are released.

Goals

  • be fast
  • be reusable and extensible
  • be well documented
  • have high test coverage
  • allow for efficient evaluation against data retrieved from external data sources
  • have Json rules conversion builtin in the library
  • run on Java 11+

Basic usage

  1. Add to your maven dependencies:
<dependency>
<groupId>com.adobe.abp</groupId>
<artifactId>regola</artifactId>
<version>0.0.24</version>
</dependency>
  1. Write a rule
varrule = newStringRule();
rule.setKey("MARKET_SEGMENT");
rule.setOperator(Operator.EQUALS);
rule.setValue("COM");
rule.setDescription("The market segment should be COM");

You could also use fluent setters:

varrule = newStringRule()
.setValue("COM")
.setOperator(Operator.EQUALS)
.setKey("MARKET_SEGMENT")
.setDescription("The market segment should be COM");

For some rules, you could also pass some parameters directly via the constructor for conciseness:

varrule = newStringRule("MARKET_SEGMENT", Operator.EQUALS, "COM");
rule.setDescription("The market segment should be COM");
  1. Define how data for the "MARKET_SEGMENT" key must be retrieved
varfactsResolver = newSimpleFactsResolver<>();
factsResolver.addFact(newFact<>("MARKET_SEGMENT", data -> "COM"));
  1. Evaluate
varevaluationResult = newEvaluator().evaluate(rule, factsResolver);
// The evaluation is an asynchronous process, so the associated CompletableFuture must be executed to get a result.// The following line returns the result value when complete, or throws an (unchecked) exception if completed exceptionally.evaluationResult.status().join(); varresult = evaluationResult.snapshot();

The result object will contain information on whether the evaluation was valid or not, plus any relevant information about the rule run.

  1. If we were to print the result as json
{"result" : "VALID","type" : "STRING","operator" : "EQUALS","key" : "MARKET_SEGMENT","description": "The market segment should be COM","expectedValue" : "COM","actualValue": "COM"}

Rules Vocabulary

Boolean Rules

Boolean rules are used to combine rules together.

And Rule

The "And Rule" is used to combine multiple rules together, where all the rules must evaluate to VALID for it to evaluate to VALID.

{"type" : "AND","rules" : [// list of other rules]}
ABA && B
VALIDVALIDVALID
VALIDINVALIDINVALID
VALIDMAYBEMAYBE
VALIDFAILEDFAILED
VALIDOPERATION_NOT_SUPPORTEDOPERATION_NOT_SUPPORTED

The AND rule is commutative: A && B = B && A.

An empty AND evaluates to VALID. This follows the identity of conjunction: with no subrules, there is nothing to invalidate the expression.

Or Rule

The "Or Rule" is used to combine multiple rules together, where at least one rule must evaluate to VALID for it to evaluate to VALID.

{"type" : "OR","rules" : [// list of other rules]}
ABA || B
VALIDanyVALID
INVALIDINVALIDINVALID

The order of precedence for non-VALID results is: FAILED, OPERATION_NOT_SUPPORTED, INVALID, MAYBE. So, for example: FAILED || INVALID == FAILED, while MAYBE || INVALID == INVALID and so on.

The OR rule is commutative: A || B = B || A.

An empty OR evaluates to INVALID. This follows the identity of disjunction: with no subrules, there is nothing that can make the expression valid.

Not Rule

The "Not Rule" is used to negate the result of another rule.

{"type" : "NOT","rule" : {// rule to negate}}
A!A
VALIDINVALID
INVALIDVALID
MAYBEMAYBE
FAILEDFAILED
OPERATION_NOT_SUPPORTEDOPERATION_NOT_SUPPORTED

A NOT rule must contain a non-null rule. If the operand is missing, the evaluation is treated as malformed configuration and fails.

Fact-only Rules

Exists Rule

The "Exists Rule" is used to check whether a fact exists or not.

{"type": "EXISTS","key": "foo"}
Some examples
KeyFactResult
"foo"{ "foo": "bar" }VALID
"foo"{ "foo": null }INVALID
"foo"{ "not_foo": "bar" }INVALID

Fixed Rules

CONSTANT Rule

The "Constant Rule" is used to always return the same result, regardless of the fact.

{"type": "CONSTANT","result": "VALID"// INVALID, MAYBE, FAILED, OPERATION_NOT_SUPPORTED}

Value-based Rules

These rules evaluate facts against a value set in the rule. When creating a value-based rule, you must also set an operator (e.g., EQUALS, GREATER_THAN, IN, etc...).

The relationship between facts, values and operators is: fact OPERATOR value.

So, for example a rule having value Cat, operator EQUALS, and evaluated against the fact Dog reads as: Dog EQUALS Cat (false). A rule having value Cat, operator CONTAINS, and evaluated against the fact [Dog, Bird, Cat] reads as: [Dog, Bird, Cat] CONTAINS Cat (true).

Number Rule

The "Number Rule" is used to evaluate facts against a number. The number can be an integer or a double.

{"type": "NUMBER","operator": "GREATER_THAN","key": "foo","value": 7// you can also have 7.0}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS, DIVISIBLE_BY

Integer-Double comparisons between the rule's value and the data provided by the fact work for all operators except CONTAINS.

The DIVISIBLE_BY operator is valid only for integer values since divisibility is not well-defined for floating-point numbers.

Some examples
Rule ValueOperatorFactResult
7EQUALS7VALID
7GREATER_THAN7INVALID
7GREATER_THAN8VALID
7GREATER_THAN_EQUAL7VALID
7GREATER_THAN_EQUAL7.5VALID
7.4GREATER_THAN7.5VALID
7.5GREATER_THAN7.5INVALID
7CONTAINS[ 6, 7, 8]VALID
7CONTAINS[ 6, 8]INVALID
7CONTAINS[ 6, 7.0, 8]INVALID
7.0CONTAINS[ 6, 7.0, 8]VALID
7DIVISIBLE_BY7VALID
7DIVISIBLE_BY8INVALID
0DIVISIBLE_BY8FAILED
7DIVISIBLE_BY0VALID
any numbersupported operatornullINVALID
nullsupported operatorany numberINVALID

When using the CONTAINS operator, the Fact must be a Set of numbers.

String Rule

{"type": "STRING","operator": "EQUALS","key": "foo","value": "bar"}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS

Comparisons are case-sensitive.

Some examples
Rule ValueOperatorFactResult
"bar"EQUALS"bar"VALID
"bar"EQUALS"BAR"INVALID
"bar"EQUALS"baz"INVALID
"bar"GREATER_THAN"car"VALID
"bar"GREATER_THAN_EQUAL"bar"VALID
"bar"GREATER_THAN"are"INVALID
"bar"STARTS_WITH"bar"VALID
"bar"STARTS_WITH"barfoo"VALID
"bar"STARTS_WITH"foobar"INVALID
"bar"ENDS_WITH"bar"VALID
"bar"ENDS_WITH"foobar"VALID
"bar"ENDS_WITH"barfoo"INVALID
"bar"CONTAINS["are", "bar", "baz"]VALID
"bar"CONTAINS["are", "baz"]INVALID
any stringsupported operatornullINVALID
nullsupported operatorany stringINVALID

Set Rule

A "Set rule" is a rule that evaluates a fact against a set of values. The set rule has two operators: IN and INTERSECTS.

IN: evaluates to VALID if the fact is a subset of the rule's value. INTERSECTS: evaluates to VALID if the fact and the rule's value have at least one item in common.

{"type": "SET","operator": "IN","key": "foo","values": ["bar","baz"]}

Supported operators: IN, INTERSECTS

String comparisons are case-sensitive.

Some examples
Rule ValueOperatorFactResult
["bar", "baz"]IN"bar"VALID
["bar", "baz"]IN"waz"INVALID
[1, 2, 3]IN2VALID
[1, 2, 3]IN4INVALID
["bar", "baz"]IN[]INVALID
[]IN[]VALID
[]IN["bar"]INVALID
["bar", "baz"]IN["bar"]VALID
["bar", "baz"]IN["waz"]INVALID
["bar", "baz"]IN["bar", "waz"]INVALID
["bar", "baz"]INTERSECTS"bar"VALID
["bar", "baz"]INTERSECTS"waz"INVALID
["bar", "baz"]INTERSECTS["bar", "waz"]VALID
["bar", "baz"]INTERSECTS["wiz", "waz"]INVALID
["bar", "baz"]INTERSECTS[]INVALID
[]INTERSECTS[]INVALID
any setany operatornullINVALID
Comparing complex objects

The Set rule can be used on more complex objects too.

SetRule<Patient> setRule = newSetRule<>();
setRule.setKey("PATIENT");
setRule.setOperator(Operator.IN);
setRule.setValues(bob, alice);

Where bob and alice are instances of Patient. You must also make sure that the Patient class overrides the equals and hashCode methods. Regola expects the equals method to perform a commutative comparison between objects.

Also note that regola does not support JSON serialization/deserialization for SET rules with complex objects.

Date Rule

The "Date Rule" is a rule that evaluates a fact against a date value.

{"type" : "DATE","operator" : "GREATER_THAN","key" : "foo","value" : "2021-07-07T12:30:00Z"}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS

Dates must be parsable to an OffsetDateTime:

A date-time with an offset from UTC/Greenwich in the ISO-8601 calendar system

Some examples
Rule ValueOperatorFactResult
"2021-07-07T12:30:00Z"EQUALS"2021-07-07T12:30:00Z"VALID
"2021-07-07T12:30:00Z"GREATER_THAN"2022-07-07T12:30:00Z"VALID
"2021-07-07T12:30:00Z"GREATER_THAN"2020-07-07T12:30:00Z"INVALID
"2021-07-07T12:30:00Z"LESS_THAN"2020-07-07T12:30:00Z"VALID
any datesupported operatornullINVALID
nullsupported operatorany dateINVALID

Null Rule

A "Null Rule" is a rule that evaluates a fact against a null value.

{"type": "NULL","key": "foo"}
Some examples
KeyFactResult
"foo"nullVALID
"foo""foo"INVALID

Combining Rules

Rules can be combined using the boolean rules: AND, OR, NOT.

{"type" : "AND","rules" : [{"type" : "STRING","operator" : "EQUALS","key" : "foo","value" : "bar"},{"type" : "OR","rules" : [{"type" : "EXISTS","key" : "waz"},{"type" : "NUMBER","operator" : "EQUALS","key" : "foobar","value" : 21}]}]}

Ignoring results

Rules can be set to be ignored, so that the evaluation of AND/OR/NOT rules does not take them into account.

This matters for empty or effectively-empty boolean rules too:

  • AND with no subrules evaluates to VALID.
  • OR with no subrules evaluates to INVALID.
  • OR where every subrule is present but marked as ignored evaluates to VALID, because all configured subrules are excluded from the final decision.
  • NOT with an ignored subrule evaluates to VALID.
  • NOT with no subrule fails.

Example of a rule marked as ignored:

{"type" : "STRING","operator" : "EQUALS","key" : "foo","value" : "bar","ignored" : true}

This is useful when you want to run a rule but not have it affect the evaluation of the tree.

For example, the following tree evaluated to VALID even thought one of the subrules of AND was INVALID:

{"result": "VALID","type": "AND","description": "Example of a Composite Rule with subrules ignored",// Optional, can be added to any rule in the tree"ignored": false,"rules": [{"result": "INVALID","type": "STRING","ignored": true,"operator": "EQUALS","key": "foo","expectedValue": "bar","actualValue": "not_bar"},{"result": "VALID","type": "EXISTS","ignored": false,"key": "foo","expectedValue": "<any>","actualValue": "not_bar"}]}

Json Deserialization with Jackson

You can use Jackson to deserialize rules from regola. These are the dependencies you will need:

<!-- pom.xml -->
<properties>
<jackson.version>2.13.1</jackson.version>
</properties>
<depdendencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
</depdendencies>

And this is the minimum setup for your ObjectMapper.

ObjectMappermapper = newObjectMapper()
.registerModule(newJavaTimeModule())
.registerModule(newRuleModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

Then you can deserialize a rule as such:

StringjsonRule = readRuleFromDataSource(); // This method will vary depending on your applicationRulerule = mapper.readValue(jsonRule, Rule.class);

New rules

  1. Create a new Rule by extending Rule (or one of the provided abstract classes)
publicclassYourRuleextendsRule {
publicYourRule() {
super("YOUR_TYPE"); // You should make sure this does not conflict with the type of any existing rule
}
@OverridepublicEvaluationResultevaluate(FactsResolverfactsResolver) {
returnnewEvaluationResult() {
privateResultresult = Result.MAYBE;
@OverridepublicRuleResultsnapshot() {
// Build and return a RuleResult
}
@OverridepublicCompletableFuture<Result> status() {
returnfacts.resolveFact(getKey())
.thenCompose(fact -> CompletableFuture.supplyAsync(() -> {
result = ... // perform the relevant checks for this rule and update the resultreturnresult;
}))
.exceptionally(throwable -> {
result = Result.FAILED;
returnresult;
});
}
};
}
}
  1. (Optional) If you need to parse rules from Json, then you must extend the RuleModule as such:
mapper.registerModule(newRuleModule()
.addRule("YOUR_TYPE", YourRule.class));
  1. Start using your new rule!

Programmatic Rule Creation

While transforming rules from Json is convenient, sometimes you may want to create rules programmatically.

Here is an example of how to do that:

SetRule<String> stringSetRule = newSetRule<>();
stringSetRule.setKey("MARKET_SEGMENT");
stringSetRule.setOperator(Operator.IN);
stringSetRule.setValues(Set.of("COM", "EDU"));
NumberRule<Integer> numberRule = newNumberRule<>();
numberRule.setKey("capacity");
numberRule.setOperator(Operator.EQUALS);
numberRule.setValue(3);
OrRuleorRule = newOrRule();
orRule.setRules(List.of(numberRule, stringSetRule));

Facts

Now that we have got some rules, we want to do something with it.

We do that by creating facts and supplying those to the evaluator which will check whether they satisfy our rule or not.

An example of a fact for the foo data point is:

varfact = newFact<>("foo", data -> "bar");

In regola, we can also write facts that use custom data fetchers to retrieve additional data:

Fact<Offer> fact = newFact<>("segment", CustomDataSources.OFFER, Offer::getSegment);

The FactsResolver

Defining facts, if using a custom data source, is not enough. We must tell our evaluator how to get those facts when a rule is run. This is done using a FactsResolver as shown below:

Map<DataSource, DataFetcher<?, YourContext>> dataFetchers = Map.of(
CustomDataSource.OFFER, offerDataFetcher,
...
);
FactsResolverfactsResolver = newSimpleFactsResolver<>(yourContext, dataFetchers);
factsResolver.addFact(newFact<>("segment", CustomDataSources.OFFER, Offer::getSegment));

Writing a Data Fetcher

Example of a data fetcher getting data over HTTP:

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
privatefinalOfferHttpConnectorofferHttpConnector;
publicOfferDataFetcher(OfferHttpConnectorofferHttpConnector) {
this.offerHttpConnector = offerHttpConnector;
}
@OverridepublicCompletableFuture<FetchResponse<Offer>> fetchResponse(YourContextcontext) {
GetOffersRequestrequest = buildRequest(context);
returnCompletableFuture.supplyAsync(() -> {
finalvarresponse = newFetchResponse<>();
response.setData(offerHttpConnector.getOffers(request)
.stream()
.findFirst());
returnresponse;
});
}
// Do not override this method if you do not want to cache results from this data fetcher.@OverridepublicStringcalculateRequestKey(YourContextcontext) {
GetOffersRequestrequest = buildRequest(context);
returnrequest.URL().toString();
}
privateRequestbuildRequest(YourContextcontext) {
returnnewGetOffersRequest(Set.of(requestContext.getOfferId()), Set.of());
}
}
publicclassYourContextimplementsContext {
privateStringofferId;
publicStringgetOfferId() {
returnofferId;
}
publicvoidsetOfferId(StringofferId) {
this.offerId = offerId;
}
}

The initialization of a data fetcher can be expensive, depending on your implementation, so it is recommended that data fetchers are re-used across multiple evaluations.

Caching in the Data Fetcher

The abstract DataFetcher uses caffeine to cache the results of the fetch results.

The default cache is setup with an expiry policy of 1 minute and max size of 1_000 entries, but a custom configuration can be setup by passing a DataFetcherConfiguration to the DataFetcher's constructor.

Custom Cache

If the default caffeine-based cache does not satisfy your requirements, you can provide your own implementation.

First, create a class for your custom cache:

classYourCustomCache<V> implementsDataFetcherCache<V> {
publicYourCustomCache(DataCacheConfigurationconfiguration) {
// (optional) construct your cache using the given configuration
}
@OverridepublicCompletableFuture<V> get(Stringkey, Function<String, CompletableFuture<V>> mappingFunction) {
// implement your "if cached, return; otherwise create, cache and return" cache function here
}
}

Then pass an instance of your custom cache to your data fetchers:

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
privatefinalOfferHttpConnectorofferHttpConnector;
publicOfferDataFetcher(OfferHttpConnectorofferHttpConnector, YourCustomCache<Offer> cache) {
super(cache);
this.offerHttpConnector = offerHttpConnector;
}
// rest of this data fetcher implementation
}

SLA handling within the Data Fetcher

By default, your custom data fetcher will not have any SLA failure handling. However, if needed you can specify an SLA on the fetch requestTime and override the whenFailingSlaFetchTime to handle any SLA failures.

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
publicOfferDataFetcher(/* other params */, longslaFetchTime) {
super(newDataFetcherConfiguration().setSlaFetchTime(slaFetchTime));
// any other initialization
}
@OverridepublicvoidwhenFailingSlaFetchTime(StringrequestKey, longslaFetchTime, doublerequestTime) {
// This method gets called whenever "requestTime > slaFetchTime"
}
}

Actions

Actions are used to define operations we want to perform after a rule is evaluated.

Basic usage

varaction = newAction()
.setDescription("Print 'Hello' if VALID")
.setOnCompletion((result, throwable, ruleResult) -> {
if (result == Result.VALID) {
System.out.println("Hello");
}
});
rule.setAction(action);

In this particular example, this action will be executed when the rule is evaluated and the result is VALID.

Chaining

It is possible to chain actions using the andThen method on the TriConsumer:

TriConsumer<Result, Throwable, RuleResult> actionConsumer = (result, throwable, ruleResult) -> {
if (result == Result.VALID) {
System.out.println("Hello");
}
};
// Chain the action to always print "World"actionConsumer = actionConsumer.andThen((result, throwable, ruleResult) -> System.out.println("World"));
varaction = newAction()
.setDescription("Print 'Hello' if VALID and the word 'World' irrespective of result")
.setOnCompletion(actionConsumer);
rule.setAction(action);

Understanding the Results of a Rule Evaluation

The following is an example of a result (pretty printed in json) returned upon evaluating a tree of rules:

{"result": "VALID","type": "AND","description": "Example of a Composite Rule",// Optional, can be added to any rule in the tree"ignored": false,"rules": [{"result": "VALID","type": "STRING","ignored": false,"operator": "EQUALS","key": "foo","expectedValue": "bar","actualValue": "bar"},{"result": "VALID","type": "OR","ignored": false,"rules": [{"result": "VALID","type": "EXISTS","ignored": false,"key": "waz","expectedValue": "<any>",// <any> is a special keyword matching any actual value for the EXISTS rule"actualValue": "wazab"},{"result": "MAYBE","type": "NUMBER","ignored": false,"operator": "EQUALS","key": "foobar","expectedValue": 21,// no actual value for MAYBE, since the rule was not evaluated due to short-circuiting}]}]}

The top-level "result" is the overall result of the rule.

  • If VALID, the subresults must be all VALID or MAYBE (i.e., rule did not need to be evaluated due to short-circuiting).
  • If not VALID, one or more of the subresults are: INVALID, OPERATION_NOT_SUPPORTED or FAILED.

Oh, by the way, what does "regola" mean?

regola is the italian word for rule.

About

A rule evaluator in Java

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

8 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

regola

Maven CentralCI

regola is a rule evaluator written in Java.

Disclaimer: This library is in development mode and there could be breaking changes as new versions are released.

Goals

  • be fast
  • be reusable and extensible
  • be well documented
  • have high test coverage
  • allow for efficient evaluation against data retrieved from external data sources
  • have Json rules conversion builtin in the library
  • run on Java 11+

Basic usage

  1. Add to your maven dependencies:
<dependency>
<groupId>com.adobe.abp</groupId>
<artifactId>regola</artifactId>
<version>0.0.24</version>
</dependency>
  1. Write a rule
varrule = newStringRule();
rule.setKey("MARKET_SEGMENT");
rule.setOperator(Operator.EQUALS);
rule.setValue("COM");
rule.setDescription("The market segment should be COM");

You could also use fluent setters:

varrule = newStringRule()
.setValue("COM")
.setOperator(Operator.EQUALS)
.setKey("MARKET_SEGMENT")
.setDescription("The market segment should be COM");

For some rules, you could also pass some parameters directly via the constructor for conciseness:

varrule = newStringRule("MARKET_SEGMENT", Operator.EQUALS, "COM");
rule.setDescription("The market segment should be COM");
  1. Define how data for the "MARKET_SEGMENT" key must be retrieved
varfactsResolver = newSimpleFactsResolver<>();
factsResolver.addFact(newFact<>("MARKET_SEGMENT", data -> "COM"));
  1. Evaluate
varevaluationResult = newEvaluator().evaluate(rule, factsResolver);
// The evaluation is an asynchronous process, so the associated CompletableFuture must be executed to get a result.// The following line returns the result value when complete, or throws an (unchecked) exception if completed exceptionally.evaluationResult.status().join(); varresult = evaluationResult.snapshot();

The result object will contain information on whether the evaluation was valid or not, plus any relevant information about the rule run.

  1. If we were to print the result as json
{"result" : "VALID","type" : "STRING","operator" : "EQUALS","key" : "MARKET_SEGMENT","description": "The market segment should be COM","expectedValue" : "COM","actualValue": "COM"}

Rules Vocabulary

Boolean Rules

Boolean rules are used to combine rules together.

And Rule

The "And Rule" is used to combine multiple rules together, where all the rules must evaluate to VALID for it to evaluate to VALID.

{"type" : "AND","rules" : [// list of other rules]}
ABA && B
VALIDVALIDVALID
VALIDINVALIDINVALID
VALIDMAYBEMAYBE
VALIDFAILEDFAILED
VALIDOPERATION_NOT_SUPPORTEDOPERATION_NOT_SUPPORTED

The AND rule is commutative: A && B = B && A.

An empty AND evaluates to VALID. This follows the identity of conjunction: with no subrules, there is nothing to invalidate the expression.

Or Rule

The "Or Rule" is used to combine multiple rules together, where at least one rule must evaluate to VALID for it to evaluate to VALID.

{"type" : "OR","rules" : [// list of other rules]}
ABA || B
VALIDanyVALID
INVALIDINVALIDINVALID

The order of precedence for non-VALID results is: FAILED, OPERATION_NOT_SUPPORTED, INVALID, MAYBE. So, for example: FAILED || INVALID == FAILED, while MAYBE || INVALID == INVALID and so on.

The OR rule is commutative: A || B = B || A.

An empty OR evaluates to INVALID. This follows the identity of disjunction: with no subrules, there is nothing that can make the expression valid.

Not Rule

The "Not Rule" is used to negate the result of another rule.

{"type" : "NOT","rule" : {// rule to negate}}
A!A
VALIDINVALID
INVALIDVALID
MAYBEMAYBE
FAILEDFAILED
OPERATION_NOT_SUPPORTEDOPERATION_NOT_SUPPORTED

A NOT rule must contain a non-null rule. If the operand is missing, the evaluation is treated as malformed configuration and fails.

Fact-only Rules

Exists Rule

The "Exists Rule" is used to check whether a fact exists or not.

{"type": "EXISTS","key": "foo"}
Some examples
KeyFactResult
"foo"{ "foo": "bar" }VALID
"foo"{ "foo": null }INVALID
"foo"{ "not_foo": "bar" }INVALID

Fixed Rules

CONSTANT Rule

The "Constant Rule" is used to always return the same result, regardless of the fact.

{"type": "CONSTANT","result": "VALID"// INVALID, MAYBE, FAILED, OPERATION_NOT_SUPPORTED}

Value-based Rules

These rules evaluate facts against a value set in the rule. When creating a value-based rule, you must also set an operator (e.g., EQUALS, GREATER_THAN, IN, etc...).

The relationship between facts, values and operators is: fact OPERATOR value.

So, for example a rule having value Cat, operator EQUALS, and evaluated against the fact Dog reads as: Dog EQUALS Cat (false). A rule having value Cat, operator CONTAINS, and evaluated against the fact [Dog, Bird, Cat] reads as: [Dog, Bird, Cat] CONTAINS Cat (true).

Number Rule

The "Number Rule" is used to evaluate facts against a number. The number can be an integer or a double.

{"type": "NUMBER","operator": "GREATER_THAN","key": "foo","value": 7// you can also have 7.0}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS, DIVISIBLE_BY

Integer-Double comparisons between the rule's value and the data provided by the fact work for all operators except CONTAINS.

The DIVISIBLE_BY operator is valid only for integer values since divisibility is not well-defined for floating-point numbers.

Some examples
Rule ValueOperatorFactResult
7EQUALS7VALID
7GREATER_THAN7INVALID
7GREATER_THAN8VALID
7GREATER_THAN_EQUAL7VALID
7GREATER_THAN_EQUAL7.5VALID
7.4GREATER_THAN7.5VALID
7.5GREATER_THAN7.5INVALID
7CONTAINS[ 6, 7, 8]VALID
7CONTAINS[ 6, 8]INVALID
7CONTAINS[ 6, 7.0, 8]INVALID
7.0CONTAINS[ 6, 7.0, 8]VALID
7DIVISIBLE_BY7VALID
7DIVISIBLE_BY8INVALID
0DIVISIBLE_BY8FAILED
7DIVISIBLE_BY0VALID
any numbersupported operatornullINVALID
nullsupported operatorany numberINVALID

When using the CONTAINS operator, the Fact must be a Set of numbers.

String Rule

{"type": "STRING","operator": "EQUALS","key": "foo","value": "bar"}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS

Comparisons are case-sensitive.

Some examples
Rule ValueOperatorFactResult
"bar"EQUALS"bar"VALID
"bar"EQUALS"BAR"INVALID
"bar"EQUALS"baz"INVALID
"bar"GREATER_THAN"car"VALID
"bar"GREATER_THAN_EQUAL"bar"VALID
"bar"GREATER_THAN"are"INVALID
"bar"STARTS_WITH"bar"VALID
"bar"STARTS_WITH"barfoo"VALID
"bar"STARTS_WITH"foobar"INVALID
"bar"ENDS_WITH"bar"VALID
"bar"ENDS_WITH"foobar"VALID
"bar"ENDS_WITH"barfoo"INVALID
"bar"CONTAINS["are", "bar", "baz"]VALID
"bar"CONTAINS["are", "baz"]INVALID
any stringsupported operatornullINVALID
nullsupported operatorany stringINVALID

Set Rule

A "Set rule" is a rule that evaluates a fact against a set of values. The set rule has two operators: IN and INTERSECTS.

IN: evaluates to VALID if the fact is a subset of the rule's value. INTERSECTS: evaluates to VALID if the fact and the rule's value have at least one item in common.

{"type": "SET","operator": "IN","key": "foo","values": ["bar","baz"]}

Supported operators: IN, INTERSECTS

String comparisons are case-sensitive.

Some examples
Rule ValueOperatorFactResult
["bar", "baz"]IN"bar"VALID
["bar", "baz"]IN"waz"INVALID
[1, 2, 3]IN2VALID
[1, 2, 3]IN4INVALID
["bar", "baz"]IN[]INVALID
[]IN[]VALID
[]IN["bar"]INVALID
["bar", "baz"]IN["bar"]VALID
["bar", "baz"]IN["waz"]INVALID
["bar", "baz"]IN["bar", "waz"]INVALID
["bar", "baz"]INTERSECTS"bar"VALID
["bar", "baz"]INTERSECTS"waz"INVALID
["bar", "baz"]INTERSECTS["bar", "waz"]VALID
["bar", "baz"]INTERSECTS["wiz", "waz"]INVALID
["bar", "baz"]INTERSECTS[]INVALID
[]INTERSECTS[]INVALID
any setany operatornullINVALID
Comparing complex objects

The Set rule can be used on more complex objects too.

SetRule<Patient> setRule = newSetRule<>();
setRule.setKey("PATIENT");
setRule.setOperator(Operator.IN);
setRule.setValues(bob, alice);

Where bob and alice are instances of Patient. You must also make sure that the Patient class overrides the equals and hashCode methods. Regola expects the equals method to perform a commutative comparison between objects.

Also note that regola does not support JSON serialization/deserialization for SET rules with complex objects.

Date Rule

The "Date Rule" is a rule that evaluates a fact against a date value.

{"type" : "DATE","operator" : "GREATER_THAN","key" : "foo","value" : "2021-07-07T12:30:00Z"}

Supported operators: EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL, CONTAINS

Dates must be parsable to an OffsetDateTime:

A date-time with an offset from UTC/Greenwich in the ISO-8601 calendar system

Some examples
Rule ValueOperatorFactResult
"2021-07-07T12:30:00Z"EQUALS"2021-07-07T12:30:00Z"VALID
"2021-07-07T12:30:00Z"GREATER_THAN"2022-07-07T12:30:00Z"VALID
"2021-07-07T12:30:00Z"GREATER_THAN"2020-07-07T12:30:00Z"INVALID
"2021-07-07T12:30:00Z"LESS_THAN"2020-07-07T12:30:00Z"VALID
any datesupported operatornullINVALID
nullsupported operatorany dateINVALID

Null Rule

A "Null Rule" is a rule that evaluates a fact against a null value.

{"type": "NULL","key": "foo"}
Some examples
KeyFactResult
"foo"nullVALID
"foo""foo"INVALID

Combining Rules

Rules can be combined using the boolean rules: AND, OR, NOT.

{"type" : "AND","rules" : [{"type" : "STRING","operator" : "EQUALS","key" : "foo","value" : "bar"},{"type" : "OR","rules" : [{"type" : "EXISTS","key" : "waz"},{"type" : "NUMBER","operator" : "EQUALS","key" : "foobar","value" : 21}]}]}

Ignoring results

Rules can be set to be ignored, so that the evaluation of AND/OR/NOT rules does not take them into account.

This matters for empty or effectively-empty boolean rules too:

  • AND with no subrules evaluates to VALID.
  • OR with no subrules evaluates to INVALID.
  • OR where every subrule is present but marked as ignored evaluates to VALID, because all configured subrules are excluded from the final decision.
  • NOT with an ignored subrule evaluates to VALID.
  • NOT with no subrule fails.

Example of a rule marked as ignored:

{"type" : "STRING","operator" : "EQUALS","key" : "foo","value" : "bar","ignored" : true}

This is useful when you want to run a rule but not have it affect the evaluation of the tree.

For example, the following tree evaluated to VALID even thought one of the subrules of AND was INVALID:

{"result": "VALID","type": "AND","description": "Example of a Composite Rule with subrules ignored",// Optional, can be added to any rule in the tree"ignored": false,"rules": [{"result": "INVALID","type": "STRING","ignored": true,"operator": "EQUALS","key": "foo","expectedValue": "bar","actualValue": "not_bar"},{"result": "VALID","type": "EXISTS","ignored": false,"key": "foo","expectedValue": "<any>","actualValue": "not_bar"}]}

Json Deserialization with Jackson

You can use Jackson to deserialize rules from regola. These are the dependencies you will need:

<!-- pom.xml -->
<properties>
<jackson.version>2.13.1</jackson.version>
</properties>
<depdendencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
</depdendencies>

And this is the minimum setup for your ObjectMapper.

ObjectMappermapper = newObjectMapper()
.registerModule(newJavaTimeModule())
.registerModule(newRuleModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

Then you can deserialize a rule as such:

StringjsonRule = readRuleFromDataSource(); // This method will vary depending on your applicationRulerule = mapper.readValue(jsonRule, Rule.class);

New rules

  1. Create a new Rule by extending Rule (or one of the provided abstract classes)
publicclassYourRuleextendsRule {
publicYourRule() {
super("YOUR_TYPE"); // You should make sure this does not conflict with the type of any existing rule
}
@OverridepublicEvaluationResultevaluate(FactsResolverfactsResolver) {
returnnewEvaluationResult() {
privateResultresult = Result.MAYBE;
@OverridepublicRuleResultsnapshot() {
// Build and return a RuleResult
}
@OverridepublicCompletableFuture<Result> status() {
returnfacts.resolveFact(getKey())
.thenCompose(fact -> CompletableFuture.supplyAsync(() -> {
result = ... // perform the relevant checks for this rule and update the resultreturnresult;
}))
.exceptionally(throwable -> {
result = Result.FAILED;
returnresult;
});
}
};
}
}
  1. (Optional) If you need to parse rules from Json, then you must extend the RuleModule as such:
mapper.registerModule(newRuleModule()
.addRule("YOUR_TYPE", YourRule.class));
  1. Start using your new rule!

Programmatic Rule Creation

While transforming rules from Json is convenient, sometimes you may want to create rules programmatically.

Here is an example of how to do that:

SetRule<String> stringSetRule = newSetRule<>();
stringSetRule.setKey("MARKET_SEGMENT");
stringSetRule.setOperator(Operator.IN);
stringSetRule.setValues(Set.of("COM", "EDU"));
NumberRule<Integer> numberRule = newNumberRule<>();
numberRule.setKey("capacity");
numberRule.setOperator(Operator.EQUALS);
numberRule.setValue(3);
OrRuleorRule = newOrRule();
orRule.setRules(List.of(numberRule, stringSetRule));

Facts

Now that we have got some rules, we want to do something with it.

We do that by creating facts and supplying those to the evaluator which will check whether they satisfy our rule or not.

An example of a fact for the foo data point is:

varfact = newFact<>("foo", data -> "bar");

In regola, we can also write facts that use custom data fetchers to retrieve additional data:

Fact<Offer> fact = newFact<>("segment", CustomDataSources.OFFER, Offer::getSegment);

The FactsResolver

Defining facts, if using a custom data source, is not enough. We must tell our evaluator how to get those facts when a rule is run. This is done using a FactsResolver as shown below:

Map<DataSource, DataFetcher<?, YourContext>> dataFetchers = Map.of(
CustomDataSource.OFFER, offerDataFetcher,
...
);
FactsResolverfactsResolver = newSimpleFactsResolver<>(yourContext, dataFetchers);
factsResolver.addFact(newFact<>("segment", CustomDataSources.OFFER, Offer::getSegment));

Writing a Data Fetcher

Example of a data fetcher getting data over HTTP:

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
privatefinalOfferHttpConnectorofferHttpConnector;
publicOfferDataFetcher(OfferHttpConnectorofferHttpConnector) {
this.offerHttpConnector = offerHttpConnector;
}
@OverridepublicCompletableFuture<FetchResponse<Offer>> fetchResponse(YourContextcontext) {
GetOffersRequestrequest = buildRequest(context);
returnCompletableFuture.supplyAsync(() -> {
finalvarresponse = newFetchResponse<>();
response.setData(offerHttpConnector.getOffers(request)
.stream()
.findFirst());
returnresponse;
});
}
// Do not override this method if you do not want to cache results from this data fetcher.@OverridepublicStringcalculateRequestKey(YourContextcontext) {
GetOffersRequestrequest = buildRequest(context);
returnrequest.URL().toString();
}
privateRequestbuildRequest(YourContextcontext) {
returnnewGetOffersRequest(Set.of(requestContext.getOfferId()), Set.of());
}
}
publicclassYourContextimplementsContext {
privateStringofferId;
publicStringgetOfferId() {
returnofferId;
}
publicvoidsetOfferId(StringofferId) {
this.offerId = offerId;
}
}

The initialization of a data fetcher can be expensive, depending on your implementation, so it is recommended that data fetchers are re-used across multiple evaluations.

Caching in the Data Fetcher

The abstract DataFetcher uses caffeine to cache the results of the fetch results.

The default cache is setup with an expiry policy of 1 minute and max size of 1_000 entries, but a custom configuration can be setup by passing a DataFetcherConfiguration to the DataFetcher's constructor.

Custom Cache

If the default caffeine-based cache does not satisfy your requirements, you can provide your own implementation.

First, create a class for your custom cache:

classYourCustomCache<V> implementsDataFetcherCache<V> {
publicYourCustomCache(DataCacheConfigurationconfiguration) {
// (optional) construct your cache using the given configuration
}
@OverridepublicCompletableFuture<V> get(Stringkey, Function<String, CompletableFuture<V>> mappingFunction) {
// implement your "if cached, return; otherwise create, cache and return" cache function here
}
}

Then pass an instance of your custom cache to your data fetchers:

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
privatefinalOfferHttpConnectorofferHttpConnector;
publicOfferDataFetcher(OfferHttpConnectorofferHttpConnector, YourCustomCache<Offer> cache) {
super(cache);
this.offerHttpConnector = offerHttpConnector;
}
// rest of this data fetcher implementation
}

SLA handling within the Data Fetcher

By default, your custom data fetcher will not have any SLA failure handling. However, if needed you can specify an SLA on the fetch requestTime and override the whenFailingSlaFetchTime to handle any SLA failures.

publicclassOfferDataFetcherimplementsDataFetcher<Offer, YourContext> {
publicOfferDataFetcher(/* other params */, longslaFetchTime) {
super(newDataFetcherConfiguration().setSlaFetchTime(slaFetchTime));
// any other initialization
}
@OverridepublicvoidwhenFailingSlaFetchTime(StringrequestKey, longslaFetchTime, doublerequestTime) {
// This method gets called whenever "requestTime > slaFetchTime"
}
}

Actions

Actions are used to define operations we want to perform after a rule is evaluated.

Basic usage

varaction = newAction()
.setDescription("Print 'Hello' if VALID")
.setOnCompletion((result, throwable, ruleResult) -> {
if (result == Result.VALID) {
System.out.println("Hello");
}
});
rule.setAction(action);

In this particular example, this action will be executed when the rule is evaluated and the result is VALID.

Chaining

It is possible to chain actions using the andThen method on the TriConsumer:

TriConsumer<Result, Throwable, RuleResult> actionConsumer = (result, throwable, ruleResult) -> {
if (result == Result.VALID) {
System.out.println("Hello");
}
};
// Chain the action to always print "World"actionConsumer = actionConsumer.andThen((result, throwable, ruleResult) -> System.out.println("World"));
varaction = newAction()
.setDescription("Print 'Hello' if VALID and the word 'World' irrespective of result")
.setOnCompletion(actionConsumer);
rule.setAction(action);

Understanding the Results of a Rule Evaluation

The following is an example of a result (pretty printed in json) returned upon evaluating a tree of rules:

{"result": "VALID","type": "AND","description": "Example of a Composite Rule",// Optional, can be added to any rule in the tree"ignored": false,"rules": [{"result": "VALID","type": "STRING","ignored": false,"operator": "EQUALS","key": "foo","expectedValue": "bar","actualValue": "bar"},{"result": "VALID","type": "OR","ignored": false,"rules": [{"result": "VALID","type": "EXISTS","ignored": false,"key": "waz","expectedValue": "<any>",// <any> is a special keyword matching any actual value for the EXISTS rule"actualValue": "wazab"},{"result": "MAYBE","type": "NUMBER","ignored": false,"operator": "EQUALS","key": "foobar","expectedValue": 21,// no actual value for MAYBE, since the rule was not evaluated due to short-circuiting}]}]}

The top-level "result" is the overall result of the rule.

  • If VALID, the subresults must be all VALID or MAYBE (i.e., rule did not need to be evaluated due to short-circuiting).
  • If not VALID, one or more of the subresults are: INVALID, OPERATION_NOT_SUPPORTED or FAILED.

Oh, by the way, what does "regola" mean?

regola is the italian word for rule.

About

A rule evaluator in Java

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

8 watching

Forks

Releases

Used by

Contributors

Languages