diff --git a/README.md b/README.md index a3997fa..79035e5 100644 --- a/README.md +++ b/README.md @@ -51,27 +51,29 @@ Assuming we want to check: - The person has a UK citizenship Let's write the rules! + ```scala -import erules.core.Rule -import erules.core.RuleVerdict.* +import erules.Rule +import erules.PureRule +import erules.RuleVerdict.* import cats.data.NonEmptyList import cats.Id -val checkCitizenship: Rule[Id, Citizenship] = - Rule("Check UK citizenship").apply[Id, Citizenship]{ +val checkCitizenship: PureRule[Citizenship] = + Rule("Check UK citizenship") { case Citizenship(Country("UK")) => Allow.withoutReasons - case _ => Deny.because("Only UK citizenship is allowed!") + case _ => Deny.because("Only UK citizenship is allowed!") } -// checkCitizenship: Rule[Id, Citizenship] = RuleImpl(,Check UK citizenship,None,None) +// checkCitizenship: PureRule[Citizenship] = RuleImpl(,RuleInfo(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!") +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: Rule[Id, Age] = RuleImpl(,Check Age >= 18,None,None) +// checkAdultAge: PureRule[Age] = RuleImpl(,RuleInfo(Check Age >= 18,None,None)) -val allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList.of( +val allPersonRules: NonEmptyList[PureRule[Person]] = NonEmptyList.of( checkCitizenship .targetInfo("citizenship") .contramap(_.citizenship), @@ -79,7 +81,7 @@ val allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList.of( .targetInfo("age") .contramap(_.age) ) -// allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList(RuleImpl(scala.Function1$$Lambda$12152/0x0000000802cb7950@3df931c5,Check UK citizenship,None,Some(citizenship)), RuleImpl(scala.Function1$$Lambda$12152/0x0000000802cb7950@6b57b3a7,Check Age >= 18,None,Some(age))) +// allPersonRules: NonEmptyList[PureRule[Person]] = NonEmptyList(RuleImpl(scala.Function1$$Lambda$11131/0x000000080283b368@4e8bcb62,RuleInfo(Check UK citizenship,None,Some(citizenship))), RuleImpl(scala.Function1$$Lambda$11131/0x000000080283b368@4a39095c,RuleInfo(Check Age >= 18,None,Some(age)))) ``` N.B. Importing even the `erules-generic` you can use macro to auto-generate the target info using `contramapTarget` method. @@ -93,7 +95,7 @@ We can evaluate rules in two different ways: - allowAllNotDenied ```scala -import erules.core.* +import erules.* import erules.implicits.* import cats.effect.IO import cats.effect.unsafe.implicits.* @@ -101,10 +103,11 @@ 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 +val result: IO[EngineResult[Person]] = + RulesEngine + .withRules[Id, Person](allPersonRules) + .denyAllNotAllowed[IO] + .map(_.seqEvalPure(person)) // result: IO[EngineResult[Person]] = IO(...) //yolo @@ -119,7 +122,7 @@ result.unsafeRunSync().asReport[String] // - Rule: Check UK citizenship // - Description: // - Target: citizenship -// - Execution time: 115458 nanoseconds +// - Execution time: *not measured* // // - Verdict: Right(Deny) // - Because: Only UK citizenship is allowed! @@ -128,7 +131,7 @@ result.unsafeRunSync().asReport[String] // - Rule: Check Age >= 18 // - Description: // - Target: age -// - Execution time: 9125 nanoseconds +// - Execution time: *not measured* // // - Verdict: Right(Deny) // - Because: Only >= 18 age are allowed! diff --git a/core/docs/README.md b/core/docs/README.md index 774681f..6d4e85c 100644 --- a/core/docs/README.md +++ b/core/docs/README.md @@ -51,25 +51,27 @@ Assuming we want to check: - The person has a UK citizenship Let's write the rules! + ```scala mdoc:to-string -import erules.core.Rule -import erules.core.RuleVerdict.* +import erules.Rule +import erules.PureRule +import erules.RuleVerdict.* import cats.data.NonEmptyList import cats.Id -val checkCitizenship: Rule[Id, Citizenship] = - Rule("Check UK citizenship").apply[Id, Citizenship]{ +val checkCitizenship: PureRule[Citizenship] = + Rule("Check UK citizenship") { case Citizenship(Country("UK")) => Allow.withoutReasons - case _ => Deny.because("Only UK citizenship is allowed!") + 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 checkAdultAge: PureRule[Age] = + Rule("Check Age >= 18") { + 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( +val allPersonRules: NonEmptyList[PureRule[Person]] = NonEmptyList.of( checkCitizenship .targetInfo("citizenship") .contramap(_.citizenship), @@ -90,17 +92,18 @@ We can evaluate rules in two different ways: - allowAllNotDenied ```scala mdoc:to-string -import erules.core.* +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"))) -val result: IO[EngineResult[Person]] = for { - engine <- RulesEngine[IO].withRules[Id, Person](allPersonRules).denyAllNotAllowed - result <- engine.parEval(person) -} yield result +val result: IO[EngineResult[Person]] = + RulesEngine + .withRules[Id, Person](allPersonRules) + .denyAllNotAllowed[IO] + .map(_.seqEvalPure(person)) //yolo result.unsafeRunSync().asReport[String] diff --git a/core/examples/simple.sc b/core/examples/simple.sc index 129f70a..3cd27d9 100644 --- a/core/examples/simple.sc +++ b/core/examples/simple.sc @@ -11,23 +11,22 @@ case class Person( ) //------------- CREATE RULES -------------// -import erules.core.Rule -import erules.core.RuleVerdict._ +import erules.* +import erules.RuleVerdict.* import cats.data.NonEmptyList import cats.Id -val checkCitizenship: Rule[Id, Citizenship] = Rule("Check UK citizenship").apply[Id, Citizenship] { +val checkCitizenship: PureRule[Citizenship] = Rule("Check UK 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] { +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!") } -val allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList.of( +val allPersonRules: NonEmptyList[PureRule[Person]] = NonEmptyList.of( checkCitizenship .targetInfo("citizenship") .contramap(_.citizenship), @@ -37,17 +36,17 @@ val allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList.of( ) //-------------- RULES ENGINE --------------// -import erules.core.RulesEngine +import erules.RulesEngine import cats.effect.IO import cats.effect.unsafe.implicits._ val person: Person = Person("Mimmo", "Rossi", Age(16), Citizenship(Country("IT"))) val result = for { - engine <- RulesEngine[IO] + engine <- RulesEngine .withRules[Id, Person](allPersonRules) - .denyAllNotAllowed - result <- engine.parEval(person) + .denyAllNotAllowed[IO] + result = engine.seqEvalPure(person) } yield result result.unsafeRunSync() @@ -65,23 +64,23 @@ case class Person( ) //------------- CREATE RULES -------------// -import erules.core.Rule -import erules.core.RuleVerdict._ +import erules.* +import erules.RuleVerdict.* import cats.data.NonEmptyList -val checkCitizenship: Rule[Id, Citizenship] = - Rule("Check UK citizenship")[Id, Citizenship] { +val checkCitizenship: PureRule[Citizenship] = + Rule("Check UK citizenship") { case Citizenship(Country("UK")) => Allow.withoutReasons case _ => Deny.because("Only UK citizenship is allowed!") } -val checkAdultAge: Rule[Id, Age] = - Rule("Check Age >= 18")[Id, Age] { +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!") } -val allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList.of( +val allPersonRules: NonEmptyList[PureRule[Person]] = NonEmptyList.of( checkCitizenship .targetInfo("person.citizenship") .contramap(_.citizenship), @@ -91,16 +90,17 @@ val allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList.of( ) //-------------- RULES ENGINE --------------// -import erules.core.RulesEngine +import erules.* import cats.effect.IO import cats.effect.unsafe.implicits._ import erules.implicits._ val person: Person = Person("Mimmo", "Rossi", Age(16), Citizenship(Country("IT"))) -val result = for { - engine <- RulesEngine[IO].withRules[Id, Person](allPersonRules).denyAllNotAllowed - result <- engine.parEval(person) -} yield result +val result: IO[EngineResult[Person]] = + RulesEngine + .withRules(allPersonRules) + .denyAllNotAllowed[IO] + .map(_.seqEvalPure(person)) Console.println(result.unsafeRunSync().asReport) diff --git a/core/src/main/scala/erules/core/EngineResult.scala b/core/src/main/scala/erules/EngineResult.scala similarity index 71% rename from core/src/main/scala/erules/core/EngineResult.scala rename to core/src/main/scala/erules/EngineResult.scala index d8b154b..2e7d27b 100644 --- a/core/src/main/scala/erules/core/EngineResult.scala +++ b/core/src/main/scala/erules/EngineResult.scala @@ -1,6 +1,6 @@ -package erules.core +package erules -import erules.core.RuleResultsInterpreterVerdict.{Allowed, Denied} +import erules.RuleResultsInterpreterVerdict.{Allowed, Denied} /** Describes the engine output. * @@ -13,13 +13,13 @@ import erules.core.RuleResultsInterpreterVerdict.{Allowed, Denied} */ case class EngineResult[T]( data: T, - verdict: RuleResultsInterpreterVerdict[T] + verdict: RuleResultsInterpreterVerdict ) extends Serializable { def drainExecutionsTime: EngineResult[T] = copy(verdict = this.verdict match { - case a @ Allowed(erules) => a.copy(evaluatedRules = erules.map(_.drainExecutionTime)) - case a @ Denied(erules) => a.copy(evaluatedRules = erules.map(_.drainExecutionTime)) + case a @ Allowed(erules) => a.copy(evaluatedResults = erules.map(_.drainExecutionTime)) + case a @ Denied(erules) => a.copy(evaluatedResults = erules.map(_.drainExecutionTime)) }) } object EngineResult extends EngineResultInstances { @@ -35,7 +35,11 @@ object EngineResult extends EngineResultInstances { } ) - def combineAll[T](data: T, er1: EngineResult[T], erN: EngineResult[T]*): EngineResult[T] = + def combineAll[T]( + data: T, + er1: EngineResult[T], + erN: EngineResult[T]* + ): EngineResult[T] = (er1 +: erN).toList.reduce((a, b) => combine(data, a, b)) } diff --git a/core/src/main/scala/erules/core/EvalReason.scala b/core/src/main/scala/erules/EvalReason.scala similarity index 97% rename from core/src/main/scala/erules/core/EvalReason.scala rename to core/src/main/scala/erules/EvalReason.scala index 782224d..95d48f9 100644 --- a/core/src/main/scala/erules/core/EvalReason.scala +++ b/core/src/main/scala/erules/EvalReason.scala @@ -1,4 +1,4 @@ -package erules.core +package erules import cats.Show diff --git a/core/src/main/scala/erules/core/Rule.scala b/core/src/main/scala/erules/Rule.scala similarity index 53% rename from core/src/main/scala/erules/core/Rule.scala rename to core/src/main/scala/erules/Rule.scala index 18a3453..bf5c95c 100644 --- a/core/src/main/scala/erules/core/Rule.scala +++ b/core/src/main/scala/erules/Rule.scala @@ -1,36 +1,37 @@ -package erules.core +package erules -import cats.{Applicative, ApplicativeThrow, Contravariant, Functor, Order, Show} +import cats.{~>, Applicative, ApplicativeThrow, Contravariant, Functor, Id, Order, Show} import cats.data.NonEmptyList import cats.effect.Clock import cats.implicits.* -import erules.core.Rule.RuleBuilder -import erules.core.RuleVerdict.Ignore +import erules.RuleVerdict.{Allow, Deny, Ignore} +import erules.utils.IsId -sealed trait Rule[+F[_], -T] extends Serializable { +sealed trait Rule[F[_], -T] extends Serializable { + + /** Represent the rule info + */ + val info: RuleInfo + + /** The unique rule reference + */ + final lazy val uniqueRef: RuleRef = info.uniqueRef /** A string to describe in summary this rule. */ - val name: String + final val name: String = info.name /** A string to add more information to this rule. */ - val description: Option[String] + final val description: Option[String] = info.description /** A string to describe what/who is the target of this rule. */ - val targetInfo: Option[String] + final val targetInfo: Option[String] = info.targetInfo /** A full description of the rule, that contains name, description and target info where defined. */ - def fullDescription: String = { - (description, targetInfo) match { - case (None, None) => name - case (Some(description), None) => s"$name: $description" - case (None, Some(targetInfo)) => s"$name for $targetInfo" - case (Some(description), Some(targetInfo)) => s"$name for $targetInfo: $description" - } - } + final val fullDescription: String = info.fullDescription /** Set rule description */ @@ -112,26 +113,44 @@ sealed trait Rule[+F[_], -T] extends Serializable { * @tparam G * Effect * @return - * A lifted rule to specifed effect type `G` + * A lifted rule to specified effect type `G` */ - def covary[G[_]: Applicative](implicit env: F[RuleVerdict] <:< RuleVerdict): Rule[G, T] + def liftK[G[_]: Applicative](implicit isId: IsId[F]): Rule[G, T] + + /** Lift a rule with effect `F[_]` to specified `G[_]`. Value is lifted using specified + * `FunctionK` instance + * @param f + * FunctionK instance to lift `F[_]` to `G[_]` + * @tparam G + * new effect for the rule + * @return + * A lifted rule to specified effect of type `G` + */ + def mapK[G[_]](f: F ~> G): Rule[G, T] // eval /** Same as `eval` but has only the `RuleVerdict` value */ - def evalRaw[FF[X] >: F[X], TT <: T](data: TT): FF[RuleVerdict] + def evalRaw[TT <: T](data: TT): F[RuleVerdict] + + /** Same as `eval` return a pure `RuleVerdict` value without side effects. + * + * `executionTime` is always set to `None` + */ + def evalPure[TT <: T](data: TT)(implicit F: IsId[F]): RuleResult.Unbiased = + RuleResult.forRule(this)( + verdict = Right(F.unliftId(evalRaw[TT](data))), + executionTime = None + ) /** 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]( + final def eval[TT <: T]( data: TT - )(implicit F: ApplicativeThrow[FF], C: Clock[FF]): FF[RuleResult.Free[TT]] = - C.timed( - evalRaw[FF, TT](data).attempt - ).map { case (duration, res) => - RuleResult[TT, RuleVerdict]( - rule = this, + )(implicit F: ApplicativeThrow[F], C: Clock[F]): F[RuleResult.Unbiased] = + C.timed(evalRaw[TT](data).attempt).map { case (duration, res) => + RuleResult.forRule(this)( verdict = res, executionTime = Some(duration) ) @@ -146,71 +165,90 @@ sealed trait Rule[+F[_], -T] extends Serializable { } } -object Rule extends RuleInstances with RuleSyntax { +object Rule extends RuleInstances { - import erules.core.utils.CollectionsUtils.* + import utils.CollectionsUtils.* + import IsId.* // =================/ BUILDER /================= - def apply(name: String): RuleBuilder = new RuleBuilder(name) + def apply[F[_], T](name: String): RuleBuilder[F, T] = new RuleBuilder[F, T](name) + def pure[T](name: String): RuleBuilder[Id, T] = apply[Id, T](name) + + class RuleBuilder[F[_], T] private[erules] (name: String) { $this => - class RuleBuilder private[erules] (name: String) { $this => + def apply(f: Function[T, F[RuleVerdict]]): Rule[F, T] = + RuleImpl(f, RuleInfo($this.name)) - def apply[F[_], T](f: Function[T, F[RuleVerdict]]): Rule[F, T] = - check(f) + def partially(f: PartialFunction[T, F[RuleVerdict]])(implicit F: Applicative[F]): Rule[F, T] = + apply(f.lift.andThen(_.getOrElse(F.pure(Ignore.noMatch)))) - def check[F[_], T](f: Function[T, F[RuleVerdict]]): Rule[F, T] = - RuleImpl( - f = f, - name = $this.name, - description = None, - targetInfo = None + def failed(ex: Throwable)(implicit F: ApplicativeThrow[F]): Rule[F, T] = + apply(_ => F.raiseError(ex)) + + def const(v: RuleVerdict)(implicit F: Applicative[F]): Rule[F, T] = + apply(_ => F.pure(v)) + + // assertions + def assert(f: Function[T, F[Boolean]])(implicit + F: Applicative[F] + ): Rule[F, T] = + fromBooleanF(f)( + ifTrue = Allow.withoutReasons, + ifFalse = Deny.withoutReasons ) - def partially[F[_]: Applicative, T]( - f: PartialFunction[T, F[RuleVerdict]] + def assert(ifFalse: String)(f: Function[T, F[Boolean]])(implicit + F: Applicative[F] ): Rule[F, T] = - apply( - f.lift.andThen(_.getOrElse(Applicative[F].pure(Ignore.noMatch))) + fromBooleanF(f)( + ifTrue = Allow.withoutReasons, + ifFalse = Deny.because(ifFalse) ) - def failed[F[_]: ApplicativeThrow, T](ex: Throwable): Rule[F, T] = - apply(_ => ApplicativeThrow[F].raiseError(ex)) + def assertNot(f: Function[T, F[Boolean]])(implicit F: Applicative[F]): Rule[F, T] = + assert(f.andThen(_.map(!_))) - def const[F[_]: Applicative, T](v: RuleVerdict): Rule[F, T] = - apply(_ => Applicative[F].pure(v)) + def assertNot(ifTrue: String)(f: Function[T, F[Boolean]])(implicit + F: Applicative[F] + ): Rule[F, T] = assert(ifTrue)(f.andThen(_.map(!_))) + + def fromBooleanF( + f: Function[T, F[Boolean]] + )(ifTrue: => RuleVerdict, ifFalse: => RuleVerdict)(implicit + F: Applicative[F] + ): Rule[F, T] = apply(f.andThen(_.ifF(ifTrue, ifFalse))) } - private[erules] case class RuleImpl[+F[_], -TT]( + private[erules] case class RuleImpl[F[_], -TT]( f: TT => F[RuleVerdict], - name: String, - description: Option[String] = None, - targetInfo: Option[String] = None + info: RuleInfo ) extends Rule[F, TT] { // docs - def describe(description: String): Rule[F, TT] = - copy(description = Option(description)) + override def describe(description: String): Rule[F, TT] = + copy(info = info.copy(description = Option(description))) - def targetInfo(targetInfo: String): Rule[F, TT] = - copy(targetInfo = Option(targetInfo)) + override def targetInfo(targetInfo: String): Rule[F, TT] = + copy(info = info.copy(targetInfo = Option(targetInfo))) // map - def contramap[U](cf: U => TT): Rule[F, U] = + override def contramap[U](cf: U => TT): Rule[F, U] = copy(f = cf.andThen(f)) // eval - def evalRaw[FF[X] >: F[X], TT2 <: TT](data: TT2): FF[RuleVerdict] = + override def evalRaw[TT2 <: TT](data: TT2): F[RuleVerdict] = f(data) - def covary[G[_]: Applicative](implicit - env: F[RuleVerdict] <:< RuleVerdict - ): Rule[G, TT] = - copy[G, TT](f = f.andThen(fa => Applicative[G].pure(env(fa)))) + override def liftK[G[_]: Applicative](implicit isId: IsId[F]): Rule[G, TT] = + copy[G, TT](f = f.andThen(fa => Applicative[G].pure(fa))) + + override def mapK[G[_]](f: F ~> G): Rule[G, TT] = + copy[G, TT](f = this.f.andThen(f.apply)) } // =================/ UTILS /================= def findDuplicated[F[_], T](rules: NonEmptyList[Rule[F, T]]): List[Rule[F, T]] = - rules.findDuplicatedNem(identity) + rules.findDuplicatedNem(_.fullDescription) } private[erules] trait RuleInstances { @@ -225,9 +263,7 @@ private[erules] trait RuleInstances { if ( x != null && y != null - && x.name.equals(y.name) - && x.targetInfo.equals(y.targetInfo) - && x.description.equals(y.description) + && x.info.eqv(y.info) ) 0 else -1 ) @@ -235,13 +271,7 @@ private[erules] trait RuleInstances { implicit def catsShowInstanceForRule[F[_], T]: Show[Rule[F, T]] = r => s"Rule('${r.fullDescription}')" - implicit class PureRuleOps[F[_]: Functor, T](fa: F[PureRule[T]]) { - 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)) + implicit class PureRuleOps[F[_]: Functor, I[_]: IsId, T](fa: F[Rule[I, T]]) { + def mapLift[G[_]: Applicative]: F[Rule[G, T]] = fa.map(_.liftK[G]) } } diff --git a/core/src/main/scala/erules/RuleInfo.scala b/core/src/main/scala/erules/RuleInfo.scala new file mode 100644 index 0000000..df13974 --- /dev/null +++ b/core/src/main/scala/erules/RuleInfo.scala @@ -0,0 +1,29 @@ +package erules + +import cats.kernel.Eq + +case class RuleInfo( + name: String, + description: Option[String] = None, + targetInfo: Option[String] = None +) { + + /** Unique rule reference + */ + // noinspection ScalaWeakerAccess + lazy val uniqueRef: RuleRef = RuleRef.fromString(fullDescription) + + /** A full description of the rule, that contains name, description and target info where defined. + */ + def fullDescription: String = { + (description, targetInfo) match { + case (None, None) => name + case (Some(description), None) => s"$name: $description" + case (None, Some(targetInfo)) => s"$name for $targetInfo" + case (Some(description), Some(targetInfo)) => s"$name for $targetInfo: $description" + } + } +} +object RuleInfo { + implicit val eq: Eq[RuleInfo] = Eq.by(_.uniqueRef) +} diff --git a/core/src/main/scala/erules/RuleRef.scala b/core/src/main/scala/erules/RuleRef.scala new file mode 100644 index 0000000..2678fc6 --- /dev/null +++ b/core/src/main/scala/erules/RuleRef.scala @@ -0,0 +1,19 @@ +package erules + +import cats.{Eq, Show} +import cats.kernel.Hash + +import java.math.BigInteger + +/** A unique reference to a rule. + */ +final class RuleRef private[erules] (val value: String) extends AnyVal with Serializable +object RuleRef { + + def fromString(value: String): RuleRef = + new RuleRef(new BigInteger(value.getBytes()).toString()) + + implicit val eq: Eq[RuleRef] = Eq.by(_.value) + implicit val show: Show[RuleRef] = Show.fromToString[RuleRef] + implicit val hash: Hash[RuleRef] = Hash.fromUniversalHashCode[RuleRef] +} diff --git a/core/src/main/scala/erules/RuleResult.scala b/core/src/main/scala/erules/RuleResult.scala new file mode 100644 index 0000000..a31f114 --- /dev/null +++ b/core/src/main/scala/erules/RuleResult.scala @@ -0,0 +1,77 @@ +package erules + +import cats.Order +import erules.RuleVerdict.* + +import scala.concurrent.duration.FiniteDuration + +case class RuleResult[+V <: RuleVerdict] private ( + ruleInfo: RuleInfo, + verdict: EitherThrow[V], + executionTime: Option[FiniteDuration] +) extends Serializable { + + def drainExecutionTime: RuleResult[V] = + copy(executionTime = None) +} +object RuleResult extends RuleResultInstances { + + type Unbiased = RuleResult[RuleVerdict] + + def apply(ruleInfo: RuleInfo): RuleResultBuilder = + new RuleResultBuilder(ruleInfo) + + def forRuleName(ruleName: String): RuleResultBuilder = + apply(RuleInfo(ruleName)) + + def forRule[F[_], T](rule: Rule[F, T]): RuleResultBuilder = + apply(rule.info) + + def noMatch[V <: RuleVerdict](v: V): RuleResult[V] = + forRuleName("No match").succeeded(v) + + class RuleResultBuilder(private val ruleInfo: RuleInfo) { + + def apply[V <: RuleVerdict]( + verdict: EitherThrow[V], + executionTime: Option[FiniteDuration] = None + ): RuleResult[V] = + new RuleResult(ruleInfo, verdict, executionTime) + + def succeeded[V <: RuleVerdict]( + v: V, + executionTime: Option[FiniteDuration] = None + ): RuleResult[V] = + apply(Right(v), executionTime) + + def failed[V <: RuleVerdict]( + ex: Throwable, + executionTime: Option[FiniteDuration] = None + ): RuleResult[V] = + apply(Left(ex), executionTime) + + def allow(executionTime: Option[FiniteDuration] = None): RuleResult[Allow] = + succeeded(Allow.withoutReasons, executionTime) + + def denyForSafetyInCaseOfError( + ex: Throwable, + executionTime: Option[FiniteDuration] = None + ): RuleResult[Deny] = + apply(Left(ex), executionTime) + } +} + +private[erules] trait RuleResultInstances { + + implicit def catsOrderInstanceForRuleRuleResult[V <: RuleVerdict]: Order[RuleResult[V]] = + Order.from((x, y) => + if ( + x != null + && y != null + && x.ruleInfo.equals(y.ruleInfo) + && x.verdict.equals(y.verdict) + && x.executionTime.equals(y.executionTime) + ) 0 + else -1 + ) +} diff --git a/core/src/main/scala/erules/core/RuleResultsInterpreter.scala b/core/src/main/scala/erules/RuleResultsInterpreter.scala similarity index 53% rename from core/src/main/scala/erules/core/RuleResultsInterpreter.scala rename to core/src/main/scala/erules/RuleResultsInterpreter.scala index 78d5dde..90eeaea 100644 --- a/core/src/main/scala/erules/core/RuleResultsInterpreter.scala +++ b/core/src/main/scala/erules/RuleResultsInterpreter.scala @@ -1,10 +1,10 @@ -package erules.core +package erules import cats.data.NonEmptyList -import erules.core.RuleVerdict.{Allow, Deny, Ignore} +import erules.RuleVerdict.{Allow, Deny, Ignore} trait RuleResultsInterpreter { - def interpret[T](report: NonEmptyList[RuleResult.Free[T]]): RuleResultsInterpreterVerdict[T] + def interpret(report: NonEmptyList[RuleResult.Unbiased]): RuleResultsInterpreterVerdict } object RuleResultsInterpreter extends EvalResultsInterpreterInstances { object Defaults { @@ -16,48 +16,50 @@ object RuleResultsInterpreter extends EvalResultsInterpreterInstances { private[erules] trait EvalResultsInterpreterInstances { class AllowAllNotDeniedRuleResultsInterpreter extends RuleResultsInterpreter { - override def interpret[T]( - report: NonEmptyList[RuleResult.Free[T]] - ): RuleResultsInterpreterVerdict[T] = + override def interpret( + report: NonEmptyList[RuleResult.Unbiased] + ): RuleResultsInterpreterVerdict = partialEval(report).getOrElse( RuleResultsInterpreterVerdict.Allowed( NonEmptyList.one( - RuleResult.noMatch[T, Allow](Allow.allNotExplicitlyDenied) + RuleResult.noMatch[Allow](Allow.allNotExplicitlyDenied) ) ) ) } class DenyAllNotAllowedRuleResultsInterpreter extends RuleResultsInterpreter { - override def interpret[T]( - report: NonEmptyList[RuleResult.Free[T]] - ): RuleResultsInterpreterVerdict[T] = + override def interpret( + report: NonEmptyList[RuleResult.Unbiased] + ): RuleResultsInterpreterVerdict = partialEval(report).getOrElse( RuleResultsInterpreterVerdict.Denied( NonEmptyList.one( - RuleResult.noMatch[T, Deny](Deny.allNotExplicitlyAllowed) + RuleResult.noMatch[Deny](Deny.allNotExplicitlyAllowed) ) ) ) } - private def partialEval[T]( - report: NonEmptyList[RuleResult.Free[T]] - ): Option[RuleResultsInterpreterVerdict[T]] = { - type Res[+V <: RuleVerdict] = RuleResult[T, V] + private def partialEval( + report: NonEmptyList[RuleResult.Unbiased] + ): Option[RuleResultsInterpreterVerdict] = { + type Res[+V <: RuleVerdict] = RuleResult[V] report.toList .flatMap { - case _ @RuleResult(_: Rule[?, T], Right(_: Ignore), _) => + case _ @RuleResult(_: RuleInfo, Right(_: Ignore), _) => None - case _ @RuleResult(r: Rule[?, T], Left(ex), _) => - Some(Left(RuleResult.denyForSafetyInCaseOfError(r.asInstanceOf[Rule[Nothing, T]], ex))) - case re @ RuleResult(_: Rule[?, T], Right(_: Deny), _) => + case _ @RuleResult(info: RuleInfo, Left(ex), _) => + Some( + Left(RuleResult(info).denyForSafetyInCaseOfError(ex).asInstanceOf[Res[Deny]]) + ) + case re @ RuleResult(_: RuleInfo, Right(_: Deny), _) => Some(Left(re.asInstanceOf[Res[Deny]])) - case re @ RuleResult(_: Rule[?, T], Right(_: Allow), _) => + case re @ RuleResult(_: RuleInfo, Right(_: Allow), _) => Some(Right(re.asInstanceOf[Res[Allow]])) } - .partitionMap[Res[Deny], Res[Allow]](identity) match { + .partitionMap[Res[Deny], Res[Allow]]((a: Either[Res[Deny], Res[Allow]]) => a) match { case (_ @Nil, _ @Nil) => None case (_ @Nil, allow :: allows) => diff --git a/core/src/main/scala/erules/core/RuleResultsInterpreterVerdict.scala b/core/src/main/scala/erules/RuleResultsInterpreterVerdict.scala similarity index 55% rename from core/src/main/scala/erules/core/RuleResultsInterpreterVerdict.scala rename to core/src/main/scala/erules/RuleResultsInterpreterVerdict.scala index 7730902..9a4124f 100644 --- a/core/src/main/scala/erules/core/RuleResultsInterpreterVerdict.scala +++ b/core/src/main/scala/erules/RuleResultsInterpreterVerdict.scala @@ -1,15 +1,15 @@ -package erules.core +package erules import cats.data.NonEmptyList -import erules.core.RuleResultsInterpreterVerdict.{Allowed, Denied} +import erules.RuleResultsInterpreterVerdict.{Allowed, Denied} /** ADT to define the possible responses of the engine evaluation. */ -sealed trait RuleResultsInterpreterVerdict[-T] extends Serializable { +sealed trait RuleResultsInterpreterVerdict extends Serializable { - /** Result reasons + /** Evaluated rules results */ - val evaluatedRules: NonEmptyList[RuleResult.Free[T]] + val evaluatedResults: NonEmptyList[RuleResult.Unbiased] /** Check if this is an instance of `Allowed` or not */ @@ -31,9 +31,9 @@ sealed trait RuleResultsInterpreterVerdict[-T] extends Serializable { } object RuleResultsInterpreterVerdict { - case class Allowed[T](evaluatedRules: NonEmptyList[RuleResult[T, RuleVerdict.Allow]]) - extends RuleResultsInterpreterVerdict[T] + case class Allowed(evaluatedResults: NonEmptyList[RuleResult[RuleVerdict.Allow]]) + extends RuleResultsInterpreterVerdict - case class Denied[T](evaluatedRules: NonEmptyList[RuleResult[T, RuleVerdict.Deny]]) - extends RuleResultsInterpreterVerdict[T] + case class Denied(evaluatedResults: NonEmptyList[RuleResult[RuleVerdict.Deny]]) + extends RuleResultsInterpreterVerdict } diff --git a/core/src/main/scala/erules/core/RuleVerdict.scala b/core/src/main/scala/erules/RuleVerdict.scala similarity index 78% rename from core/src/main/scala/erules/core/RuleVerdict.scala rename to core/src/main/scala/erules/RuleVerdict.scala index 2d8ed0f..d15ad24 100644 --- a/core/src/main/scala/erules/core/RuleVerdict.scala +++ b/core/src/main/scala/erules/RuleVerdict.scala @@ -1,13 +1,13 @@ -package erules.core +package erules import cats.{Monoid, Show} -import erules.core.RuleVerdict.{Allow, Deny, Ignore} +import erules.RuleVerdict.{Allow, Deny, Ignore} import scala.annotation.unused /** ADT to define the possible output of a [[Rule]] evaluation. */ -sealed trait RuleVerdict extends Serializable { this: RuleVerdictBecauseSupport[RuleVerdict] => +sealed trait RuleVerdict extends Serializable with RuleVerdictBecauseSupport[RuleVerdict] { /** Result reasons */ @@ -56,14 +56,31 @@ private[erules] trait RuleVerdictBecauseSupport[+T <: RuleVerdict] { object RuleVerdict extends RuleVerdictInstances { + // noinspection ScalaWeakerAccess val noReasons: List[EvalReason] = Nil + // TODO: add test + def whenNot(b: Boolean)(ifTrue: => RuleVerdict, ifFalse: => RuleVerdict): RuleVerdict = + when(!b)(ifFalse, ifTrue) + + // TODO: add test + def when(b: Boolean)(ifTrue: => RuleVerdict, ifFalse: => RuleVerdict): RuleVerdict = + if (b) ifTrue else ifFalse + // ------------------------------ ALLOW ------------------------------ sealed trait Allow extends RuleVerdict with RuleVerdictBecauseSupport[Allow] object Allow extends RuleVerdictBecauseSupport[Allow] { val allNotExplicitlyDenied: Allow = Allow.because("Allow All Not Explicitly Denied") + // TODO: add test + def when(b: Boolean)(ifFalse: => RuleVerdict): RuleVerdict = + RuleVerdict.when(b)(Allow.withoutReasons, ifFalse) + + // TODO: add test + def whenNot(b: Boolean)(ifTrue: => RuleVerdict): RuleVerdict = + RuleVerdict.whenNot(b)(ifTrue, Allow.withoutReasons) + /** Create a [[Allow]] result with the specified reasons */ override def because(newReasons: List[EvalReason]): Allow = AllowImpl(newReasons) @@ -92,6 +109,14 @@ object RuleVerdict extends RuleVerdictInstances { val allNotExplicitlyAllowed: Deny = Deny.because("Deny All Not Explicitly Allowed") + // TODO: add test + def when(b: Boolean)(ifFalse: => RuleVerdict): RuleVerdict = + RuleVerdict.when(b)(Deny.withoutReasons, ifFalse) + + // TODO: add test + def whenNot(b: Boolean)(ifTrue: => RuleVerdict): RuleVerdict = + RuleVerdict.whenNot(b)(ifTrue, Deny.withoutReasons) + /** Create a [[Deny]] result with the specified reasons */ override def because(newReasons: List[EvalReason]): Deny = DenyImpl(newReasons) @@ -120,6 +145,14 @@ object RuleVerdict extends RuleVerdictInstances { val noMatch: Ignore = Ignore.because("No match") + // TODO: add test + def when(b: Boolean)(ifFalse: => RuleVerdict): RuleVerdict = + RuleVerdict.when(b)(Ignore.withoutReasons, ifFalse) + + // TODO: add test + def whenNot(b: Boolean)(ifTrue: => RuleVerdict): RuleVerdict = + RuleVerdict.whenNot(b)(ifTrue, Ignore.withoutReasons) + /** Create a [[Ignore]] result with the specified reasons */ override def because(newReasons: List[EvalReason]): Ignore = IgnoreImpl(newReasons) diff --git a/core/src/main/scala/erules/RulesEngine.scala b/core/src/main/scala/erules/RulesEngine.scala new file mode 100644 index 0000000..95fb886 --- /dev/null +++ b/core/src/main/scala/erules/RulesEngine.scala @@ -0,0 +1,98 @@ +package erules + +import cats.{~>, Applicative, ApplicativeThrow, Parallel} +import cats.data.NonEmptyList +import cats.effect.kernel.Async +import cats.effect.Sync +import erules.utils.IsId + +case class RulesEngine[F[_], T] private ( + rules: NonEmptyList[Rule[F, T]], + interpreter: RuleResultsInterpreter +) { + + import cats.implicits.* + import erules.utils.IsId.* + + // execution + def parEval(data: T)(implicit + F: Async[F], + P: Parallel[F] + ): F[EngineResult[T]] = + createResult( + data, + rules.parTraverse(_.eval(data)) + ) + + def parEvalN(data: T, parallelismLevel: Int)(implicit F: Async[F]): F[EngineResult[T]] = + createResult( + data, + F.parTraverseN(parallelismLevel)(rules)(_.eval(data)) + ) + + def seqEval(data: T)(implicit F: Sync[F]): F[EngineResult[T]] = + createResult( + data, + rules.map(_.eval(data)).sequence + ) + + def seqEvalPure(data: T)(implicit F: IsId[F]): EngineResult[T] = + createResult( + data, + rules.map(_.evalPure(data)).liftId[F] + )(F.applicative) + + private def createResult( + data: T, + evalRes: F[NonEmptyList[RuleResult.Unbiased]] + )(implicit F: Applicative[F]): F[EngineResult[T]] = + evalRes + .map(evaluatedRules => + EngineResult( + data = data, + verdict = interpreter.interpret(evaluatedRules) + ) + ) +} +object RulesEngine { + + def withRules[F[_], T](head1: Rule[F, T], tail: Rule[F, T]*): RulesEngineIntBuilder[F, T] = + withRules[F, T](NonEmptyList.of[Rule[F, T]](head1, tail*)) + + def withRules[F[_], T](rules: NonEmptyList[Rule[F, T]]): RulesEngineIntBuilder[F, T] = + new RulesEngineIntBuilder[F, T](rules) + + class RulesEngineIntBuilder[F[_], T] private[RulesEngine] ( + rules: NonEmptyList[Rule[F, T]] + ) { + + def withInterpreter[G[_]: ApplicativeThrow]( + interpreter: RuleResultsInterpreter + ): G[RulesEngine[F, T]] = + Rule.findDuplicated(rules) match { + case Nil => + ApplicativeThrow[G].pure(RulesEngine(rules, interpreter)) + case duplicatedDescriptions => + ApplicativeThrow[G].raiseError(DuplicatedRulesError(duplicatedDescriptions)) + } + + def allowAllNotDenied[G[_]: ApplicativeThrow]: G[RulesEngine[F, T]] = + withInterpreter(RuleResultsInterpreter.Defaults.allowAllNotDenied) + + def denyAllNotAllowed[G[_]: ApplicativeThrow]: G[RulesEngine[F, T]] = + withInterpreter(RuleResultsInterpreter.Defaults.denyAllNotAllowed) + + def liftK[G[_]: Applicative](implicit isId: IsId[F]): RulesEngineIntBuilder[G, T] = + mapK[G](new ~>[F, G] { + def apply[A](fa: F[A]): G[A] = Applicative[G].pure(isId.unliftId(fa)) + }) + + def mapK[G[_]](fg: F ~> G): RulesEngineIntBuilder[G, T] = + new RulesEngineIntBuilder[G, T](rules.map(_.mapK(fg))) + } + + case class DuplicatedRulesError[F[_], T](duplicates: List[Rule[F, T]]) + extends RuntimeException(s"Duplicated rules found!\n${duplicates + .map(_.fullDescription.prependedAll("- ").mkString) + .mkString(",")}") +} diff --git a/core/src/main/scala/erules/core/RuleResult.scala b/core/src/main/scala/erules/core/RuleResult.scala deleted file mode 100644 index af0613a..0000000 --- a/core/src/main/scala/erules/core/RuleResult.scala +++ /dev/null @@ -1,52 +0,0 @@ -package erules.core - -import cats.{Eq, Order} -import erules.core.RuleVerdict.Deny - -import scala.concurrent.duration.FiniteDuration - -case class RuleResult[-T, +V <: RuleVerdict]( - rule: AnyTypedRule[T], - verdict: EitherThrow[V], - executionTime: Option[FiniteDuration] = None -) extends Serializable { - - def mapRule[TT <: T](f: AnyTypedRule[TT] => AnyTypedRule[TT]): RuleResult[TT, V] = - copy(rule = f(rule)) - - def drainExecutionTime: RuleResult[T, V] = - copy(executionTime = None) -} -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[EitherThrow, T](v), Right(v)) - - def failed[T, V <: RuleVerdict](ruleName: String, ex: Throwable): RuleResult[T, V] = - 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, Left(ex)) -} - -private[erules] trait RuleResultInstances { - - implicit def catsOrderInstanceForRuleRuleResult[T, V <: RuleVerdict](implicit - ruleEq: Eq[AnyTypedRule[T]] - ): Order[RuleResult[T, V]] = - Order.from((x, y) => - if ( - x != null - && y != null - && ruleEq.eqv(x.rule, y.rule) - && x.verdict.equals(y.verdict) - && x.executionTime.equals(y.executionTime) - ) 0 - else -1 - ) -} diff --git a/core/src/main/scala/erules/core/RulesEngine.scala b/core/src/main/scala/erules/core/RulesEngine.scala deleted file mode 100644 index ad810cf..0000000 --- a/core/src/main/scala/erules/core/RulesEngine.scala +++ /dev/null @@ -1,101 +0,0 @@ -package erules.core - -import cats.{Functor, Id, MonadThrow, Parallel} -import cats.data.NonEmptyList -import cats.effect.kernel.Async - -case class RulesEngine[F[_], T] private ( - rules: NonEmptyList[Rule[F, T]], - private val interpreter: RuleResultsInterpreter -) { - - import cats.implicits.* - - // execution - def parEval(data: T)(implicit - F: Async[F], - P: Parallel[F] - ): F[EngineResult[T]] = - createResult( - data, - rules.parTraverse(_.eval(data)) - ) - - def parEvalN(data: T, parallelismLevel: Int)(implicit F: Async[F]): F[EngineResult[T]] = - createResult( - data, - F.parTraverseN(parallelismLevel)(rules)(_.eval(data)) - ) - - def seqEval(data: T)(implicit F: Async[F]): F[EngineResult[T]] = - createResult( - data, - rules.map(_.eval(data)).sequence - ) - - private def createResult( - data: T, - evalRes: F[NonEmptyList[RuleResult.Free[T]]] - )(implicit F: Functor[F]): F[EngineResult[T]] = - evalRes - .map(evaluatedRules => - EngineResult( - data = data, - verdict = interpreter.interpret(evaluatedRules) - ) - ) -} -object RulesEngine { - - def apply[F[_]: MonadThrow]: RulesEngineRulesBuilder[F] = - new RulesEngineRulesBuilder[F] - - class RulesEngineRulesBuilder[F[_]: MonadThrow] private[RulesEngine] { - - // effect - def withRules[T]( - head1: Rule[F, T], - tail: Rule[F, T]* - ): RulesEngineIntBuilder[F, T] = - withRules[T](NonEmptyList.of[Rule[F, T]](head1, tail*)) - - def withRules[T](rules: NonEmptyList[Rule[F, T]]): RulesEngineIntBuilder[F, T] = - new RulesEngineIntBuilder[F, T](rules) - - // pure - def withRules[G[X] <: Id[X], T]( - 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*).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.mapLift[F]) - } - - class RulesEngineIntBuilder[F[_]: MonadThrow, T] private[RulesEngine] ( - rules: NonEmptyList[Rule[F, T]] - ) { - - def withInterpreter(interpreter: RuleResultsInterpreter): F[RulesEngine[F, T]] = - Rule.findDuplicated(rules) match { - case Nil => - MonadThrow[F].pure(RulesEngine(rules, interpreter)) - case duplicatedDescriptions => - MonadThrow[F].raiseError(DuplicatedRulesException(duplicatedDescriptions)) - } - - def allowAllNotDenied: F[RulesEngine[F, T]] = - withInterpreter(RuleResultsInterpreter.Defaults.allowAllNotDenied) - - def denyAllNotAllowed: F[RulesEngine[F, T]] = - withInterpreter(RuleResultsInterpreter.Defaults.denyAllNotAllowed) - } - - case class DuplicatedRulesException(duplicates: List[AnyRule]) - extends RuntimeException(s"Duplicated rules found!\n${duplicates - .map(_.fullDescription.prependedAll("- ").mkString) - .mkString(",")}") -} diff --git a/core/src/main/scala/erules/implicits.scala b/core/src/main/scala/erules/implicits.scala index 04d64c0..fb2b7f6 100644 --- a/core/src/main/scala/erules/implicits.scala +++ b/core/src/main/scala/erules/implicits.scala @@ -1,7 +1,6 @@ package erules -import erules.core.* -import erules.core.report.{ReportEncoderInstances, ReportEncoderSyntax, StringReportInstances} +import erules.report.{ReportEncoderInstances, ReportEncoderSyntax, StringReportInstances} object implicits extends AllCoreInstances with AllCoreSyntax @@ -19,7 +18,4 @@ private[erules] trait AllCoreInstances //---------- SYNTAX ---------- object syntax extends AllCoreSyntax -private[erules] trait AllCoreSyntax - extends RuleSyntax - with EvalReasonSyntax - with ReportEncoderSyntax +private[erules] trait AllCoreSyntax extends EvalReasonSyntax with ReportEncoderSyntax diff --git a/core/src/main/scala/erules/core/package.scala b/core/src/main/scala/erules/package.scala similarity index 61% rename from core/src/main/scala/erules/core/package.scala rename to core/src/main/scala/erules/package.scala index 5fda077..b87dff0 100644 --- a/core/src/main/scala/erules/core/package.scala +++ b/core/src/main/scala/erules/package.scala @@ -1,17 +1,10 @@ -package erules - import cats.Id import cats.effect.IO -package object core { - - type AnyF[_] = Any +package object erules { 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/ReportEncoder.scala b/core/src/main/scala/erules/report/ReportEncoder.scala similarity index 89% rename from core/src/main/scala/erules/core/report/ReportEncoder.scala rename to core/src/main/scala/erules/report/ReportEncoder.scala index 90141fb..f141e4a 100644 --- a/core/src/main/scala/erules/core/report/ReportEncoder.scala +++ b/core/src/main/scala/erules/report/ReportEncoder.scala @@ -1,4 +1,4 @@ -package erules.core.report +package erules.report import cats.{Functor, Show} @@ -20,7 +20,7 @@ object ReportEncoder extends ReportEncoderInstances with ReportEncoderSyntax { (t: T) => f(t) } -private[erules] trait ReportEncoderInstances { +private[erules] trait ReportEncoderInstances extends StringReportInstances { 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) diff --git a/core/src/main/scala/erules/core/report/StringReportEncoder.scala b/core/src/main/scala/erules/report/StringReportEncoder.scala similarity index 74% rename from core/src/main/scala/erules/core/report/StringReportEncoder.scala rename to core/src/main/scala/erules/report/StringReportEncoder.scala index c0567f0..9d63b0d 100644 --- a/core/src/main/scala/erules/core/report/StringReportEncoder.scala +++ b/core/src/main/scala/erules/report/StringReportEncoder.scala @@ -1,8 +1,9 @@ -package erules.core.report +package erules.report import cats.Show -import erules.core.* -import erules.core.RuleResultsInterpreterVerdict.{Allowed, Denied} +import cats.effect.std.Console +import erules.{EngineResult, EvalReason, RuleResult, RuleResultsInterpreterVerdict} +import erules.RuleResultsInterpreterVerdict.{Allowed, Denied} object StringReportEncoder extends StringReportInstances with StringReportSyntax { val defaultHeaderMaxLen: Int = 60 @@ -42,25 +43,25 @@ private[erules] trait StringReportInstances { implicit def stringReportEncoderForEngineResult[T](implicit showT: Show[T] = Show.fromToString[T], - reportEncoderERIR: StringReportEncoder[RuleResultsInterpreterVerdict[T]] + reportEncoderERIR: StringReportEncoder[RuleResultsInterpreterVerdict] ): StringReportEncoder[EngineResult[T]] = er => StringReportEncoder.paragraph("ENGINE VERDICT", "#")( s""" |Data: ${showT.show(er.data)} - |Rules: ${er.verdict.evaluatedRules.size} + |Rules: ${er.verdict.evaluatedResults.size} |${reportEncoderERIR.report(er.verdict)} |""".stripMargin ) - implicit def stringReportEncoderForRuleResultsInterpreterVerdict[T](implicit - reportEncoderEvalRule: StringReportEncoder[RuleResult[T, ? <: RuleVerdict]] - ): StringReportEncoder[RuleResultsInterpreterVerdict[T]] = + implicit def stringReportEncoderForRuleResultsInterpreterVerdict(implicit + reportEncoderEvalRule: StringReportEncoder[RuleResult.Unbiased] + ): StringReportEncoder[RuleResultsInterpreterVerdict] = t => { - val rulesReport: String = t.evaluatedRules + val rulesReport: String = t.evaluatedResults .map(er => - StringReportEncoder.paragraph(er.rule.fullDescription)( + StringReportEncoder.paragraph(er.ruleInfo.fullDescription)( reportEncoderEvalRule.report(er) ) ) @@ -78,8 +79,7 @@ private[erules] trait StringReportInstances { |""".stripMargin } - implicit def stringReportEncoderForRuleRuleResult[T] - : StringReportEncoder[RuleResult[T, ? <: RuleVerdict]] = + implicit val stringReportEncoderForRuleRuleResult: StringReportEncoder[RuleResult.Unbiased] = er => { val reasons: String = er.verdict.map(_.reasons) match { @@ -88,9 +88,9 @@ private[erules] trait StringReportInstances { case Right(reasons) => s"- Because: ${EvalReason.stringifyList(reasons)}" } - s"""|- Rule: ${er.rule.name} - |- Description: ${er.rule.description.getOrElse("")} - |- Target: ${er.rule.targetInfo.getOrElse("")} + s"""|- Rule: ${er.ruleInfo.name} + |- Description: ${er.ruleInfo.description.getOrElse("")} + |- Target: ${er.ruleInfo.targetInfo.getOrElse("")} |- Execution time: ${er.executionTime .map(Show.catsShowForFiniteDuration.show) .getOrElse("*not measured*")} @@ -102,6 +102,10 @@ private[erules] trait StringReportInstances { private[erules] trait StringReportSyntax { implicit class StringReportEncoderForAny[T](t: T) { - def asStringReport(implicit re: StringReportEncoder[T]): String = re.report(t) + def asStringReport(implicit re: StringReportEncoder[T]): String = + re.report(t) + + def printStringReport[F[_]: Console](implicit re: StringReportEncoder[T]): F[Unit] = + Console[F].println(asStringReport) } } diff --git a/core/src/main/scala/erules/core/report/package.scala b/core/src/main/scala/erules/report/package.scala similarity index 80% rename from core/src/main/scala/erules/core/report/package.scala rename to core/src/main/scala/erules/report/package.scala index bcb4b9c..36cba35 100644 --- a/core/src/main/scala/erules/core/report/package.scala +++ b/core/src/main/scala/erules/report/package.scala @@ -1,4 +1,4 @@ -package erules.core +package erules package object report { type StringReportEncoder[T] = ReportEncoder[T, String] diff --git a/core/src/main/scala/erules/core/utils/CollectionsUtils.scala b/core/src/main/scala/erules/utils/CollectionsUtils.scala similarity index 97% rename from core/src/main/scala/erules/core/utils/CollectionsUtils.scala rename to core/src/main/scala/erules/utils/CollectionsUtils.scala index 6220bba..2121e29 100644 --- a/core/src/main/scala/erules/core/utils/CollectionsUtils.scala +++ b/core/src/main/scala/erules/utils/CollectionsUtils.scala @@ -1,4 +1,4 @@ -package erules.core.utils +package erules.utils import cats.{Foldable, Order} import cats.data.NonEmptyList diff --git a/core/src/main/scala/erules/utils/IsId.scala b/core/src/main/scala/erules/utils/IsId.scala new file mode 100644 index 0000000..12008fb --- /dev/null +++ b/core/src/main/scala/erules/utils/IsId.scala @@ -0,0 +1,31 @@ +package erules.utils + +import cats.{Applicative, Id} + +@annotation.implicitNotFound("${F} is not cats.Id") +trait IsId[F[_]] { + def liftId[A](a: A): F[A] + def unliftId[A](fa: F[A]): A + def applicative: Applicative[F] +} +object IsId { + + def apply[F[_]: IsId]: IsId[F] = implicitly[IsId[F]] + + implicit def isIdInstance[F[X] <: Id[X]: Applicative]: IsId[F] = new IsId[F] { + override def unliftId[A](fa: F[A]): A = fa + override def liftId[A](a: A): F[A] = applicative.pure(a) + override def applicative: Applicative[F] = Applicative[F] + } + + implicit def unliftIdConversion[F[_]: IsId, A](fa: F[A]): A = IsId[F].unliftId(fa) + implicit def liftIdConversion[A](fa: A): Id[A] = IsId[Id].unliftId(fa) + + implicit class IsIdUnliftOps[F[_]: IsId, A](fa: F[A]) { + def unliftId: A = IsId[F].unliftId(fa) + } + + implicit class IsIdLiftOps[A](a: A) { + def liftId[F[_]: IsId]: F[A] = IsId[F].liftId(a) + } +} diff --git a/core/src/test/scala/erules/core/EngineResultSpec.scala b/core/src/test/scala/erules/EngineResultSpec.scala similarity index 68% rename from core/src/test/scala/erules/core/EngineResultSpec.scala rename to core/src/test/scala/erules/EngineResultSpec.scala index b0637f0..2fd89b6 100644 --- a/core/src/test/scala/erules/core/EngineResultSpec.scala +++ b/core/src/test/scala/erules/EngineResultSpec.scala @@ -1,8 +1,7 @@ -package erules.core +package erules import cats.data.NonEmptyList -import cats.Id -import erules.core.RuleVerdict.{Allow, Deny} +import erules.RuleVerdict.{Allow, Deny} import org.scalatest.EitherValues import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -15,12 +14,12 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { case class Foo(value: String) - val rule1: PureRule[Foo] = Rule("Check Foo").partially[Id, Foo] { + val rule1: PureRule[Foo] = Rule("Check Foo").partially { case Foo("") => Deny.because("Empty Value") case Foo("TEST") => Allow.withoutReasons } - val rule2: PureRule[Foo] = Rule("Check Foo").partially[Id, Foo] { + val rule2: PureRule[Foo] = Rule("Check Foo").partially { case Foo("") => Deny.because("Empty Value") case Foo("TEST") => Allow.withoutReasons } @@ -29,7 +28,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Allowed( NonEmptyList.of( - RuleResult(rule1, Right(RuleVerdict.Allow.because("R1"))) + RuleResult.forRule(rule1).succeeded(RuleVerdict.Allow.because("R1")) ) ) ) @@ -38,7 +37,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Allowed( NonEmptyList.of( - RuleResult(rule2, Right(RuleVerdict.Allow.because("R2"))) + RuleResult.forRule(rule2).succeeded(RuleVerdict.Allow.because("R2")) ) ) ) @@ -47,8 +46,8 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Allowed( NonEmptyList.of( - RuleResult(rule1, Right(RuleVerdict.Allow.because("R1"))), - RuleResult(rule2, Right(RuleVerdict.Allow.because("R2"))) + RuleResult.forRule(rule1).succeeded(RuleVerdict.Allow.because("R1")), + RuleResult.forRule(rule2).succeeded(RuleVerdict.Allow.because("R2")) ) ) ) @@ -58,12 +57,12 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { case class Foo(value: String) - val rule1: PureRule[Foo] = Rule("Check Foo").partially[Id, Foo] { + val rule1: PureRule[Foo] = Rule("Check Foo").partially { case Foo("") => Deny.because("Empty Value") case Foo("TEST") => Allow.withoutReasons } - val rule2: PureRule[Foo] = Rule("Check Foo").partially[Id, Foo] { + val rule2: PureRule[Foo] = Rule("Check Foo").partially { case Foo("") => Deny.because("Empty Value") case Foo("TEST") => Deny.withoutReasons } @@ -72,7 +71,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Allowed( NonEmptyList.of( - RuleResult(rule1, Right(RuleVerdict.Allow.because("R1"))) + RuleResult.forRule(rule1).succeeded(RuleVerdict.Allow.because("R1")) ) ) ) @@ -81,7 +80,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Denied( NonEmptyList.of( - RuleResult(rule2, Right(RuleVerdict.Deny.because("R2"))) + RuleResult.forRule(rule2).succeeded(RuleVerdict.Deny.because("R2")) ) ) ) @@ -90,7 +89,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Denied( NonEmptyList.of( - RuleResult(rule2, Right(RuleVerdict.Deny.because("R2"))) + RuleResult.forRule(rule2).succeeded(RuleVerdict.Deny.because("R2")) ) ) ) @@ -100,12 +99,12 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { case class Foo(value: String) - val rule1: PureRule[Foo] = Rule("Check Foo").partially[Id, Foo] { + val rule1: PureRule[Foo] = Rule("Check Foo").partially { case Foo("") => Deny.because("Empty Value") case Foo("TEST") => Deny.withoutReasons } - val rule2: PureRule[Foo] = Rule("Check Foo").partially[Id, Foo] { + val rule2: PureRule[Foo] = Rule("Check Foo").partially { case Foo("") => Deny.because("Empty Value") case Foo("TEST") => Allow.withoutReasons } @@ -114,7 +113,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Denied( NonEmptyList.of( - RuleResult(rule1, Right(RuleVerdict.Deny.because("R1"))) + RuleResult.forRule(rule1).succeeded(RuleVerdict.Deny.because("R1")) ) ) ) @@ -123,7 +122,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Allowed( NonEmptyList.of( - RuleResult(rule2, Right(RuleVerdict.Allow.because("R2"))) + RuleResult.forRule(rule2).succeeded(RuleVerdict.Allow.because("R2")) ) ) ) @@ -132,7 +131,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Denied( NonEmptyList.of( - RuleResult(rule1, Right(RuleVerdict.Deny.because("R1"))) + RuleResult.forRule(rule1).succeeded(RuleVerdict.Deny.because("R1")) ) ) ) @@ -142,12 +141,12 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { case class Foo(value: String) - val rule1: PureRule[Foo] = Rule("Check Foo").partially[Id, Foo] { + val rule1: PureRule[Foo] = Rule("Check Foo").partially { case Foo("") => Deny.because("Empty Value") case Foo("TEST") => Deny.withoutReasons } - val rule2: PureRule[Foo] = Rule("Check Foo").partially[Id, Foo] { + val rule2: PureRule[Foo] = Rule("Check Foo").partially { case Foo("") => Deny.because("Empty Value") case Foo("TEST") => Deny.withoutReasons } @@ -156,7 +155,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Denied( NonEmptyList.of( - RuleResult(rule1, Right(RuleVerdict.Deny.because("R1"))) + RuleResult.forRule(rule1).succeeded(RuleVerdict.Deny.because("R1")) ) ) ) @@ -165,7 +164,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Denied( NonEmptyList.of( - RuleResult(rule2, Right(RuleVerdict.Deny.because("R2"))) + RuleResult.forRule(rule2).succeeded(RuleVerdict.Deny.because("R2")) ) ) ) @@ -174,8 +173,8 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Denied( NonEmptyList.of( - RuleResult(rule1, Right(RuleVerdict.Deny.because("R1"))), - RuleResult(rule2, Right(RuleVerdict.Deny.because("R2"))) + RuleResult.forRule(rule1).succeeded(RuleVerdict.Deny.because("R1")), + RuleResult.forRule(rule2).succeeded(RuleVerdict.Deny.because("R2")) ) ) ) @@ -188,17 +187,17 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { case class Foo(value: String) - val rule1: PureRule[Foo] = Rule("Check Foo 1").partially[Id, Foo] { + val rule1: PureRule[Foo] = Rule("Check Foo 1").partially { case Foo("") => Deny.because("Empty Value") case Foo("TEST") => Allow.withoutReasons } - val rule2: PureRule[Foo] = Rule("Check Foo 2").partially[Id, Foo] { + val rule2: PureRule[Foo] = Rule("Check Foo 2").partially { case Foo("") => Deny.because("Empty Value") case Foo("TEST") => Allow.withoutReasons } - val rule3: PureRule[Foo] = Rule("Check Foo 3").partially[Id, Foo] { + val rule3: PureRule[Foo] = Rule("Check Foo 3").partially { case Foo("") => Deny.because("Empty Value") case Foo("TEST") => Allow.withoutReasons } @@ -207,7 +206,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Allowed( NonEmptyList.of( - RuleResult(rule1, Right(RuleVerdict.Allow.because("R1"))) + RuleResult.forRule(rule1).succeeded(RuleVerdict.Allow.because("R1")) ) ) ) @@ -216,7 +215,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Allowed( NonEmptyList.of( - RuleResult(rule2, Right(RuleVerdict.Allow.because("R2"))) + RuleResult.forRule(rule2).succeeded(RuleVerdict.Allow.because("R2")) ) ) ) @@ -225,7 +224,7 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Allowed( NonEmptyList.of( - RuleResult(rule3, Right(RuleVerdict.Allow.because("R3"))) + RuleResult.forRule(rule3).succeeded(RuleVerdict.Allow.because("R3")) ) ) ) @@ -234,9 +233,9 @@ class EngineResultSpec extends AnyWordSpec with Matchers with EitherValues { data = Foo("TEST"), verdict = RuleResultsInterpreterVerdict.Allowed( NonEmptyList.of( - RuleResult(rule1, Right(RuleVerdict.Allow.because("R1"))), - RuleResult(rule2, Right(RuleVerdict.Allow.because("R2"))), - RuleResult(rule3, Right(RuleVerdict.Allow.because("R3"))) + RuleResult.forRule(rule1).succeeded(RuleVerdict.Allow.because("R1")), + RuleResult.forRule(rule2).succeeded(RuleVerdict.Allow.because("R2")), + RuleResult.forRule(rule3).succeeded(RuleVerdict.Allow.because("R3")) ) ) ) diff --git a/core/src/test/scala/erules/core/RuleResultsInterpreterSpec.scala b/core/src/test/scala/erules/RuleResultsInterpreterSpec.scala similarity index 71% rename from core/src/test/scala/erules/core/RuleResultsInterpreterSpec.scala rename to core/src/test/scala/erules/RuleResultsInterpreterSpec.scala index 37777ff..1785a7f 100644 --- a/core/src/test/scala/erules/core/RuleResultsInterpreterSpec.scala +++ b/core/src/test/scala/erules/RuleResultsInterpreterSpec.scala @@ -1,8 +1,8 @@ -package erules.core +package erules import cats.data.NonEmptyList -import erules.core.RuleResultsInterpreterVerdict.{Allowed, Denied} -import erules.core.RuleVerdict.{Allow, Deny, Ignore} +import erules.RuleResultsInterpreterVerdict.{Allowed, Denied} +import erules.RuleVerdict.{Allow, Deny, Ignore} import org.scalatest.wordspec.AnyWordSpec import org.scalatest.EitherValues import org.scalatest.matchers.should.Matchers @@ -19,7 +19,7 @@ class RuleResultsInterpreterSpec extends AnyWordSpec with Matchers with EitherVa val result = interpreter.interpret( NonEmptyList.of( - RuleResult.const("Ignore", Ignore.withoutReasons) + RuleResult.forRuleName("Ignore").succeeded(Ignore.withoutReasons) ) ) @@ -36,13 +36,13 @@ class RuleResultsInterpreterSpec extends AnyWordSpec with Matchers with EitherVa val result = interpreter.interpret( NonEmptyList.of( - RuleResult.const("Allow all", Allow.withoutReasons) + RuleResult.forRuleName("Allow all").succeeded(Allow.withoutReasons) ) ) result shouldBe Allowed( NonEmptyList.of( - RuleResult.const("Allow all", Allow.withoutReasons) + RuleResult.forRuleName("Allow all").succeeded(Allow.withoutReasons) ) ) } @@ -53,14 +53,14 @@ class RuleResultsInterpreterSpec extends AnyWordSpec with Matchers with EitherVa val result = interpreter.interpret( NonEmptyList.of( - RuleResult.const("Allow all", Allow.withoutReasons), - RuleResult.const("Deny all", Deny.withoutReasons) + RuleResult.forRuleName("Allow all").succeeded(Allow.withoutReasons), + RuleResult.forRuleName("Deny all").succeeded(Deny.withoutReasons) ) ) result shouldBe Denied( NonEmptyList.of( - RuleResult.const("Deny all", Deny.withoutReasons) + RuleResult.forRuleName("Deny all").succeeded(Deny.withoutReasons) ) ) } @@ -72,17 +72,17 @@ class RuleResultsInterpreterSpec extends AnyWordSpec with Matchers with EitherVa val ex = new RuntimeException("BOOM") - val allowAll: Rule[EitherThrow, Foo] = Rule("Allow all").failed[EitherThrow, Foo](ex) + val allowAll: Rule[EitherThrow, Foo] = Rule("Allow all").failed(ex) val result = interpreter.interpret( NonEmptyList.one( - RuleResult(allowAll, Left(ex)) + RuleResult.forRule(allowAll).failed(ex) ) ) result shouldBe Denied( NonEmptyList.of( - RuleResult.denyForSafetyInCaseOfError(allowAll, ex) + RuleResult.forRule(allowAll).denyForSafetyInCaseOfError(ex) ) ) } @@ -96,7 +96,7 @@ class RuleResultsInterpreterSpec extends AnyWordSpec with Matchers with EitherVa val result = interpreter.interpret( NonEmptyList.of( - RuleResult.const("Ignore", Ignore.withoutReasons) + RuleResult.forRuleName("Ignore").succeeded(Ignore.withoutReasons) ) ) @@ -113,13 +113,13 @@ class RuleResultsInterpreterSpec extends AnyWordSpec with Matchers with EitherVa val result = interpreter.interpret( NonEmptyList.one( - RuleResult.const("Allow all", Allow.withoutReasons) + RuleResult.forRuleName("Allow all").succeeded(Allow.withoutReasons) ) ) result shouldBe Allowed( NonEmptyList.of( - RuleResult.const("Allow all", Allow.withoutReasons) + RuleResult.forRuleName("Allow all").succeeded(Allow.withoutReasons) ) ) } @@ -131,14 +131,14 @@ class RuleResultsInterpreterSpec extends AnyWordSpec with Matchers with EitherVa val result = interpreter.interpret( NonEmptyList.of( - RuleResult.const("Allow all", Allow.withoutReasons), - RuleResult.const("Deny all", Deny.withoutReasons) + RuleResult.forRuleName("Allow all").succeeded(Allow.withoutReasons), + RuleResult.forRuleName("Deny all").succeeded(Deny.withoutReasons) ) ) result shouldBe Denied( NonEmptyList.of( - RuleResult.const[Foo, Deny]("Deny all", Deny.withoutReasons) + RuleResult.forRuleName("Deny all").succeeded(Deny.withoutReasons) ) ) } @@ -150,17 +150,17 @@ class RuleResultsInterpreterSpec extends AnyWordSpec with Matchers with EitherVa val ex = new RuntimeException("BOOM") - val allowAll: Rule[EitherThrow, Foo] = Rule("Allow all").failed[EitherThrow, Foo](ex) + val allowAll: Rule[EitherThrow, Foo] = Rule("Allow all").failed(ex) val result = interpreter.interpret( NonEmptyList.one( - RuleResult(allowAll, Left(ex)) + RuleResult.forRule(allowAll).failed(ex) ) ) result shouldBe Denied( NonEmptyList.of( - RuleResult.denyForSafetyInCaseOfError(allowAll, ex) + RuleResult.forRule(allowAll).denyForSafetyInCaseOfError(ex) ) ) } diff --git a/core/src/test/scala/erules/core/RuleSpec.scala b/core/src/test/scala/erules/RuleSpec.scala similarity index 67% rename from core/src/test/scala/erules/core/RuleSpec.scala rename to core/src/test/scala/erules/RuleSpec.scala index d7dfabf..c5f9b99 100644 --- a/core/src/test/scala/erules/core/RuleSpec.scala +++ b/core/src/test/scala/erules/RuleSpec.scala @@ -1,11 +1,10 @@ -package erules.core +package erules import cats.data.NonEmptyList import cats.effect.testing.scalatest.AsyncIOSpec import cats.effect.IO -import cats.Id -import erules.core.RuleVerdict.{Allow, Deny, Ignore} -import erules.core.testings.{ErulesAsyncAssertingSyntax, ReportValues} +import erules.RuleVerdict.{Allow, Deny, Ignore} +import erules.testings.{ErulesAsyncAssertingSyntax, ReportValues} import org.scalatest.wordspec.AsyncWordSpec import org.scalatest.EitherValues import org.scalatest.matchers.should.Matchers @@ -28,7 +27,7 @@ class RuleSpec case class Bar(baz: Baz) case class Baz(value: String) - val bazRule: PureRule[Baz] = Rule("Check Baz value").check[Id, Baz]( + val bazRule: PureRule[Baz] = Rule("Check Baz value")( _.value match { case "" => Deny.because("Empty value") case _ => Allow.withoutReasons @@ -44,15 +43,15 @@ class RuleSpec } } - // ------------------------- EVAL ZIP ------------------------- - "Rule.check.eval" should { + // ------------------------- EVAL ------------------------- + "Rule.apply.eval" should { "return the right result once evaluated" in { sealed trait ADT case class Foo(@unused x: String, @unused y: Int) extends ADT case class Bar(@unused x: String, @unused y: Int) extends ADT - val rule: RuleIO[ADT] = Rule("Check Y value").check[IO, ADT] { + val rule: RuleIO[ADT] = Rule("Check Y value") { case Foo(_, 0) => IO.pure(Allow.withoutReasons) case Bar(_, 1) => IO.pure(Deny.withoutReasons) case _ => IO.pure(Ignore.withoutReasons) @@ -61,10 +60,14 @@ class RuleSpec for { _ <- rule .eval(Foo("TEST", 0)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Allow.withoutReasons))) + .assertingIgnoringTimes( + _ shouldBe RuleResult.forRule(rule).succeeded(Allow.withoutReasons) + ) _ <- rule .eval(Bar("TEST", 1)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Deny.withoutReasons))) + .assertingIgnoringTimes( + _ shouldBe RuleResult.forRule(rule).succeeded(Deny.withoutReasons) + ) } yield () } @@ -76,7 +79,7 @@ class RuleSpec val ex = new RuntimeException("BOOM") - val rule: RuleIO[ADT] = Rule("Check Y value").check[IO, ADT] { + val rule: RuleIO[ADT] = Rule("Check Y value") { case Foo(_, 0) => IO.raiseError(ex) case Bar(_, 1) => IO.pure(Deny.withoutReasons) case _ => IO.pure(Ignore.withoutReasons) @@ -86,15 +89,44 @@ class RuleSpec for { _ <- rule .eval(Foo("TEST", 0)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Left(ex))) + .assertingIgnoringTimes(_ shouldBe RuleResult.forRule(rule).failed(ex)) _ <- rule .eval(Bar("TEST", 1)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Deny.withoutReasons))) + .assertingIgnoringTimes( + _ shouldBe RuleResult.forRule(rule).succeeded(Deny.withoutReasons) + ) } yield () } + + "return the right result once evaluated when exhaustive" in { + sealed trait ADT + case class Foo() extends ADT + case class Bar() extends ADT + + val rule: PureRule[ADT] = Rule("Check Y value") { + case Foo() => Allow.withoutReasons + case Bar() => Deny.withoutReasons + } + + for { + _ <- rule + .liftK[IO] + .eval(Foo()) + .assertingIgnoringTimes( + _ shouldBe RuleResult.forRule(rule).succeeded(Allow.withoutReasons) + ) + _ <- rule + .liftK[IO] + .eval(Bar()) + .assertingIgnoringTimes( + _ shouldBe RuleResult.forRule(rule).succeeded(Deny.withoutReasons) + ) + } yield () + } + } - "Rule.checkOrIgnore.eval" should { + "Rule.partially.eval" should { "return the right result once evaluated" in { case class Foo(@unused x: String, @unused y: Int) @@ -106,10 +138,14 @@ class RuleSpec for { _ <- rule .eval(Foo("TEST", 0)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Allow.withoutReasons))) + .assertingIgnoringTimes( + _ shouldBe RuleResult.forRule(rule).succeeded(Allow.withoutReasons) + ) _ <- rule .eval(Foo("TEST", 1)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Deny.withoutReasons))) + .assertingIgnoringTimes( + _ shouldBe RuleResult.forRule(rule).succeeded(Deny.withoutReasons) + ) } yield () } @@ -125,75 +161,95 @@ class RuleSpec for { _ <- rule .eval(Foo("TEST", 0)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Left(ex))) + .assertingIgnoringTimes(_ shouldBe RuleResult.forRule(rule).failed(ex)) _ <- rule .eval(Foo("TEST", 1)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Deny.withoutReasons))) - } yield () - } - } - - "Rule.check.eval" should { - "return the right result once evaluated when exhaustive" in { - sealed trait ADT - case class Foo() extends ADT - case class Bar() extends ADT - - val rule: PureRule[ADT] = Rule("Check Y value").check[Id, ADT] { - case Foo() => Allow.withoutReasons - case Bar() => Deny.withoutReasons - } - - for { - _ <- rule - .covary[IO] - .eval(Foo()) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Allow.withoutReasons))) - _ <- rule - .covary[IO] - .eval(Bar()) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Deny.withoutReasons))) + .assertingIgnoringTimes( + _ shouldBe RuleResult.forRule(rule).succeeded(Deny.withoutReasons) + ) } yield () } - } - "Rule.checkOrIgnore.eval" should { "return the right result once evaluated in defined domain" in { case class Foo(@unused x: String, @unused y: Int) - val rule: PureRule[Foo] = Rule("Check Y value").partially[Id, Foo] { case Foo(_, 0) => + val rule: PureRule[Foo] = Rule("Check Y value").partially { case Foo(_, 0) => Allow.withoutReasons } rule - .covary[IO] + .liftK[IO] .eval(Foo("TEST", 0)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Allow.withoutReasons))) + .assertingIgnoringTimes(_ shouldBe RuleResult.forRule(rule).succeeded(Allow.withoutReasons)) } "return the Ignore once evaluated out of the defined domain" in { case class Foo(@unused x: String, @unused y: Int) - val rule: PureRule[Foo] = Rule("Check Y value").partially[Id, Foo] { case Foo(_, 0) => + val rule: PureRule[Foo] = Rule("Check Y value").partially { case Foo(_, 0) => Allow.withoutReasons } rule - .covary[IO] + .liftK[IO] .eval(Foo("TEST", 1)) - .assertingIgnoringTimes(_ shouldBe RuleResult(rule, Right(Ignore.noMatch))) + .assertingIgnoringTimes( + _ shouldBe RuleResult.forRule(rule).succeeded(Ignore.noMatch) + ) } } + "Rule.assert*" should { + + "assert" in { + Rule + .pure[String]("Non Empty String") + .assert("String must be non empty")(_.nonEmpty) + .evalPure("NON_EMPTY_STRING") + .verdict shouldBe Right(Allow.withoutReasons) + + Rule + .pure[String]("Non Empty String") + .assert("String must be non empty")(_.nonEmpty) + .evalPure("") + .verdict shouldBe Right(Deny.because("String must be non empty")) + + Rule + .pure[String]("Non Empty String") + .assert(_.nonEmpty) + .evalPure("") + .verdict shouldBe Right(Deny.withoutReasons) + } + + "assertNot" in { + Rule + .pure[String]("Non Empty String") + .assertNot("String must be non empty")(_.isEmpty) + .evalPure("NON_EMPTY_STRING") + .verdict shouldBe Right(Allow.withoutReasons) + + Rule + .pure[String]("Non Empty String") + .assertNot("String must be non empty")(_.isEmpty) + .evalPure("") + .verdict shouldBe Right(Deny.because("String must be non empty")) + + Rule + .pure[String]("Non Empty String") + .assertNot(_.isEmpty) + .evalPure("") + .verdict shouldBe Right(Deny.withoutReasons) + } + } // ------------------------- EVAL RAW ------------------------- - "Rule.check.evalRaw" should { + "Rule.apply.evalRaw" should { "return the right result once evaluated" in { sealed trait ADT case class Foo(@unused x: String, @unused y: Int) extends ADT case class Bar(@unused x: String, @unused y: Int) extends ADT - val rule: Rule[IO, ADT] = Rule("Check Y value").check[IO, ADT] { + val rule: Rule[IO, ADT] = Rule("Check Y value") { case Foo(_, 0) => IO.pure(Allow.withoutReasons) case Bar(_, 1) => IO.pure(Deny.withoutReasons) case _ => IO.pure(Ignore.withoutReasons) @@ -211,7 +267,7 @@ class RuleSpec case class Foo(@unused x: String, @unused y: Int) extends ADT case class Bar(@unused x: String, @unused y: Int) extends ADT - val rule: RuleIO[ADT] = Rule("Check Y value").check[IO, ADT] { + val rule: RuleIO[ADT] = Rule("Check Y value") { case Foo(_, 0) => IO.raiseError(new RuntimeException("BOOM")) case Bar(_, 1) => IO.pure(Deny.withoutReasons) case _ => IO.pure(Ignore.withoutReasons) @@ -222,9 +278,23 @@ class RuleSpec _ <- rule.evalRaw(Bar("TEST", 1)).asserting(_ shouldBe RuleVerdict.Deny.withoutReasons) } yield () } + + "return the right result once evaluated when exhaustive" in { + sealed trait ADT + case class Foo() extends ADT + case class Bar() extends ADT + + val rule: PureRule[ADT] = Rule("Check Y value") { + case Foo() => Allow.withoutReasons + case Bar() => Deny.withoutReasons + } + + rule.evalRaw(Foo()) shouldBe RuleVerdict.Allow.withoutReasons + rule.evalRaw(Bar()) shouldBe RuleVerdict.Deny.withoutReasons + } } - "Rule.checkOrIgnore.evalRaw" should { + "Rule.partially.evalRaw" should { "return the right result once evaluated" in { case class Foo(@unused x: String, @unused y: Int) @@ -252,30 +322,12 @@ class RuleSpec _ <- rule.evalRaw(Foo("TEST", 1)).asserting(_ shouldBe RuleVerdict.Deny.withoutReasons) } yield () } - } - - "Rule.check.evalRaw" should { - "return the right result once evaluated when exhaustive" in { - sealed trait ADT - case class Foo() extends ADT - case class Bar() extends ADT - - val rule: Rule[Id, ADT] = Rule("Check Y value").check[Id, ADT] { - case Foo() => Allow.withoutReasons - case Bar() => Deny.withoutReasons - } - - rule.evalRaw(Foo()) shouldBe RuleVerdict.Allow.withoutReasons - rule.evalRaw(Bar()) shouldBe RuleVerdict.Deny.withoutReasons - } - } - "Rule.checkOrIgnore.evalRaw" should { "return the right result once evaluated in defined domain" in { case class Foo(@unused x: String, @unused y: Int) val rule: PureRule[Foo] = - Rule("Check Y value").partially[Id, Foo] { case Foo(_, 0) => + Rule("Check Y value").partially { case Foo(_, 0) => Allow.withoutReasons } @@ -286,7 +338,7 @@ class RuleSpec case class Foo(@unused x: String, @unused y: Int) val rule: PureRule[Foo] = - Rule("Check Y value").partially[Id, Foo] { case Foo(_, 0) => + Rule("Check Y value").partially { case Foo(_, 0) => Allow.withoutReasons } @@ -301,10 +353,10 @@ class RuleSpec val duplicated: List[PureRule[Foo]] = Rule.findDuplicated( NonEmptyList.of( - Rule("Check Y value").partially[Id, Foo] { case Foo(_, 0) => + Rule("Check Y value").partially { case Foo(_, 0) => Allow.withoutReasons }, - Rule("Check Y value").partially[Id, Foo] { case Foo(_, 1) => + Rule("Check Y value").partially { case Foo(_, 1) => Allow.withoutReasons } ) @@ -318,10 +370,10 @@ class RuleSpec val duplicated: Seq[PureRule[Foo]] = Rule.findDuplicated( NonEmptyList.of( - Rule("Check Y value").partially[Id, Foo] { case Foo(_, 0) => + Rule("Check Y value").partially { case Foo(_, 0) => Allow.withoutReasons }, - Rule("Check X value").partially[Id, Foo] { case Foo("Foo", _) => + Rule("Check X value").partially { case Foo("Foo", _) => Allow.withoutReasons } ) diff --git a/core/src/test/scala/erules/core/RuleVerdictSpec.scala b/core/src/test/scala/erules/RuleVerdictSpec.scala similarity index 97% rename from core/src/test/scala/erules/core/RuleVerdictSpec.scala rename to core/src/test/scala/erules/RuleVerdictSpec.scala index 38a525c..e0ef517 100644 --- a/core/src/test/scala/erules/core/RuleVerdictSpec.scala +++ b/core/src/test/scala/erules/RuleVerdictSpec.scala @@ -1,14 +1,13 @@ -package erules.core +package erules import cats.kernel.Monoid +import erules.RuleVerdict.{Allow, Deny, Ignore} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec import org.scalatest.EitherValues class RuleVerdictSpec extends AnyWordSpec with Matchers with EitherValues { - import RuleVerdict.* - "Monoid for RuleVerdict" should { "Return ignore when combining an empty list of results" in { diff --git a/core/src/test/scala/erules/core/RulesEngineSpec.scala b/core/src/test/scala/erules/RulesEngineSpec.scala similarity index 62% rename from core/src/test/scala/erules/core/RulesEngineSpec.scala rename to core/src/test/scala/erules/RulesEngineSpec.scala index 9e1839b..191dcd7 100644 --- a/core/src/test/scala/erules/core/RulesEngineSpec.scala +++ b/core/src/test/scala/erules/RulesEngineSpec.scala @@ -1,17 +1,16 @@ -package erules.core +package erules import cats.data.NonEmptyList import cats.effect.IO import cats.effect.testing.scalatest.AsyncIOSpec -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 +import erules.RuleResultsInterpreterVerdict.{Allowed, Denied} +import erules.RuleVerdict.{Allow, Deny} +import erules.testings.ErulesAsyncAssertingSyntax import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AsyncWordSpec -import scala.util.{Right, Try} +import scala.annotation.unused +import scala.util.Try class RulesEngineSpec extends AsyncWordSpec @@ -19,28 +18,30 @@ class RulesEngineSpec with Matchers with ErulesAsyncAssertingSyntax { + case class Foo(@unused x: String, @unused y: Int) + "RulesEngine" should { "Return a DuplicatedRulesException with duplicated rules" in { - case class Foo(x: String, y: Int) - val allowYEqZero1: Rule[Id, Foo] = Rule("Check Y value").partially[Id, Foo] { - case Foo(_, 0) => - Allow.withoutReasons + val allowYEqZero1: PureRule[Foo] = Rule("Check Y value").partially { case Foo(_, 0) => + Allow.withoutReasons } - val allowYEqZero2: PureRule[Foo] = Rule("Check Y value").partially[Id, Foo] { - case Foo(_, 0) => - Allow.withoutReasons + val allowYEqZero2: PureRule[Foo] = Rule("Check Y value").partially { case Foo(_, 0) => + Allow.withoutReasons } - RulesEngine[Try] - .withRules( - allowYEqZero1, - allowYEqZero2 - ) - .denyAllNotAllowed - .failed - .get shouldBe a[DuplicatedRulesException] + assert( + RulesEngine + .withRules( + allowYEqZero1, + allowYEqZero2 + ) + .liftK[IO] + .denyAllNotAllowed[Try] + .failed + .isSuccess + ) } } @@ -49,15 +50,15 @@ class RulesEngineSpec "Respond with DENIED when there are no rules for the target" in { - case class Foo(x: String, y: Int) - val allowYEqZero: PureRule[Foo] = Rule("Check Y value").partially[Id, Foo] { case Foo(_, 0) => + val allowYEqZero: PureRule[Foo] = Rule("Check Y value").partially { case Foo(_, 0) => Allow.withoutReasons } val engine: IO[RulesEngineIO[Foo]] = - RulesEngine[IO] + RulesEngine .withRules(allowYEqZero) - .denyAllNotAllowed + .liftK[IO] + .denyAllNotAllowed[IO] val result: IO[EngineResult[Foo]] = engine.flatMap(_.parEval(Foo("TEST", 1))) @@ -75,22 +76,22 @@ class RulesEngineSpec "Respond with DENIED when a rule Deny the target" in { - case class Foo(x: String, y: Int) - val denyXEqTest: PureRule[Foo] = Rule("Check X value").partially[Id, Foo] { - case Foo("TEST", _) => Deny.withoutReasons + val denyXEqTest: PureRule[Foo] = Rule("Check X value").partially { case Foo("TEST", _) => + Deny.withoutReasons } - val allowYEqZero: PureRule[Foo] = Rule("Check Y value").partially[Id, Foo] { case Foo(_, 0) => + val allowYEqZero: PureRule[Foo] = Rule("Check Y value").partially { case Foo(_, 0) => Allow.withoutReasons } val engine: IO[RulesEngineIO[Foo]] = - RulesEngine[IO] + RulesEngine .withRules( denyXEqTest, allowYEqZero ) - .denyAllNotAllowed + .liftK[IO] + .denyAllNotAllowed[IO] val result: IO[EngineResult[Foo]] = engine.flatMap(_.parEval(Foo("TEST", 0))) @@ -100,7 +101,9 @@ class RulesEngineSpec data = Foo("TEST", 0), verdict = Denied( NonEmptyList.of( - RuleResult(denyXEqTest, Right(RuleVerdict.Deny.withoutReasons)) + RuleResult + .forRule(denyXEqTest) + .succeeded(RuleVerdict.Deny.withoutReasons) ) ) ) @@ -109,16 +112,15 @@ class RulesEngineSpec "Respond with ALLOWED when a ALL rules allow the target" in { - case class Foo(x: String, y: Int) - - val allowYEqZero: Rule[Id, Foo] = Rule("Check Y value").partially[Id, Foo] { case Foo(_, 0) => + val allowYEqZero: PureRule[Foo] = Rule("Check Y value").partially { case Foo(_, 0) => Allow.withoutReasons } val engine: IO[RulesEngineIO[Foo]] = - RulesEngine[IO] + RulesEngine .withRules(allowYEqZero) - .denyAllNotAllowed + .liftK[IO] + .denyAllNotAllowed[IO] val result: IO[EngineResult[Foo]] = engine.flatMap(_.parEval(Foo("TEST", 0))) @@ -127,7 +129,7 @@ class RulesEngineSpec data = Foo("TEST", 0), verdict = Allowed( NonEmptyList.of( - RuleResult(allowYEqZero, Right(RuleVerdict.Allow.withoutReasons)) + RuleResult.forRule(allowYEqZero).succeeded(RuleVerdict.Allow.withoutReasons) ) ) ) @@ -138,23 +140,22 @@ class RulesEngineSpec "RulesEngine.allowAllNotDenied.eval" should { "Respond with DENIED for safety in case of rule evaluation error" in { - case class Foo(x: String, y: Int) val ex1 = new RuntimeException("BOOM") val ex2 = new RuntimeException("PUFF") - val allow1: RuleIO[Foo] = Rule("ALLOW").const[IO, Foo](Allow.withoutReasons) - val failed1: RuleIO[Foo] = Rule("BOOM").failed[IO, Foo](ex1) - val failed2: RuleIO[Foo] = Rule("PUFF").failed[IO, Foo](ex2) + val allow1: RuleIO[Foo] = Rule("ALLOW").const(Allow.withoutReasons) + val failed1: RuleIO[Foo] = Rule("BOOM").failed(ex1) + val failed2: RuleIO[Foo] = Rule("PUFF").failed(ex2) val engine: IO[RulesEngineIO[Foo]] = - RulesEngine[IO] + RulesEngine .withRules( allow1, failed1, failed2 ) - .denyAllNotAllowed + .denyAllNotAllowed[IO] val result: IO[EngineResult[Foo]] = engine.flatMap(_.parEval(Foo("TEST", 1))) @@ -163,8 +164,8 @@ class RulesEngineSpec data = Foo("TEST", 1), verdict = Denied( NonEmptyList.of( - RuleResult.denyForSafetyInCaseOfError(failed1, ex1), - RuleResult.denyForSafetyInCaseOfError(failed2, ex2) + RuleResult.forRule(failed1).denyForSafetyInCaseOfError(ex1), + RuleResult.forRule(failed2).denyForSafetyInCaseOfError(ex2) ) ) ) @@ -173,15 +174,15 @@ class RulesEngineSpec "Respond with ALLOWED when there are no rules for the target" in { - case class Foo(x: String, y: Int) - val denyYEqZero: Rule[Id, Foo] = Rule("Check Y value").partially[Id, Foo] { case Foo(_, 0) => + val denyYEqZero: PureRule[Foo] = Rule("Check Y value").partially { case Foo(_, 0) => Deny.withoutReasons } val engine: IO[RulesEngineIO[Foo]] = - RulesEngine[IO] + RulesEngine .withRules(denyYEqZero) - .allowAllNotDenied + .liftK[IO] + .allowAllNotDenied[IO] val result: IO[EngineResult[Foo]] = engine.flatMap(_.parEval(Foo("TEST", 1))) @@ -199,23 +200,22 @@ class RulesEngineSpec "Respond with DENIED when a rule Deny the target" in { - case class Foo(x: String, y: Int) - val denyXEqTest: Rule[Id, Foo] = Rule("Check X value").partially[Id, Foo] { - case Foo("TEST", _) => - Deny.withoutReasons + val denyXEqTest: PureRule[Foo] = Rule("Check X value").partially { case Foo("TEST", _) => + Deny.withoutReasons } - val allowYEqZero: PureRule[Foo] = Rule("Check Y value").partially[Id, Foo] { case Foo(_, 0) => + val allowYEqZero: PureRule[Foo] = Rule("Check Y value").partially { case Foo(_, 0) => Allow.withoutReasons } val engine: IO[RulesEngineIO[Foo]] = - RulesEngine[IO] - .withRules[Id, Foo]( + RulesEngine + .withRules( denyXEqTest, allowYEqZero ) - .allowAllNotDenied + .liftK[IO] + .allowAllNotDenied[IO] val result: IO[EngineResult[Foo]] = engine.flatMap(_.parEval(Foo("TEST", 0))) @@ -224,7 +224,7 @@ class RulesEngineSpec data = Foo("TEST", 0), verdict = Denied( NonEmptyList.of( - RuleResult(denyXEqTest, Right(RuleVerdict.Deny.withoutReasons)) + RuleResult.forRule(denyXEqTest).succeeded(RuleVerdict.Deny.withoutReasons) ) ) ) @@ -233,25 +233,23 @@ class RulesEngineSpec "Respond with ALLOWED when a ALL rules allow the target" in { - case class Foo(x: String, y: Int) - - val allowYEqZero: PureRule[Foo] = Rule("Check Y value").partially[Id, Foo] { case Foo(_, 0) => + val allowYEqZero: PureRule[Foo] = Rule("Check Y value").partially { case Foo(_, 0) => Allow.withoutReasons } - val engine: IO[RulesEngineIO[Foo]] = - RulesEngine[IO] + val engine: IO[PureRulesEngine[Foo]] = + RulesEngine .withRules(allowYEqZero) - .allowAllNotDenied + .allowAllNotDenied[IO] - val result: IO[EngineResult[Foo]] = engine.flatMap(_.parEval(Foo("TEST", 0))) + val result: IO[EngineResult[Foo]] = engine.map(_.seqEvalPure(Foo("TEST", 0))) result.assertingIgnoringTimes( _ shouldBe EngineResult[Foo]( data = Foo("TEST", 0), verdict = Allowed( NonEmptyList.of( - RuleResult(allowYEqZero, Right(RuleVerdict.Allow.withoutReasons)) + RuleResult.forRule(allowYEqZero).succeeded(RuleVerdict.Allow.withoutReasons) ) ) ) diff --git a/core/src/test/scala/erules/UsabilityTestSpec.scala b/core/src/test/scala/erules/UsabilityTestSpec.scala new file mode 100644 index 0000000..99af905 --- /dev/null +++ b/core/src/test/scala/erules/UsabilityTestSpec.scala @@ -0,0 +1,64 @@ +package erules + +import cats.data.NonEmptyList +import cats.effect.IO +import cats.effect.testing.scalatest.AsyncIOSpec +import cats.Id +import erules.testings.* +import erules.RuleVerdict.{Allow, Deny} +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AsyncWordSpec + +class UsabilityTestSpec + extends AsyncWordSpec + with AsyncIOSpec + with Matchers + with ErulesAsyncAssertingSyntax { + + import erules.report.ReportEncoder.* + + "This library" should { + "be functional and with a nice syntax" in { + + val returnRules: NonEmptyList[PureRule[Order]] = NonEmptyList.of( + Rule("ShipTo IT order only") { + case Order(_, ShipTo(_, Country.`IT`), _, _) => Allow.withoutReasons + case _ => Deny.because("Ship to not to Italy") + }, + Rule("BillTo UK order only") { + case Order(_, _, BillTo(_, Country.`UK`), _) => Allow.because(EvalReason("Bill to UK")) + case _ => Deny.because("Bill to not from UK") + }, + Rule[Id, BigDecimal]("Prince under 5k") + .assert("Be under 5k")(_.toInt < 5000) + .targetInfo("Total price") + .contramap(_.items.map(_.price).sum), + Rule[Id, BigDecimal]("Prince under 5k - 2") + .apply(o => Allow.when(o.toInt < 5000)(Deny.because("Be under 5k"))) + .targetInfo("Total price") + .contramap(_.items.map(_.price).sum) + ) + + val engine: IO[PureRulesEngine[Order]] = + RulesEngine + .withRules(returnRules) + .denyAllNotAllowed[IO] + + val result: IO[RuleResultsInterpreterVerdict] = engine + .map( + _.seqEvalPure( + Order( + id = "123", + shipTo = ShipTo("Via Roma 1", Country.IT), + billTo = BillTo("Via Roma 1", Country.IT), + items = List(Item("123", 1, BigDecimal(10))) + ) + ) + ) + .flatTap(_.printStringReport[IO]) + .map(_.verdict) + + result.asserting(_.isAllowed shouldBe false) + } + } +} diff --git a/core/src/test/scala/erules/core/report/StringReportEncoderSpec.scala b/core/src/test/scala/erules/report/StringReportEncoderSpec.scala similarity index 78% rename from core/src/test/scala/erules/core/report/StringReportEncoderSpec.scala rename to core/src/test/scala/erules/report/StringReportEncoderSpec.scala index f12d104..dd8d33a 100644 --- a/core/src/test/scala/erules/core/report/StringReportEncoderSpec.scala +++ b/core/src/test/scala/erules/report/StringReportEncoderSpec.scala @@ -1,10 +1,9 @@ -package erules.core.report +package erules.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 erules.{PureRule, PureRulesEngine, Rule, RulesEngine} +import erules.RuleVerdict.Allow import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AsyncWordSpec @@ -18,18 +17,18 @@ class StringReportEncoderSpec extends AsyncWordSpec with AsyncIOSpec with Matche case class Foo(x: String, y: Int) - val allowYEqZero: Rule[Id, Foo] = Rule("Check Y value").partially[Id, Foo] { case Foo(_, 0) => + val allowYEqZero: PureRule[Foo] = Rule("Check Y value").partially { case Foo(_, 0) => Allow.withoutReasons } - val engine: IO[RulesEngineIO[Foo]] = - RulesEngine[IO] + val engine: IO[PureRulesEngine[Foo]] = + RulesEngine .withRules(allowYEqZero) - .denyAllNotAllowed + .denyAllNotAllowed[IO] val result: IO[String] = engine - .flatMap(_.parEval(Foo("TEST", 0))) + .map(_.seqEvalPure(Foo("TEST", 0))) .map(_.drainExecutionsTime.asReport[String]) result diff --git a/core/src/test/scala/erules/core/testings/ErulesAsyncAssertingSyntax.scala b/core/src/test/scala/erules/testings/ErulesAsyncAssertingSyntax.scala similarity index 72% rename from core/src/test/scala/erules/core/testings/ErulesAsyncAssertingSyntax.scala rename to core/src/test/scala/erules/testings/ErulesAsyncAssertingSyntax.scala index 96a3936..947559e 100644 --- a/core/src/test/scala/erules/core/testings/ErulesAsyncAssertingSyntax.scala +++ b/core/src/test/scala/erules/testings/ErulesAsyncAssertingSyntax.scala @@ -1,7 +1,7 @@ -package erules.core.testings +package erules.testings import cats.Functor -import erules.core.{EngineResult, RuleResult, RuleVerdict} +import erules.{EngineResult, RuleResult, RuleVerdict} import org.scalatest.matchers.should.Matchers import org.scalatest.Assertion @@ -10,10 +10,10 @@ trait ErulesAsyncAssertingSyntax { this: Matchers => import cats.implicits.* implicit class RuleResultAssertingOps[F[_]: Functor, -T, +V <: RuleVerdict]( - fa: F[RuleResult[T, V]] + fa: F[RuleResult[V]] ) { - def assertingIgnoringTimes(f: RuleResult[T, V] => Assertion): F[Assertion] = + def assertingIgnoringTimes(f: RuleResult[V] => Assertion): F[Assertion] = fa.map(a => f(a.drainExecutionTime)) } diff --git a/core/src/test/scala/erules/testings/Order.scala b/core/src/test/scala/erules/testings/Order.scala new file mode 100644 index 0000000..779fe50 --- /dev/null +++ b/core/src/test/scala/erules/testings/Order.scala @@ -0,0 +1,37 @@ +package erules.testings + +import cats.Show + +case class Order( + id: String, + shipTo: ShipTo, + billTo: BillTo, + items: List[Item] +) +object Order { + implicit val show: Show[Order] = Show.fromToString +} + +case class ShipTo( + address: String, + country: Country +) + +case class BillTo( + address: String, + country: Country +) + +case class Item( + id: String, + quantity: Int, + price: BigDecimal +) + +case class Country(value: String) extends AnyVal +object Country { + val IT: Country = Country("IT") + val US: Country = Country("US") + val UK: Country = Country("UK") + val FR: Country = Country("FR") +} diff --git a/core/src/test/scala/erules/core/testings/ReportValues.scala b/core/src/test/scala/erules/testings/ReportValues.scala similarity index 82% rename from core/src/test/scala/erules/core/testings/ReportValues.scala rename to core/src/test/scala/erules/testings/ReportValues.scala index fa54c22..8962ed5 100644 --- a/core/src/test/scala/erules/core/testings/ReportValues.scala +++ b/core/src/test/scala/erules/testings/ReportValues.scala @@ -1,7 +1,7 @@ -package erules.core.testings +package erules.testings import cats.effect.kernel.Async -import erules.core.report.ReportEncoder +import erules.report.ReportEncoder trait ReportValues { diff --git a/core/src/test/scala/erules/utils/IsIdSpec.scala b/core/src/test/scala/erules/utils/IsIdSpec.scala new file mode 100644 index 0000000..0324c6a --- /dev/null +++ b/core/src/test/scala/erules/utils/IsIdSpec.scala @@ -0,0 +1,59 @@ +package erules.utils + +import cats.Id +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class IsIdSpec extends AnyWordSpec with Matchers { + + import erules.utils.IsId.* + + "IsId.liftId" should { + "lift A to Id[A]" in { + 1.liftId[Id] shouldBe 1 + } + } + + "IsId.unliftId" should { + "unlift Id[A] to A" in { + Id(1).unliftId shouldBe 1 + } + } + + "IsId implicit syntax" should { + + "implicitly unliftId F[A] to A when F is Id" in { + def foo[F[_]: IsId](x: F[Int]): Int = x + foo[Id](1) + } + + "unliftId F[A] to A when F is Id" in { + def foo[F[_]: IsId](x: F[Int]): Int = x.unliftId + foo[Id](1) + } + + "liftId A to F[A] when F is Id" in { + def foo[F[_]: IsId](x: Int): F[Int] = x.liftId[F] + foo[Id](1) + } + } + + "IsId" should { + "not compile when F is not Id" in { + """ + |import cats.data.NonEmptyList + |import erules.utils.IsId + | + |implicitly[IsId[NonEmptyList]] + """.stripMargin shouldNot compile + } + + "compile when F is Id" in { + """ + |import erules.utils.IsId + | + |implicitly[IsId[Id]] + """.stripMargin should compile + } + } +} diff --git a/modules/cats-xml/README.md b/modules/cats-xml/README.md index e668bbd..cdad122 100644 --- a/modules/cats-xml/README.md +++ b/modules/cats-xml/README.md @@ -25,27 +25,29 @@ case class Person( ``` Let's write the rules! + ```scala -import erules.core.Rule -import erules.core.RuleVerdict.* +import erules.Rule +import erules.PureRule +import erules.RuleVerdict.* import cats.data.NonEmptyList import cats.Id -val checkCitizenship: Rule[Id, Citizenship] = - Rule("Check UK citizenship").apply[Id, Citizenship]{ +val checkCitizenship: PureRule[Citizenship] = + Rule("Check UK citizenship") { case Citizenship(Country("UK")) => Allow.withoutReasons - case _ => Deny.because("Only UK citizenship is allowed!") + case _ => Deny.because("Only UK citizenship is allowed!") } -// checkCitizenship: Rule[Id, Citizenship] = RuleImpl(,Check UK citizenship,None,None) +// checkCitizenship: PureRule[Citizenship] = RuleImpl(,RuleInfo(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!") +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: Rule[Id, Age] = RuleImpl(,Check Age >= 18,None,None) +// checkAdultAge: PureRule[Age] = RuleImpl(,RuleInfo(Check Age >= 18,None,None)) -val allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList.of( +val allPersonRules: NonEmptyList[PureRule[Person]] = NonEmptyList.of( checkCitizenship .targetInfo("citizenship") .contramap(_.citizenship), @@ -53,7 +55,7 @@ val allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList.of( .targetInfo("age") .contramap(_.age) ) -// allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList(RuleImpl(scala.Function1$$Lambda$12152/0x0000000802cb7950@5f180ce6,Check UK citizenship,None,Some(citizenship)), RuleImpl(scala.Function1$$Lambda$12152/0x0000000802cb7950@2cb1276,Check Age >= 18,None,Some(age))) +// allPersonRules: NonEmptyList[PureRule[Person]] = NonEmptyList(RuleImpl(scala.Function1$$Lambda$11131/0x000000080283b368@64e6f1cd,RuleInfo(Check UK citizenship,None,Some(citizenship))), RuleImpl(scala.Function1$$Lambda$11131/0x000000080283b368@30142743,RuleInfo(Check Age >= 18,None,Some(age)))) ``` Import @@ -66,6 +68,7 @@ Define the `Person` encoder import cats.xml.codec.Encoder import cats.xml.XmlNode import cats.xml.implicits.* +import scala.util.Try implicit val personEncoder: Encoder[Person] = Encoder.of(person => XmlNode("Person") @@ -85,25 +88,23 @@ implicit val personEncoder: Encoder[Person] = Encoder.of(person => And create the XML report ```scala -import erules.core.* +import erules.* import erules.implicits.* import erules.cats.xml.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(...) +val result: Try[EngineResult[Person]] = + RulesEngine + .withRules(allPersonRules) + .denyAllNotAllowed[Try] + .map(_.seqEvalPure(person)) +// result: Try[EngineResult[Person]] = Success(EngineResult(Person(Mimmo,Rossi,Age(16),Citizenship(Country(IT))),Denied(NonEmptyList(RuleResult(RuleInfo(Check UK citizenship,None,Some(citizenship)),Right(DenyImpl(List(EvalReason(Only UK citizenship is allowed!)))),None), RuleResult(RuleInfo(Check Age >= 18,None,Some(age)),Right(DenyImpl(List(EvalReason(Only >= 18 age are allowed!)))),None))))) //yolo -result.unsafeRunSync().asXmlReport -// res0: cats.xml.Xml = +result.get.asXmlReport +// res0: ..cats.xml.Xml = // // // @@ -112,11 +113,11 @@ result.unsafeRunSync().asXmlReport // // // -// +// // // Check UK citizenship for citizenship // -// +// // // // @@ -124,22 +125,16 @@ result.unsafeRunSync().asXmlReport // // // -// -// -// // // -// +// // Check Age >= 18 for age -// +// // // // Only >= 18 age are allowed! // // -// -// -// // // // diff --git a/modules/cats-xml/docs/README.md b/modules/cats-xml/docs/README.md index 7714887..b5f4244 100644 --- a/modules/cats-xml/docs/README.md +++ b/modules/cats-xml/docs/README.md @@ -25,25 +25,27 @@ case class Person( ``` Let's write the rules! + ```scala mdoc:to-string -import erules.core.Rule -import erules.core.RuleVerdict.* +import erules.Rule +import erules.PureRule +import erules.RuleVerdict.* import cats.data.NonEmptyList import cats.Id -val checkCitizenship: Rule[Id, Citizenship] = - Rule("Check UK citizenship").apply[Id, Citizenship]{ +val checkCitizenship: PureRule[Citizenship] = + Rule("Check UK citizenship") { case Citizenship(Country("UK")) => Allow.withoutReasons - case _ => Deny.because("Only UK citizenship is allowed!") + 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 checkAdultAge: PureRule[Age] = + Rule("Check Age >= 18") { + 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( +val allPersonRules: NonEmptyList[PureRule[Person]] = NonEmptyList.of( checkCitizenship .targetInfo("citizenship") .contramap(_.citizenship), @@ -63,6 +65,7 @@ Define the `Person` encoder import cats.xml.codec.Encoder import cats.xml.XmlNode import cats.xml.implicits.* +import scala.util.Try implicit val personEncoder: Encoder[Person] = Encoder.of(person => XmlNode("Person") @@ -82,20 +85,18 @@ implicit val personEncoder: Encoder[Person] = Encoder.of(person => And create the XML report ```scala mdoc:to-string -import erules.core.* +import erules.* import erules.implicits.* import erules.cats.xml.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 +val result: Try[EngineResult[Person]] = + RulesEngine + .withRules(allPersonRules) + .denyAllNotAllowed[Try] + .map(_.seqEvalPure(person)) //yolo -result.unsafeRunSync().asXmlReport +result.get.asXmlReport ``` \ No newline at end of file diff --git a/modules/cats-xml/src/main/scala/erules/cats/xml/instances.scala b/modules/cats-xml/src/main/scala/erules/cats/xml/instances.scala index 06a0668..9505c64 100644 --- a/modules/cats-xml/src/main/scala/erules/cats/xml/instances.scala +++ b/modules/cats-xml/src/main/scala/erules/cats/xml/instances.scala @@ -3,12 +3,18 @@ package erules.cats.xml import cats.xml.{XmlData, XmlNode} import cats.xml.codec.Encoder import erules.cats.xml.report.{XmlReportInstances, XmlReportSyntax} -import erules.core.* +import erules.{ + EitherThrow, + EngineResult, + EvalReason, + RuleInfo, + RuleResult, + RuleResultsInterpreterVerdict, + RuleVerdict +} import scala.concurrent.duration.FiniteDuration -//import scala.concurrent.duration.FiniteDuration - object implicits extends CatsXmlAllInstances with CatsXmlAllSyntax //---------- INSTANCES ---------- @@ -29,29 +35,29 @@ private[xml] trait BasicTypesCatsXmlInstances { .withChildren( List( XmlNode("Data").withChildren(Encoder[T].encode(res.data).asNode.toList) - ) ++ Encoder[RuleResultsInterpreterVerdict[T]].encode(res.verdict).asNode.toList + ) ++ Encoder[RuleResultsInterpreterVerdict].encode(res.verdict).asNode.toList ) }) - implicit def ruleResultsInterpreterCatsXmlEncoder[T] - : Encoder[RuleResultsInterpreterVerdict[T]] = { + implicit final val ruleResultsInterpreterCatsXmlEncoder + : Encoder[RuleResultsInterpreterVerdict] = { Encoder.of { v => XmlNode("Verdict") .withAttributes( "type" := v.typeName ) .withChildren( - XmlNode("EvaluatedRules").withChildren(v.evaluatedRules.toList.flatMap(_.toXml.asNode)) + XmlNode("EvaluatedRules").withChildren(v.evaluatedResults.toList.flatMap(_.toXml.asNode)) ) } } - implicit def ruleResultCatsXmlEncoder[T]: Encoder[RuleResult[T, RuleVerdict]] = { - Encoder.of[RuleResult[T, RuleVerdict]](result => { + implicit def ruleResultCatsXmlEncoder: Encoder[RuleResult[RuleVerdict]] = { + Encoder.of[RuleResult[RuleVerdict]](result => { XmlNode("RuleResult") .withChildren( List( - Encoder[AnyTypedRule[T]].encode(result.rule).asNode, + Encoder[RuleInfo].encode(result.ruleInfo).asNode, Encoder[EitherThrow[RuleVerdict]].encode(result.verdict).asNode, result.executionTime .flatMap(Encoder[FiniteDuration].encode(_).asNode) @@ -61,9 +67,9 @@ private[xml] trait BasicTypesCatsXmlInstances { }) } - implicit def ruleCatsXmlEncoder[T]: Encoder[AnyTypedRule[T]] = + implicit def ruleInfoCatsXmlEncoder: Encoder[RuleInfo] = Encoder.of { v => - XmlNode("Rule") + XmlNode("RuleInfo") .withAttributes( "name" := v.name, "description" := v.description.map(XmlData.fromString).getOrElse(XmlData.empty), diff --git a/modules/cats-xml/src/main/scala/erules/cats/xml/report/XmlReport.scala b/modules/cats-xml/src/main/scala/erules/cats/xml/report/XmlReport.scala index 4caa265..dc2adfc 100644 --- a/modules/cats-xml/src/main/scala/erules/cats/xml/report/XmlReport.scala +++ b/modules/cats-xml/src/main/scala/erules/cats/xml/report/XmlReport.scala @@ -2,7 +2,7 @@ package erules.cats.xml.report import cats.xml.codec.Encoder import cats.xml.Xml -import erules.core.* +import erules.{EngineResult, RuleResult, RuleResultsInterpreterVerdict, RuleVerdict} object XmlReport extends XmlReportInstances with XmlReportSyntax { def fromEncoder[T: Encoder]: XmlReportEncoder[T] = @@ -15,13 +15,12 @@ private[xml] trait XmlReportInstances { implicit def engineResultXmlReportEncoder[T: Encoder]: XmlReportEncoder[EngineResult[T]] = XmlReport.fromEncoder[EngineResult[T]] - implicit def ruleResultsInterpreterVerdictXmlReportEncoder[T] - : XmlReportEncoder[RuleResultsInterpreterVerdict[T]] = - XmlReport.fromEncoder[RuleResultsInterpreterVerdict[T]] + implicit val ruleResultsInterpreterVerdictXmlReportEncoder + : XmlReportEncoder[RuleResultsInterpreterVerdict] = + XmlReport.fromEncoder[RuleResultsInterpreterVerdict] - implicit def ruleRuleResultXmlReportEncoder[T] - : XmlReportEncoder[RuleResult[T, ? <: RuleVerdict]] = - XmlReport.fromEncoder[RuleResult[T, ? <: RuleVerdict]] + implicit val ruleRuleResultXmlReportEncoder: XmlReportEncoder[RuleResult[? <: RuleVerdict]] = + XmlReport.fromEncoder[RuleResult[? <: RuleVerdict]] implicit val ruleVerdictXmlReportEncoder: XmlReportEncoder[RuleVerdict] = XmlReport.fromEncoder[RuleVerdict] diff --git a/modules/cats-xml/src/main/scala/erules/cats/xml/report/package.scala b/modules/cats-xml/src/main/scala/erules/cats/xml/report/package.scala index d1a2a6a..07d7d9c 100644 --- a/modules/cats-xml/src/main/scala/erules/cats/xml/report/package.scala +++ b/modules/cats-xml/src/main/scala/erules/cats/xml/report/package.scala @@ -1,7 +1,7 @@ package erules.cats.xml import cats.xml.Xml -import erules.core.report.ReportEncoder +import erules.report.ReportEncoder package object report { type XmlReportEncoder[T] = ReportEncoder[T, Xml] diff --git a/modules/cats-xml/src/test/scala/erules/cats/xml/report/XmlReportEncoderSpec.scala b/modules/cats-xml/src/test/scala/erules/cats/xml/report/XmlReportEncoderSpec.scala index 7a39e12..305cc93 100644 --- a/modules/cats-xml/src/test/scala/erules/cats/xml/report/XmlReportEncoderSpec.scala +++ b/modules/cats-xml/src/test/scala/erules/cats/xml/report/XmlReportEncoderSpec.scala @@ -1,11 +1,10 @@ package erules.cats.xml.report import cats.effect.IO -import cats.Id -import erules.core.{Rule, RulesEngine, RulesEngineIO} -import erules.core.RuleVerdict.Allow import cats.xml.{Xml, XmlNode} import cats.xml.codec.Encoder +import erules.{PureRule, PureRulesEngine, Rule, RulesEngine} +import erules.RuleVerdict.Allow class XmlReportEncoderSpec extends munit.CatsEffectSuite { @@ -25,18 +24,18 @@ class XmlReportEncoderSpec extends munit.CatsEffectSuite { ) } - val allowYEqZero: Rule[Id, Foo] = Rule("Check Y value").partially[Id, Foo] { case Foo(_, 0) => + val allowYEqZero: PureRule[Foo] = Rule("Check Y value").partially { case Foo(_, 0) => Allow.because("because yes!") } - val engine: IO[RulesEngineIO[Foo]] = - RulesEngine[IO] + val engine: IO[PureRulesEngine[Foo]] = + RulesEngine .withRules(allowYEqZero) - .denyAllNotAllowed + .denyAllNotAllowed[IO] val result: IO[Xml] = engine - .flatMap(_.parEval(Foo("TEST", 0))) + .map(_.seqEvalPure(Foo("TEST", 0))) .map(_.drainExecutionsTime.asXmlReport) assertIO( @@ -48,9 +47,9 @@ class XmlReportEncoderSpec extends munit.CatsEffectSuite { - + Check Y value - + because yes! diff --git a/modules/circe/README.md b/modules/circe/README.md index 6b7160c..c212a80 100644 --- a/modules/circe/README.md +++ b/modules/circe/README.md @@ -25,27 +25,29 @@ case class Person( ``` Let's write the rules! + ```scala -import erules.core.Rule -import erules.core.RuleVerdict.* +import erules.Rule +import erules.PureRule +import erules.RuleVerdict.* import cats.data.NonEmptyList import cats.Id -val checkCitizenship: Rule[Id, Citizenship] = - Rule("Check UK citizenship").apply[Id, Citizenship]{ +val checkCitizenship: PureRule[Citizenship] = + Rule("Check UK citizenship") { case Citizenship(Country("UK")) => Allow.withoutReasons - case _ => Deny.because("Only UK citizenship is allowed!") + case _ => Deny.because("Only UK citizenship is allowed!") } -// checkCitizenship: Rule[Id, Citizenship] = RuleImpl(,Check UK citizenship,None,None) +// checkCitizenship: PureRule[Citizenship] = RuleImpl(,RuleInfo(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!") +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: Rule[Id, Age] = RuleImpl(,Check Age >= 18,None,None) +// checkAdultAge: PureRule[Age] = RuleImpl(,RuleInfo(Check Age >= 18,None,None)) -val allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList.of( +val allPersonRules: NonEmptyList[PureRule[Person]] = NonEmptyList.of( checkCitizenship .targetInfo("citizenship") .contramap(_.citizenship), @@ -53,7 +55,7 @@ val allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList.of( .targetInfo("age") .contramap(_.age) ) -// allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList(RuleImpl(scala.Function1$$Lambda$12152/0x0000000802cb7950@3a4e9ea8,Check UK citizenship,None,Some(citizenship)), RuleImpl(scala.Function1$$Lambda$12152/0x0000000802cb7950@717c3b21,Check Age >= 18,None,Some(age))) +// allPersonRules: NonEmptyList[PureRule[Person]] = NonEmptyList(RuleImpl(scala.Function1$$Lambda$11131/0x000000080283b368@159e1b62,RuleInfo(Check UK citizenship,None,Some(citizenship))), RuleImpl(scala.Function1$$Lambda$11131/0x000000080283b368@31935e2f,RuleInfo(Check Age >= 18,None,Some(age)))) ``` Import @@ -68,24 +70,23 @@ import io.circe.generic.auto.* And create the JSON report ```scala -import erules.core.* +import erules.* import erules.implicits.* import erules.circe.implicits.* - -import cats.effect.IO -import cats.effect.unsafe.implicits.* +import scala.util.Try 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(...) +val result: Try[EngineResult[Person]] = + RulesEngine + .withRules(allPersonRules) + .denyAllNotAllowed[Try] + .map(_.seqEvalPure(person)) +// result: Try[EngineResult[Person]] = Success(EngineResult(Person(Mimmo,Rossi,Age(16),Citizenship(Country(IT))),Denied(NonEmptyList(RuleResult(RuleInfo(Check UK citizenship,None,Some(citizenship)),Right(DenyImpl(List(EvalReason(Only UK citizenship is allowed!)))),None), RuleResult(RuleInfo(Check Age >= 18,None,Some(age)),Right(DenyImpl(List(EvalReason(Only >= 18 age are allowed!)))),None))))) //yolo -result.unsafeRunSync().asJsonReport +result.get.asJsonReport // res0: io.circe.Json = { // "data" : { // "name" : "Mimmo", @@ -103,7 +104,7 @@ result.unsafeRunSync().asJsonReport // "type" : "Denied", // "evaluatedRules" : [ // { -// "rule" : { +// "ruleInfo" : { // "name" : "Check UK citizenship", // "targetInfo" : "citizenship", // "fullDescription" : "Check UK citizenship for citizenship" @@ -113,14 +114,10 @@ result.unsafeRunSync().asJsonReport // "reasons" : [ // "Only UK citizenship is allowed!" // ] -// }, -// "executionTime" : { -// "length" : 107042, -// "unit" : "NANOSECONDS" // } // }, // { -// "rule" : { +// "ruleInfo" : { // "name" : "Check Age >= 18", // "targetInfo" : "age", // "fullDescription" : "Check Age >= 18 for age" @@ -130,10 +127,6 @@ result.unsafeRunSync().asJsonReport // "reasons" : [ // "Only >= 18 age are allowed!" // ] -// }, -// "executionTime" : { -// "length" : 10084, -// "unit" : "NANOSECONDS" // } // } // ] diff --git a/modules/circe/docs/README.md b/modules/circe/docs/README.md index 98ace9a..183d31d 100644 --- a/modules/circe/docs/README.md +++ b/modules/circe/docs/README.md @@ -25,25 +25,27 @@ case class Person( ``` Let's write the rules! + ```scala mdoc:to-string -import erules.core.Rule -import erules.core.RuleVerdict.* +import erules.Rule +import erules.PureRule +import erules.RuleVerdict.* import cats.data.NonEmptyList import cats.Id -val checkCitizenship: Rule[Id, Citizenship] = - Rule("Check UK citizenship").apply[Id, Citizenship]{ +val checkCitizenship: PureRule[Citizenship] = + Rule("Check UK citizenship") { case Citizenship(Country("UK")) => Allow.withoutReasons - case _ => Deny.because("Only UK citizenship is allowed!") + 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 checkAdultAge: PureRule[Age] = + Rule("Check Age >= 18") { + 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( +val allPersonRules: NonEmptyList[PureRule[Person]] = NonEmptyList.of( checkCitizenship .targetInfo("citizenship") .contramap(_.citizenship), @@ -65,20 +67,19 @@ import io.circe.generic.auto.* And create the JSON report ```scala mdoc:to-string -import erules.core.* +import erules.* import erules.implicits.* import erules.circe.implicits.* - -import cats.effect.IO -import cats.effect.unsafe.implicits.* +import scala.util.Try 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 +val result: Try[EngineResult[Person]] = + RulesEngine + .withRules(allPersonRules) + .denyAllNotAllowed[Try] + .map(_.seqEvalPure(person)) //yolo -result.unsafeRunSync().asJsonReport +result.get.asJsonReport ``` \ No newline at end of file diff --git a/modules/circe/src/main/scala/erules/circe/instances.scala b/modules/circe/src/main/scala/erules/circe/instances.scala index 3f703e1..7a0aa6a 100644 --- a/modules/circe/src/main/scala/erules/circe/instances.scala +++ b/modules/circe/src/main/scala/erules/circe/instances.scala @@ -1,8 +1,7 @@ package erules.circe +import erules.* import erules.circe.report.{JsonReportInstances, JsonReportSyntax} -import erules.core.* -import io.circe.generic.semiauto.deriveEncoder object implicits extends CirceAllInstances with CirceAllSyntax @@ -22,18 +21,15 @@ private[circe] trait BasicTypesCirceInstances { implicit def engineResultCirceEncoder[T: Encoder]: Encoder[EngineResult[T]] = io.circe.generic.semiauto.deriveEncoder[EngineResult[T]] - implicit def ruleResultsInterpreterCirceEncoder[T]: Encoder[RuleResultsInterpreterVerdict[T]] = + implicit final val ruleResultsInterpreterCirceEncoder: Encoder[RuleResultsInterpreterVerdict] = Encoder.instance { v => Json.obj( "type" -> Json.fromString(v.typeName), - "evaluatedRules" -> Json.fromValues(v.evaluatedRules.toList.map(_.asJson)) + "evaluatedRules" -> Json.fromValues(v.evaluatedResults.toList.map(_.asJson)) ) } - implicit def ruleResultCirceEncoder[T]: Encoder[RuleResult[T, RuleVerdict]] = - deriveEncoder[RuleResult[T, RuleVerdict]] - - implicit def ruleCirceEncoder[T]: Encoder[AnyTypedRule[T]] = + implicit final val ruleInfoCirceEncoder: Encoder[RuleInfo] = Encoder.instance { v => Json.obj( "name" -> Json.fromString(v.name), @@ -43,6 +39,16 @@ private[circe] trait BasicTypesCirceInstances { ) } + implicit final def ruleResultCirceEncoder[V <: RuleVerdict: Encoder]: Encoder[RuleResult[V]] = { + Encoder.instance { v => + Json.obj( + "ruleInfo" -> v.ruleInfo.asJson, + "verdict" -> v.verdict.asJson, + "executionTime" -> v.executionTime.asJson + ) + } + } + implicit final val ruleVerdictCirceEncoder: Encoder[RuleVerdict] = Encoder.instance { v => Json.obj( 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 bbc343b..85ef8e6 100644 --- a/modules/circe/src/main/scala/erules/circe/report/JsonReport.scala +++ b/modules/circe/src/main/scala/erules/circe/report/JsonReport.scala @@ -1,6 +1,6 @@ package erules.circe.report -import erules.core.* +import erules.{EngineResult, RuleResult, RuleResultsInterpreterVerdict, RuleVerdict} import io.circe.{Encoder, Json} object JsonReport extends JsonReportInstances with JsonReportSyntax { @@ -14,17 +14,16 @@ private[circe] trait JsonReportInstances { implicit def engineResultJsonReportEncoder[T: Encoder]: JsonReportEncoder[EngineResult[T]] = JsonReport.fromEncoder[EngineResult[T]] - implicit def ruleResultsInterpreterVerdictJsonReportEncoder[T] - : JsonReportEncoder[RuleResultsInterpreterVerdict[T]] = - JsonReport.fromEncoder[RuleResultsInterpreterVerdict[T]] + implicit final val ruleResultsInterpreterVerdictJsonReportEncoder + : JsonReportEncoder[RuleResultsInterpreterVerdict] = + JsonReport.fromEncoder[RuleResultsInterpreterVerdict] - implicit def ruleRuleResultJsonReportEncoder[T] - : JsonReportEncoder[RuleResult[T, ? <: RuleVerdict]] = - JsonReport.fromEncoder[RuleResult[T, ? <: RuleVerdict]] + implicit final val ruleRuleResultJsonReportEncoder + : JsonReportEncoder[RuleResult[? <: RuleVerdict]] = + JsonReport.fromEncoder[RuleResult[? <: RuleVerdict]] - implicit val ruleVerdictJsonReportEncoder: JsonReportEncoder[RuleVerdict] = + implicit final val ruleVerdictJsonReportEncoder: JsonReportEncoder[RuleVerdict] = JsonReport.fromEncoder[RuleVerdict] - } private[circe] trait JsonReportSyntax { diff --git a/modules/circe/src/main/scala/erules/circe/report/package.scala b/modules/circe/src/main/scala/erules/circe/report/package.scala index e0219c5..83c6262 100644 --- a/modules/circe/src/main/scala/erules/circe/report/package.scala +++ b/modules/circe/src/main/scala/erules/circe/report/package.scala @@ -1,6 +1,6 @@ package erules.circe -import erules.core.report.ReportEncoder +import erules.report.ReportEncoder import io.circe.Json package object report { diff --git a/modules/circe/src/test/scala/erules/circe/report/JsonReportEncoderSpec.scala b/modules/circe/src/test/scala/erules/circe/report/JsonReportEncoderSpec.scala index 39c072b..6c61c4a 100644 --- a/modules/circe/src/test/scala/erules/circe/report/JsonReportEncoderSpec.scala +++ b/modules/circe/src/test/scala/erules/circe/report/JsonReportEncoderSpec.scala @@ -1,9 +1,8 @@ package erules.circe.report import cats.effect.IO -import cats.Id -import erules.core.{Rule, RulesEngine, RulesEngineIO} -import erules.core.RuleVerdict.Allow +import erules.{PureRule, Rule, RulesEngine, RulesEngineIO} +import erules.RuleVerdict.Allow import io.circe.Json class JsonReportEncoderSpec extends munit.CatsEffectSuite { @@ -15,14 +14,15 @@ class JsonReportEncoderSpec extends munit.CatsEffectSuite { 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) => + val allowYEqZero: PureRule[Foo] = Rule("Check Y value").partially { case Foo(_, 0) => Allow.because("reason") } val engine: IO[RulesEngineIO[Foo]] = - RulesEngine[IO] + RulesEngine .withRules(allowYEqZero) - .denyAllNotAllowed + .liftK[IO] + .denyAllNotAllowed[IO] val result: IO[Json] = engine @@ -41,7 +41,7 @@ class JsonReportEncoderSpec extends munit.CatsEffectSuite { "type" : "Allowed", "evaluatedRules" : [ { - "rule" : { + "ruleInfo" : { "name" : "Check Y value", "fullDescription" : "Check Y value" }, diff --git a/modules/generic/README.md b/modules/generic/README.md index 32f437d..a4ae549 100644 --- a/modules/generic/README.md +++ b/modules/generic/README.md @@ -1 +1,23 @@ # Erules Generic + +**Sbt** +```sbt + libraryDependencies += "com.github.geirolz" %% "erules-core" % "0.0.9" + libraryDependencies += "com.github.geirolz" %% "erules-generic" % "0.0.9" +``` + +### Usage + +```scala +import cats.Id +import erules.Rule +import erules.PureRule +import erules.RuleVerdict +import erules.generic.implicits.* + +case class Person(name: String, age: Int) + +Rule.pure[Int]("Check age") + .const(RuleVerdict.Allow.withoutReasons) + .contramapTarget[Person](_.age) +``` \ No newline at end of file diff --git a/modules/generic/docs/README.md b/modules/generic/docs/README.md index 32f437d..e1282fd 100644 --- a/modules/generic/docs/README.md +++ b/modules/generic/docs/README.md @@ -1 +1,23 @@ # Erules Generic + +**Sbt** +```sbt + libraryDependencies += "com.github.geirolz" %% "erules-core" % "@VERSION@" + libraryDependencies += "com.github.geirolz" %% "erules-generic" % "@VERSION@" +``` + +### Usage + +```scala +import cats.Id +import erules.Rule +import erules.PureRule +import erules.RuleVerdict +import erules.generic.implicits.* + +case class Person(name: String, age: Int) + +Rule.pure[Int]("Check age") + .const(RuleVerdict.Allow.withoutReasons) + .contramapTarget[Person](_.age) +``` \ No newline at end of file diff --git a/modules/generic/src/main/scala-2/erules/generic/RuleMacros.scala b/modules/generic/src/main/scala-2/erules/generic/RuleMacros.scala index 882578b..b67ccc7 100644 --- a/modules/generic/src/main/scala-2/erules/generic/RuleMacros.scala +++ b/modules/generic/src/main/scala-2/erules/generic/RuleMacros.scala @@ -1,6 +1,6 @@ package erules.generic -import erules.core.Rule +import erules.Rule import scala.annotation.{tailrec, unused} import scala.reflect.macros.blackbox @@ -17,8 +17,8 @@ private[generic] trait RuleMacros { * case class Bar(test: Test) * case class Test(value: Int) * - * val rule: Rule[Int] = Rule("RULE").const(RuleVerdict.Ignore.withoutReasons) - * val fooRule: Rule[Foo] = rule.contramapTarget[Foo](_.bar.test.value) + * val rule: Rule[F, Int] = Rule("RULE").const(RuleVerdict.Ignore.withoutReasons) + * val fooRule: Rule[F, Foo] = rule.contramapTarget[Foo](_.bar.test.value) * * fooRule.targetInfo * scala> val res0: Option[String] = Some(bar.test.value) diff --git a/modules/generic/src/main/scala-3/erules/generic/RuleMacros.scala b/modules/generic/src/main/scala-3/erules/generic/RuleMacros.scala index 8160ad2..fda47aa 100644 --- a/modules/generic/src/main/scala-3/erules/generic/RuleMacros.scala +++ b/modules/generic/src/main/scala-3/erules/generic/RuleMacros.scala @@ -1,6 +1,6 @@ package erules.generic -import erules.core.Rule +import erules.Rule import scala.annotation.tailrec diff --git a/modules/generic/src/test/scala-2/erules/generic/RuleMacrosTest.scala b/modules/generic/src/test/scala-2/erules/generic/RuleMacrosTest.scala index 050ae9a..a47b0c1 100644 --- a/modules/generic/src/test/scala-2/erules/generic/RuleMacrosTest.scala +++ b/modules/generic/src/test/scala-2/erules/generic/RuleMacrosTest.scala @@ -1,7 +1,6 @@ package erules.generic -import cats.Id -import erules.core.{PureRule, Rule, RuleVerdict} +import erules.{PureRule, Rule, RuleVerdict} import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers @@ -15,7 +14,7 @@ class RuleMacrosTest extends AnyFunSuite with Matchers { case class Bar(test: Test) case class Test(value: Int) - val rule: PureRule[Int] = Rule("RULE").const[Id, Int](RuleVerdict.Ignore.withoutReasons) + val rule: PureRule[Int] = Rule("RULE").const(RuleVerdict.Ignore.withoutReasons) val fooRule: PureRule[Foo] = rule.contramapTarget[Foo](_.bar.test.value) fooRule.targetInfo shouldBe Some("bar.test.value") diff --git a/modules/generic/src/test/scala-3/erules/generic/RuleMacrosTest.scala b/modules/generic/src/test/scala-3/erules/generic/RuleMacrosTest.scala index 050ae9a..8456b14 100644 --- a/modules/generic/src/test/scala-3/erules/generic/RuleMacrosTest.scala +++ b/modules/generic/src/test/scala-3/erules/generic/RuleMacrosTest.scala @@ -1,7 +1,7 @@ package erules.generic import cats.Id -import erules.core.{PureRule, Rule, RuleVerdict} +import erules.{PureRule, Rule, RuleVerdict} import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers @@ -15,7 +15,7 @@ class RuleMacrosTest extends AnyFunSuite with Matchers { case class Bar(test: Test) case class Test(value: Int) - val rule: PureRule[Int] = Rule("RULE").const[Id, Int](RuleVerdict.Ignore.withoutReasons) + val rule: PureRule[Int] = Rule("RULE").const(RuleVerdict.Ignore.withoutReasons) val fooRule: PureRule[Foo] = rule.contramapTarget[Foo](_.bar.test.value) fooRule.targetInfo shouldBe Some("bar.test.value") @@ -29,7 +29,7 @@ class RuleMacrosTest extends AnyFunSuite with Matchers { case class Bar(t: Option[Test]) case class Test(value: Int) - val rule: PureRule[Int] = Rule("RULE").const[Id, Int](RuleVerdict.Ignore.withoutReasons) + val rule: PureRule[Int] = Rule("RULE").const(RuleVerdict.Ignore.withoutReasons) rule.contramapTarget[Foo](_.b.flatMap(_.t.map(_.value)).get) """ shouldNot compile diff --git a/modules/scalatest/README.md b/modules/scalatest/README.md index f8aa133..b9a26cb 100644 --- a/modules/scalatest/README.md +++ b/modules/scalatest/README.md @@ -8,7 +8,7 @@ Using scalatest we can easily test our engine importing the `erules-scalatest` m #### Matchers ```scala -import erules.core.* +import erules.* import erules.testing.scaltest.* import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers @@ -20,7 +20,7 @@ class MyTest extends AnyFunSuite test("testing engine verdict - denied"){ - val verdict: RuleResultsInterpreterVerdict[String] = ??? + val verdict: RuleResultsInterpreterVerdict = ??? verdict shouldBe denied verdict should not be allowed @@ -45,7 +45,7 @@ to support cats `IO` monad. #### Matchers ```scala -import erules.core.* +import erules.* import erules.testing.scaltest.* import org.scalatest.funsuite.AsyncFunSuite import org.scalatest.matchers.should.Matchers @@ -60,10 +60,10 @@ class MyTest extends AsyncFunSuite test("testing rule result") { val rule: Rule[IO, String] = ??? - val result: IO[RuleResult.Free[String]] = rule.eval("FOO") + val result: IO[RuleResult.Unbiased] = rule.eval("FOO") result.assertingIgnoringTimes( - _ shouldBe RuleResult.const("Allow all", RuleVerdict.Allow.withoutReasons) + _ shouldBe RuleResult.forRuleName("Allow all").succeeded(RuleVerdict.Allow.withoutReasons) ) } } diff --git a/modules/scalatest/docs/README.md b/modules/scalatest/docs/README.md index ae7cf64..3f4b6e8 100644 --- a/modules/scalatest/docs/README.md +++ b/modules/scalatest/docs/README.md @@ -8,7 +8,7 @@ Using scalatest we can easily test our engine importing the `erules-scalatest` m #### Matchers ```scala mdoc:nest:to-string -import erules.core.* +import erules.* import erules.testing.scaltest.* import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers @@ -20,7 +20,7 @@ class MyTest extends AnyFunSuite test("testing engine verdict - denied"){ - val verdict: RuleResultsInterpreterVerdict[String] = ??? + val verdict: RuleResultsInterpreterVerdict = ??? verdict shouldBe denied verdict should not be allowed @@ -45,7 +45,7 @@ to support cats `IO` monad. #### Matchers ```scala mdoc:nest:to-string -import erules.core.* +import erules.* import erules.testing.scaltest.* import org.scalatest.funsuite.AsyncFunSuite import org.scalatest.matchers.should.Matchers @@ -60,10 +60,10 @@ class MyTest extends AsyncFunSuite test("testing rule result") { val rule: Rule[IO, String] = ??? - val result: IO[RuleResult.Free[String]] = rule.eval("FOO") + val result: IO[RuleResult.Unbiased] = rule.eval("FOO") result.assertingIgnoringTimes( - _ shouldBe RuleResult.const("Allow all", RuleVerdict.Allow.withoutReasons) + _ shouldBe RuleResult.forRuleName("Allow all").succeeded(RuleVerdict.Allow.withoutReasons) ) } } diff --git a/modules/scalatest/src/main/scala/erules/testing/scaltest/ErulesAsyncAssertingSyntax.scala b/modules/scalatest/src/main/scala/erules/testing/scaltest/ErulesAsyncAssertingSyntax.scala index 12873fc..853d6c5 100644 --- a/modules/scalatest/src/main/scala/erules/testing/scaltest/ErulesAsyncAssertingSyntax.scala +++ b/modules/scalatest/src/main/scala/erules/testing/scaltest/ErulesAsyncAssertingSyntax.scala @@ -1,7 +1,7 @@ package erules.testing.scaltest import cats.Functor -import erules.core.{EngineResult, RuleResult, RuleVerdict} +import erules.{EngineResult, RuleResult, RuleVerdict} import org.scalatest.Assertion trait ErulesAsyncAssertingSyntax { @@ -9,10 +9,10 @@ trait ErulesAsyncAssertingSyntax { import cats.implicits.* implicit class RuleResultAssertingOps[F[_]: Functor, -T, +V <: RuleVerdict]( - fa: F[RuleResult[T, V]] + fa: F[RuleResult[V]] ) { - def assertingIgnoringTimes(f: RuleResult[T, V] => Assertion): F[Assertion] = + def assertingIgnoringTimes(f: RuleResult[V] => Assertion): F[Assertion] = fa.map(a => f(a.drainExecutionTime)) } diff --git a/modules/scalatest/src/main/scala/erules/testing/scaltest/ErulesMatchers.scala b/modules/scalatest/src/main/scala/erules/testing/scaltest/ErulesMatchers.scala index 848861a..ff7fcf8 100644 --- a/modules/scalatest/src/main/scala/erules/testing/scaltest/ErulesMatchers.scala +++ b/modules/scalatest/src/main/scala/erules/testing/scaltest/ErulesMatchers.scala @@ -1,6 +1,6 @@ package erules.testing.scaltest -import erules.core.{RuleResult, RuleResultsInterpreterVerdict, RuleVerdict} +import erules.{RuleResult, RuleResultsInterpreterVerdict, RuleVerdict} import org.scalatest.matchers.{BeMatcher, MatchResult, Matcher} import scala.concurrent.duration.{FiniteDuration, MILLISECONDS} @@ -11,8 +11,8 @@ trait ErulesMatchers trait ErulesRuleTypedEvaluatedMatchers { - def executedInMax[T](maxDuration: FiniteDuration): Matcher[RuleResult.Free[T]] = - (actual: RuleResult.Free[T]) => { + def executedInMax(maxDuration: FiniteDuration): Matcher[RuleResult.Unbiased] = + (actual: RuleResult.Unbiased) => { val actualET = actual.executionTime.getOrElse(FiniteDuration(0, MILLISECONDS)).toMillis val expectedET = maxDuration.toMillis @@ -29,16 +29,16 @@ trait ErulesRuleTypedEvaluatedMatchers { trait ErulesRuleResultsInterpreterVerdictMatchers { - val allowed: BeMatcher[RuleResultsInterpreterVerdict[Nothing]] = - (left: RuleResultsInterpreterVerdict[Nothing]) => + def allowed: BeMatcher[RuleResultsInterpreterVerdict] = + (left: RuleResultsInterpreterVerdict) => MatchResult( matches = left.isAllowed, rawFailureMessage = s"Expected to be Allowed but got ${left.typeName}", rawNegatedFailureMessage = s"Expected to be Denied but got ${left.typeName}" ) - val denied: BeMatcher[RuleResultsInterpreterVerdict[Nothing]] = - (left: RuleResultsInterpreterVerdict[Nothing]) => + def denied: BeMatcher[RuleResultsInterpreterVerdict] = + (left: RuleResultsInterpreterVerdict) => MatchResult( matches = left.isDenied, rawFailureMessage = s"Expected to be Denied but got ${left.typeName}", diff --git a/modules/scalatest/src/main/scala/erules/testing/scaltest/ReportValues.scala b/modules/scalatest/src/main/scala/erules/testing/scaltest/ReportValues.scala index 784ff72..625f0d9 100644 --- a/modules/scalatest/src/main/scala/erules/testing/scaltest/ReportValues.scala +++ b/modules/scalatest/src/main/scala/erules/testing/scaltest/ReportValues.scala @@ -1,7 +1,7 @@ package erules.testing.scaltest import cats.effect.Async -import erules.core.report.ReportEncoder +import erules.report.ReportEncoder trait ReportValues { diff --git a/modules/scalatest/src/test/scala/erules/testing/scalatest/TestErulesAsyncAssertingSyntax.scala b/modules/scalatest/src/test/scala/erules/testing/scalatest/TestErulesAsyncAssertingSyntax.scala index feda9e1..2e0dd77 100644 --- a/modules/scalatest/src/test/scala/erules/testing/scalatest/TestErulesAsyncAssertingSyntax.scala +++ b/modules/scalatest/src/test/scala/erules/testing/scalatest/TestErulesAsyncAssertingSyntax.scala @@ -3,8 +3,8 @@ package erules.testing.scalatest import cats.data.NonEmptyList import cats.effect.IO import cats.effect.testing.scalatest.AsyncIOSpec -import erules.core.{EngineResult, RuleResult, RuleVerdict} -import erules.core.RuleResultsInterpreterVerdict.Allowed +import erules.{EngineResult, RuleResult, RuleVerdict} +import erules.RuleResultsInterpreterVerdict.Allowed import erules.testing.scaltest.ErulesAsyncAssertingSyntax import org.scalatest.funsuite.AsyncFunSuite import org.scalatest.matchers.should.Matchers @@ -22,10 +22,10 @@ class TestErulesAsyncAssertingSyntax ) { IO( RuleResult - .const("Allow all", RuleVerdict.Allow.withoutReasons) - .copy(executionTime = Some(1.seconds)) + .forRuleName("Allow all") + .succeeded(RuleVerdict.Allow.withoutReasons, Some(1.seconds)) ).assertingIgnoringTimes( - _ shouldBe RuleResult.const("Allow all", RuleVerdict.Allow.withoutReasons) + _ shouldBe RuleResult.forRuleName("Allow all").succeeded(RuleVerdict.Allow.withoutReasons) ) } @@ -38,11 +38,11 @@ class TestErulesAsyncAssertingSyntax verdict = Allowed( NonEmptyList.of( RuleResult - .const("Allow all 1", RuleVerdict.Allow.withoutReasons) - .copy(executionTime = Some(1.seconds)), + .forRuleName("Allow all 1") + .succeeded(RuleVerdict.Allow.withoutReasons, Some(1.seconds)), RuleResult - .const("Allow all 2", RuleVerdict.Allow.withoutReasons) - .copy(executionTime = Some(2.seconds)) + .forRuleName("Allow all 2") + .succeeded(RuleVerdict.Allow.withoutReasons, Some(2.seconds)) ) ) ) @@ -52,9 +52,11 @@ class TestErulesAsyncAssertingSyntax verdict = Allowed( NonEmptyList.of( RuleResult - .const("Allow all 1", RuleVerdict.Allow.withoutReasons), + .forRuleName("Allow all 1") + .succeeded(RuleVerdict.Allow.withoutReasons), RuleResult - .const("Allow all 2", RuleVerdict.Allow.withoutReasons) + .forRuleName("Allow all 2") + .succeeded(RuleVerdict.Allow.withoutReasons) ) ) ) diff --git a/modules/scalatest/src/test/scala/erules/testing/scalatest/TestErulesMatchers.scala b/modules/scalatest/src/test/scala/erules/testing/scalatest/TestErulesMatchers.scala index 54e4071..adf527e 100644 --- a/modules/scalatest/src/test/scala/erules/testing/scalatest/TestErulesMatchers.scala +++ b/modules/scalatest/src/test/scala/erules/testing/scalatest/TestErulesMatchers.scala @@ -2,10 +2,10 @@ package erules.testing.scalatest import cats.data.NonEmptyList import cats.effect.testing.scalatest.AsyncIOSpec -import erules.core.RuleResult -import erules.core.RuleResultsInterpreterVerdict.{Allowed, Denied} -import erules.core.RuleVerdict.{Allow, Deny, Ignore} +import erules.RuleResultsInterpreterVerdict.{Allowed, Denied} +import erules.RuleVerdict.{Allow, Deny, Ignore} import erules.testing.scaltest.ErulesMatchers +import erules.RuleResult import org.scalatest.funsuite.AsyncFunSuite import org.scalatest.matchers.should.Matchers @@ -13,9 +13,9 @@ class TestErulesMatchers extends AsyncFunSuite with AsyncIOSpec with ErulesMatch test("RuleResultsInterpreterVerdict should be allowed and should not be denied") { - val verdict: Allowed[Nothing] = Allowed( + val verdict: Allowed = Allowed( NonEmptyList.of( - RuleResult.const("Foo", Allow.withoutReasons) + RuleResult.forRuleName("Foo").succeeded(Allow.withoutReasons) ) ) @@ -25,9 +25,9 @@ class TestErulesMatchers extends AsyncFunSuite with AsyncIOSpec with ErulesMatch test("RuleResultsInterpreterVerdict should be denied and should not be allowed") { - val verdict: Denied[Nothing] = Denied( + val verdict: Denied = Denied( NonEmptyList.of( - RuleResult.const("Foo", Deny.withoutReasons) + RuleResult.forRuleName("Foo").succeeded(Deny.withoutReasons) ) ) diff --git a/project/ProjectDependencies.scala b/project/ProjectDependencies.scala index 42fbf56..6401878 100644 --- a/project/ProjectDependencies.scala +++ b/project/ProjectDependencies.scala @@ -32,8 +32,8 @@ object ProjectDependencies { "io.circe" %% "circe-core" % circeVersion, "io.circe" %% "circe-generic" % circeVersion, // test - "io.circe" %% "circe-parser" % circeVersion, - "io.circe" %% "circe-literal" % circeVersion + "io.circe" %% "circe-parser" % circeVersion % Test, + "io.circe" %% "circe-literal" % circeVersion % Test ) }