Skip to main content

Writing Rules

Rules are classes that contain instance of Input filled with UnaryTests and instance of Output filled with OutputValues.

class Rule[Input[_[_]], Output[_[_]]](
matching: Input[UnaryTest],
output: Output[OutputValue]
)

UnaryTest is a simple predicate that loosely follows the FEEL model. OutputValue exposes implicit conversions for better UX. Both are specialized views on Expr.

trait UnaryTest[-T] extends Expr[T => Boolean]

opaque type OutputValue[T] <: Expr[T] = Expr[T]
object OutputValue {
implicit def toLiteral[T](t: T)(using LiteralShow[T]): OutputValue[T] = Literal(t)
implicit def fromExpr[T](expr: Expr[T]): OutputValue[T] = expr
}

So what is Expr? It's an expression that can be statically rendered into a string.

trait Expr[+Out] {
def evaluate: Out
def renderExpression: String
}

All the most common ways of building UnaryTests are accessible through it object. Implicit conversion between Expr[Boolean] and UnaryTest is also defined.

Built-in Expressions

Decisions4s provides basic numeric and boolean expressions that can be used by invoking methods on expressions. Raw values can be converted into expression through Literal and asLiteral extension method.

import decisions4s.*

val a: Expr[Int] = 1.asLiteral
val b: Expr[Boolean] = a > 0

val lowerThan5: UnaryTest[Int] = it < 5
val equalFoo: UnaryTest[String] = it.equalsTo("foo")
val complex: UnaryTest[Int] = it.satisfies(v => v > 1 && v < 5)

Custom Expressions

To define a custom expression its enough to extend Expr trait.

case class EndsWithFoo(argument: Expr[String]) extends Expr[Boolean] {
override def evaluate: Boolean = argument.evaluate.endsWith("foo")
override def renderExpression: String = s"endsWithFoo(${argument.renderExpression})"
}
val endsWithFoo: UnaryTest[String] = it.satisfies(EndsWithFoo.apply)
val endsWithFoo2: Expr[Boolean] = EndsWithFoo(Literal("myfoo"))

This can be further streamlined by using Function helper

extension (arg1: Expr[String]) {
def endsWith(arg2: Expr[String]) = Function.autoNamed[Boolean](arg1, arg2)(_.endsWith(_))
}
val endsWithFoo: UnaryTest[String] = it.satisfies(_.endsWith("foo".asLiteral))

FEEL Compatibility

The expressions provided by the library guarantee compatibility with FEEL. This means their rendered form, when evaluated, yields the same result as direct evaluation. This guarantee is provided to lower the mental load so that we can rely on a properly specified format instead of defining our own. Having said that, it's important to remember that rendered form is not intended to be evaluated. Decisions4s will use direct evaluation when evaluating decision tables.

User-defined expressions don't have to keep FEEL compatibility.

Accessing Other Inputs

By default, all matching logic operates on the input it is defined for. To access other pieces of input one can use wholeInput method. The same way can be used to build the output value based on inputs. The example below compares a with b and returns their sum if they are equal.

Rule(
matching = Input(
a = it.equalsTo(wholeInput.b),
b = it.catchAll,
),
output = Output(
c = wholeInput.a + wholeInput.b,
),
)

Using Data Structures As Inputs And Outputs

warning

This feature is experimental and might come with significantly rough edges around its API.

For more complicated decisions, it might be useful to group inputs or outputs into dedicated objects. Decisions4s supports this scenario through nested higher kinded data structures.

case class Name[F[_]](first: F[String], last: F[String]) derives HKD
case class Input[F[_]](motherName: F[Name[F]], fatherName: F[Name[F]]) derives HKD
case class Output[F[_]](childName: F[Name[F]]) derives HKD

Rule(
matching = Input(
motherName = it.catchAll,
fatherName = wholeInput[Input].fatherName.projection.last === "Smith",
),
output = ctx ?=>
Output(
childName = Name[OutputValue](
wholeInput.fatherName.projection.first,
wholeInput.motherName.projection.last,
),
),
)

As of now, matching has to be done through wholeInput and no method of it will work.

Nested data structures will be rendered as FEEL Context Expressions.