Skip to content
Kamil Sobolewski edited this page Mar 13, 2019 · 14 revisions

Yet Another Rules Engine User Guide

Introduction

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.

About YARE

What is Yet Another Rules Engine (YARE)?

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.

Why Yet Another Rules Engine?

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

How it works

YARE works similarly to database. Rules are equivalent to database queries. Facts can be treated as database tables and each instance of fact is just like row from this table.

In conclusion:

YARE

Database

Fact

Table

Instance of fact

Table row

Rule

Query

User Guide

Dictionary

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.

The Basics

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"
}

Action

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!");
    }
}

Fact

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 {
}

Rule

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()
);
Let’s describe methods in this rule step by step:
  • 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.

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();
Let’s describe methods in this engine step by step:
  • 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 using CallMetadata 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.

Working example

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.

Simple execution 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.

Moving forward

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.

Action

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.

Fact

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:

  1. By making field public.

  2. 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.

  3. 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.

Rule

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.

Expressions

Supported types

YARE operator

Arrays, ZonedDateTime, primitives, classes with equals implementation

equal

ZonedDateTime, primitives, classes implementing java.lang.Comparable

less

lessOrEqual

greater

greaterOrEqual

java.lang.String

match

Classes implementing java.util.Collection

contains

containsAny

Anything

isNull

Anything that evaluates to Boolean

isTrue

isFalse

Referring to a field

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.

Chaining dot operator

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.

Fact structure

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.

Chaining dot operator

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
[*] operator

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.

Chaining dot operator flatten operator

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.

Chaining dot operator null behavior

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

Passing reference to a fact

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.

Rules Engine

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.

Working example

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”.

Function usage

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.

Before we begin

Warning
Functions in YARE are considered as pure functions.

Action

In terms of action nothing changes here. TestAction will be still in usage.

Function

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);
    }
}

Fact

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

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.

Connectives

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.
Functions

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.

Rules Engine

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.

Working example

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.

JavaScript functions

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.

Rule formats

Until now we have been using rules created directly using Java DSL.

YARE allows four methods of rule modeling:
  1. Java DSL

  2. XML

  3. JSON

  4. 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.

Java DSL

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();

XML

<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>

JSON

{
  "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}"
    } ]
  } ]
}

YAML

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}"

Simplified type aliases

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.

Example:

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"/>

Default aliases

Following aliases are available by default:

Alias

Type names

Object

java.lang.Object

String

java.lang.String

Integer

java.lang.Integer

Long

java.lang.Long

Double

java.lang.Double

Boolean

java.lang.Boolean

Byte

java.lang.Byte

Short

java.lang.Short

Character

java.lang.Character

Float

java.lang.Float

Void

java.lang.Void

int

int

long

long

double

double

boolean

boolean

byte

byte

short

short

char

char

float

float

void

void

ZonedDateTime

java.time.ZonedDateTime

BigDecimal

java.math.BigDecimal

List

java.util.List

Map

java.util.Map

Set

java.util.Set

Expression.Undefined

com.sabre.oss.yare.core.model.Expression$Undefined

Rule validation

Another worth mentioning feature of Java Rules Engine is rule validation.

Java DSL

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.

XML/JSON/YAML

First of all add dependency to appropriate YARE module to your project.

  1. 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"
}
  1. 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.

JSON Schema validation

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".

XSD validation

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.

Operands in rules

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)

java.lang.Boolean

Boolean

true , false

java.lang.Integer

Integer

21682653

java.lang.Long

Long

1910928874

java.lang.String

String

any string

java.math.BigDecimal

BigDecimal

-123.20

java.time.ZonedDateTime

ZonedDateTime

2012-12-02T11:15:00+00:00

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

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.

JSON/YAML

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.

Rule sets

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.

How to use?

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.

Map approach

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.

Session

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.

Rule repository

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.

Modifying facts during execution

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.

Fact

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;
    }
}

Action

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);
    }
}

Rules

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()
);

Rules Engine

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.

Working example

Executing this example is similar to previous ones, where we haven’t used rule sets.

Stopping evaluation of facts

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.

EngineController registration
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())
Example action
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.

Multi-type fact input

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.

What’s next?

After studying this User Guide we suggest you to take the following steps to develop your YARE skills.

yare-examples

After you checkout yare project, please head to yare-examples. It contains more complex examples of YARE usage, but not going beyond the scope of this User Guide. What is more, there is performance test, so you can verify yourself how fast is YARE.