Skip to the content.

Erules

Build Status codecov Codacy Badge Sonatype Nexus (Releases) Scala Steward badge Mergify Status GitHub license

A lightweight, simple, typed, and functional rules engine evaluator using the Cats core.

How to import

eRules supports Scala 2.13 and 3

Sbt

  libraryDependencies += "com.github.geirolz" %% "erules-core" % "0.1.0"

Glossary


How to use

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:

There are several ways to define a rule:

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!
// ------------------------------------------------------------
// 
// 
// ############################################################

Modules