A lightweight, simple, typed, and functional rules engine evaluator using the Cats core.
eRules supports Scala 2.13 and 3
Sbt
libraryDependencies += "com.github.geirolz" %% "erules-core" % "0.1.2"
- Rule: the definition of a rule, the check is pure and can be async. Each Rule must have a description. Each rule can have a targetInfo that is a string that describes the rule check target.
- RuleVerdict: Is the verdict of a rule, can be
Allow
,Deny
orIgnore
. Each kind of verdict can have 0 or more reasons. - RuleResult: The rule result is just a case class to couple the
Rule
with its resultRuleVerdict
and some other information like the execution time. - EngineVerdict: Same as
RuleVerdict
but related to the whole engine. Can beAllowed
orDenied
Given these data classes
case class Country(value: String)
case class Age(value: Int)
case class Citizenship(country: Country)
case class Person(
name: String,
lastName: String,
age: Age,
citizenship: Citizenship
)
Assuming we want to check:
- The person is an adult
- The person has UK citizenship
Let's write the rules!
Each Rule must have a unique name and can be:
- Pure: a pure function that takes a value and returns a
RuleVerdict
- Effect-ful: a function that takes a value and returns a
F[RuleVerdict]
whereF
is a monad.
There are several ways to define a rule:
- apply: defines a complete rule from
T
toF[RuleVerdict]
( orId
for Pure Rules) - matchOrIgnore: defines a partial function from
T
toF[RuleVerdict]
( orId
for Pure Rules). If the function is not defined for the input value, the rule is ignored. - const: defines a rule that always returns the same
RuleVerdict
(e.g.Allow
orDeny
) - failed: defines a rule that always fails with an exception
- assert: defines a rule from
T
toF[Boolean]
( orId
for Pure Rules) and returnsAllow
fortrue
orDeny
forfalse
- assertNot: defines a rule from
T
toF[Boolean]
( orId
for Pure Rules) and returnsAllow
forfalse
orDeny
fortrue
- fromBooleanF: defines a rule from
T
toF[Boolean]
( orId
for Pure Rules) where you can specify the behavior fortrue
andfalse
values.
import erules.Rule
import erules.PureRule
import erules.RuleVerdict.*
import cats.data.NonEmptyList
import cats.Id
val checkCitizenship: PureRule[Citizenship] =
Rule("Check UK citizenship") {
case Citizenship(Country("UK")) => Allow.withoutReasons
case _ => Deny.because("Only UK citizenship is allowed!")
}
// checkCitizenship: PureRule[Citizenship] = RuleImpl(<function1>,RuleInfo(Check UK citizenship,None,None))
val checkAdultAge: PureRule[Age] =
Rule("Check Age >= 18") {
case a: Age if a.value >= 18 => Allow.withoutReasons
case _ => Deny.because("Only >= 18 age are allowed!")
}
// checkAdultAge: PureRule[Age] = RuleImpl(<function1>,RuleInfo(Check Age >= 18,None,None))
val allPersonRules: NonEmptyList[PureRule[Person]] = NonEmptyList.of(
checkCitizenship
.targetInfo("citizenship")
.contramap(_.citizenship),
checkAdultAge
.targetInfo("age")
.contramap(_.age)
)
// allPersonRules: NonEmptyList[PureRule[Person]] = NonEmptyList(RuleImpl(scala.Function1$$Lambda$12770/0x000000080343ed50@4f3549e4,RuleInfo(Check UK citizenship,None,Some(citizenship))), RuleImpl(scala.Function1$$Lambda$12770/0x000000080343ed50@5c9b046d,RuleInfo(Check Age >= 18,None,Some(age))))
N.B. Importing even the erules-generic
you can use a macro to auto-generate the target info using the contramapTarget
method. contramapTarget
applies contramap and derives the target info by the contramap parameter. The contramap parameter must be inline and have the following form: _.bar.foo.test
.
Once we define rules, we just need to create the RuleEngine
to evaluate those rules.
We can run the engine in two ways:
- denyAllNotAllowed: to deny all is not explicitly allowed.
- allowAllNotDenied: to allow all is not explicitly denied.
Moreover, we can choose to run the engine in a pure way( with pure rules ) or in a monadic way (e.g. IO) using:
- seqEvalPure: to run the engine in a pure way with pure rules.
- seqEval: to sequentially run the engine in a monadic way.
- parEval: to parallel run the engine in a monadic way.
- parEvalN: to parallel run the engine in a monadic way with a fixed parallelism level.
import erules.*
import erules.implicits.*
import cats.effect.IO
import cats.effect.unsafe.implicits.*
val person: Person = Person("Mimmo", "Rossi", Age(16), Citizenship(Country("IT")))
// person: Person = Person(Mimmo,Rossi,Age(16),Citizenship(Country(IT)))
val result: IO[EngineResult[Person]] =
RulesEngine
.withRules[Id, Person](allPersonRules)
.denyAllNotAllowed[IO]
.map(_.seqEvalPure(person))
// result: IO[EngineResult[Person]] = IO(...)
//yolo
result.unsafeRunSync().asReport[String]
// res0: String = ###################### ENGINE VERDICT ######################
//
// Data: Person(Mimmo,Rossi,Age(16),Citizenship(Country(IT)))
// Rules: 2
// Interpreter verdict: Denied
//
// ------------ Check UK citizenship for citizenship -----------
// - Rule: Check UK citizenship
// - Description:
// - Target: citizenship
// - Execution time: *not measured*
//
// - Verdict: Right(Deny)
// - Because: Only UK citizenship is allowed!
// ------------------------------------------------------------
// ------------------ Check Age >= 18 for age -----------------
// - Rule: Check Age >= 18
// - Description:
// - Target: age
// - Execution time: *not measured*
//
// - Verdict: Right(Deny)
// - Because: Only >= 18 age are allowed!
// ------------------------------------------------------------
//
//
// ############################################################