From c6444fb6b853389e8a6765e1cd028dc2bc55f1ed Mon Sep 17 00:00:00 2001 From: David Geirola Date: Tue, 9 Aug 2022 11:26:18 +0200 Subject: [PATCH 1/7] Refactoring Report --- build.sbt | 10 ++ .../main/scala/erules/core/EngineResult.scala | 18 +--- core/src/main/scala/erules/core/Rule.scala | 12 +-- .../main/scala/erules/core/RuleResult.scala | 31 +----- .../erules/core/RuleResultsInterpreter.scala | 10 +- .../core/RuleResultsInterpreterVerdict.scala | 27 ----- .../main/scala/erules/core/RuleVerdict.scala | 8 +- .../main/scala/erules/core/RulesEngine.scala | 9 +- core/src/main/scala/erules/core/package.scala | 13 +-- .../erules/core/report/StringReport.scala | 41 ------- .../core/report/StringReportEncoder.scala | 101 ++++++++++++++++++ .../scala/erules/core/report/package.scala | 2 +- .../erules/circe/GenericCirceInstances.scala | 33 ++++++ .../main/scala/erules/circe/instances.scala | 46 ++++++++ .../erules/circe/report/JsonReport.scala | 28 +++++ .../scala/erules/circe/report/package.scala | 8 ++ project/ProjectDependencies.scala | 13 +++ 17 files changed, 271 insertions(+), 139 deletions(-) delete mode 100644 core/src/main/scala/erules/core/report/StringReport.scala create mode 100644 core/src/main/scala/erules/core/report/StringReportEncoder.scala create mode 100644 modules/circe/src/main/scala/erules/circe/GenericCirceInstances.scala create mode 100644 modules/circe/src/main/scala/erules/circe/instances.scala create mode 100644 modules/circe/src/main/scala/erules/circe/report/JsonReport.scala create mode 100644 modules/circe/src/main/scala/erules/circe/report/package.scala diff --git a/build.sbt b/build.sbt index c7a9ae7..6064722 100644 --- a/build.sbt +++ b/build.sbt @@ -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", diff --git a/core/src/main/scala/erules/core/EngineResult.scala b/core/src/main/scala/erules/core/EngineResult.scala index 089fb26..d8b154b 100644 --- a/core/src/main/scala/erules/core/EngineResult.scala +++ b/core/src/main/scala/erules/core/EngineResult.scala @@ -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. * @@ -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 diff --git a/core/src/main/scala/erules/core/Rule.scala b/core/src/main/scala/erules/core/Rule.scala index 7628b22..cf5d94a 100644 --- a/core/src/main/scala/erules/core/Rule.scala +++ b/core/src/main/scala/erules/core/Rule.scala @@ -122,8 +122,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 @@ -132,8 +132,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) ) } @@ -179,7 +179,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, @@ -235,6 +235,6 @@ 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]) } } diff --git a/core/src/main/scala/erules/core/RuleResult.scala b/core/src/main/scala/erules/core/RuleResult.scala index c2c9473..af0613a 100644 --- a/core/src/main/scala/erules/core/RuleResult.scala +++ b/core/src/main/scala/erules/core/RuleResult.scala @@ -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 { @@ -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 { @@ -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 - } } diff --git a/core/src/main/scala/erules/core/RuleResultsInterpreter.scala b/core/src/main/scala/erules/core/RuleResultsInterpreter.scala index 2b6f97b..78d5dde 100644 --- a/core/src/main/scala/erules/core/RuleResultsInterpreter.scala +++ b/core/src/main/scala/erules/core/RuleResultsInterpreter.scala @@ -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] } @@ -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 { diff --git a/core/src/main/scala/erules/core/RuleResultsInterpreterVerdict.scala b/core/src/main/scala/erules/core/RuleResultsInterpreterVerdict.scala index 1e44d0f..3d3f741 100644 --- a/core/src/main/scala/erules/core/RuleResultsInterpreterVerdict.scala +++ b/core/src/main/scala/erules/core/RuleResultsInterpreterVerdict.scala @@ -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. */ @@ -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 - } } diff --git a/core/src/main/scala/erules/core/RuleVerdict.scala b/core/src/main/scala/erules/core/RuleVerdict.scala index 0a24a57..2d8ed0f 100644 --- a/core/src/main/scala/erules/core/RuleVerdict.scala +++ b/core/src/main/scala/erules/core/RuleVerdict.scala @@ -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" diff --git a/core/src/main/scala/erules/core/RulesEngine.scala b/core/src/main/scala/erules/core/RulesEngine.scala index 7c893ad..0c8b2c5 100644 --- a/core/src/main/scala/erules/core/RulesEngine.scala +++ b/core/src/main/scala/erules/core/RulesEngine.scala @@ -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]], @@ -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 @@ -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] ( diff --git a/core/src/main/scala/erules/core/package.scala b/core/src/main/scala/erules/core/package.scala index a2e06bc..5fda077 100644 --- a/core/src/main/scala/erules/core/package.scala +++ b/core/src/main/scala/erules/core/package.scala @@ -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] + } diff --git a/core/src/main/scala/erules/core/report/StringReport.scala b/core/src/main/scala/erules/core/report/StringReport.scala deleted file mode 100644 index 6c00ec0..0000000 --- a/core/src/main/scala/erules/core/report/StringReport.scala +++ /dev/null @@ -1,41 +0,0 @@ -package erules.core.report - -import cats.Show - -object StringReport extends StringReportInstances { - val defaultHeaderMaxLen: Int = 60 - val defaultSeparatorSymbol: String = "-" - - def apply[T](implicit re: StringReport[T]): StringReport[T] = re - - def fromShow[T: Show]: StringReport[T] = - (t: T) => Show[T].show(t) - - def paragraph( - title: String, - epSymbol: String = defaultSeparatorSymbol, - maxLen: Int = defaultHeaderMaxLen - )( - body: String - ): String = - s"""|${buildSeparatorAsString(title, epSymbol, maxLen)} - |$body - |${buildSeparatorAsString("", epSymbol, maxLen)}""".stripMargin - - def buildSeparatorAsString( - message: String = "", - sepSymbol: String = defaultSeparatorSymbol, - maxLen: Int = defaultHeaderMaxLen - ): String = { - - val fixedMessage = if (message.isEmpty) "" else s" $message " - val halfSize: Float = (maxLen - fixedMessage.length) / 2f - val halfSep: String = (0 until halfSize.toInt).map(_ => sepSymbol).mkString - val compensator: String = if (halfSize % 2 == 0) "" else sepSymbol - - s"$halfSep$compensator$fixedMessage$halfSep" - } -} -private[erules] trait StringReportInstances { - implicit def deriveStringReportFromShow[T: Show]: StringReport[T] = StringReport.fromShow[T] -} diff --git a/core/src/main/scala/erules/core/report/StringReportEncoder.scala b/core/src/main/scala/erules/core/report/StringReportEncoder.scala new file mode 100644 index 0000000..fa0ee55 --- /dev/null +++ b/core/src/main/scala/erules/core/report/StringReportEncoder.scala @@ -0,0 +1,101 @@ +package erules.core.report + +import cats.Show +import erules.core.* +import erules.core.RuleResultsInterpreterVerdict.{Allowed, Denied} + +object StringReportEncoder extends StringReportInstances { + val defaultHeaderMaxLen: Int = 60 + val defaultSeparatorSymbol: String = "-" + + def apply[T](implicit re: StringReportEncoder[T]): StringReportEncoder[T] = re + + def paragraph( + title: String, + epSymbol: String = defaultSeparatorSymbol, + maxLen: Int = defaultHeaderMaxLen + )( + body: String + ): String = + s"""|${buildSeparatorAsString(title, epSymbol, maxLen)} + |$body + |${buildSeparatorAsString("", epSymbol, maxLen)}""".stripMargin + + def buildSeparatorAsString( + message: String = "", + sepSymbol: String = defaultSeparatorSymbol, + maxLen: Int = defaultHeaderMaxLen + ): String = { + + val fixedMessage = if (message.isEmpty) "" else s" $message " + val halfSize: Float = (maxLen - fixedMessage.length) / 2f + val halfSep: String = (0 until halfSize.toInt).map(_ => sepSymbol).mkString + val compensator: String = if (halfSize % 2 == 0) "" else sepSymbol + + s"$halfSep$compensator$fixedMessage$halfSep" + } +} +private[erules] trait StringReportInstances { + + implicit def stringReportEncoderToShow[T](implicit encoder: StringReportEncoder[T]): Show[T] = + encoder.toShow + + implicit def stringReportEncoderForEngineResult[T](implicit + showT: Show[T] = Show.fromToString[T], + reportEncoderERIR: StringReportEncoder[RuleResultsInterpreterVerdict[T]] + ): StringReportEncoder[EngineResult[T]] = + er => + StringReportEncoder.paragraph("ENGINE VERDICT", "#")( + s""" + |Data: ${showT.show(er.data)} + |Rules: ${er.verdict.evaluatedRules.size} + |${reportEncoderERIR.report(er.verdict)} + |""".stripMargin + ) + + implicit def stringReportEncoderForRuleResultsInterpreterVerdict[T](implicit + reportEncoderEvalRule: StringReportEncoder[RuleResult[T, ? <: RuleVerdict]] + ): StringReportEncoder[RuleResultsInterpreterVerdict[T]] = + t => { + + val rulesReport: String = t.evaluatedRules + .map(er => + StringReportEncoder.paragraph(er.rule.fullDescription)( + reportEncoderEvalRule.report(er) + ) + ) + .toList + .mkString("\n") + + val tpe: String = t match { + case Allowed(_) => "Allowed" + case Denied(_) => "Denied" + } + + s"""Interpreter verdict: $tpe + | + |$rulesReport + |""".stripMargin + } + + implicit def stringReportEncoderForRuleRuleResult[T] + : StringReportEncoder[RuleResult[T, ? <: RuleVerdict]] = + er => { + + val reasons: String = er.verdict.map(_.reasons) match { + case Left(ex) => s"- Failed: $ex" + case Right(Nil) => "" + case Right(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 + } +} diff --git a/core/src/main/scala/erules/core/report/package.scala b/core/src/main/scala/erules/core/report/package.scala index aca5e87..bcb4b9c 100644 --- a/core/src/main/scala/erules/core/report/package.scala +++ b/core/src/main/scala/erules/core/report/package.scala @@ -1,5 +1,5 @@ package erules.core package object report { - type StringReport[T] = ReportEncoder[T, String] + type StringReportEncoder[T] = ReportEncoder[T, String] } diff --git a/modules/circe/src/main/scala/erules/circe/GenericCirceInstances.scala b/modules/circe/src/main/scala/erules/circe/GenericCirceInstances.scala new file mode 100644 index 0000000..8ab5ffe --- /dev/null +++ b/modules/circe/src/main/scala/erules/circe/GenericCirceInstances.scala @@ -0,0 +1,33 @@ +package erules.circe + +import io.circe.{Encoder, Json} + +import scala.concurrent.duration.FiniteDuration + +private[circe] object GenericCirceInstances extends GenericCirceEncoderInstances +private[circe] sealed trait GenericCirceEncoderInstances { + + import io.circe.syntax.* + + implicit final val finiteDurationEncoder: Encoder[FiniteDuration] = + Encoder.instance(a => + Json.obj( + "length" -> Json.fromLong(a.length), + "unit" -> Json.fromString(a.unit.name) + ) + ) + + implicit def eitherEncoder[A, B](implicit a: Encoder[A], b: Encoder[B]): Encoder[Either[A, B]] = + Encoder.instance { + case Left(v) => v.asJson + case Right(v) => v.asJson + } + + implicit val throwableEncoder: Encoder[Throwable] = + Encoder.instance(ex => + Json.obj( + "message" -> Json.fromString(ex.getMessage), + "causeMessage" -> Json.fromString(Option(ex.getCause).map(_.getMessage).getOrElse("")) + ) + ) +} diff --git a/modules/circe/src/main/scala/erules/circe/instances.scala b/modules/circe/src/main/scala/erules/circe/instances.scala new file mode 100644 index 0000000..5dd9f24 --- /dev/null +++ b/modules/circe/src/main/scala/erules/circe/instances.scala @@ -0,0 +1,46 @@ +package erules.circe + +import erules.core.* + +object instances extends ErulesTypesCirceInstances +private[erules] trait ErulesTypesCirceInstances { + + import erules.circe.GenericCirceInstances.* + import io.circe.* + import io.circe.syntax.* + + implicit def engineResultCirceEncoder[T: Encoder]: Encoder[EngineResult[T]] = + io.circe.generic.semiauto.deriveEncoder[EngineResult[T]] + + implicit def ruleResultsInterpreterCirceEncoder[T]: Encoder[RuleResultsInterpreterVerdict[T]] = + Encoder.instance { v => + Json.obj( + "type" -> Json.fromString(v.typeName), + "evaluatedRules" -> Json.fromValues(v.evaluatedRules.toList.map(_.asJson)) + ) + } + + implicit def ruleResultCirceEncoder[T]: Encoder[RuleResult[T, RuleVerdict]] = + io.circe.generic.semiauto.deriveEncoder[RuleResult[T, RuleVerdict]] + + implicit def ruleCirceEncoder[T]: Encoder[AnyTypedRule[T]] = + Encoder.instance { v => + Json.obj( + "name" -> Json.fromString(v.name), + "description" -> v.description.map(Json.fromString).getOrElse(Json.Null), + "targetInfo" -> v.targetInfo.map(Json.fromString).getOrElse(Json.Null), + "fullDescription" -> Json.fromString(v.fullDescription) + ) + } + + implicit final val ruleVerdictCirceEncoder: Encoder[RuleVerdict] = + Encoder.instance { v => + Json.obj( + "type" -> Json.fromString(v.typeName), + "reasons" -> Json.fromValues(v.reasons.map(_.asJson)) + ) + } + + implicit final val evalReasonCirceEncoder: Encoder[EvalReason] = + io.circe.generic.semiauto.deriveEncoder[EvalReason] +} diff --git a/modules/circe/src/main/scala/erules/circe/report/JsonReport.scala b/modules/circe/src/main/scala/erules/circe/report/JsonReport.scala new file mode 100644 index 0000000..c3cb044 --- /dev/null +++ b/modules/circe/src/main/scala/erules/circe/report/JsonReport.scala @@ -0,0 +1,28 @@ +package erules.circe.report + +import erules.core.* +import io.circe.Encoder + +object JsonReport extends JsonReportInstances { + def fromEncoder[T: Encoder]: JsonReportEncoder[T] = + (t: T) => Encoder[T].apply(t) +} +private[circe] trait JsonReportInstances { + + import erules.circe.instances.* + + implicit def engineResultJsonReportEncoder[T: Encoder]: JsonReportEncoder[EngineResult[T]] = + JsonReport.fromEncoder[EngineResult[T]] + + implicit def ruleResultsInterpreterVerdictJsonReportEncoder[T] + : JsonReportEncoder[RuleResultsInterpreterVerdict[T]] = + JsonReport.fromEncoder[RuleResultsInterpreterVerdict[T]] + + implicit def ruleRuleResultJsonReportEncoder[T] + : JsonReportEncoder[RuleResult[T, ? <: RuleVerdict]] = + JsonReport.fromEncoder[RuleResult[T, ? <: RuleVerdict]] + + implicit val ruleVerdictJsonReportEncoder: JsonReportEncoder[RuleVerdict] = + JsonReport.fromEncoder[RuleVerdict] + +} diff --git a/modules/circe/src/main/scala/erules/circe/report/package.scala b/modules/circe/src/main/scala/erules/circe/report/package.scala new file mode 100644 index 0000000..e0219c5 --- /dev/null +++ b/modules/circe/src/main/scala/erules/circe/report/package.scala @@ -0,0 +1,8 @@ +package erules.circe + +import erules.core.report.ReportEncoder +import io.circe.Json + +package object report { + type JsonReportEncoder[T] = ReportEncoder[T, Json] +} diff --git a/project/ProjectDependencies.scala b/project/ProjectDependencies.scala index daa9052..df44e0d 100644 --- a/project/ProjectDependencies.scala +++ b/project/ProjectDependencies.scala @@ -22,6 +22,19 @@ object ProjectDependencies { lazy val dedicated: Seq[ModuleID] = Nil } + object Circe { + lazy val dedicated: Seq[ModuleID] = Seq( + "io.circe" %% "circe-core" % "0.14.1", + "io.circe" %% "circe-generic" % "0.14.1" + ) + } + + object Xml { + lazy val dedicated: Seq[ModuleID] = Seq( + "org.scala-lang.modules" %% "scala-xml" % "2.1.0" + ) + } + object Scalatest { lazy val dedicated: Seq[ModuleID] = Seq( "org.typelevel" %% "cats-effect-testing-scalatest" % "1.4.0" From 432ca7187a2e95eb6e152acf3b81aa1627ba478e Mon Sep 17 00:00:00 2001 From: David Geirola Date: Tue, 9 Aug 2022 11:53:01 +0200 Subject: [PATCH 2/7] Fix test --- .../scala/erules/core/EngineResultSpec.scala | 46 +++++++++---------- .../core/RuleResultsInterpreterSpec.scala | 15 +++--- .../src/test/scala/erules/core/RuleSpec.scala | 29 ++++++------ .../scala/erules/core/RuleVerdictSpec.scala | 4 +- .../scala/erules/core/RulesEngineSpec.scala | 13 +++--- 5 files changed, 50 insertions(+), 57 deletions(-) diff --git a/core/src/test/scala/erules/core/EngineResultSpec.scala b/core/src/test/scala/erules/core/EngineResultSpec.scala index df1d6f9..b0637f0 100644 --- a/core/src/test/scala/erules/core/EngineResultSpec.scala +++ b/core/src/test/scala/erules/core/EngineResultSpec.scala @@ -3,13 +3,11 @@ package erules.core import cats.data.NonEmptyList import cats.Id import erules.core.RuleVerdict.{Allow, Deny} -import org.scalatest.TryValues +import org.scalatest.EitherValues import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec -import scala.util.Success - -class EngineResultSpec extends AnyWordSpec with Matchers with TryValues { +class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { "EngineResult.combine" should { @@ -31,7 +29,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with TryValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Allowed( NonEmptyList.of( - RuleResult(rule1, Success(RuleVerdict.Allow.because("R1"))) + RuleResult(rule1, Right(RuleVerdict.Allow.because("R1"))) ) ) ) @@ -40,7 +38,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with TryValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Allowed( NonEmptyList.of( - RuleResult(rule2, Success(RuleVerdict.Allow.because("R2"))) + RuleResult(rule2, Right(RuleVerdict.Allow.because("R2"))) ) ) ) @@ -49,8 +47,8 @@ class EngineResultSpec extends AnyWordSpec with Matchers with TryValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Allowed( NonEmptyList.of( - RuleResult(rule1, Success(RuleVerdict.Allow.because("R1"))), - RuleResult(rule2, Success(RuleVerdict.Allow.because("R2"))) + RuleResult(rule1, Right(RuleVerdict.Allow.because("R1"))), + RuleResult(rule2, Right(RuleVerdict.Allow.because("R2"))) ) ) ) @@ -74,7 +72,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with TryValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Allowed( NonEmptyList.of( - RuleResult(rule1, Success(RuleVerdict.Allow.because("R1"))) + RuleResult(rule1, Right(RuleVerdict.Allow.because("R1"))) ) ) ) @@ -83,7 +81,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with TryValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Denied( NonEmptyList.of( - RuleResult(rule2, Success(RuleVerdict.Deny.because("R2"))) + RuleResult(rule2, Right(RuleVerdict.Deny.because("R2"))) ) ) ) @@ -92,7 +90,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with TryValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Denied( NonEmptyList.of( - RuleResult(rule2, Success(RuleVerdict.Deny.because("R2"))) + RuleResult(rule2, Right(RuleVerdict.Deny.because("R2"))) ) ) ) @@ -116,7 +114,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with TryValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Denied( NonEmptyList.of( - RuleResult(rule1, Success(RuleVerdict.Deny.because("R1"))) + RuleResult(rule1, Right(RuleVerdict.Deny.because("R1"))) ) ) ) @@ -125,7 +123,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with TryValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Allowed( NonEmptyList.of( - RuleResult(rule2, Success(RuleVerdict.Allow.because("R2"))) + RuleResult(rule2, Right(RuleVerdict.Allow.because("R2"))) ) ) ) @@ -134,7 +132,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with TryValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Denied( NonEmptyList.of( - RuleResult(rule1, Success(RuleVerdict.Deny.because("R1"))) + RuleResult(rule1, Right(RuleVerdict.Deny.because("R1"))) ) ) ) @@ -158,7 +156,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with TryValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Denied( NonEmptyList.of( - RuleResult(rule1, Success(RuleVerdict.Deny.because("R1"))) + RuleResult(rule1, Right(RuleVerdict.Deny.because("R1"))) ) ) ) @@ -167,7 +165,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with TryValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Denied( NonEmptyList.of( - RuleResult(rule2, Success(RuleVerdict.Deny.because("R2"))) + RuleResult(rule2, Right(RuleVerdict.Deny.because("R2"))) ) ) ) @@ -176,8 +174,8 @@ class EngineResultSpec extends AnyWordSpec with Matchers with TryValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Denied( NonEmptyList.of( - RuleResult(rule1, Success(RuleVerdict.Deny.because("R1"))), - RuleResult(rule2, Success(RuleVerdict.Deny.because("R2"))) + RuleResult(rule1, Right(RuleVerdict.Deny.because("R1"))), + RuleResult(rule2, Right(RuleVerdict.Deny.because("R2"))) ) ) ) @@ -209,7 +207,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with TryValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Allowed( NonEmptyList.of( - RuleResult(rule1, Success(RuleVerdict.Allow.because("R1"))) + RuleResult(rule1, Right(RuleVerdict.Allow.because("R1"))) ) ) ) @@ -218,7 +216,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with TryValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Allowed( NonEmptyList.of( - RuleResult(rule2, Success(RuleVerdict.Allow.because("R2"))) + RuleResult(rule2, Right(RuleVerdict.Allow.because("R2"))) ) ) ) @@ -227,7 +225,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with TryValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Allowed( NonEmptyList.of( - RuleResult(rule3, Success(RuleVerdict.Allow.because("R3"))) + RuleResult(rule3, Right(RuleVerdict.Allow.because("R3"))) ) ) ) @@ -236,9 +234,9 @@ class EngineResultSpec extends AnyWordSpec with Matchers with TryValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Allowed( NonEmptyList.of( - RuleResult(rule1, Success(RuleVerdict.Allow.because("R1"))), - RuleResult(rule2, Success(RuleVerdict.Allow.because("R2"))), - RuleResult(rule3, Success(RuleVerdict.Allow.because("R3"))) + RuleResult(rule1, Right(RuleVerdict.Allow.because("R1"))), + RuleResult(rule2, Right(RuleVerdict.Allow.because("R2"))), + RuleResult(rule3, Right(RuleVerdict.Allow.because("R3"))) ) ) ) diff --git a/core/src/test/scala/erules/core/RuleResultsInterpreterSpec.scala b/core/src/test/scala/erules/core/RuleResultsInterpreterSpec.scala index 9c6c493..37777ff 100644 --- a/core/src/test/scala/erules/core/RuleResultsInterpreterSpec.scala +++ b/core/src/test/scala/erules/core/RuleResultsInterpreterSpec.scala @@ -4,13 +4,12 @@ import cats.data.NonEmptyList import erules.core.RuleResultsInterpreterVerdict.{Allowed, Denied} import erules.core.RuleVerdict.{Allow, Deny, Ignore} import org.scalatest.wordspec.AnyWordSpec -import org.scalatest.TryValues +import org.scalatest.EitherValues import org.scalatest.matchers.should.Matchers import scala.annotation.unused -import scala.util.{Failure, Try} -class RuleResultsInterpreterSpec extends AnyWordSpec with Matchers with TryValues { +class RuleResultsInterpreterSpec extends AnyWordSpec with Matchers with EitherValues { "EvalResultsInterpreter.Defaults.allowAllNotDenied" should { @@ -73,11 +72,11 @@ class RuleResultsInterpreterSpec extends AnyWordSpec with Matchers with TryValue val ex = new RuntimeException("BOOM") - val allowAll: Rule[Try, Foo] = Rule("Allow all").failed[Try, Foo](ex) + val allowAll: Rule[EitherThrow, Foo] = Rule("Allow all").failed[EitherThrow, Foo](ex) val result = interpreter.interpret( NonEmptyList.one( - RuleResult(allowAll, Failure(ex)) + RuleResult(allowAll, Left(ex)) ) ) @@ -93,7 +92,6 @@ class RuleResultsInterpreterSpec extends AnyWordSpec with Matchers with TryValue "return Denied for all not all explicitly denied values" in { - case class Foo(@unused x: String, @unused y: Int) val interpreter = RuleResultsInterpreter.Defaults.denyAllNotAllowed val result = interpreter.interpret( @@ -111,7 +109,6 @@ class RuleResultsInterpreterSpec extends AnyWordSpec with Matchers with TryValue "return Allowed for allowed value" in { - case class Foo(@unused x: String, @unused y: Int) val interpreter = RuleResultsInterpreter.Defaults.denyAllNotAllowed val result = interpreter.interpret( @@ -153,11 +150,11 @@ class RuleResultsInterpreterSpec extends AnyWordSpec with Matchers with TryValue val ex = new RuntimeException("BOOM") - val allowAll: Rule[Try, Foo] = Rule("Allow all").failed[Try, Foo](ex) + val allowAll: Rule[EitherThrow, Foo] = Rule("Allow all").failed[EitherThrow, Foo](ex) val result = interpreter.interpret( NonEmptyList.one( - RuleResult(allowAll, Failure(ex)) + RuleResult(allowAll, Left(ex)) ) ) diff --git a/core/src/test/scala/erules/core/RuleSpec.scala b/core/src/test/scala/erules/core/RuleSpec.scala index 44c57c8..d7dfabf 100644 --- a/core/src/test/scala/erules/core/RuleSpec.scala +++ b/core/src/test/scala/erules/core/RuleSpec.scala @@ -7,17 +7,16 @@ import cats.Id import erules.core.RuleVerdict.{Allow, Deny, Ignore} import erules.core.testings.{ErulesAsyncAssertingSyntax, ReportValues} import org.scalatest.wordspec.AsyncWordSpec -import org.scalatest.TryValues +import org.scalatest.EitherValues import org.scalatest.matchers.should.Matchers import scala.annotation.unused -import scala.util.{Failure, Success} class RuleSpec extends AsyncWordSpec with AsyncIOSpec with Matchers - with TryValues + with EitherValues with ErulesAsyncAssertingSyntax with ReportValues { @@ -62,10 +61,10 @@ class RuleSpec for { _ <- rule .eval(Foo("TEST", 0)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Success(Allow.withoutReasons))) + .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Allow.withoutReasons))) _ <- rule .eval(Bar("TEST", 1)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Success(Deny.withoutReasons))) + .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Deny.withoutReasons))) } yield () } @@ -87,10 +86,10 @@ class RuleSpec for { _ <- rule .eval(Foo("TEST", 0)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Failure(ex))) + .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Left(ex))) _ <- rule .eval(Bar("TEST", 1)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Success(Deny.withoutReasons))) + .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Deny.withoutReasons))) } yield () } } @@ -107,10 +106,10 @@ class RuleSpec for { _ <- rule .eval(Foo("TEST", 0)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Success(Allow.withoutReasons))) + .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Allow.withoutReasons))) _ <- rule .eval(Foo("TEST", 1)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Success(Deny.withoutReasons))) + .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Deny.withoutReasons))) } yield () } @@ -126,10 +125,10 @@ class RuleSpec for { _ <- rule .eval(Foo("TEST", 0)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Failure(ex))) + .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Left(ex))) _ <- rule .eval(Foo("TEST", 1)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Success(Deny.withoutReasons))) + .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Deny.withoutReasons))) } yield () } } @@ -149,11 +148,11 @@ class RuleSpec _ <- rule .covary[IO] .eval(Foo()) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Success(Allow.withoutReasons))) + .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Allow.withoutReasons))) _ <- rule .covary[IO] .eval(Bar()) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Success(Deny.withoutReasons))) + .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Deny.withoutReasons))) } yield () } } @@ -169,7 +168,7 @@ class RuleSpec rule .covary[IO] .eval(Foo("TEST", 0)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Success(Allow.withoutReasons))) + .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Allow.withoutReasons))) } "return the Ignore once evaluated out of the defined domain" in { @@ -182,7 +181,7 @@ class RuleSpec rule .covary[IO] .eval(Foo("TEST", 1)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Success(Ignore.noMatch))) + .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Ignore.noMatch))) } } diff --git a/core/src/test/scala/erules/core/RuleVerdictSpec.scala b/core/src/test/scala/erules/core/RuleVerdictSpec.scala index 54f116a..38a525c 100644 --- a/core/src/test/scala/erules/core/RuleVerdictSpec.scala +++ b/core/src/test/scala/erules/core/RuleVerdictSpec.scala @@ -3,9 +3,9 @@ package erules.core import cats.kernel.Monoid import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec -import org.scalatest.TryValues +import org.scalatest.EitherValues -class RuleVerdictSpec extends AnyWordSpec with Matchers with TryValues { +class RuleVerdictSpec extends AnyWordSpec with Matchers with EitherValues { import RuleVerdict.* diff --git a/core/src/test/scala/erules/core/RulesEngineSpec.scala b/core/src/test/scala/erules/core/RulesEngineSpec.scala index 04c66eb..c9700f3 100644 --- a/core/src/test/scala/erules/core/RulesEngineSpec.scala +++ b/core/src/test/scala/erules/core/RulesEngineSpec.scala @@ -12,7 +12,7 @@ import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AsyncWordSpec import org.scalatest.TryValues -import scala.util.{Success, Try} +import scala.util.{Right, Try} class RulesEngineSpec extends AsyncWordSpec @@ -80,8 +80,7 @@ class RulesEngineSpec case class Foo(x: String, y: Int) val denyXEqTest: PureRule[Foo] = Rule("Check X value").partially[Id, Foo] { - case Foo("TEST", _) => - Deny.withoutReasons + case Foo("TEST", _) => Deny.withoutReasons } val allowYEqZero: PureRule[Foo] = Rule("Check Y value").partially[Id, Foo] { case Foo(_, 0) => @@ -104,7 +103,7 @@ class RulesEngineSpec data = Foo("TEST", 0), verdict = Denied( NonEmptyList.of( - RuleResult(denyXEqTest, Success(RuleVerdict.Deny.withoutReasons)) + RuleResult(denyXEqTest, Right(RuleVerdict.Deny.withoutReasons)) ) ) ) @@ -131,7 +130,7 @@ class RulesEngineSpec data = Foo("TEST", 0), verdict = Allowed( NonEmptyList.of( - RuleResult(allowYEqZero, Success(RuleVerdict.Allow.withoutReasons)) + RuleResult(allowYEqZero, Right(RuleVerdict.Allow.withoutReasons)) ) ) ) @@ -228,7 +227,7 @@ class RulesEngineSpec data = Foo("TEST", 0), verdict = Denied( NonEmptyList.of( - RuleResult(denyXEqTest, Success(RuleVerdict.Deny.withoutReasons)) + RuleResult(denyXEqTest, Right(RuleVerdict.Deny.withoutReasons)) ) ) ) @@ -255,7 +254,7 @@ class RulesEngineSpec data = Foo("TEST", 0), verdict = Allowed( NonEmptyList.of( - RuleResult(allowYEqZero, Success(RuleVerdict.Allow.withoutReasons)) + RuleResult(allowYEqZero, Right(RuleVerdict.Allow.withoutReasons)) ) ) ) From de62801625367030ddfad6a5f5530cb3d669b162 Mon Sep 17 00:00:00 2001 From: David Geirola Date: Tue, 9 Aug 2022 12:02:33 +0200 Subject: [PATCH 3/7] Update Circe --- project/ProjectDependencies.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/ProjectDependencies.scala b/project/ProjectDependencies.scala index df44e0d..d8c1b55 100644 --- a/project/ProjectDependencies.scala +++ b/project/ProjectDependencies.scala @@ -24,8 +24,8 @@ object ProjectDependencies { object Circe { lazy val dedicated: Seq[ModuleID] = Seq( - "io.circe" %% "circe-core" % "0.14.1", - "io.circe" %% "circe-generic" % "0.14.1" + "io.circe" %% "circe-core" % "0.14.2", + "io.circe" %% "circe-generic" % "0.14.2" ) } From e45cb8f1bcaf2184cc2e2e769251760b44a19b6d Mon Sep 17 00:00:00 2001 From: David Geirola Date: Tue, 9 Aug 2022 16:44:20 +0200 Subject: [PATCH 4/7] Refactoring instances and syntax --- build.sbt | 2 +- .../main/scala/erules/core/EvalReason.scala | 8 ++- core/src/main/scala/erules/core/Rule.scala | 9 ++- .../erules/core/report/ReportEncoder.scala | 20 ++++++- .../core/report/StringReportEncoder.scala | 8 ++- .../erules/core/syntax/AllCoreSyntax.scala | 3 - .../core/syntax/EvalResultReasonSyntax.scala | 15 ----- .../core/syntax/ReportEncoderSyntax.scala | 9 --- core/src/main/scala/erules/implicits.scala | 13 +++- core/src/main/scala/erules/syntax.scala | 5 -- .../scala/erules/core/RulesEngineSpec.scala | 7 +-- .../core/report/StringReportEncoderSpec.scala | 60 +++++++++++++++++++ .../main/scala/erules/circe/instances.scala | 14 ++++- .../erules/circe/report/JsonReport.scala | 12 +++- .../circe/report/JsonReportEncoderSpec.scala | 60 +++++++++++++++++++ project/ProjectDependencies.scala | 34 ++++++----- 16 files changed, 213 insertions(+), 66 deletions(-) delete mode 100644 core/src/main/scala/erules/core/syntax/AllCoreSyntax.scala delete mode 100644 core/src/main/scala/erules/core/syntax/EvalResultReasonSyntax.scala delete mode 100644 core/src/main/scala/erules/core/syntax/ReportEncoderSyntax.scala delete mode 100644 core/src/main/scala/erules/syntax.scala create mode 100644 core/src/test/scala/erules/core/report/StringReportEncoderSpec.scala create mode 100644 modules/circe/src/test/scala/erules/circe/report/JsonReportEncoderSpec.scala diff --git a/build.sbt b/build.sbt index 6064722..434f146 100644 --- a/build.sbt +++ b/build.sbt @@ -31,7 +31,7 @@ lazy val erules: Project = project ) ) ) - .aggregate(core, generic, scalatest) + .aggregate(core, generic, circe, scalatest) lazy val core: Project = buildModule( diff --git a/core/src/main/scala/erules/core/EvalReason.scala b/core/src/main/scala/erules/core/EvalReason.scala index fc8e2d7..782224d 100644 --- a/core/src/main/scala/erules/core/EvalReason.scala +++ b/core/src/main/scala/erules/core/EvalReason.scala @@ -8,7 +8,7 @@ import cats.Show * reason message */ case class EvalReason(message: String) extends AnyVal -object EvalReason extends EvalReasonInstances { +object EvalReason extends EvalReasonInstances with EvalReasonSyntax { def stringifyList(reasons: List[EvalReason]): String = reasons match { @@ -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)) + } +} diff --git a/core/src/main/scala/erules/core/Rule.scala b/core/src/main/scala/erules/core/Rule.scala index cf5d94a..e73b42a 100644 --- a/core/src/main/scala/erules/core/Rule.scala +++ b/core/src/main/scala/erules/core/Rule.scala @@ -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 @@ -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.* @@ -238,3 +239,9 @@ private[erules] trait RuleInstances { 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)) + } +} diff --git a/core/src/main/scala/erules/core/report/ReportEncoder.scala b/core/src/main/scala/erules/core/report/ReportEncoder.scala index db39b64..90141fb 100644 --- a/core/src/main/scala/erules/core/report/ReportEncoder.scala +++ b/core/src/main/scala/erules/core/report/ReportEncoder.scala @@ -1,18 +1,34 @@ package erules.core.report -import cats.Show +import cats.{Functor, Show} trait ReportEncoder[T, R] { def report(t: T): R + def map[U](f: R => U): ReportEncoder[T, U] = + ReportEncoder.of(t => f(report(t))) + def toShow(implicit env: R <:< String): Show[T] = (t: T) => report(t) } -object ReportEncoder { +object ReportEncoder extends ReportEncoderInstances with ReportEncoderSyntax { def apply[T, R](implicit re: ReportEncoder[T, R]): ReportEncoder[T, R] = re def of[T, R](f: T => R): ReportEncoder[T, R] = (t: T) => f(t) } + +private[erules] trait ReportEncoderInstances { + implicit def reportEncoderFunctor[T]: Functor[ReportEncoder[T, *]] = + new Functor[ReportEncoder[T, *]] { + override def map[A, B](fa: ReportEncoder[T, A])(f: A => B): ReportEncoder[T, B] = fa.map(f) + } +} + +private[erules] trait ReportEncoderSyntax extends StringReportSyntax { + implicit class ReportableForAny[T](t: T) { + def asReport[R](implicit re: ReportEncoder[T, R]): R = re.report(t) + } +} diff --git a/core/src/main/scala/erules/core/report/StringReportEncoder.scala b/core/src/main/scala/erules/core/report/StringReportEncoder.scala index fa0ee55..c0567f0 100644 --- a/core/src/main/scala/erules/core/report/StringReportEncoder.scala +++ b/core/src/main/scala/erules/core/report/StringReportEncoder.scala @@ -4,7 +4,7 @@ import cats.Show import erules.core.* import erules.core.RuleResultsInterpreterVerdict.{Allowed, Denied} -object StringReportEncoder extends StringReportInstances { +object StringReportEncoder extends StringReportInstances with StringReportSyntax { val defaultHeaderMaxLen: Int = 60 val defaultSeparatorSymbol: String = "-" @@ -99,3 +99,9 @@ private[erules] trait StringReportInstances { |$reasons""".stripMargin } } + +private[erules] trait StringReportSyntax { + implicit class StringReportEncoderForAny[T](t: T) { + def asStringReport(implicit re: StringReportEncoder[T]): String = re.report(t) + } +} diff --git a/core/src/main/scala/erules/core/syntax/AllCoreSyntax.scala b/core/src/main/scala/erules/core/syntax/AllCoreSyntax.scala deleted file mode 100644 index ddc5a80..0000000 --- a/core/src/main/scala/erules/core/syntax/AllCoreSyntax.scala +++ /dev/null @@ -1,3 +0,0 @@ -package erules.core.syntax - -private[erules] trait AllCoreSyntax extends EvalResultReasonSyntax with ReportEncoderSyntax diff --git a/core/src/main/scala/erules/core/syntax/EvalResultReasonSyntax.scala b/core/src/main/scala/erules/core/syntax/EvalResultReasonSyntax.scala deleted file mode 100644 index a5eb4f8..0000000 --- a/core/src/main/scala/erules/core/syntax/EvalResultReasonSyntax.scala +++ /dev/null @@ -1,15 +0,0 @@ -package erules.core.syntax - -import erules.core.EvalReason -import erules.core.Rule.RuleBuilder - -private[syntax] trait EvalResultReasonSyntax { - - implicit class RuleBuilderStringOps(private val ctx: StringContext) { - def r(args: Any*): RuleBuilder = new RuleBuilder(ctx.s(args)) - } - - implicit class EvalResultReasonStringOps(private val ctx: StringContext) { - def er(args: Any*): EvalReason = EvalReason(ctx.s(args)) - } -} diff --git a/core/src/main/scala/erules/core/syntax/ReportEncoderSyntax.scala b/core/src/main/scala/erules/core/syntax/ReportEncoderSyntax.scala deleted file mode 100644 index 6371b3a..0000000 --- a/core/src/main/scala/erules/core/syntax/ReportEncoderSyntax.scala +++ /dev/null @@ -1,9 +0,0 @@ -package erules.core.syntax - -import erules.core.report.ReportEncoder - -private[syntax] trait ReportEncoderSyntax { - implicit class ReportableForAny[T](t: T) { - def asReport[R](implicit re: ReportEncoder[T, R]): R = re.report(t) - } -} diff --git a/core/src/main/scala/erules/implicits.scala b/core/src/main/scala/erules/implicits.scala index 085c705..04d64c0 100644 --- a/core/src/main/scala/erules/implicits.scala +++ b/core/src/main/scala/erules/implicits.scala @@ -1,11 +1,12 @@ package erules import erules.core.* -import erules.core.report.StringReportInstances -import erules.core.syntax.AllCoreSyntax +import erules.core.report.{ReportEncoderInstances, ReportEncoderSyntax, StringReportInstances} object implicits extends AllCoreInstances with AllCoreSyntax +//---------- INSTANCES ---------- +object instances extends AllCoreInstances private[erules] trait AllCoreInstances extends EngineResultInstances with EvalResultsInterpreterInstances @@ -13,4 +14,12 @@ private[erules] trait AllCoreInstances with RuleResultInstances with RuleVerdictInstances with RuleInstances + with ReportEncoderInstances with StringReportInstances + +//---------- SYNTAX ---------- +object syntax extends AllCoreSyntax +private[erules] trait AllCoreSyntax + extends RuleSyntax + with EvalReasonSyntax + with ReportEncoderSyntax diff --git a/core/src/main/scala/erules/syntax.scala b/core/src/main/scala/erules/syntax.scala deleted file mode 100644 index 4f692e8..0000000 --- a/core/src/main/scala/erules/syntax.scala +++ /dev/null @@ -1,5 +0,0 @@ -package erules - -import erules.core.syntax.AllCoreSyntax - -object syntax extends AllCoreSyntax diff --git a/core/src/test/scala/erules/core/RulesEngineSpec.scala b/core/src/test/scala/erules/core/RulesEngineSpec.scala index c9700f3..9e1839b 100644 --- a/core/src/test/scala/erules/core/RulesEngineSpec.scala +++ b/core/src/test/scala/erules/core/RulesEngineSpec.scala @@ -7,10 +7,9 @@ import cats.Id import erules.core.RuleResultsInterpreterVerdict.{Allowed, Denied} import erules.core.RulesEngine.DuplicatedRulesException import erules.core.RuleVerdict.{Allow, Deny} -import erules.core.testings.{ErulesAsyncAssertingSyntax, ReportValues} +import erules.core.testings.ErulesAsyncAssertingSyntax import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AsyncWordSpec -import org.scalatest.TryValues import scala.util.{Right, Try} @@ -18,9 +17,7 @@ class RulesEngineSpec extends AsyncWordSpec with AsyncIOSpec with Matchers - with TryValues - with ErulesAsyncAssertingSyntax - with ReportValues { + with ErulesAsyncAssertingSyntax { "RulesEngine" should { "Return a DuplicatedRulesException with duplicated rules" in { diff --git a/core/src/test/scala/erules/core/report/StringReportEncoderSpec.scala b/core/src/test/scala/erules/core/report/StringReportEncoderSpec.scala new file mode 100644 index 0000000..f12d104 --- /dev/null +++ b/core/src/test/scala/erules/core/report/StringReportEncoderSpec.scala @@ -0,0 +1,60 @@ +package erules.core.report + +import cats.effect.IO +import cats.effect.testing.scalatest.AsyncIOSpec +import cats.Id +import erules.core.{Rule, RulesEngine, RulesEngineIO} +import erules.core.RuleVerdict.Allow +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AsyncWordSpec + +class StringReportEncoderSpec extends AsyncWordSpec with AsyncIOSpec with Matchers { + + import erules.implicits.* + + "EngineResult.asReport" should { + + "Return a well-formatted string report" in { + + case class Foo(x: String, y: Int) + + val allowYEqZero: Rule[Id, Foo] = Rule("Check Y value").partially[Id, Foo] { case Foo(_, 0) => + Allow.withoutReasons + } + + val engine: IO[RulesEngineIO[Foo]] = + RulesEngine[IO] + .withRules(allowYEqZero) + .denyAllNotAllowed + + val result: IO[String] = + engine + .flatMap(_.parEval(Foo("TEST", 0))) + .map(_.drainExecutionsTime.asReport[String]) + + result + .asserting(str => + str shouldBe + """|###################### ENGINE VERDICT ###################### + | + |Data: Foo(TEST,0) + |Rules: 1 + |Interpreter verdict: Allowed + | + |----------------------- Check Y value ---------------------- + |- Rule: Check Y value + |- Description: + |- Target: + |- Execution time: *not measured* + | + |- Verdict: Right(Allow) + | + |------------------------------------------------------------ + | + | + |############################################################""".stripMargin + ) + } + } + +} diff --git a/modules/circe/src/main/scala/erules/circe/instances.scala b/modules/circe/src/main/scala/erules/circe/instances.scala index 5dd9f24..699097e 100644 --- a/modules/circe/src/main/scala/erules/circe/instances.scala +++ b/modules/circe/src/main/scala/erules/circe/instances.scala @@ -1,9 +1,15 @@ package erules.circe +import erules.circe.report.{JsonReportInstances, JsonReportSyntax} import erules.core.* -object instances extends ErulesTypesCirceInstances -private[erules] trait ErulesTypesCirceInstances { +object implicits extends CirceAllInstances with CirceAllSyntax + +//---------- INSTANCES ---------- +object instances extends CirceAllInstances +private[circe] trait CirceAllInstances extends BasicTypesCirceInstances with JsonReportInstances + +private[circe] trait BasicTypesCirceInstances { import erules.circe.GenericCirceInstances.* import io.circe.* @@ -44,3 +50,7 @@ private[erules] trait ErulesTypesCirceInstances { implicit final val evalReasonCirceEncoder: Encoder[EvalReason] = io.circe.generic.semiauto.deriveEncoder[EvalReason] } + +//---------- SYNTAX ---------- +object syntax extends CirceAllSyntax +private[circe] trait CirceAllSyntax extends JsonReportSyntax diff --git a/modules/circe/src/main/scala/erules/circe/report/JsonReport.scala b/modules/circe/src/main/scala/erules/circe/report/JsonReport.scala index c3cb044..bbc343b 100644 --- a/modules/circe/src/main/scala/erules/circe/report/JsonReport.scala +++ b/modules/circe/src/main/scala/erules/circe/report/JsonReport.scala @@ -1,11 +1,11 @@ package erules.circe.report import erules.core.* -import io.circe.Encoder +import io.circe.{Encoder, Json} -object JsonReport extends JsonReportInstances { +object JsonReport extends JsonReportInstances with JsonReportSyntax { def fromEncoder[T: Encoder]: JsonReportEncoder[T] = - (t: T) => Encoder[T].apply(t) + (t: T) => Encoder[T].apply(t).deepDropNullValues } private[circe] trait JsonReportInstances { @@ -26,3 +26,9 @@ private[circe] trait JsonReportInstances { JsonReport.fromEncoder[RuleVerdict] } + +private[circe] trait JsonReportSyntax { + implicit class JsonReportEncoderForAny[T](t: T) { + def asJsonReport(implicit re: JsonReportEncoder[T]): Json = re.report(t) + } +} diff --git a/modules/circe/src/test/scala/erules/circe/report/JsonReportEncoderSpec.scala b/modules/circe/src/test/scala/erules/circe/report/JsonReportEncoderSpec.scala new file mode 100644 index 0000000..f86c8c2 --- /dev/null +++ b/modules/circe/src/test/scala/erules/circe/report/JsonReportEncoderSpec.scala @@ -0,0 +1,60 @@ +package erules.circe.report + +import cats.effect.IO +import cats.Id +import erules.core.{Rule, RulesEngine, RulesEngineIO} +import erules.core.RuleVerdict.Allow +import io.circe.Json + +class JsonReportEncoderSpec extends munit.CatsEffectSuite { + + import erules.circe.implicits.* + import io.circe.generic.auto.* + import io.circe.literal.* + + test("EngineResult.asJsonReport return a well-formatted JSON report") { + case class Foo(x: String, y: Int) + + val allowYEqZero: Rule[Id, Foo] = Rule("Check Y value").partially[Id, Foo] { case Foo(_, 0) => + Allow.withoutReasons + } + + val engine: IO[RulesEngineIO[Foo]] = + RulesEngine[IO] + .withRules(allowYEqZero) + .denyAllNotAllowed + + val result: IO[Json] = + engine + .flatMap(_.parEval(Foo("TEST", 0))) + .map(_.drainExecutionsTime.asJsonReport) + + assertIO( + obtained = result, + returns = json""" + { + "data" : { + "x" : "TEST", + "y" : 0 + }, + "verdict" : { + "type" : "Allowed", + "evaluatedRules" : [ + { + "rule" : { + "name" : "Check Y value", + "fullDescription" : "Check Y value" + }, + "verdict" : { + "type" : "Allow", + "reasons" : [ + ] + } + } + ] + } + }""" + ) + } + +} diff --git a/project/ProjectDependencies.scala b/project/ProjectDependencies.scala index d8c1b55..ca16e20 100644 --- a/project/ProjectDependencies.scala +++ b/project/ProjectDependencies.scala @@ -3,6 +3,10 @@ import sbt.Keys.scalaVersion object ProjectDependencies { + private val circeVersion = "0.14.2" + private val catsVersion = "2.8.0" + private val catsEffectVersion = "3.3.14" + object Plugins { val compilerPluginsFor2_13: Seq[ModuleID] = Seq( compilerPlugin("org.typelevel" %% "kind-projector" % "0.13.2" cross CrossVersion.full), @@ -24,8 +28,11 @@ object ProjectDependencies { object Circe { lazy val dedicated: Seq[ModuleID] = Seq( - "io.circe" %% "circe-core" % "0.14.2", - "io.circe" %% "circe-generic" % "0.14.2" + "io.circe" %% "circe-core" % circeVersion, + "io.circe" %% "circe-generic" % circeVersion, + // test + "io.circe" %% "circe-parser" % circeVersion, + "io.circe" %% "circe-literal" % circeVersion ) } @@ -43,20 +50,15 @@ object ProjectDependencies { // -------------------------------------------------------// lazy val common: Seq[ModuleID] = Seq( - effects, - tests - ).flatten - - private val effects: Seq[ModuleID] = - Seq( - "org.typelevel" %% "cats-core" % "2.8.0", - "org.typelevel" %% "cats-effect" % "3.3.14", - "org.typelevel" %% "log4cats-slf4j" % "2.4.0", - "org.typelevel" %% "cats-effect-testing-scalatest" % "1.4.0" % Test - ) - - private val tests: Seq[ModuleID] = Seq( + "org.typelevel" %% "cats-core" % catsVersion, + "org.typelevel" %% "cats-effect" % catsEffectVersion, + "org.typelevel" %% "log4cats-slf4j" % "2.4.0", + // test + "org.typelevel" %% "cats-effect-testing-scalatest" % "1.4.0" % Test, "org.scalactic" %% "scalactic" % "3.2.13" % Test, - "org.scalatest" %% "scalatest" % "3.2.13" % Test + "org.scalatest" %% "scalatest" % "3.2.13" % Test, + "org.scalameta" %% "munit" % "0.7.29" % Test, + "org.typelevel" %% "munit-cats-effect-3" % "1.0.7" % Test ) + } From 06289f7a6ed0b8ad3196258fc60b08005bfdc835 Mon Sep 17 00:00:00 2001 From: David Geirola Date: Tue, 9 Aug 2022 16:59:53 +0200 Subject: [PATCH 5/7] Add docs --- core/docs/README.md | 3 +- modules/circe/README.md | 146 +++++++++++++++++++++++++++++++++++ modules/circe/docs/README.md | 84 ++++++++++++++++++++ 3 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 modules/circe/README.md create mode 100644 modules/circe/docs/README.md diff --git a/core/docs/README.md b/core/docs/README.md index c713ce7..ce8adc9 100644 --- a/core/docs/README.md +++ b/core/docs/README.md @@ -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) diff --git a/modules/circe/README.md b/modules/circe/README.md new file mode 100644 index 0000000..80e8077 --- /dev/null +++ b/modules/circe/README.md @@ -0,0 +1,146 @@ +# Erules Circe +The purpose of this module is to provid `Encoder` instances of `erules` types +and the `JsonReportEncoder` instances to produce a json report. + +**Sbt** +```sbt + libraryDependencies += "com.github.geirolz" %% "erules-core" % "0.0.4" + libraryDependencies += "com.github.geirolz" %% "erules-circe" % "0.0.4" +``` + +### Usage + +Given these data classes +```scala +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 +) +``` + +Let's write the rules! +```scala +import erules.core.Rule +import erules.core.RuleVerdict.* +import cats.data.NonEmptyList +import cats.Id + +val checkCitizenship: Rule[Id, Citizenship] = + Rule("Check UK citizenship").apply[Id, Citizenship]{ + case Citizenship(Country("UK")) => Allow.withoutReasons + case _ => Deny.because("Only UK citizenship is allowed!") + } +// checkCitizenship: Rule[Id, Citizenship] = RuleImpl(,Check UK citizenship,None,None) + +val checkAdultAge: Rule[Id, Age] = + Rule("Check Age >= 18").apply[Id, Age] { + case a: Age if a.value >= 18 => Allow.withoutReasons + case _ => Deny.because("Only >= 18 age are allowed!") + } +// checkAdultAge: Rule[Id, Age] = RuleImpl(,Check Age >= 18,None,None) + +val allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList.of( + checkCitizenship + .targetInfo("citizenship") + .contramap(_.citizenship), + checkAdultAge + .targetInfo("age") + .contramap(_.age) +) +// allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList(RuleImpl(scala.Function1$$Lambda$8400/0x0000000802349d20@495e880,Check UK citizenship,None,Some(citizenship)), RuleImpl(scala.Function1$$Lambda$8400/0x0000000802349d20@78151a35,Check Age >= 18,None,Some(age))) +``` + +Import +```scala +import erules.circe.implicits.* +``` + +And `circe-generic` to derive the `Person` encoder automatically +```scala +import io.circe.generic.auto.* +``` + +And create the JSON report +```scala +import erules.core.* +import erules.implicits.* +import erules.circe.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]] = for { + engine <- RulesEngine[IO].withRules[Id, Person](allPersonRules).denyAllNotAllowed + result <- engine.parEval(person) +} yield result +// result: IO[EngineResult[Person]] = IO(...) + +//yolo +result.unsafeRunSync().asJsonReport +// res0: io.circe.Json = { +// "data" : { +// "name" : "Mimmo", +// "lastName" : "Rossi", +// "age" : { +// "value" : 16 +// }, +// "citizenship" : { +// "country" : { +// "value" : "IT" +// } +// } +// }, +// "verdict" : { +// "type" : "Denied", +// "evaluatedRules" : [ +// { +// "rule" : { +// "name" : "Check UK citizenship", +// "targetInfo" : "citizenship", +// "fullDescription" : "Check UK citizenship for citizenship" +// }, +// "verdict" : { +// "type" : "Deny", +// "reasons" : [ +// { +// "message" : "Only UK citizenship is allowed!" +// } +// ] +// }, +// "executionTime" : { +// "length" : 140042, +// "unit" : "NANOSECONDS" +// } +// }, +// { +// "rule" : { +// "name" : "Check Age >= 18", +// "targetInfo" : "age", +// "fullDescription" : "Check Age >= 18 for age" +// }, +// "verdict" : { +// "type" : "Deny", +// "reasons" : [ +// { +// "message" : "Only >= 18 age are allowed!" +// } +// ] +// }, +// "executionTime" : { +// "length" : 12500, +// "unit" : "NANOSECONDS" +// } +// } +// ] +// } +// } +``` \ No newline at end of file diff --git a/modules/circe/docs/README.md b/modules/circe/docs/README.md new file mode 100644 index 0000000..c7bc138 --- /dev/null +++ b/modules/circe/docs/README.md @@ -0,0 +1,84 @@ +# Erules Circe +The purpose of this module is to provid `Encoder` instances of `erules` types +and the `JsonReportEncoder` instances to produce a json report. + +**Sbt** +```sbt + libraryDependencies += "com.github.geirolz" %% "erules-core" % "@VERSION@" + libraryDependencies += "com.github.geirolz" %% "erules-circe" % "@VERSION@" +``` + +### Usage + +Given these data classes +```scala mdoc:to-string +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 +) +``` + +Let's write the rules! +```scala mdoc:to-string +import erules.core.Rule +import erules.core.RuleVerdict.* +import cats.data.NonEmptyList +import cats.Id + +val checkCitizenship: Rule[Id, Citizenship] = + Rule("Check UK citizenship").apply[Id, Citizenship]{ + case Citizenship(Country("UK")) => Allow.withoutReasons + case _ => Deny.because("Only UK citizenship is allowed!") + } + +val checkAdultAge: Rule[Id, Age] = + Rule("Check Age >= 18").apply[Id, Age] { + case a: Age if a.value >= 18 => Allow.withoutReasons + case _ => Deny.because("Only >= 18 age are allowed!") + } + +val allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList.of( + checkCitizenship + .targetInfo("citizenship") + .contramap(_.citizenship), + checkAdultAge + .targetInfo("age") + .contramap(_.age) +) +``` + +Import +```scala mdoc:silent +import erules.circe.implicits.* +``` + +And `circe-generic` to derive the `Person` encoder automatically +```scala mdoc:silent +import io.circe.generic.auto.* +``` + +And create the JSON report +```scala mdoc:to-string +import erules.core.* +import erules.implicits.* +import erules.circe.implicits.* + +import cats.effect.IO +import cats.effect.unsafe.implicits.* + +val person: Person = Person("Mimmo", "Rossi", Age(16), Citizenship(Country("IT"))) + +val result: IO[EngineResult[Person]] = for { + engine <- RulesEngine[IO].withRules[Id, Person](allPersonRules).denyAllNotAllowed + result <- engine.parEval(person) +} yield result + +//yolo +result.unsafeRunSync().asJsonReport +``` \ No newline at end of file From df859591dca9c0931217e12d6e43f0ad0a9b35eb Mon Sep 17 00:00:00 2001 From: David Geirola Date: Tue, 9 Aug 2022 17:10:29 +0200 Subject: [PATCH 6/7] Fix Scala 3 tests --- core/src/main/scala/erules/core/EvalReason.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/scala/erules/core/EvalReason.scala b/core/src/main/scala/erules/core/EvalReason.scala index 782224d..9e30a7d 100644 --- a/core/src/main/scala/erules/core/EvalReason.scala +++ b/core/src/main/scala/erules/core/EvalReason.scala @@ -7,7 +7,7 @@ import cats.Show * @param message * reason message */ -case class EvalReason(message: String) extends AnyVal +case class EvalReason(message: String) object EvalReason extends EvalReasonInstances with EvalReasonSyntax { def stringifyList(reasons: List[EvalReason]): String = From 9836994ae00daf3097d741a3ce0b8f1ee3bb7fde Mon Sep 17 00:00:00 2001 From: David Geirola Date: Tue, 9 Aug 2022 17:12:40 +0200 Subject: [PATCH 7/7] Update Scala to 3.1.3 --- .github/workflows/cicd.yml | 2 +- build.sbt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 3dff3a9..9f44010 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -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 diff --git a/build.sbt b/build.sbt index 434f146..3c61493 100644 --- a/build.sbt +++ b/build.sbt @@ -117,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