A simple purely functional engine to evaluate rules
APACHE-2.0 License
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"
Allow
, Deny
or Ignore
. Each kind of verdict can have 0 or more reasons.Rule
with its result RuleVerdict
and some other information like the execution time.RuleVerdict
but related to the whole engine. Can be Allowed
or Denied
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:
Let's write the rules!
Each Rule must have a unique name and can be:
RuleVerdict
F[RuleVerdict]
where F
is a monad.There are several ways to define a rule:
T
to F[RuleVerdict]
( or Id
for Pure Rules)T
to F[RuleVerdict]
( or Id
for Pure Rules). If the function is not defined for the input value, the rule is ignored.RuleVerdict
(e.g. Allow
or Deny
)T
to F[Boolean]
( or Id
for Pure Rules) and returns Allow
for true
or Deny
for false
T
to F[Boolean]
( or Id
for Pure Rules) and returns Allow
for false
or Deny
for true
T
to F[Boolean]
( or Id
for Pure Rules) where you can specify the behavior for true
and false
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:
Moreover, we can choose to run the engine in a pure way( with pure rules ) or in a monadic way (e.g. IO) using:
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!
// ------------------------------------------------------------
//
//
// ############################################################