-
Notifications
You must be signed in to change notification settings - Fork 16
Home
We provide brief insight into rules engine’s world and afterwards we are going to discover YARE’s functionalities step by step, starting with basic examples, going further towards more complex ones. This guide is addressed to people, who have never used rules engines before, as well as for those who have experience in this field, but want to use our solution.
Yet Another Rules Engine (YARE) is a rules engine written completely in Java. It is our approach to implement an Expert System which is reliable, fast and full of capabilities.
Beyond the advantages of business rules engines YARE makes its own contribution to user experience:
-
YARE is significantly faster than other rules engines for single-type collection input (e.q. List<Flight>)
-
YARE allows sequential evaluation, which is useful when it comes to modifying facts during execution
-
YARE allows function evaluation in condition segment of rule
-
YARE is using three-valued logic (true/false/null)
-
YARE provides XML/JSON/YAML converters for rules
In case you are new to whole rule-based systems world we suggest you familiarize yourself with the terminology:
- Fact
-
Input data model. Facts will be matched against rules in the execution.
- Rule
-
Logic driving reasoning process in rules engine. Basing on its conditions, rule will select matching facts and call actions.
- Action
-
Function being invoked when fact is matched against specified rule.
- Rules engine
-
Alternative to computational model. It is responsible for registering actions, functions, as well as managing rules. Its job is to evaluate rules using input facts and provide result of this computation.
So where do we get started? Just like learning any programming language starts with Hello world! example so we will stick to this tradition but in more rule-based manner. In order to use YARE we need to add a proper dependency:
Maven
<dependency>
<groupId>com.sabre.oss.yare</groupId>
<artifactId>yare-engine</artifactId>
<version>${yare.version}</version>
</dependency>
Gradle
dependencies {
compile "com.sabre.oss.yare:yare-engine:$yareVersion"
}
First of all, let’s create action - method invoked when fact is matched. Below you can see example of action with one method printing “Hello World!” to STDOUT.
public class HelloWorldAction {
public void printHelloWorld() {
System.out.print("Hello World!");
}
}
Secondly we have to create some fact, which will be processed by our rules engine. Below you can see example of the simplest possible fact.
public class TestFact {
}
Next step is creating rule. Below you can see rule, whose predicate is always true so it matches every single input fact and then invokes registered action.
List<Rule> rule = Collections.singletonList(
RuleDsl.ruleBuilder()
.name("Always matching rule, printing HelloWorld")
.fact("fact", TestFact.class)
.predicate(
value(true)
)
.action("printHelloWorld")
.build()
);
-
RuleBuilder name(String name)
- set name of the rule. -
RuleBuilder fact(String identifier, Class<?> type)
- define fact, which will be used in the rule. You can register multiple facts. identifier of fact can be used in predicate. -
RuleBuilder predicate(Expression<Boolean> expression)
- create predicate. It’s a “body” of a rule. It is responsible for matching facts fulfilling specified conditions. -
RuleBuilder action(String name, Parameter<?>… args)
- define action. It is basically method invoked when matching fact is processed by the rule. Note that action has its name, which should be registered in rules engine.
Now it’s time for main part, creating rules engine. It is responsible for managing facts, rules, actions, functions and much more.
RulesEngine rulesEngine = new RulesEngineBuilder()
.withRulesRepository(i -> rule)
.withActionMapping("printHelloWorld", method(new HelloWorldAction(), HelloWorldAction::printHelloWorld))
.build();
-
RulesEngineFactory withRulesRepository(RulesRepository rulesRepository)
- specify a repository of rules. -
PlainJavaRulesExecutorFactory withActionMapping(String actionName, CallMetadata callMetadata)
- register action. Here we are registering certain method as an action. We have to provide name for an action. You can see that, earlier, in our rule we have used this name to indicate, which action to invoke after matching. We need to provide metadata of the method as well. -
<T> CallMetadata method(T instance, Consumer<T> catchingInvocation)
- specify metadata of function being registered. Firstly we provide object whose method will be used as an action. Secondly, we need to say which function we are registering. It is recommended to use lambda expression to do so, but you can describe function explicitly usingCallMetadata method(T instance, String methodName, Type… argumentsTypes)
. It could be done like this.
MethodCallMetadata callMetadata = method(new HelloWorldAction(), "printHelloWorld");
Important
|
To register static methods please use MethodCallMetadata::method(Class<?> targetType, String methodName, Type… argumentsTypes) method.
|
Note
|
It doesn’t matter what arguments you pass when registering function or action using lambda expression. It won’t be invoked anyway. |
After some preparation we are ready to fire up our example. Just few more lines of code and we will see “Hello World!” on our screens.
Let’s create instances of our fact like the following:
List<TestFact> fact = Collections.singletonList(
new TestFact()
);
Well, we have fact, we have rule, we have engine. So what now? We have to create session basing on our rules engine. We do this like below.
RuleSession ruleSession = rulesEngine.createSession("helloWorldExample");
You may be wondering what this mysterious “helloWorldExample” means. For now, let’s ignore it, but be sure we will come back to this in further part of User Guide.
Okay, so let’s execute our example.
ruleSession.execute(null, fact);
If everything went fine, we should see “Hello World!” on our screens.
At this point you may think that every single time “Hello World!” will be printed to STDOUT. But let’s focus on such input facts collection.
List<TestFact> fact = Collections.emptyList();
After execution we see that nothing appeared on screen. Why? Consider this diagram.
This time there is no input facts. Because of that, there is no possibility that fact will be matched against the rule, so it is impossible to invoke action. That’s why we don’t see “Hello World!” on our screens.
In this example we will cover the possibilities of predicate creation that YARE gives us. We will also study how to pass field of object to function or action. What is more, we will learn how to construct more advanced facts.
In this example, action will be fairly simple but definitely more useful than printing “Hello World!” to STDOUT. We will use action to collect matching facts.
public class TestAction {
public void collect(List<TestFact> facts, TestFact fact) {
facts.add(fact);
}
}
Remember that action is invoked only for matching facts, so we don’t have to filter them in this method.
This time we will be using more “life-like” example of fact - Hotel
.
public class Hotel {
private final String chainCode;
public Hotel(String chainCode) {
this.chainCode = chainCode;
}
public String getChainCode() {
return chainCode;
}
}
If you plan to refer to fact’s fields you need to remember about that during fact creation.
.You can make field of fact accesible from Rule
in three ways:
-
By making field public.
-
By providing appropriate getter method for not accessible field. Getter should be named “get” + capitalized field name. In case of boolean fields, you are allowed to use “is” + capitalized field name.
-
By implementing
java.util.Map
interface.
Warning
|
If you provide both "get" getter, as well as "is" getter, the "get" one will be preferred. |
Important
|
YARE is case-sensitive. |
Rules engine will be used to filter hotels with specified chain code. The following rule should do the job. In this particular example we are filtering hotel with chain code equals to “HH”, so we are looking for Hilton Hotels.
List<Rule> rule = Collections.singletonList(
RuleDsl.ruleBuilder()
.name("Rule matching when hotel has specified chain code")
.fact("hotel", Hotel.class)
.predicate(
equal(
value("${hotel.chainCode}"),
value("HH")
)
)
.action("collect",
param("context", value("${ctx}")),
param("fact", value("${hotel}")))
.build()
);
There are few new capabilities of YARE presented in this rule. Let’s describe each of them.
Supported types |
YARE operator |
Arrays, |
|
|
|
|
|
|
|
|
|
|
|
Classes implementing |
|
|
|
Anything |
|
Anything that evaluates to |
|
|
As you can see, we can refer to field of our fact, which is pretty handy operation. To do this
you should use function ExpressionOperand<T> value(String value)
. As a parameter you should pass path to a field of
the fact (e.g. ${fact.field}
). Please notice that additional ${}
notation is used here.
You may be wondering if it is possible to refer in nested fact structure using multiple dots. Actually YARE meets the expectations and allows such operations. Even with collections met during operation.
Important
|
If you want to collect elements from collections, Collection derivatives are being supported. |
Let’s say Itinerary is the fact that you are using in rule. Let’s also say that its structure looks like this.
In YARE we are able to refer to field like this value("${itinerary.airlines.flights.name}")
.
As a result we will get Collection<String>
containing objects marked with red rectangle.
Important
|
When using multiple dots referring to nested field remember about properly managing access to field. For more details please head to: How to refer to field |
If your reference ends with collection you can use [*]
operator.
What is it for?
We will use above fact structure as an example and we will be referring like this value("${itinerary.airlines.flights}")
.
Of course, as a result we expect objects within red rectangle.
In this case our result will be type Collection<Collection<Flight>> right? We will collect elements from specified level of tree into list.
Since you know how result will look like, let’s approach this problem different way. We will use [*]
operator like this
value("${itinerary.airlines.flights[*]}")
.
Of course elements will be collected from the same level, but with slight distinction. Instead of getting Collection<Collection<Flight>>,
this operation will return Collection<Flight>. Nested lists will be flatten.
To be exact, inner lists will be traversed from left to right and each element of this lists will be put into result collection.
Place of null |
Behavior |
Null is not element of collection, no collection in chain so far |
Return null |
Null is not element of collection, already in upper level collection in chain, end of chaining |
Add to result collection |
Null is not element of collection, already in upper level collection in chain, middle of chaining |
Skip |
Null is element of collection, middle of chaining |
Skip |
Null is element of collection, end of chaining |
Add to result collection |
In case you would like to pass reference to an object to your function or action you should do this in the following way.
Of course it is parameter so you have to use <T> Parameter<T> param(String name, Operand<T> operand)
method.
First parameter should be a String
with description of the parameter. Next we want to pass reference to a fact so we use method
ExpressionOperand<T> value(String value)
executed as value("${factName}")
. In this situation we are passing Hotel
which is processed by our rule. When it comes to functions,
it looks identical.
This time rules engine will look like this.
RulesEngine rulesEngine = new RulesEngineBuilder()
.withRulesRepository(i -> rule)
.withActionMapping("collect", method(new TestAction(), (action) -> action.collect(null, null)))
.withInterceptor(new InputOutputLogger())
.build();
As you can see there is only one new construction here. We are using RulesEngineFactory withInterceptor(Interceptor<ExecutionContext, ExecutionContext> interceptor)
in order to create interceptor in the rules engine. To be exact we take advantage of InputOutputLogger
, as you will probably
do most of the time.
Just like before we prepare some input data for the rules engine.
List<Hotel> facts = Arrays.asList(
new Hotel("HH"),
new Hotel("BV"),
new Hotel("SE"),
new Hotel("BW"),
new Hotel("HH")
);
Next we create session.
RuleSession ruleSession = rulesEngine.createSession("collectExample");
And finally we can execute.
List<Hotel> matchingHotels = ruleSession.execute(new ArrayList<>(), facts);
As you can see we are passing empty ArrayList
as first argument. It’s because it will be later used in action as a context.
What is more, this first argument is basically object for execution results.
In case you are confused with this context thing. In YARE context
appearing in rule is just an object which we
passed as the first parameter in execute
invocation. You should treat it as a result object.
As a result we get two Hotel
s, both with chain code equals to “HH”.
In this chapter of User Guide we will tackle the problem of functions in YARE. We will learn how to register them, how to use them and how they can help with problem solving in YARE. To discover even more possibilities of YARE it will be shown how to use connectives like “or” or “and” in rules.
Warning
|
Functions in YARE are considered as pure functions. |
Just like action, we have to create class containing functions used in rules. We are creating function which job is to calculate hours between two points in time.
public class TestFunction {
private final Clock clock;
public TestFunction() {
clock = Clock.systemDefaultZone();
}
public TestFunction(Clock clock) {
this.clock = clock;
}
public Long getDiffInHours(LocalDateTime date) {
return HOURS.between(LocalDateTime.now(clock), date);
}
}
This time Flight
will serve as a fact in our example.
public class Flight {
private final BigDecimal price;
private final LocalDateTime dateOfDeparture;
public Flight(BigDecimal price, LocalDateTime dateOfDeparture) {
this.price = price;
this.dateOfDeparture = dateOfDeparture;
}
public BigDecimal getPrice() {
return price;
}
public LocalDateTime getDateOfDeparture() {
return dateOfDeparture;
}
}
Rule will be used to filter flights fulfilling two conditions. Firstly, it’s price must be lower or equal to 100$. Secondly, it must depart within 24 hours from now. Rule responsible for such matching will look like this.
List<Rule> rule = Collections.singletonList(
RuleDsl.ruleBuilder()
.name("Rule matching when flight departs in 24h and it's price is less or equal 100$")
.fact("flight", Flight.class)
.predicate(
and(
lessOrEqual(
value("${flight.price}"),
value(new BigDecimal(100))
),
less(
function("getDiffInHours", Long.class,
param("date", value("${flight.dateOfDeparture}"))),
value(24L)
)
)
)
.action("collect",
param("context", value("${ctx}")),
param("fact", value("${flight}")))
.build()
);
There is few new capabilities of YARE presented in this rule. Let’s describe each of them.
YARE allows to connect expression with logical operators. Currently it supports three logical operators.
-
and
-
or
-
not
Important
|
YARE logical operators behave similar to those in Java. They are calculated to the moment when it is possible to determine logical result of operator. |
Another handy opportunity to create more complex rules is functions creation. In order to use function in rule, you should use
one of function
method of class com.sabre.oss.yare.dsl.RuleDsl
. As a first parameter you ought to pass
the name of function, the same you used to register function in Rules Engine. Afterwards there is place for all
parameters passed directly to function.
Note that actually function
is equivalent to result of function. It can be compared using YARE’s operators.
As always we have to create rules engine.
RulesEngine rulesEngine = new RulesEngineBuilder()
.withRulesRepository(i -> rule)
.withActionMapping("collect", method(new TestAction(), (action) -> action.collect(null, null)))
.withFunctionMapping("getDiffInHours", method(new TestFunction(clock), (function) -> function.getDiffInHours(null)))
.build();
As you have probably noticed, there are a few new constructions.
First of all in order to use functions in our rules, we have to register them in rules engine just like actions.
You have already noticed that it is identical to registering actions with one difference, we are using withFunctionMapping
method instead. In particular you may think about action as a special type of function.
Tip
|
To avoid typos with functions names, class containing functions etc. create appropriate constants. |
As an input data will serve the following list.
List<Flight> facts = Arrays.asList(
new Flight(new BigDecimal(100), LocalDateTime.now(clock).plusHours(23)),
new Flight(new BigDecimal(120), LocalDateTime.now(clock).plusHours(10)),
new Flight(new BigDecimal(50), LocalDateTime.now(clock).plusHours(25)),
new Flight(new BigDecimal(250), LocalDateTime.now(clock).plusHours(30))
);
As a result we should get one flight with price equal to 100$ and time of departure 23 hours from now. After appropriate execution (same as in previous examples) we get correct result.
YARE allows to define functions and actions using JavaScript. This is the way to externalize those definitions and change rules engine’s behavior in the runtime.
Warning
|
This is an experimental feature. Evaluating JavaScript from untrusted sources is highly not recommended. |
In order to take advantage of this feature, make sure to add dependency to yare-invoker-js
module to your project.
Maven
<dependency>
<groupId>com.sabre.oss.yare</groupId>
<artifactId>yare-invoker-js</artifactId>
<version>${yare.version}</version>
</dependency>
Gradle
dependencies {
compile "com.sabre.oss.yare:yare-engine:$yareVersion"
}
Next you should register your JavaScript using JavaScriptCallMetadata::js(String functionName, String script)
method.
You have to provide body of the script and name of the function you want to invoke.
As an example, let’s consider this simple rule.
List<Rule> rules = Collections.singletonList(
RuleDsl.ruleBuilder()
.name("Rule matching when hotel has specified chain code")
.fact("hotel", Hotel.class)
.predicate(
equal(
function("concat", String.class,
param("str1", value("Chain code:")),
param("str2", value("${hotel.chainCode}"))),
value("Chain code:HH")
)
)
.action("collect",
param("context", value("${ctx}")),
param("fact", value("${hotel}")))
.build()
);
As you have probably noticed, we can’t differ this rule from ones using Java methods. The only difference is in rules engine configuration.
String script = "" +
"function concat(str1, str2) { " +
" return str1 + str2; " +
"}" +
"function collect(ctx, fact) {" +
" ctx.add(fact);" +
"}";
RulesEngine rulesEngine = new RulesEngineBuilder()
.withRulesRepository(i -> rules)
.withActionMapping("collect", js("collect", script))
.withFunctionMapping("concat", js("concat", script))
.build();
What is important is that JavaScript can be used for both functions and actions.
Until now we have been using rules created directly using Java DSL.
-
Java DSL
-
XML
-
JSON
-
YAML
If you decide to create rules using XML or JSON format, we are providing converters allowing marshalling and unmarshalling rules.
In order to use it please head to com.sabre.oss.yare.serializer.xml.RuleToXmlConverter
class or
com.sabre.oss.yare.serializer.json.RuleToJsonConverter
class.
As an example we will present same rule using all of above formats.
Rule rule = RuleDsl.ruleBuilder()
.name("Rule matching when flight departs in 24h and it's price is less or equal 100$")
.fact("flight", Flight.class)
.predicate(
and(
lessOrEqual(
value("${flight.price}"),
value(new BigDecimal(100))
),
less(
function("getDiffInHours", Long.class,
param("date", value("${flight.dateOfDeparture}"))),
value(24L)
)
)
)
.action("collect",
param("context", value("${ctx}")),
param("fact", value("${flight}")))
.build();
<Rule xmlns="http://www.sabre.com/schema/oss/yare/rules/v1">
<Attribute name="ruleName" value="Rule matching when flight departs in 24h and it's price is less or equal 100$"
type="java.lang.String"/>
<Fact name="flight" type="com.sabre.oss.yare.documentation.userguide.Flight"/>
<Predicate>
<And>
<Operator type="less-or-equal">
<Value>${flight.price}</Value>
<Value type="BigDecimal">100</Value>
</Operator>
<Operator type="less">
<Function name="getDiffInHours" returnType="java.lang.Long">
<Parameter name="date">
<Value>${flight.dateOfDeparture}</Value>
</Parameter>
</Function>
<Value type="Long">24</Value>
</Operator>
</And>
</Predicate>
<Action name="collect">
<Parameter name="context">
<Value>${ctx}</Value>
</Parameter>
<Parameter name="fact">
<Value>${flight}</Value>
</Parameter>
</Action>
</Rule>
{
"attributes" : [ {
"name" : "ruleName",
"value" : "Rule matching when flight departs in 24h and it's price is less or equal 100$",
"type" : "String"
} ],
"facts" : [ {
"name" : "flight",
"type" : "com.sabre.oss.yare.documentation.userguide.Flight"
} ],
"predicate" : {
"and" : [ {
"less-or-equal" : [ {
"value" : "${flight.price}"
}, {
"value" : 100,
"type" : "BigDecimal"
} ]
}, {
"less" : [ {
"function" : {
"name" : "getDiffInHours",
"returnType" : "Long",
"parameters" : [ {
"name" : "date",
"value" : "${flight.dateOfDeparture}"
} ]
}
}, {
"value" : 24,
"type" : "Long"
} ]
} ]
},
"actions" : [ {
"name" : "collect",
"parameters" : [ {
"name" : "context",
"value" : "${ctx}"
}, {
"name" : "fact",
"value" : "${flight}"
} ]
} ]
}
attributes:
- name: "ruleName"
value: "Rule matching when flight departs in 24h and it's price is less or equal 100$"
type: "String"
facts:
- name: "flight"
type: "com.sabre.oss.yare.documentation.userguide.Flight"
predicate:
and:
- less-or-equal:
- value: "${flight.price}"
- value: 100
type: "BigDecimal"
- less:
- function:
name: "getDiffInHours"
returnType: "Long"
parameters:
- name: "date"
value: "${flight.dateOfDeparture}"
- value: 24
type: "Long"
actions:
- name: collect
parameters:
- name: "context"
value: "${ctx}"
- name: "fact"
value: "${flight}"
When working with marshalled formats like XML it is required to specify types of variables used within rule definition.
By default all of the types are specified with its fully qualified name (e.g. java.math.BigDecimal
).
This may be found cumbersome, especially for well-know or common types wildly used in rule definitions.
Therefore, YARE provides type aliases. Aliases may be used as simplified type representation.
Attribute definition with fully qualified type name:
<Attribute name="ruleName" value="Rule matching when..." type="java.lang.String"/>
Attribute definition with alias type:
<Attribute name="ruleName" value="Rule matching when..." type="String"/>
Following aliases are available by default:
Alias |
Type names |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Another worth mentioning feature of Java Rules Engine is rule validation.
By default every rule created using Java DSL is validated. In case you want to disable validation you ought to use
Rule build(boolean validate)
method of com.sabre.oss.yare.dsl.RuleDsl
class.
For example:
Rule rule = RuleDsl.ruleBuilder()
.fact("flight", Flight.class)
.predicate(
and(
lessOrEqual(
value("${flight.price}"),
value(new BigDecimal(100))
),
less(
function("getDiffInHours", Long.class,
param("date", value("${flight.dateOfDeparture}"))),
value(24L)
)
)
)
.action("collect",
param("context", value("${ctx}")),
param("fact", value("${flight}")))
.build(false);
If you try to create above rule with validation enabled, IllegalStateException
with
“[ERROR] Attribute Error: "ruleName" was not specified” message will be thrown.
First of all add dependency to appropriate YARE module to your project.
-
For XML:
Maven
<dependency>
<groupId>com.sabre.oss.yare</groupId>
<artifactId>yare-serializer-xml</artifactId>
<version>${yare.version}</version>
</dependency>
Gradle
dependencies {
compile "com.sabre.oss.yare:yare-serializer-xml:$yareVersion"
}
-
For JSON/YAML:
Maven
<dependency>
<groupId>com.sabre.oss.yare</groupId>
<artifactId>yare-serializer-json</artifactId>
<version>${yare.version}</version>
</dependency>
Gradle
dependencies {
compile "com.sabre.oss.yare:yare-serializer-json:$yareVersion"
}
In case you are planning to use XML/JSON/YAML format of rule, you should bare in mind that public Rule unmarshal(String value)
method is not validating unmarshalled rule.
This is because you might want to modify such rule after unmarshalling.
You may wonder if it is possible to validate previously serialized rule. Actually it is possible and fairly easy to do.
All you have to do is to use com.sabre.oss.yare.model.validator.DefaultRuleValidator
.
In the example we will use XML format. For JSON you need to replace converter.
<Rule xmlns="http://www.sabre.com/schema/oss/yare/rules/v1">
<Fact name="flight" type="com.sabre.oss.yare.documentation.userguide.Flight"/>
<Predicate>
<And>
<Operator type="less-or-equal">
<Value>${flight.price}</Value>
<Value type="java.math.BigDecimal">100</Value>
</Operator>
<Operator type="less">
<Function name="getDiffInHours">
<Parameter name="date">
<Value>${flight.dateOfDeparture}</Value>
</Parameter>
</Function>
<Value type="java.lang.Long">24</Value>
</Operator>
</And>
</Predicate>
<Action name="collect">
<Parameter name="context">
<Value>${ctx}</Value>
</Parameter>
<Parameter name="fact">
<Value>${flight}</Value>
</Parameter>
</Action>
</Rule>
As you have probably already noticed, this rule is invalid. ruleName
attribute is missing.
Of course firstly we need to unmarshal this rule from XML format to Java instance. For reminders we do it as follows.
Rule unmarshalledRule = RuleToXmlConverter.getInstance().unmarshal(invalidRuleInXmlString);
To validate unmarshalled rule we use com.sabre.oss.yare.engine.model.validator.DefaultRuleValidator
like this.
ValidationResults validationResults = DefaultRuleValidator.getRuleValidator().validate(unmarshalledRule);
As a result of validation we get com.sabre.oss.yare.engine.model.validator.ValidationResults
object. It contains list of
com.sabre.oss.yare.model.validator.ValidationResult
, where every ValidationResult
is made of message and appropriate
level (ERROR/WARNING/INFO), as well as String with message code (e.g. “rule.attribute.attributes-not-unique”).
Tip
|
To avoid runtime errors caused by inappropriate rule format, validate each rule as shown above. |
Sometimes there is a need to validate JSON rule syntactically. YARE uses JSON Schema
to describe rule format. It can be found in /yare-serializer/yare-serializer-json/src/main/resources/schema/v1.0/yare-rules.json
file.
What is more, YARE comes with the schema validation tool to make analyzing syntax errors easier.
In order to use it just add the following dependency to yare-serializer-json
module:
Maven
<dependency>
<groupId>com.sabre.oss.yare</groupId>
<artifactId>yare-serializer-json</artifactId>
<version>${yare.version}</version>
</dependency>
Gradle
dependencies {
compile "com.sabre.oss.yare:yare-serializer-json:$yareVersion"
}
Now, let’s validate some JSON rule as an example:
{
"attributes": [
{
"name": "ruleName",
"value": "Name of the rule",
"type": "String"
}
],
"predicate": {
"equal": [
{
"value": true,
"type": "Boolean"
},
{
"function": {
"name": "FUNCTION_NAME",
"returnType": "Boolean",
"parameters": [
{
"name": "PARAMETER_NAME",
"value": 100
}
]
}
}
]
},
"actions": [
{
"name": "ACTION_NAME",
"parameters": [
{
"name": "PARAMETER_NAME",
"value": 100,
"type": "BigDecimal"
}
]
}
]
}
Using validator is that simple:
JsonSchemaValidator validator = new JsonSchemaValidator();
SchemaValidationResults results = validator.validate(jsonRule);
As a result we get SchemaValidationResults
containing every schema violation. In this case we will receive the following
message "#: required key [facts] not found"
.
The above also applies to XML rule format. YARE uses XSD to define XML rule structure. Schema can be found in /yare-serializer/yare-serializer-xml/src/main/resources/schema/v1.0/yare-rules.xsd
file.
As in the case of JSON Schema, YARE comes with XSD validation utilities.
First step to take advantage of this feature, is to add dependency to yare-serializer-xml
module:
Maven
<dependency>
<groupId>com.sabre.oss.yare</groupId>
<artifactId>yare-serializer-xml</artifactId>
<version>${yare.version}</version>
</dependency>
Gradle
dependencies {
compile "com.sabre.oss.yare:yare-serializer-xml:$yareVersion"
}
As an example let’s validate such XML rule:
<yare:Rule xmlns:yare="http://www.sabre.com/schema/oss/yare/rules/v1">
<yare:Attribute name="attributeName">
<yare:Value>attributeValue</yare:Value>
</yare:Attribute>
<yare:Fact name="fact" type="Object"/>
<yare:Action name="actionName">
<yare:Parameter name="parameterName">
<yare:Value type="BigDecimal">100</yare:Value>
</yare:Parameter>
</yare:Action>
</yare:Rule>
Last but not least, we need to validate our input:
Xsd validator = new XsdValidator();
SchemaValidationResults results = validator.validate(xmlRule);
As a result we get SchemaValidationResults
containing every schema violation. In this case we will receive the following
message "cvc-complex-type.2.4.a: Invalid content was found starting with element 'yare:Action'. One of '{\"http://www.sabre.com/schema/oss/yare/rules/v1\":Fact, \"http://www.sabre.com/schema/oss/yare/rules/v1\":Predicate}' is expected."
with specific location (row, column) of the error.
Important
|
Every single XML rule unmarshalling operation is preceded by schema validation. |
All of the above examples use build-in supported types' objects as arguments of operators, function or actions.
Let’s list them all:
Build-in supported type |
Type alias |
String representation example(s) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Note
|
In addition, collections of all of the above types are also supported. |
To create argument of build-in supported type use one of RuleDsl::value
or RuleDsl::values
overloaded method.
See the examples below:
// build-in type operands
Operand<Boolean> booleanArg = RuleDsl.value(true);
Operand<String> stringArg = RuleDsl.value("any string");
Operand<Integer> integerArg = RuleDsl.value(10);
Operand<ZonedDateTime> zonedDataTimeArgFromInstance = RuleDsl.value(ZonedDateTime.now(), ZonedDateTime.class);
Operand<ZonedDateTime> zonedDataTimeArgFromString = RuleDsl.value("2012-12-02T11:15:00+00:00", ZonedDateTime.class);
Operand<String> nullStringArg = RuleDsl.value(null, String.class);
Operand<Long> nullLongArg = RuleDsl.value((Long) null, Long.class);
// collections of build-in type operands
CollectionOperand<String> stringsCollectionArg = RuleDsl.values(String.class, "Stella", "Aurelia");
CollectionOperand<BigDecimal> bigDecimalsCollectionArg = RuleDsl.values(BigDecimal.class, BigDecimal.valueOf(10.1), null);
Tip
|
To create null operand of class A, please use RuleDsl.value((A) null, A.class) . For java.lang.String type it’s legal to omit type casting.
|
But what, if we want to crate argument of any other type? Well, we do it the same way.
Having the following custom type (Car):
public class Car {
private final String model;
private final int productionYear;
public Car(String model, int productionYear) {
this.model = model;
this.productionYear = productionYear;
}
}
We are able to create the following operands:
Operand<Car> car = RuleDsl.value(new Car("Audi", 2017));
CollectionOperand<Car> cars = RuleDsl.values(Car.class, new Car("Jeep", 2015), new Car("Mitsubishi", 2010));
Please notice that all operands have to be marshallable. We would like to highlight the fact that custom type operands require adjustment to the chosen marshalling format/technology.
XML marshalling is provided by YARE internal JAXB implementation.
To make Car
class (un)marshallable from/to XML we have to:
-
add the
@XmlRootElement
annotation at the class level -
add no-args constructor to it
-
add getters (compliant with JavaBeans spec.) for all the properties we want to serialize
-
annotate added getters with
@XmlElement
or@XmlAttribute
annotations -
add setters (compliant with JavaBeans spec.) for all the properties we want to serialize (which means that fields no longer can be declared as
final
ones)
@XmlRootElement(namespace = "http://example.sabre.com/example/model/v1")
class Car {
private String model;
private int productionYear;
public Car() {
}
public Car(String model, int productionYear) {
this.model = model;
this.productionYear = productionYear;
}
@XmlElement
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
@XmlAttribute(name = "year")
public int getProductionYear() {
return productionYear;
}
public void setProductionYear(int productionYear) {
this.productionYear = productionYear;
}
}
Warning
|
It is required to pass all the custom types we want to marshal during obtaining the RuleConverter instance via RuleToXmlConverter::getInstance(Class<?>… types) method.
|
For JSON/YAML serialization YARE uses Jackson.
In order to customize format please use
com.sabre.oss.yare.serializer.json.RuleToJsonConverter::createObjectMapper
static method for JSON and com.sabre.oss.yare.serializer.json.RuleToYamlConverter::createObjectMapper
static method for YAML. It returns preconfigured ObjectMapper
used by the converter. After your modification you have to create converter using
RuleToJsonConverter(ObjectMapper objectMapper)
constructor.
In this chapter of User Guide we will cover the topic of rule sets. It is a way of grouping rules to manage execution, so in case you have multiple rules you can use only sufficient part of them.
Actually, this one of this moments when example will be more pivotal than theory.
Let’s say we are operating on two facts: Hotel
and Flight
. We have two rules using them.
List<Rule> hotelRule = Collections.singletonList(
RuleDsl.ruleBuilder()
.name("Rule matching when hotel has chain code different from specified")
.fact("hotel", Hotel.class)
.predicate(
not(
equal(
value("${hotel.chainCode}"),
value("AH")
)
)
)
.action("collect",
param("context", value("${ctx}")),
param("fact", value("${hotel}")))
.build()
);
List<Rule> flightRule = Collections.singletonList(
RuleDsl.ruleBuilder()
.name("Rule matching when flight departs in 24h and it's price is less or equal 100$")
.fact("flight", Flight.class)
.predicate(
not(
or(
lessOrEqual(
value("${flight.price}"),
value(new BigDecimal(100))
),
less(
function("getDiffInHours", Long.class,
param("date", value("${flight.dateOfDeparture}"))),
value(24L)
)
)
)
)
.action("collect",
param("context", value("${ctx}")),
param("fact", value("${flight}")))
.build()
);
Of course at this point we could create engine, session and execute using two types of facts.
But imagine you have 1000 rules, 500 for Hotel
s and 500 for Flight
s. It would be inefficient to use all rules in case
you know that you will provide only Hotel
s. Let’s find solution to this problem.
First we create java.util.Map
like this.
Map<String, List<Rule>> ruleSets = new HashMap<>();
ruleSets.put("hotelRuleSet", hotelRule);
ruleSets.put("flightRuleSet", flightRule);
As you can see, I have grouped list of rules related to Hotel
within one key “hotelRuleSet”. The same goes for Flight
.
Afterwards we have to create Rules Engine as follows.
RulesEngine rulesEngine = new RulesEngineBuilder()
.withRulesRepository(ruleSets::get)
.withActionMapping("collect", method(new TestAction(), (action) -> action.collect(null, null)))
.withFunctionMapping("getDiffInHours", method(new TestFunction(), (function) -> function.getDiffInHours(null)))
.build();
You may think that everything looks just like in previous examples. But there is minor, yet crucial, change.
In previous example during creation of Rules Engine we did something like: (String i) → rule
. What we did is basically mapping every single URI
to same, concrete Rule
. This time we are doing something a bit different. We map URIs to values from “ruleSets” Map
.
Important
|
When you create session, rules will remain the same despite possible changes. If you want to modify rules and work on updated ones you have to create a new session. |
At this point you may wonder what exactly does it change and what are those URIs. URI is this mysterious String
from session creation.
Maybe example will brighten things.
RuleSession ruleSession = rulesEngine.createSession("hotelRuleSet");
We are specifying URI to “hotelRuleSet”. Just like we said before, in rules engine we are mapping URI to concrete rule set. So in this case “hotelRuleSet” will map to list of rules related to hotel, just like we have put into map.
Note
|
URI is identifier of rule set. |
Please notice that implementation of rule sets using HashMap
is just one way to do this. As long as you map URI to rule set
everything should be fine.
Most of the time you don’t want to store all rules in memory. You may prefer to keep rules in database or file system or somewhere else.
All you have to do is implement com.sabre.oss.yare.core.RulesRepository
interface.
public interface RulesRepository {
/**
* Returns rules for given rules execution set uri.
*
* @param uri name of rule set
* @return rules for given rules execution set uri
*/
Collection<Rule> get(String uri);
}
loadRules
method takes rule set identifier as a parameter and return collection of rules associated with this identifier.
In order to use your own implementation of RuleRepository
you have to specify that during rules engine creation like this:
RulesEngine rulesEngine = new RulesEngineBuilder()
.withRulesRepository(new CustomRulesRepository())
.build();
As you can see we are using create
method from RulesRepositoryAdapterFactory
and as a parameter we are passing our
custom rule repository.
Another important feature of YARE is possibility of fact modification during execution. For example you are allowed to set flags in fact using first rule and then in second rule check for this flag and take determined action.
To show this capability of YARE we will use Airline
fact. In first rule we will set flag according to our
logic and then in second rule we will filter facts with changed flag.
As I said before, Airline
will serve as a fact this time.
public class Airline {
private final String name;
private boolean rejected = false;
public Airline(String name) {
this.name = name;
}
public String getName() {
return name;
}
public boolean getRejected() {
return rejected;
}
public Airline withRejected(boolean rejected) {
this.rejected = rejected;
return this;
}
}
In this example TestAction
will not be good enough. This time we need two actions: one collecting and one setting rejected
flag.
public class AirlineTestAction {
public void setFlag(Airline airlineFact, Boolean value) {
airlineFact.withRejected(value);
}
public void collect(List<Airline> facts, Airline fact) {
facts.add(fact);
}
}
First rule will be responsible for setting rejected
flag to true
if name
of airline is equal to given.
Afterwards second rule will filter airlines with flag set to true
.
List<Rule> rules = Arrays.asList(
RuleDsl.ruleBuilder()
.name("Rule matching when airline name is equal to \"Lufthansa\"")
.fact("airline", Airline.class)
.predicate(
equal(
value("${airline.name}"),
value("Lufthansa")
)
)
.action("setFlag",
param("airlineFact", value("${airline}")),
param("value", value(true)))
.build(),
RuleDsl.ruleBuilder()
.name("Rule matching when rejected flag set to true")
.fact("airline", Airline.class)
.predicate(
value("${airline.rejected}")
)
.action("collect",
param("context", value("${ctx}")),
param("fact", value("${airline}")))
.build()
);
As you probably suspect, rules engine need to vary a bit from previous ones since we are using two actions. But there will also be more significant change.
RulesEngine rulesEngine = new RulesEngineBuilder()
.withRulesRepository(i -> rules)
.withActionMapping("setFlag", method(airlineTestAction, (action) -> action.setFlag(null, null)))
.withActionMapping("collect", method(airlineTestAction, (action) -> action.collect(null, null)))
.withRulesExecutorBuilder(new DefaultRulesExecutorBuilder()
.withSequentialMode(true))
.build();
What is different is that we are using DefaultRulesExecutorBuilder withSequentialMode(boolean sequentialMode)
method. It is responsible for setting mode where rules and actions are executed in order of addition to rules engine. By default this flag is set
to false
, so rules engine may take effort to optimize rules execution. That is the reason why order of execution is not guaranteed.
Important
|
If you are planning to depend on modifying facts during execution, you have to turn on sequential mode. |
In order to control flow of facts processing, there is an optional action parameter EngineController
available.
The EngineController
exposes a method closeSession()
. When method is called within an action,
the whole procession of fact is stopped and current result is returned.
List<Rule> rules = Collections.singletonList(RuleDsl.ruleBuilder()
.name("Should match airline when airline name equal given")
.fact("airline", Airline.class)
.predicate(
equal(
value("${airline.name}"),
value("Lufthansa")
)
)
.action("collectUpToTwoMatchFacts",
param("facts", value("${ctx}")),
param("fact", value("${airline}")),
param("engineController", value("${engineController}")))
.build())
public class CloseSessionTestAction {
private static final int MAX_FACTS = 2;
public void collect(List<Airline> facts, Airline fact, EngineController engineController) {
facts.add(fact);
if (facts.size() == MAX_FACTS) {
engineController.closeSession();
}
}
}
In this example evaluation will be stopped after MAX_FACTS
of facts is collected.
In previous examples we have used collections of facts of the same type as an input. YARE allows to use multi-type ones as well.
If you provide YARE with input of multi-type facts collection and there will be many facts of the same type,
only first instance of each type fact will be used during computation. It is possible to change this behavior. In order to
do so please use DefaultRulesExecutorBuilder::withCrossProductMode
. With this mode on, cartesian product of all facts grouped
by type will be created.
Warning
|
Cross product mode can cause serious performance implications since rules engine must compute significantly more fact tuples. |
After studying this User Guide we suggest you to take the following steps to develop your YARE skills.