{"id":17343276,"url":"https://github.com/geirolz/erules","last_synced_at":"2025-04-14T19:54:03.655Z","repository":{"id":38020095,"uuid":"402995121","full_name":"geirolz/erules","owner":"geirolz","description":"A simple purely functional engine to evaluate rules","archived":false,"fork":false,"pushed_at":"2025-03-24T14:54:41.000Z","size":490,"stargazers_count":26,"open_issues_count":6,"forks_count":3,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-03-28T08:12:11.171Z","etag":null,"topics":["dsl","engine","rule-engine","rules","rules-engine","scala"],"latest_commit_sha":null,"homepage":"https://geirolz.github.io/erules/","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/geirolz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-09-04T07:38:00.000Z","updated_at":"2025-03-13T08:29:40.000Z","dependencies_parsed_at":"2023-11-13T12:26:51.770Z","dependency_job_id":"f1c3add7-dae1-4519-b4ea-373617c63d2d","html_url":"https://github.com/geirolz/erules","commit_stats":null,"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/geirolz%2Ferules","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/geirolz%2Ferules/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/geirolz%2Ferules/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/geirolz%2Ferules/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/geirolz","download_url":"https://codeload.github.com/geirolz/erules/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248952030,"owners_count":21188421,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dsl","engine","rule-engine","rules","rules-engine","scala"],"created_at":"2024-10-15T16:08:57.355Z","updated_at":"2025-04-14T19:54:03.634Z","avatar_url":"https://github.com/geirolz.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Erules\n[![Build Status](https://github.com/geirolz/erules/actions/workflows/cicd.yml/badge.svg)](https://github.com/geirolz/erules/actions)\n[![Codacy Badge](https://api.codacy.com/project/badge/Grade/db3274b55e0c4031803afb45f58d4413)](https://www.codacy.com/manual/david.geirola/erules?utm_source=github.com\u0026amp;utm_medium=referral\u0026amp;utm_content=geirolz/erules\u0026amp;utm_campaign=Badge_Grade)\n[![Codacy Badge](https://app.codacy.com/project/badge/Coverage/3c5de42e8bfd493d8a47478541118a4f)](https://app.codacy.com/gh/geirolz/erules/dashboard?utm_source=gh\u0026utm_medium=referral\u0026utm_content=\u0026utm_campaign=Badge_coverage)\n[![Sonatype Nexus (Releases)](https://img.shields.io/nexus/r/com.github.geirolz/erules-core_2.13?server=https%3A%2F%2Foss.sonatype.org)](https://mvnrepository.com/artifact/com.github.geirolz/erules-core)\n[![Scala Steward badge](https://img.shields.io/badge/Scala_Steward-helping-blue.svg?style=flat\u0026logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAMAAAARSr4IAAAAVFBMVEUAAACHjojlOy5NWlrKzcYRKjGFjIbp293YycuLa3pYY2LSqql4f3pCUFTgSjNodYRmcXUsPD/NTTbjRS+2jomhgnzNc223cGvZS0HaSD0XLjbaSjElhIr+AAAAAXRSTlMAQObYZgAAAHlJREFUCNdNyosOwyAIhWHAQS1Vt7a77/3fcxxdmv0xwmckutAR1nkm4ggbyEcg/wWmlGLDAA3oL50xi6fk5ffZ3E2E3QfZDCcCN2YtbEWZt+Drc6u6rlqv7Uk0LdKqqr5rk2UCRXOk0vmQKGfc94nOJyQjouF9H/wCc9gECEYfONoAAAAASUVORK5CYII=)](https://scala-steward.org)\n[![Mergify Status](https://img.shields.io/endpoint.svg?url=https://api.mergify.com/v1/badges/geirolz/erules\u0026style=flat)](https://mergify.io)\n[![GitHub license](https://img.shields.io/github/license/geirolz/erules)](https://github.com/geirolz/erules/blob/main/LICENSE)\n\nA lightweight, simple, typed, and functional rules engine evaluator using the Cats core.\n\n## How to import\n\neRules supports Scala 2.13 and 3\n\n**Sbt**\n```sbt\n  libraryDependencies += \"com.github.geirolz\" %% \"erules-core\" % \"0.1.2\"\n```\n\n---\n\n## Glossary\n- **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.\n- **RuleVerdict**: Is the verdict of a rule, can be `Allow`, `Deny` or `Ignore`. Each kind of verdict can have 0 or more reasons.\n- **RuleResult**: The rule result is just a case class to couple the `Rule` with its result `RuleVerdict` and some other information like the execution time.\n- **EngineVerdict**: Same as `RuleVerdict` but related to the whole engine. Can be `Allowed` or `Denied`\n\n---\n\n## How to use\n\nGiven these data classes\n```scala\ncase class Country(value: String)\ncase class Age(value: Int)\n\ncase class Citizenship(country: Country)\ncase class Person(\n  name: String,\n  lastName: String,\n  age: Age,\n  citizenship: Citizenship\n)\n```\n\nAssuming we want to check:\n- The person is an adult\n- The person has UK citizenship\n\nLet's write the rules!\n\nEach Rule must have a unique name and can be:\n- **Pure**: a pure function that takes a value and returns a `RuleVerdict`\n- **Effect-ful**: a function that takes a value and returns a `F[RuleVerdict]` where `F` is a monad.\n\nThere are several ways to define a rule:\n- **apply**: defines a complete rule from `T` to `F[RuleVerdict]` ( or `Id` for Pure Rules)\n- **matchOrIgnore**: defines a partial function from `T` to `F[RuleVerdict]` ( or `Id` for Pure Rules). If the function is not defined for the input value, the rule is ignored.\n- **const**: defines a rule that always returns the same `RuleVerdict` (e.g. `Allow` or `Deny`)\n- **failed**: defines a rule that always fails with an exception\n- **assert**: defines a rule from `T` to `F[Boolean]` ( or `Id` for Pure Rules) and returns `Allow` for `true` or `Deny` for `false`\n- **assertNot**: defines a rule from `T` to `F[Boolean]` ( or `Id` for Pure Rules) and returns `Allow` for `false` or `Deny` for `true`\n- **fromBooleanF**: defines a rule from `T` to `F[Boolean]` ( or `Id` for Pure Rules) where you can specify the behavior for `true` and `false` values.\n\n\n```scala\nimport erules.Rule\nimport erules.PureRule\nimport erules.RuleVerdict.*\nimport cats.data.NonEmptyList\nimport cats.Id\n\nval checkCitizenship: PureRule[Citizenship] =\n  Rule(\"Check UK citizenship\") {\n    case Citizenship(Country(\"UK\")) =\u003e Allow.withoutReasons\n    case _ =\u003e Deny.because(\"Only UK citizenship is allowed!\")\n  }\n// checkCitizenship: PureRule[Citizenship] = RuleImpl(\u003cfunction1\u003e,RuleInfo(Check UK citizenship,None,None))\n\nval checkAdultAge: PureRule[Age] =\n  Rule(\"Check Age \u003e= 18\") {\n    case a: Age if a.value \u003e= 18 =\u003e Allow.withoutReasons\n    case _ =\u003e Deny.because(\"Only \u003e= 18 age are allowed!\")\n  }\n// checkAdultAge: PureRule[Age] = RuleImpl(\u003cfunction1\u003e,RuleInfo(Check Age \u003e= 18,None,None))\n\nval allPersonRules: NonEmptyList[PureRule[Person]] = NonEmptyList.of(\n  checkCitizenship\n    .targetInfo(\"citizenship\")\n    .contramap(_.citizenship),\n  checkAdultAge\n    .targetInfo(\"age\")\n    .contramap(_.age)\n)\n// 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 \u003e= 18,None,Some(age))))\n```\n\n---\n\nN.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`.\n\nOnce we define rules, we just need to create the `RuleEngine` to evaluate those rules.\n\nWe can run the engine in two ways:\n- *denyAllNotAllowed*: to deny all is not explicitly allowed.\n- *allowAllNotDenied*: to allow all is not explicitly denied.\n\nMoreover, we can choose to run the engine in a pure way( with pure rules ) or in a monadic way (e.g. IO) using:\n- *seqEvalPure*: to run the engine in a pure way with pure rules.\n- *seqEval*: to sequentially run the engine in a monadic way.\n- *parEval*: to parallel run the engine in a monadic way.\n- *parEvalN*: to parallel run the engine in a monadic way with a fixed parallelism level.\n\n\n```scala\nimport erules.*\nimport erules.implicits.*\nimport cats.effect.IO\nimport cats.effect.unsafe.implicits.*\n\nval person: Person = Person(\"Mimmo\", \"Rossi\", Age(16), Citizenship(Country(\"IT\")))\n// person: Person = Person(Mimmo,Rossi,Age(16),Citizenship(Country(IT)))\n\nval result: IO[EngineResult[Person]] =\n  RulesEngine\n    .withRules[Id, Person](allPersonRules)\n    .denyAllNotAllowed[IO]\n    .map(_.seqEvalPure(person))\n// result: IO[EngineResult[Person]] = IO(...)\n\n//yolo\nresult.unsafeRunSync().asReport[String]\n// res0: String = ###################### ENGINE VERDICT ######################\n// \n// Data: Person(Mimmo,Rossi,Age(16),Citizenship(Country(IT)))\n// Rules: 2\n// Interpreter verdict: Denied\n// \n// ------------ Check UK citizenship for citizenship -----------\n// - Rule: Check UK citizenship\n// - Description: \n// - Target: citizenship\n// - Execution time: *not measured*\n// \n// - Verdict: Right(Deny)\n// - Because: Only UK citizenship is allowed!\n// ------------------------------------------------------------\n// ------------------ Check Age \u003e= 18 for age -----------------\n// - Rule: Check Age \u003e= 18\n// - Description: \n// - Target: age\n// - Execution time: *not measured*\n// \n// - Verdict: Right(Deny)\n// - Because: Only \u003e= 18 age are allowed!\n// ------------------------------------------------------------\n// \n// \n// ############################################################\n```\n\n---\n\n### Modules\n- [erules-generic](https://github.com/geirolz/erules/tree/main/modules/generic)\n- [erules-circe](https://github.com/geirolz/erules/tree/main/modules/circe)\n- [erules-cats-xml](https://github.com/geirolz/erules/tree/main/modules/cats-xml)\n- [erules-scalatest](https://github.com/geirolz/erules/tree/main/modules/scalatest)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgeirolz%2Ferules","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgeirolz%2Ferules","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgeirolz%2Ferules/lists"}