Simple yet powerful rules engine that offers the flexibility of using the built-in engine and creating a custom one.
Below are the available engines that can be used to evaluate expressions:
This engine uses only Kotlin code to support all Operator functions, offering expressive performance. Although it doesn't support Kotlin expressions inside the expression operands, it can be a suitable choice for simpler rule sets or projects where you prefer using a statically-typed language like Kotlin.
Supported types:
- primitive Java types, boolean, string, number (extends)
- custom objects (reflection)
- maps
- lists
- arrays
val evaluator = com.rapatao.projects.ruleset.engine.evaluator.kotlin.KotlinEvaluator()implementation "com.rapatao.ruleset:kotlin-evaluator:$rulesetVersion"<dependency>
<groupId>com.rapatao.ruleset</groupId>
<artifactId>kotlin-evaluator</artifactId>
<version>$rulesetVersion</version>
</dependency>Mozilla Rhino is an open-source, embeddable JavaScript interpreter from Mozilla. This engine implementation supports using JavaScript expressions inside the rule operands and is particularly useful when rules contain complex logic or when you want to leverage JavaScript's extensive library of functions.
val evaluator = com.rapatao.projects.ruleset.engine.evaluator.rhino.RhinoEvaluator()implementation "com.rapatao.ruleset:rhino-evaluator:$rulesetVersion"<dependency>
<groupId>com.rapatao.ruleset</groupId>
<artifactId>rhino-evaluator</artifactId>
<version>$rulesetVersion</version>
</dependency>GraalJS is a high-performance JavaScript engine. This engine implementation supports using JavaScript expressions inside the rule operands and is particularly useful when rules contain complex logic or when you want to leverage JavaScript's extensive library of functions.
val evaluator = com.rapatao.projects.ruleset.engine.evaluator.graaljs.GraalJSEvaluator()implementation "com.rapatao.ruleset:graaljs-evaluator:$rulesetVersion"<dependency>
<groupId>com.rapatao.ruleset</groupId>
<artifactId>graaljs-evaluator</artifactId>
<version>$rulesetVersion</version>
</dependency>After adding the desired engine as the application dependency, copy and past the following code, replacing
the val evaluator: Evaluator = ... by the desired engine initialization instruction.
The following example initializes an Evaluator, and check if the given rule is valid to the given input data,
printing the result in the default output.
import com.rapatao.projects.ruleset.engine.Evaluator
import com.rapatao.projects.ruleset.engine.types.builder.extensions.equalsTo
val rule = "item.price" equalsTo 0
val input = mapOf("item" to mapOf("price" to 0))
val evaluator: Evaluator = ...
val result = evaluator.evaluate(rule, input)
println(result) // true
data class Item(val price: Double)
data class Input(val item: Item)
val result2 = evaluator.evaluate(rule, Input(item = Item(price = 0.0)))
println(result2) // trueIn the context of the engine, an expression is a decision table, where many statements can be executed using defined
operators, resulting in a boolean, where true means that the given input data matches, and false when it doesn't
match.
All provided operations can be created using the
builder: com.rapatao.projects.ruleset.engine.types.builder.ExpressionBuilder
The engine provides many built-in operators, but it also allows adding new ones or event overwriting the existing one.
| operator | description |
|---|---|
| equals | Represents the equality operator (==), used to check if two values are equal. |
| not_equals | Represents the inequality operator (!=), used to check if two values are not equal. |
| greater_than | Represents the greater than operator (>), used to compare if one value is greater than another. |
| greater_or_equal_than | Represents the greater than or equal to operator (>=), used to compare if one value is greater than or equal to another. |
| less_than | Represents the less than operator (<), used to compare if one value is less than another. |
| less_or_equal_than | Represents the less than or equal to operator (<=), used to compare if one value is less than or equal to another. |
| starts_with | Represents the operation to check if a string starts with a specified sequence of characters. |
| not_starts_with | Represents the operation to check if a string not starts with a specified sequence of characters. |
| ends_with | Represents the operation to check if a string ends with a specified sequence of characters. |
| not_ends_with | Represents the operation to check if a string not ends with a specified sequence of characters. |
| contains | Represents the operation to check if a string contains a specified sequence of characters or if an array/list contains a particular element. |
| not_contains | Represents the operation to check if a string not contains a specified sequence of characters or if an array/list not contains a particular element. |
It is possible to create custom operators by creating an implementation of the
interface com.rapatao.projects.ruleset.engine.types.operators.Operators.
The function name() identifies the operator, which is used when evaluating the expressions. The engine supports a
single Operator per name, which means that it is not possible to have more than one using the same name.
Each built-in operator has its own class and all of them are located at the package
com.rapatao.projects.ruleset.engine.types.operators. To override then it is not mandatory to use these base classes, it only needs to have the same name as the built-in operator.
There is no validation related to duplicated operator names, since it is required to allow overriding the built-in operator by one implemented by the user of this library.
"field".isTrue()
"field".isFalse()
"field" equalsTo 10
"field" equalsTo "\"value\""
"field" equalsTo "value"
"field" notEqualsTo 10
"field" notEqualsTo "\"value\""
"field" notEqualsTo "value"
"field" greaterThan 10
"field" greaterOrEqualThan 10
"field" lessThan 10
"field" lessOrEqualThan 10
"field" startsWith "\"value\""
"field" notStartsWith "\"value\""
"field" endsWith "\"value\""
"field" notEndsWith "\"value\""
"field" expContains "\"value\""
"field" expNotContains "\"value\""A grouped operation is evaluated as follows:
anyMatch: at least one inner expression must evaluate totrueallMatch: all inner expressions must evaluate totruenoneMatch: all inner expressions must evaluate tofalse
allMatch(
"field".isTrue(),
"price" lessThan 10.0,
),
anyMatch(
"field".isTrue(),
"price" lessThan 10.0,
),
noneMatch(
"field".isTrue(),
"price" lessThan 10.0,
),
Expression(
allMatch = listOf(
"field".isTrue(),
"price" lessThan 10.0,
),
anyMatch = listOf(
"field".isTrue(),
"price" lessThan 10.0,
),
noneMatch = listOf(
"field".isTrue(),
"price" lessThan 10.0,
)
)Range expressions can be composed using the from/fromInclusive extensions combined with to/toInclusive.
import com.rapatao.projects.ruleset.engine.types.builder.extensions.from
import com.rapatao.projects.ruleset.engine.types.builder.extensions.fromInclusive
// price > 10 AND price < 20
"price" from 10 to 20
// price >= 10 AND price <= 20
"price" fromInclusive 10 toInclusive 20Each Expression accepts an onFailure strategy that controls what happens when its evaluation throws (for example,
when a referenced field is missing from the input data).
| value | behavior |
|---|---|
THROW |
(default) re-throws the underlying exception |
TRUE |
swallows the exception and treats the expression as true |
FALSE |
swallows the exception and treats the expression as false |
The strategy can be set directly on the Expression constructor or applied to an existing expression via the
ifFail extension:
import com.rapatao.projects.ruleset.engine.types.OnFailure
import com.rapatao.projects.ruleset.engine.types.builder.extensions.equalsTo
import com.rapatao.projects.ruleset.engine.types.builder.extensions.ifFail
"item.optional.field" equalsTo 10 ifFail OnFailure.FALSEAll provided operations support serialization using Jackson with the definition
of a Mixin. The project currently targets Jackson 3.x (tools.jackson namespace).
Mixin interface: com.rapatao.projects.ruleset.jackson.ExpressionMixin
Example of usage:
import com.fasterxml.jackson.annotation.JsonInclude
import com.rapatao.projects.ruleset.engine.types.Expression
import com.rapatao.projects.ruleset.jackson.ExpressionMixin
import tools.jackson.databind.json.JsonMapper
import tools.jackson.module.kotlin.jacksonMapperBuilder
import tools.jackson.module.kotlin.readValue
val mapper: JsonMapper = jacksonMapperBuilder()
.changeDefaultPropertyInclusion { inclusion ->
inclusion.withValueInclusion(JsonInclude.Include.NON_NULL)
}
.addMixIn(Expression::class.java, ExpressionMixin::class.java)
.build()
val json = "{ serialized definition }"
val asMatcher: Expression = mapper.readValue(json)Serialized examples can be checked here
Although the example only uses
JSONas reference, by using the givenMix-inclass, it should support any serialization format provided by the Jackson library, likeYAMLandXML.