Skip to content

Commit

Permalink
Merge pull request #98 from geirolz/circe-report
Browse files Browse the repository at this point in the history
Add Circe module
  • Loading branch information
geirolz authored Aug 9, 2022
2 parents 1bcb85d + 9836994 commit 986e64d
Show file tree
Hide file tree
Showing 35 changed files with 761 additions and 258 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
- scala: 2.13.8
name: Scala2
test-tasks: test #coverage suspended due a bug
- scala: 3.1.2
- scala: 3.1.3
name: Scala3
test-tasks: test # scoverage doesn’t support Scala 3

Expand Down
14 changes: 12 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ lazy val erules: Project = project
)
)
)
.aggregate(core, generic, scalatest)
.aggregate(core, generic, circe, scalatest)

lazy val core: Project =
buildModule(
Expand All @@ -56,6 +56,16 @@ lazy val generic: Project =
scalacOptions ++= macroSettings(scalaVersion.value)
)

lazy val circe: Project =
buildModule(
prjModuleName = "circe",
toPublish = true,
parentFolder = "modules"
).dependsOn(core)
.settings(
libraryDependencies ++= ProjectDependencies.Circe.dedicated
)

lazy val scalatest: Project =
buildModule(
prjModuleName = "scalatest",
Expand Down Expand Up @@ -107,7 +117,7 @@ lazy val noPublishSettings: Seq[Def.Setting[_]] = Seq(

lazy val baseSettings: Seq[Def.Setting[_]] = Seq(
// scala
crossScalaVersions := List("2.13.8", "3.1.2"),
crossScalaVersions := List("2.13.8", "3.1.3"),
scalaVersion := crossScalaVersions.value.head,
scalacOptions ++= scalacSettings(scalaVersion.value),
// test
Expand Down
3 changes: 2 additions & 1 deletion core/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,6 @@ result.unsafeRunSync().asReport[String]


### Modules
- [erules-generic](https://github.com/geirolz/erules/tree/main/modules/generic/src)
- [erules-generic](https://github.com/geirolz/erules/tree/main/modules/generic)
- [erules-circe](https://github.com/geirolz/erules/tree/main/modules/circe)
- [erules-scalatest](https://github.com/geirolz/erules/tree/main/modules/scalatest)
18 changes: 1 addition & 17 deletions core/src/main/scala/erules/core/EngineResult.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package erules.core

import cats.Show
import erules.core.RuleResultsInterpreterVerdict.{Allowed, Denied}
import erules.core.report.StringReport

/** Describes the engine output.
*
Expand Down Expand Up @@ -41,18 +39,4 @@ object EngineResult extends EngineResultInstances {
(er1 +: erN).toList.reduce((a, b) => combine(data, a, b))
}

private[erules] trait EngineResultInstances {

implicit def catsShowInstanceForEngineResult[T](implicit
showT: Show[T] = Show.fromToString[T],
showERIR: Show[RuleResultsInterpreterVerdict[T]]
): Show[EngineResult[T]] =
er =>
StringReport.paragraph("ENGINE VERDICT", "#")(
s"""
|Data: ${showT.show(er.data)}
|Rules: ${er.verdict.evaluatedRules.size}
|${showERIR.show(er.verdict)}
|""".stripMargin
)
}
private[erules] trait EngineResultInstances
10 changes: 8 additions & 2 deletions core/src/main/scala/erules/core/EvalReason.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import cats.Show
* @param message
* reason message
*/
case class EvalReason(message: String) extends AnyVal
object EvalReason extends EvalReasonInstances {
case class EvalReason(message: String)
object EvalReason extends EvalReasonInstances with EvalReasonSyntax {

def stringifyList(reasons: List[EvalReason]): String =
reasons match {
Expand All @@ -21,3 +21,9 @@ object EvalReason extends EvalReasonInstances {
private[erules] trait EvalReasonInstances {
implicit val showInstanceForEvalReason: Show[EvalReason] = _.message
}

private[erules] trait EvalReasonSyntax {
implicit class EvalResultReasonStringOps(private val ctx: StringContext) {
def er(args: Any*): EvalReason = EvalReason(ctx.s(args))
}
}
21 changes: 14 additions & 7 deletions core/src/main/scala/erules/core/Rule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import cats.{Applicative, ApplicativeThrow, Contravariant, Eq, Functor, Order, S
import cats.data.NonEmptyList
import cats.effect.Clock
import cats.implicits.*
import erules.core.Rule.RuleBuilder
import erules.core.RuleVerdict.Ignore

import scala.util.Try
Expand Down Expand Up @@ -122,8 +123,8 @@ sealed trait Rule[+F[_], -T] extends Serializable {
*/
def evalRaw[FF[X] >: F[X], TT <: T](data: TT): FF[RuleVerdict]

/** Eval this rules. The evaluations result is stored into a 'Try', so the `ApplicativeError`
* doesn't raise error in case of failed rule evaluation
/** Eval this rules. The evaluations result is stored into a 'Either[Throwable, T]', so the
* `ApplicativeError` doesn't raise error in case of failed rule evaluation
*/
final def eval[FF[X] >: F[X], TT <: T](
data: TT
Expand All @@ -132,8 +133,8 @@ sealed trait Rule[+F[_], -T] extends Serializable {
evalRaw[FF, TT](data).attempt
).map { case (duration, res) =>
RuleResult[TT, RuleVerdict](
rule = this,
res.toTry,
rule = this,
verdict = res,
executionTime = Some(duration)
)
}
Expand All @@ -145,7 +146,7 @@ sealed trait Rule[+F[_], -T] extends Serializable {
.getOrElse(false)
}

object Rule extends RuleInstances {
object Rule extends RuleInstances with RuleSyntax {

import erules.core.utils.CollectionsUtils.*

Expand Down Expand Up @@ -179,7 +180,7 @@ object Rule extends RuleInstances {
apply(_ => Applicative[F].pure(v))
}

private case class RuleImpl[+F[_], -TT](
private[erules] case class RuleImpl[+F[_], -TT](
f: TT => F[RuleVerdict],
name: String,
description: Option[String] = None,
Expand Down Expand Up @@ -235,6 +236,12 @@ private[erules] trait RuleInstances {
r => s"Rule('${r.fullDescription}')"

implicit class PureRuleOps[F[_]: Functor, T](fa: F[PureRule[T]]) {
def covaryAll[G[_]: Applicative]: F[Rule[G, T]] = fa.map(_.covary[G])
def mapLift[G[_]: Applicative]: F[Rule[G, T]] = fa.map(_.covary[G])
}
}

private[erules] trait RuleSyntax {
implicit class RuleBuilderStringOps(private val ctx: StringContext) {
def r(args: Any*): RuleBuilder = new RuleBuilder(ctx.s(args))
}
}
31 changes: 5 additions & 26 deletions core/src/main/scala/erules/core/RuleResult.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package erules.core

import cats.{Eq, Order, Show}
import cats.{Eq, Order}
import erules.core.RuleVerdict.Deny

import scala.concurrent.duration.FiniteDuration
import scala.util.{Failure, Success, Try}

case class RuleResult[-T, +V <: RuleVerdict](
rule: AnyTypedRule[T],
verdict: Try[V],
verdict: EitherThrow[V],
executionTime: Option[FiniteDuration] = None
) extends Serializable {

Expand All @@ -23,16 +22,16 @@ object RuleResult extends RuleResultInstances {
type Free[-T] = RuleResult[T, RuleVerdict]

def const[T, V <: RuleVerdict](ruleName: String, v: V): RuleResult[T, V] =
RuleResult(Rule(ruleName).const[Try, T](v), Success(v))
RuleResult(Rule(ruleName).const[EitherThrow, T](v), Right(v))

def failed[T, V <: RuleVerdict](ruleName: String, ex: Throwable): RuleResult[T, V] =
RuleResult(Rule(ruleName).failed[Try, T](ex), Failure(ex))
RuleResult(Rule(ruleName).failed[EitherThrow, T](ex), Left(ex))

def noMatch[T, V <: RuleVerdict](v: V): RuleResult[T, V] =
const("No match", v)

def denyForSafetyInCaseOfError[T](rule: AnyTypedRule[T], ex: Throwable): RuleResult[T, Deny] =
RuleResult(rule, Failure(ex))
RuleResult(rule, Left(ex))
}

private[erules] trait RuleResultInstances {
Expand All @@ -50,24 +49,4 @@ private[erules] trait RuleResultInstances {
) 0
else -1
)

implicit def catsShowInstanceForRuleRuleResult[T]: Show[RuleResult[T, ? <: RuleVerdict]] =
er => {

val reasons: String = er.verdict.map(_.reasons) match {
case Failure(ex) => s"- Failed: $ex"
case Success(Nil) => ""
case Success(reasons) => s"- Because: ${EvalReason.stringifyList(reasons)}"
}

s"""|- Rule: ${er.rule.name}
|- Description: ${er.rule.description.getOrElse("")}
|- Target: ${er.rule.targetInfo.getOrElse("")}
|- Execution time: ${er.executionTime
.map(Show.catsShowForFiniteDuration.show)
.getOrElse("*not measured*")}
|
|- Verdict: ${er.verdict.map(_.typeName)}
|$reasons""".stripMargin
}
}
10 changes: 4 additions & 6 deletions core/src/main/scala/erules/core/RuleResultsInterpreter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package erules.core
import cats.data.NonEmptyList
import erules.core.RuleVerdict.{Allow, Deny, Ignore}

import scala.util.{Failure, Success}

trait RuleResultsInterpreter {
def interpret[T](report: NonEmptyList[RuleResult.Free[T]]): RuleResultsInterpreterVerdict[T]
}
Expand Down Expand Up @@ -50,13 +48,13 @@ private[erules] trait EvalResultsInterpreterInstances {

report.toList
.flatMap {
case _ @RuleResult(_: Rule[?, T], Success(_: Ignore), _) =>
case _ @RuleResult(_: Rule[?, T], Right(_: Ignore), _) =>
None
case _ @RuleResult(r: Rule[?, T], Failure(ex), _) =>
case _ @RuleResult(r: Rule[?, T], Left(ex), _) =>
Some(Left(RuleResult.denyForSafetyInCaseOfError(r.asInstanceOf[Rule[Nothing, T]], ex)))
case re @ RuleResult(_: Rule[?, T], Success(_: Deny), _) =>
case re @ RuleResult(_: Rule[?, T], Right(_: Deny), _) =>
Some(Left(re.asInstanceOf[Res[Deny]]))
case re @ RuleResult(_: Rule[?, T], Success(_: Allow), _) =>
case re @ RuleResult(_: Rule[?, T], Right(_: Allow), _) =>
Some(Right(re.asInstanceOf[Res[Allow]]))
}
.partitionMap[Res[Deny], Res[Allow]](identity) match {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package erules.core

import cats.data.NonEmptyList
import cats.Show
import erules.core.RuleResultsInterpreterVerdict.{Allowed, Denied}
import erules.core.report.StringReport

/** ADT to define the possible responses of the engine evaluation.
*/
Expand Down Expand Up @@ -38,29 +36,4 @@ object RuleResultsInterpreterVerdict {

case class Denied[T](evaluatedRules: NonEmptyList[RuleResult[T, RuleVerdict.Deny]])
extends RuleResultsInterpreterVerdict[T]

implicit def catsShowInstanceForRuleResultsInterpreterVerdict[T](implicit
evalRuleShow: Show[RuleResult[T, ? <: RuleVerdict]]
): Show[RuleResultsInterpreterVerdict[T]] =
t => {

val rulesReport: String = t.evaluatedRules
.map(er =>
StringReport.paragraph(er.rule.fullDescription)(
evalRuleShow.show(er)
)
)
.toList
.mkString("\n")

val tpe: String = t match {
case Allowed(_) => "Allowed"
case Denied(_) => "Denied"
}

s"""Interpreter verdict: $tpe
|
|$rulesReport
|""".stripMargin
}
}
8 changes: 4 additions & 4 deletions core/src/main/scala/erules/core/RuleVerdict.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ sealed trait RuleVerdict extends Serializable { this: RuleVerdictBecauseSupport[

/** Returns `true` if this is an instance of `Allow`
*/
val isAllow: Boolean = this.isInstanceOf[Allow]
final val isAllow: Boolean = this.isInstanceOf[Allow]

/** Returns `true` if this is an instance of `Deny`
*/
val isDeny: Boolean = this.isInstanceOf[Deny]
final val isDeny: Boolean = this.isInstanceOf[Deny]

/** Returns `true` if this is an instance of `Ignore`
*/
val isIgnore: Boolean = this.isInstanceOf[Ignore]
final val isIgnore: Boolean = this.isInstanceOf[Ignore]

/** String that represent just the kind
*/
val typeName: String = this match {
final val typeName: String = this match {
case _: RuleVerdict.Allow => "Allow"
case _: RuleVerdict.Deny => "Deny"
case _: RuleVerdict.Ignore => "Ignore"
Expand Down
9 changes: 4 additions & 5 deletions core/src/main/scala/erules/core/RulesEngine.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import org.typelevel.log4cats.StructuredLogger
import org.typelevel.log4cats.slf4j.Slf4jLogger

import scala.annotation.unused
import scala.util.{Failure, Success}

case class RulesEngine[F[_], T] private (
rules: NonEmptyList[Rule[F, T]],
Expand Down Expand Up @@ -64,8 +63,8 @@ case class RulesEngine[F[_], T] private (
rule
.eval(data)
.flatTap {
case RuleResult(_, Success(_), _) => F.unit
case RuleResult(rule, Failure(ex), _) =>
case RuleResult(_, Right(_), _) => F.unit
case RuleResult(rule, Left(ex), _) =>
logger match {
case Some(l) => l.info(ex)(s"$rule failed!")
case None => F.unit
Expand Down Expand Up @@ -95,12 +94,12 @@ object RulesEngine {
head1: Rule[G, T],
tail: Rule[G, T]*
)(implicit env: G[Any] <:< Id[Any]): RulesEngineIntBuilder[F, T] =
withRules[T](NonEmptyList.of[Rule[Id, T]](head1, tail*).covaryAll[F])
withRules[T](NonEmptyList.of[Rule[Id, T]](head1, tail*).mapLift[F])

def withRules[G[X] <: Id[X], T](rules: NonEmptyList[PureRule[T]])(implicit
env: G[Any] <:< Id[Any]
): RulesEngineIntBuilder[F, T] =
withRules[T](rules.covaryAll[F])
withRules[T](rules.mapLift[F])
}

class RulesEngineIntBuilder[F[_]: MonadThrow, T] private[RulesEngine] (
Expand Down
13 changes: 7 additions & 6 deletions core/src/main/scala/erules/core/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import cats.effect.IO

package object core {

type AnyF[_] = Any
type AnyRule = Rule[AnyF, Nothing]
type AnyTypedRule[-T] = Rule[AnyF, T]
type PureRule[-T] = Rule[Id, T]
type RuleIO[-T] = Rule[IO, T]

type AnyF[_] = Any
type EitherThrow[+T] = Either[Throwable, T]
type AnyRule = Rule[AnyF, Nothing]
type AnyTypedRule[-T] = Rule[AnyF, T]
type PureRule[-T] = Rule[Id, T]
type RuleIO[-T] = Rule[IO, T]
type PureRulesEngine[T] = RulesEngine[Id, T]
type RulesEngineIO[T] = RulesEngine[IO, T]

}
Loading

0 comments on commit 986e64d

Please sign in to comment.