Skip to content

Commit

Permalink
feat: enable library user to extend Engines to have customizable oper…
Browse files Browse the repository at this point in the history
…ators (#27)

This change enables each engine to have its operator without enforcing
others to have the same capabilities.

All built-in operations are still supported, and it doesn't restrict
having others built-in operators in the future.

By having this change, any library users can now start customizing the
existing operators and even creating new operators without enforcing to
have them in the library code base.
  • Loading branch information
rapatao authored Jul 6, 2024
1 parent 7311cc8 commit 9bce6f3
Show file tree
Hide file tree
Showing 40 changed files with 811 additions and 560 deletions.
490 changes: 245 additions & 245 deletions JSON.md

Large diffs are not rendered by default.

47 changes: 35 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ import com.rapatao.projects.ruleset.engine.types.builder.equalsTo
val rule = "item.price" equalsTo 0
val input = mapOf("item" to mapOf("price" to 0))

val evaluator: Evaluator = ...
val evaluator: Evaluator = ...

val result = evaluator.evaluate(rule, input)
println(result) // true
Expand All @@ -130,24 +130,47 @@ val result2 = evaluator.evaluate(rule, Input(item = Item(price = 0.0)))
println(result) // true
```

## Supported operations (expressions)
## Expressions (Rule)

The engine only supports `boolean` evaluations, which means that all operations must results in a boolean value.
In 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`

### Operators

The engine provides many built-in operators, but it also allows adding new ones or event overwriting the existing one.

#### Built-in operators

| 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. |
| ENDS_WITH | Represents the operation to check if a string 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 contains a particular element. |
| 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. |
| ends_with | Represents the operation to check if a string 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 contains a particular element. |

#### Customizing the operators

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

### Examples

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ package com.rapatao.projects.ruleset.engine
import com.rapatao.projects.ruleset.engine.context.EvalContext
import com.rapatao.projects.ruleset.engine.types.Expression
import com.rapatao.projects.ruleset.engine.types.OnFailure
import com.rapatao.projects.ruleset.engine.types.errors.UnknownOperator
import com.rapatao.projects.ruleset.engine.types.operators.Operator

/**
* The Evaluator is a base class used to evaluate a given rule expression against input data.
*/
abstract class Evaluator {
abstract class Evaluator(
operators: List<Operator>,
) {

private val declaredOperators = operators.associateBy { it.name().lowercase() }

/**
* Evaluates the given rule expression against the provided input data.
Expand Down Expand Up @@ -48,6 +54,13 @@ abstract class Evaluator {
*/
abstract fun name(): String

/**
* Return the operator implementation for the given name.
*
* @return The operator.
*/
fun operator(name: String): Operator? = declaredOperators[name]

private fun List<Expression>.processNoneMatch(context: EvalContext): Boolean {
return this.none {
usingFailureWrapper(it.onFailure) {
Expand Down Expand Up @@ -89,7 +102,11 @@ abstract class Evaluator {

private fun Expression.processExpression(context: EvalContext): Boolean {
return usingFailureWrapper(this.onFailure) {
context.process(this)
requireNotNull(this.operator) { "expression operator must not be null" }

val operator = operator(this.operator) ?: throw UnknownOperator(this.operator)

context.process(this.left, operator, this.right)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
package com.rapatao.projects.ruleset.engine.context

import com.rapatao.projects.ruleset.engine.types.Expression
import com.rapatao.projects.ruleset.engine.types.operators.Operator

/**
* Represents an evaluation context for processing expressions.
*/
fun interface EvalContext {

/**
* Processes an expression.
* Process the expression using the given operator
*
* @param expression the expression to process
* @return true if the expression is successfully processed, false otherwise
* @throws Exception if the expression processing fails and onFailure is set to THROW
*/
fun process(expression: Expression): Boolean
fun process(left: Any?, operator: Operator, right: Any?): Boolean
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.rapatao.projects.ruleset.engine.types

import com.rapatao.projects.ruleset.engine.Evaluator

/**
* Represents an expression used in a logical query.
*
Expand All @@ -19,7 +21,7 @@ data class Expression(
val anyMatch: List<Expression>? = null,
val noneMatch: List<Expression>? = null,
val left: Any? = null,
val operator: Operator? = null,
val operator: String? = null,
val right: Any? = null,
val onFailure: OnFailure = OnFailure.THROW,
) {
Expand All @@ -32,15 +34,25 @@ data class Expression(
*
* @return Boolean value indicating whether the object is valid.
*/
fun isValid(): Boolean {
val any = anyMatch?.map { it.isValid() }?.firstOrNull { !it } ?: true
val none = noneMatch?.map { it.isValid() }?.firstOrNull { !it } ?: true
val all = allMatch?.map { it.isValid() }?.firstOrNull { !it } ?: true
fun isValid(engine: Evaluator): Boolean {
val any = anyMatch?.map { it.isValid(engine) }?.firstOrNull { !it } ?: true
val none = noneMatch?.map { it.isValid(engine) }?.firstOrNull { !it } ?: true
val all = allMatch?.map { it.isValid(engine) }?.firstOrNull { !it } ?: true

val something = (any && none && all) || parseable()

return isValidGroup() && something && isValidOperator(engine)
}

private fun isValidOperator(engine: Evaluator): Boolean {
val validOperator = operator == null || engine.operator(operator) != null
return validOperator
}

private fun isValidGroup(): Boolean {
val has = anyMatch == null && noneMatch == null && allMatch == null && parseable()
val group = anyMatch != null || noneMatch != null || allMatch != null
val something = (any && none && all) || parseable()

return (has || group) && something
return (has || group)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.rapatao.projects.ruleset.engine.types.builder

import com.rapatao.projects.ruleset.engine.types.Expression
import com.rapatao.projects.ruleset.engine.types.Operator

/**
* A builder class for constructing expressions representing a between condition.
Expand All @@ -10,7 +9,7 @@ import com.rapatao.projects.ruleset.engine.types.Operator
* @property from The starting value of the range.
* @property operator The comparison operator for the condition.
*/
data class BetweenBuilder(val left: Any, val from: Any, val operator: Operator) {
data class BetweenBuilder(val left: Any, val from: Any, val operator: String) {
/**
* Creates a non-inclusive range expression using the given `to` value.
*
Expand All @@ -19,7 +18,7 @@ data class BetweenBuilder(val left: Any, val from: Any, val operator: Operator)
*/
infix fun to(to: Any): Expression = MatcherBuilder.allMatch(
ExpressionBuilder.expression(left = left, operator = operator, right = from),
ExpressionBuilder.expression(left = left, operator = Operator.LESS_THAN, right = to),
ExpressionBuilder.expression(left = left, operator = "less_than", right = to),
)

/**
Expand All @@ -30,7 +29,7 @@ data class BetweenBuilder(val left: Any, val from: Any, val operator: Operator)
*/
infix fun toInclusive(to: Any): Expression = MatcherBuilder.allMatch(
ExpressionBuilder.expression(left = left, operator = operator, right = from),
ExpressionBuilder.expression(left = left, operator = Operator.LESS_OR_EQUAL_THAN, right = to),
ExpressionBuilder.expression(left = left, operator = "less_or_equal_than", right = to),
)
}

Loading

0 comments on commit 9bce6f3

Please sign in to comment.