From a7f41ad846a47ef17a8e65d35ce6ec496be60e2c Mon Sep 17 00:00:00 2001 From: Jean-Luc CANELA Date: Fri, 30 Sep 2022 01:05:24 +0200 Subject: [PATCH 1/8] adding condition --- .../degoes/afd/ruleengine/07-graduation.scala | 368 +++++++++++++++++- 1 file changed, 367 insertions(+), 1 deletion(-) diff --git a/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala b/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala index 33eb50c..4b62538 100644 --- a/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala +++ b/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala @@ -9,9 +9,375 @@ */ package net.degoes.afd.ruleengine +import zio._ +import scala.annotation._ +import scala.language.implicitConversions /** * Use your rule engine to build out an application of your choosing. You should * have a main function that executes a sample rule set on some sample data, and * prints out or executes the actions produced. */ -object graduation {} +object graduation { + + final case class Rule[-In, +Out](condition: Condition[In], action: Action[In, Out]) + + final case class RuleSet[-In, +Out](rules: Vector[Rule[In, Out]]) + + object RuleSet { + val empty: RuleSet[Any, Nothing] = RuleSet(Vector.empty) + } + + + sealed trait Numeric[A] + object Numeric { + implicit case object ByteIsNumeric extends Numeric[Byte] + implicit case object CharIsNumeric extends Numeric[Char] + implicit case object IntIsNumeric extends Numeric[Int] + implicit case object LongIsNumeric extends Numeric[Long] + implicit case object FloatIsNumeric extends Numeric[Float] + implicit case object DoubleIsNumeric extends Numeric[Double] + +} + + /** + * A type class that represents the supported types for fact values. + */ + @implicitNotFound("The type ${A} is not supported as a fact type and cannot be used for this method.") + sealed trait PrimitiveType[A] + object PrimitiveType { + def apply[T](implicit factType: PrimitiveType[T]): PrimitiveType[T] = factType + + implicit case object Int extends PrimitiveType[scala.Int] + implicit case object Long extends PrimitiveType[scala.Long] + implicit case object String extends PrimitiveType[java.lang.String] + implicit case object Double extends PrimitiveType[scala.Double] + implicit case object Byte extends PrimitiveType[scala.Byte] + implicit case object Char extends PrimitiveType[scala.Char] + implicit case object Float extends PrimitiveType[scala.Float] + implicit case object Boolean extends PrimitiveType[scala.Boolean] + implicit case object Instant extends PrimitiveType[java.time.Instant] + } + + sealed trait Expr[-In, +Out] { self => + final def +[In1 <: In, Out1 >: Out](that: Expr[In1, Out1])(implicit tag: Numeric[Out1]): Expr[In1, Out1] = + Expr.BinaryNumericOp(self.widen, that, Expr.NumericBinOpType.Add, tag) + + final def -[In1 <: In, Out1 >: Out](that: Expr[In1, Out1])(implicit tag: Numeric[Out1]): Expr[In1, Out1] = + Expr.BinaryNumericOp(self.widen, that, Expr.NumericBinOpType.Subtract, tag) + + final def *[In1 <: In, Out1 >: Out](that: Expr[In1, Out1])(implicit tag: Numeric[Out1]): Expr[In1, Out1] = + Expr.BinaryNumericOp(self.widen, that, Expr.NumericBinOpType.Multiply, tag) + + final def /[In1 <: In, Out1 >: Out](that: Expr[In1, Out1])(implicit tag: Numeric[Out1]): Expr[In1, Out1] = + Expr.BinaryNumericOp(self.widen, that, Expr.NumericBinOpType.Divide, tag) + + final def %[In1 <: In, Out1 >: Out](that: Expr[In1, Out1])(implicit tag: Numeric[Out1]): Expr[In1, Out1] = + Expr.BinaryNumericOp(self.widen, that, Expr.NumericBinOpType.Modulo, tag) + + final def &&[In1 <: In](that: Expr[In1, Boolean])(implicit ev: Out <:< Boolean): Expr[In1, Boolean] = + Expr.And(self.widen, that) + + final def ||[In1 <: In](that: Expr[In1, Boolean])(implicit ev: Out <:< Boolean): Expr[In1, Boolean] = + Expr.Or(self.widen, that) + + final def !=[In1 <: In, Out1 >: Out](that: Expr[In1, Out1]): Expr[In1, Boolean] = + !(self === that) + + final def <[In1 <: In, Out1 >: Out](that: Expr[In1, Out1]): Expr[In1, Boolean] = + Expr.LessThan(self, that) + + final def <=[In1 <: In, Out1 >: Out](that: Expr[In1, Out1]): Expr[In1, Boolean] = + Expr.LessThan(self, that) || (self === that) + + final def >[In1 <: In, Out1 >: Out](that: Expr[In1, Out1]): Expr[In1, Boolean] = + !(self <= that) + + final def >=[In1 <: In, Out1 >: Out](that: Expr[In1, Out1]): Expr[In1, Boolean] = + !(self < that) + + final def ===[In1 <: In, Out1 >: Out](that: Expr[In1, Out1]): Expr[In1, Boolean] = + Expr.EqualTo(self, that) + + final def >>>[Out2](that: Expr[Out, Out2]): Expr[In, Out2] = + Expr.Pipe(self, that) + + final def unary_!(implicit ev: Out <:< Boolean): Expr[In, Boolean] = + Expr.Not(self.widen) + + final def widen[Out2](implicit ev: Out <:< Out2): Expr[In, Out2] = self.asInstanceOf[Expr[In, Out2]] + + } + + object Expr { + + final case class Constant[Out](value: Out, tag: PrimitiveType[Out]) extends Expr[Any, Out] + final case class And[In](left: Expr[In, Boolean], right: Expr[In, Boolean]) extends Expr[In, Boolean] + final case class Or[In](left: Expr[In, Boolean], right: Expr[In, Boolean]) extends Expr[In, Boolean] + final case class Not[In](condition: Expr[In, Boolean]) extends Expr[In, Boolean] + final case class EqualTo[In, Out](lhs: Expr[In, Out], rhs: Expr[In, Out]) extends Expr[In, Boolean] + final case class LessThan[In, Out](lhs: Expr[In, Out], rhs: Expr[In, Out]) extends Expr[In, Boolean] + final case class Input[K <: Singleton with String, V](factDef: FactDefinition.KeyValue[K, V]) + extends Expr[(K, V), V] + final case class Pipe[In, Out1, Out2](left: Expr[In, Out1], right: Expr[Out1, Out2]) extends Expr[In, Out2] + final case class BinaryNumericOp[In, Out]( + lhs: Expr[In, Out], + rhs: Expr[In, Out], + op: NumericBinOpType, + tag: Numeric[Out] + ) extends Expr[In, Out] + + sealed trait NumericBinOpType + object NumericBinOpType { + case object Add extends NumericBinOpType + case object Subtract extends NumericBinOpType + case object Multiply extends NumericBinOpType + case object Divide extends NumericBinOpType + case object Modulo extends NumericBinOpType + } + + implicit def apply[Out](out: Out)(implicit tag: PrimitiveType[Out]): Expr[Any, Out] = Constant(out, tag) + + def input[K <: Singleton with String, V](factDef: FactDefinition.KeyValue[K, V]): Expr[(K, V), V] = + Input(factDef) + + } + + sealed trait FactDefinition[KeyValue] { self => + type Key <: Singleton with String + type Value + + def name: Key + + def paramType: PrimitiveType[Value] + + // Added + def get: Expr[(Key, Value), Value] = Expr.input(self.asInstanceOf[FactDefinition.KeyValue[Key, Value]]) + + override final def toString(): String = s"FactDefinition($name, $paramType)" + } + + object FactDefinition { + + type KeyValue[K <: Singleton with String, V] = FactDefinition[(K, V)] { type Key = K; type Value = V } + + def apply[N <: Singleton with String, T](name0: N)(implicit paramType0: PrimitiveType[T]): KeyValue[N, T] = + new FactDefinition[(N, T)] { + type Key = N + type Value = T + def name: N = name0 + def paramType: PrimitiveType[T] = paramType0 + } + + def boolean[N <: Singleton with String](name0: N): KeyValue[N, Boolean] = FactDefinition[N, Boolean](name0) + + def byte[N <: Singleton with String](name0: N): KeyValue[N, Byte] = FactDefinition[N, Byte](name0) + + def char[N <: Singleton with String](name0: N): KeyValue[N, Char] = FactDefinition[N, Char](name0) + + def int[N <: Singleton with String](name0: N): KeyValue[N, Int] = FactDefinition[N, Int](name0) + + def long[N <: Singleton with String](name0: N): KeyValue[N, Long] = FactDefinition[N, Long](name0) + + def float[N <: Singleton with String](name0: N): KeyValue[N, Float] = FactDefinition[N, Float](name0) + + def double[N <: Singleton with String](name0: N): KeyValue[N, Double] = FactDefinition[N, Double](name0) + + def string[N <: Singleton with String](name0: N): KeyValue[N, String] = FactDefinition[N, String](name0) + + def instant[N <: Singleton with String](name0: N): KeyValue[N, java.time.Instant] = + FactDefinition[N, java.time.Instant](name0) + + } + + final case class RuleEngine[-In, +Out](update: In => Option[List[Out]]) { self => + def contramap[In2](f: In2 => In): RuleEngine[In2, Out] = + RuleEngine(in => self.update(f(in))) + + def orElse[In1 <: In, Out1 >: Out](that: RuleEngine[In1, Out1]): RuleEngine[In1, Out1] = + RuleEngine(in => self.update(in) orElse that.update(in)) + + def updateWith[Out1 >: Out](in: In)(defaultOut: Out1, combine: (Out1, Out1) => Out1): Out1 = + self.update(in) match { + case None => defaultOut + case Some(outs) => + outs.reduceOption(combine).getOrElse(defaultOut) + } + } + + /** + * Contains a collection of facts, whose structure is described by a phantom + * type parameter. + */ + sealed abstract case class Facts[Types] private (private val data: Map[FactDefinition[_], Any]) { + def get[Key <: Singleton with String, Value: PrimitiveType](pd: FactDefinition[(Key, Value)])(implicit + subset: Types <:< (Key, Value) + ): Value = + data(pd).asInstanceOf[Value] + + /** + * Returns a new facts collection with the specified fact added. + */ + def add[Key <: Singleton with String, Value: PrimitiveType]( + pd: FactDefinition[(Key, Value)], + value: Value + ): Facts[Types & (Key, Value)] = + new Facts[Types & (Key, Value)](data + (pd -> value)) {} + + private def add[Key <: Singleton with String, Value: PrimitiveType]( + name: Key, + value: Value + ): Facts[Types & (Key, Value)] = + new Facts[Types & (Key, Value)](data + (FactDefinition[Key, Value](name) -> value)) {} + + object unsafe { + def get(pd: FactDefinition[_])(implicit unsafe: Unsafe): Option[Any] = data.get(pd) + } + } + object Facts { + + /** + * An empty facts collection. + */ + val empty: Facts[Any] = new Facts[Any](Map.empty) {} + + def apply[Key <: Singleton with String, Value: PrimitiveType](key: Key, value: Value): Facts[(Key, Value)] = + empty.add(key, value) + + def apply[ + Key1 <: Singleton with String, + Value1: PrimitiveType, + Key2 <: Singleton with String, + Value2: PrimitiveType + ]( + tuple1: (Key1, Value1), + tuple2: (Key2, Value2) + ): Facts[(Key1, Value1) & (Key2, Value2)] = + empty.add[Key1, Value1](tuple1._1, tuple1._2).add[Key2, Value2](tuple2._1, tuple2._2) + + def apply[ + Key1 <: Singleton with String, + Value1: PrimitiveType, + Key2 <: Singleton with String, + Value2: PrimitiveType, + Key3 <: Singleton with String, + Value3: PrimitiveType + ]( + tuple1: (Key1, Value1), + tuple2: (Key2, Value2), + tuple3: (Key3, Value3) + ): Facts[(Key1, Value1) & (Key2, Value2) & (Key3, Value3)] = + empty + .add[Key1, Value1](tuple1._1, tuple1._2) + .add[Key2, Value2](tuple2._1, tuple2._2) + .add[Key3, Value3](tuple3._1, tuple3._2) + + def apply[ + Key1 <: Singleton with String, + Value1: PrimitiveType, + Key2 <: Singleton with String, + Value2: PrimitiveType, + Key3 <: Singleton with String, + Value3: PrimitiveType, + Key4 <: Singleton with String, + Value4: PrimitiveType + ]( + tuple1: (Key1, Value1), + tuple2: (Key2, Value2), + tuple3: (Key3, Value3), + tuple4: (Key4, Value4) + ): Facts[(Key1, Value1) & (Key2, Value2) & (Key3, Value3) & (Key4, Value4)] = + empty + .add[Key1, Value1](tuple1._1, tuple1._2) + .add[Key2, Value2](tuple2._1, tuple2._2) + .add[Key3, Value3](tuple3._1, tuple3._2) + .add[Key4, Value4](tuple4._1, tuple4._2) + + def apply[ + Key1 <: Singleton with String, + Value1: PrimitiveType, + Key2 <: Singleton with String, + Value2: PrimitiveType, + Key3 <: Singleton with String, + Value3: PrimitiveType, + Key4 <: Singleton with String, + Value4: PrimitiveType, + Key5 <: Singleton with String, + Value5: PrimitiveType + ]( + tuple1: (Key1, Value1), + tuple2: (Key2, Value2), + tuple3: (Key3, Value3), + tuple4: (Key4, Value4), + tuple5: (Key5, Value5) + ): Facts[(Key1, Value1) & (Key2, Value2) & (Key3, Value3) & (Key4, Value4) & (Key5, Value5)] = + empty + .add[Key1, Value1](tuple1._1, tuple1._2) + .add[Key2, Value2](tuple2._1, tuple2._2) + .add[Key3, Value3](tuple3._1, tuple3._2) + .add[Key4, Value4](tuple4._1, tuple4._2) + .add[Key5, Value5](tuple5._1, tuple5._2) + } + + final case class Condition[-In] (expr: Expr[In, Boolean]) { self => + def &&[In1 <: In](that: Condition[In1]): Condition[In1] = + Condition(self.expr && that.expr) + + def ||[In1 <: In](that: Condition[In1]): Condition[In1] = + Condition(self.expr && that.expr) + + def unary_! : Condition[In] = Condition(!expr) + } + + object Condition { + val always: Condition[Any] = constant(true) + val never: Condition[Any] = constant(false) + + def constant[In](value: Boolean): Condition[In] = Condition(Expr(value)) + + + } + + sealed trait Action[-In, +Out] { self => + def ++[In1 <: In, Out1 >: Out](that: Action[In1, Out1]): Action[In1, Out1] = + Action.Concat(self, that) + + def >>>[Out2](that: Action[Out, Out2]): Action[In, Out2] = + Action.Pipe(self, that) + } + object Action { + final case class Concat[In, Out](left: Action[In, Out], right: Action[In, Out]) extends Action[In, Out] + final case class Pipe[In, Out1, Out2](left: Action[In, Out1], right: Action[Out1, Out2]) extends Action[In, Out2] + final case class FromExpr[In, Out](expr: Expr[In, Out]) extends Action[In, Out] + + def fromExpr[In, Out](expr: Expr[In, Out]): Action[In, Out] = FromExpr(expr) + + def constant[Out](out: Out)(implicit tag: PrimitiveType[Out]): Action[Any, Out] = fromExpr(Expr(out)) + } + + object loyalty { + import net.degoes.afd.examples.loyalty._ + import net.degoes.afd.examples.loyalty.LoyaltyTier._ + + object FlightBooking { + val id = FactDefinition.string("id") + val customer = FactDefinition.string("custoner") // FIXME: Support nested data + val flight = FactDefinition.string("flight") // FIXME: Suppor nested data + val price = FactDefinition.double("price") + val status = FactDefinition.string("status") + } + + object FlightBookingStatus { + val Confirmed = Expr("Confirmed") + val Cancelled = Expr("Cancelled") + val Pending = Expr("Pending") + } + + val statusCondition = + Condition(FlightBooking.status.get === FlightBookingStatus.Confirmed) + val priceCondition = Condition(FlightBooking.price.get > 1000.0) + + val exampleCondition = statusCondition && priceCondition + + } + +} From 0c22bd9c989420c72a8c9841b02bb445e6b046da Mon Sep 17 00:00:00 2001 From: Jean-Luc CANELA Date: Fri, 30 Sep 2022 01:57:53 +0200 Subject: [PATCH 2/8] adding action with primitive type --- .../degoes/afd/ruleengine/07-graduation.scala | 63 +++++++++++++++---- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala b/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala index 4b62538..13c2fe6 100644 --- a/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala +++ b/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala @@ -19,15 +19,6 @@ import scala.language.implicitConversions */ object graduation { - final case class Rule[-In, +Out](condition: Condition[In], action: Action[In, Out]) - - final case class RuleSet[-In, +Out](rules: Vector[Rule[In, Out]]) - - object RuleSet { - val empty: RuleSet[Any, Nothing] = RuleSet(Vector.empty) - } - - sealed trait Numeric[A] object Numeric { implicit case object ByteIsNumeric extends Numeric[Byte] @@ -110,6 +101,9 @@ object graduation { object Expr { + final case class Fact[In, K <: Singleton with String, V]( + factDef: FactDefinition.KeyValue[K, V], + value: Expr[In, V]) extends Expr[In, Facts[(K, V)]] final case class Constant[Out](value: Out, tag: PrimitiveType[Out]) extends Expr[Any, Out] final case class And[In](left: Expr[In, Boolean], right: Expr[In, Boolean]) extends Expr[In, Boolean] final case class Or[In](left: Expr[In, Boolean], right: Expr[In, Boolean]) extends Expr[In, Boolean] @@ -140,6 +134,7 @@ object graduation { def input[K <: Singleton with String, V](factDef: FactDefinition.KeyValue[K, V]): Expr[(K, V), V] = Input(factDef) + def fact[In, K <: Singleton with String, V](factDef: FactDefinition.KeyValue[K, V], value: Expr[In, V]) = Fact(factDef, value) } sealed trait FactDefinition[KeyValue] { self => @@ -153,6 +148,11 @@ object graduation { // Added def get: Expr[(Key, Value), Value] = Expr.input(self.asInstanceOf[FactDefinition.KeyValue[Key, Value]]) + def set[In](value: Expr[In, Value]): Expr[In, Facts[(Key, Value)]] = + Expr.fact(self.asInstanceOf[FactDefinition.KeyValue[Key, Value]], value) + + def := [In](value: Expr[In, Value]) = set(value) + override final def toString(): String = s"FactDefinition($name, $paramType)" } @@ -189,14 +189,14 @@ object graduation { } - final case class RuleEngine[-In, +Out](update: In => Option[List[Out]]) { self => - def contramap[In2](f: In2 => In): RuleEngine[In2, Out] = + final case class RuleEngine[-In, +Out](update: Facts[In] => Option[List[Out]]) { self => + def contramap[In2](f: Facts[In2] => Facts[In]): RuleEngine[In2, Out] = RuleEngine(in => self.update(f(in))) def orElse[In1 <: In, Out1 >: Out](that: RuleEngine[In1, Out1]): RuleEngine[In1, Out1] = RuleEngine(in => self.update(in) orElse that.update(in)) - def updateWith[Out1 >: Out](in: In)(defaultOut: Out1, combine: (Out1, Out1) => Out1): Out1 = + def updateWith[Out1 >: Out](in: Facts[In])(defaultOut: Out1, combine: (Out1, Out1) => Out1): Out1 = self.update(in) match { case None => defaultOut case Some(outs) => @@ -204,11 +204,46 @@ object graduation { } } + object RuleEngine { + val empty: RuleEngine[Any, Nothing] = RuleEngine(_ => None) + + def constant[Out](out: Out): RuleEngine[Any, Out] = fromFunction(_ => out) + + def fromFunction[In, Out](f: Facts[In] => Out): RuleEngine[In, Out] = RuleEngine(in => Some(List(f(in)))) + + def fromRuleSet[In, Out](ruleSet: RuleSet[In, Out]): RuleEngine[In, Out] = + RuleEngine(???) + } + + final case class Rule[-In, +Out](condition: Condition[In], action: Action[In, Out]) + + final case class RuleSet[-In, +Out](rules: Vector[Rule[In, Out]]) { self => + + def + [In1 <: In, Out1 >: Out](that: Rule[In1, Out1]): RuleSet[In1, Out1] = + RuleSet(self.rules :+ that) + + def ++[In1 <: In, Out1 >: Out](that: RuleSet[In1, Out1]): RuleSet[In1, Out1] = + RuleSet(self.rules ++ that.rules) + + def addRule[In1 <: In, Out1 >: Out](that: Rule[In1, Out1]): RuleSet[In1, Out1] = + self + that + } + + object RuleSet { + + def apply[In, Out](rule1: Rule[In, Out], rules: Rule[In, Out]*): RuleSet[In, Out] = + RuleSet(rule1 +: rules.toVector) + + val empty: RuleSet[Any, Nothing] = RuleSet(Vector.empty) + + } + + /** * Contains a collection of facts, whose structure is described by a phantom * type parameter. */ - sealed abstract case class Facts[Types] private (private val data: Map[FactDefinition[_], Any]) { + sealed abstract case class Facts[+Types] private (private val data: Map[FactDefinition[_], Any]) { def get[Key <: Singleton with String, Value: PrimitiveType](pd: FactDefinition[(Key, Value)])(implicit subset: Types <:< (Key, Value) ): Value = @@ -372,6 +407,8 @@ object graduation { val Pending = Expr("Pending") } + FlightBooking.price := FlightBooking.price.get + 1000.0 + val statusCondition = Condition(FlightBooking.status.get === FlightBookingStatus.Confirmed) val priceCondition = Condition(FlightBooking.price.get > 1000.0) From 5e75e2f33935ca8c11451c11e0fb13a17895485d Mon Sep 17 00:00:00 2001 From: Jean-Luc CANELA Date: Fri, 30 Sep 2022 02:15:01 +0200 Subject: [PATCH 3/8] adding set and combine facts operators --- .../net/degoes/afd/ruleengine/07-graduation.scala | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala b/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala index 13c2fe6..10d53a8 100644 --- a/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala +++ b/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala @@ -50,6 +50,10 @@ object graduation { } sealed trait Expr[-In, +Out] { self => + + final def ++[In1 <: In, Fields1, Fields2](that: Expr[In1, Facts[Fields2]])(implicit ev: Out <:< Facts[Fields1]): Expr[In1, Facts[Fields1 & Fields2]] = + Expr.CombineFacts(self.widen[Facts[Fields1]], that) + final def +[In1 <: In, Out1 >: Out](that: Expr[In1, Out1])(implicit tag: Numeric[Out1]): Expr[In1, Out1] = Expr.BinaryNumericOp(self.widen, that, Expr.NumericBinOpType.Add, tag) @@ -104,6 +108,10 @@ object graduation { final case class Fact[In, K <: Singleton with String, V]( factDef: FactDefinition.KeyValue[K, V], value: Expr[In, V]) extends Expr[In, Facts[(K, V)]] + final case class CombineFacts[In, V1, V2]( + left: Expr[In, Facts[V1]], + right: Expr[In, Facts[V2]] + ) extends Expr[In, Facts[V1 & V2]] final case class Constant[Out](value: Out, tag: PrimitiveType[Out]) extends Expr[Any, Out] final case class And[In](left: Expr[In, Boolean], right: Expr[In, Boolean]) extends Expr[In, Boolean] final case class Or[In](left: Expr[In, Boolean], right: Expr[In, Boolean]) extends Expr[In, Boolean] @@ -111,7 +119,7 @@ object graduation { final case class EqualTo[In, Out](lhs: Expr[In, Out], rhs: Expr[In, Out]) extends Expr[In, Boolean] final case class LessThan[In, Out](lhs: Expr[In, Out], rhs: Expr[In, Out]) extends Expr[In, Boolean] final case class Input[K <: Singleton with String, V](factDef: FactDefinition.KeyValue[K, V]) - extends Expr[(K, V), V] + extends Expr[(K, V), V] // TODO : split into READ & GET operators final case class Pipe[In, Out1, Out2](left: Expr[In, Out1], right: Expr[Out1, Out2]) extends Expr[In, Out2] final case class BinaryNumericOp[In, Out]( lhs: Expr[In, Out], @@ -244,6 +252,9 @@ object graduation { * type parameter. */ sealed abstract case class Facts[+Types] private (private val data: Map[FactDefinition[_], Any]) { + def ++ [Types2](that: Facts[Types2]): Facts[Types & Types2] = + new Facts[Types & Types2](data ++ that.data) {} + def get[Key <: Singleton with String, Value: PrimitiveType](pd: FactDefinition[(Key, Value)])(implicit subset: Types <:< (Key, Value) ): Value = @@ -407,7 +418,7 @@ object graduation { val Pending = Expr("Pending") } - FlightBooking.price := FlightBooking.price.get + 1000.0 + (FlightBooking.price := FlightBooking.price.get + 1000.0) ++ (FlightBooking.status := FlightBookingStatus.Pending) val statusCondition = Condition(FlightBooking.status.get === FlightBookingStatus.Confirmed) From 596ee1f30709f628e4b549911ccd04df20bca9bc Mon Sep 17 00:00:00 2001 From: Jean-Luc CANELA Date: Sat, 1 Oct 2022 16:52:20 +0200 Subject: [PATCH 4/8] adding nested data --- .../degoes/afd/ruleengine/07-graduation.scala | 518 ++++++++++++------ 1 file changed, 353 insertions(+), 165 deletions(-) diff --git a/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala b/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala index 10d53a8..e857bdc 100644 --- a/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala +++ b/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala @@ -12,6 +12,7 @@ package net.degoes.afd.ruleengine import zio._ import scala.annotation._ import scala.language.implicitConversions + /** * Use your rule engine to build out an application of your choosing. You should * have a main function that executes a sample rule set on some sample data, and @@ -19,16 +20,122 @@ import scala.language.implicitConversions */ object graduation { - sealed trait Numeric[A] + + sealed trait EngineType[A] + object EngineType { + final case class Primitive[A](primitiveType: PrimitiveType[A]) extends EngineType[A] + final case class Composite[Fields](factsType: FactsType[Fields]) extends EngineType[Facts[Fields]] + + def fromPrimitive[A](implicit primitiveType: PrimitiveType[A]): EngineType[A] = + Primitive(primitiveType) + + def fromFacts[Types](facts: Facts[Types]): EngineType[Facts[Types]] = + EngineType.Composite(FactsType.fromFacts(facts)) + } + + sealed trait Numeric[A] { + + type NumericType = A + + import Expr.NumericBinOpType + import Expr.NumericBinOpType._ + + def add(left: A, right: A): A + + def subtract(left: A, right: A): A + + def multiply(left: A, right: A): A + + def divide(left: A, right: A): A + + def modulo(left: A, right: A): A + + def apply(binOp: NumericBinOpType)(left: A, right: A): A = + binOp match { + case Add => add(left, right) + case Subtract => subtract(left, right) + case Multiply => multiply(left, right) + case Divide => divide(left, right) + case Modulo => modulo(left, right) + } + + } + object Numeric { - implicit case object ByteIsNumeric extends Numeric[Byte] - implicit case object CharIsNumeric extends Numeric[Char] - implicit case object IntIsNumeric extends Numeric[Int] - implicit case object LongIsNumeric extends Numeric[Long] - implicit case object FloatIsNumeric extends Numeric[Float] - implicit case object DoubleIsNumeric extends Numeric[Double] + implicit case object ByteIsNumeric extends Numeric[Byte] { + def add(left: Byte, right: Byte): Byte = (left + right).toByte -} + def subtract(left: Byte, right: Byte): Byte = (left - right).toByte + + def multiply(left: Byte, right: Byte): Byte = (left * right).toByte + + def divide(left: Byte, right: Byte): Byte = (left / right).toByte + + def modulo(left: Byte, right: Byte): Byte = (left % right).toByte + + } + implicit case object CharIsNumeric extends Numeric[Char] { + def add(left: Char, right: Char): Char = (left + right).toChar + + def subtract(left: Char, right: Char): Char = (left - right).toChar + + def multiply(left: Char, right: Char): Char = (left * right).toChar + + def divide(left: Char, right: Char): Char = (left / right).toChar + + def modulo(left: Char, right: Char): Char = (left % right).toChar + + } + implicit case object IntIsNumeric extends Numeric[Int] { + def add(left: Int, right: Int): Int = (left + right) + + def subtract(left: Int, right: Int): Int = (left - right) + + def multiply(left: Int, right: Int): Int = (left * right) + + def divide(left: Int, right: Int): Int = (left / right) + + def modulo(left: Int, right: Int): Int = (left % right) + + } + implicit case object LongIsNumeric extends Numeric[Long] { + def add(left: Long, right: Long): Long = (left + right) + + def subtract(left: Long, right: Long): Long = (left - right) + + def multiply(left: Long, right: Long): Long = (left * right) + + def divide(left: Long, right: Long): Long = (left / right) + + def modulo(left: Long, right: Long): Long = (left % right) + + } + implicit case object FloatIsNumeric extends Numeric[Float] { + def add(left: Float, right: Float): Float = (left + right) + + def subtract(left: Float, right: Float): Float = (left - right) + + def multiply(left: Float, right: Float): Float = (left * right) + + def divide(left: Float, right: Float): Float = (left / right) + + def modulo(left: Float, right: Float): Float = (left % right) + + } + implicit case object DoubleIsNumeric extends Numeric[Double] { + def add(left: Double, right: Double): Double = (left + right) + + def subtract(left: Double, right: Double): Double = (left - right) + + def multiply(left: Double, right: Double): Double = (left * right) + + def divide(left: Double, right: Double): Double = (left / right) + + def modulo(left: Double, right: Double): Double = (left % right) + + } + + } /** * A type class that represents the supported types for fact values. @@ -50,8 +157,10 @@ object graduation { } sealed trait Expr[-In, +Out] { self => - - final def ++[In1 <: In, Fields1, Fields2](that: Expr[In1, Facts[Fields2]])(implicit ev: Out <:< Facts[Fields1]): Expr[In1, Facts[Fields1 & Fields2]] = + + final def ++[In1 <: In, Fields1, Fields2](that: Expr[In1, Facts[Fields2]])(implicit + ev: Out <:< Facts[Fields1] + ): Expr[In1, Facts[Fields1 & Fields2]] = Expr.CombineFacts(self.widen[Facts[Fields1]], that) final def +[In1 <: In, Out1 >: Out](that: Expr[In1, Out1])(implicit tag: Numeric[Out1]): Expr[In1, Out1] = @@ -96,6 +205,11 @@ object graduation { final def >>>[Out2](that: Expr[Out, Out2]): Expr[In, Out2] = Expr.Pipe(self, that) + def eval(in: Facts[In]): Out = Expr.eval(in, self) + + def ifTrue[In1 <: In, Out2](ifTrue: Expr[In1, Out2])(implicit ev: Out <:< Boolean): Expr.IfTrue[In1, Out2] = + Expr.IfTrue(self.widen[Boolean], ifTrue) + final def unary_!(implicit ev: Out <:< Boolean): Expr[In, Boolean] = Expr.Not(self.widen) @@ -105,14 +219,19 @@ object graduation { object Expr { - final case class Fact[In, K <: Singleton with String, V]( - factDef: FactDefinition.KeyValue[K, V], - value: Expr[In, V]) extends Expr[In, Facts[(K, V)]] - final case class CombineFacts[In, V1, V2]( - left: Expr[In, Facts[V1]], - right: Expr[In, Facts[V2]] - ) extends Expr[In, Facts[V1 & V2]] - final case class Constant[Out](value: Out, tag: PrimitiveType[Out]) extends Expr[Any, Out] + Expr(true).ifTrue(42).otherwise(43) + + final case class IfTrue[In, Out](condition: Expr[In, Boolean], ifTrue: Expr[In, Out]) { + def otherwise(ifFalse: Expr[In, Out]) = IfThenElse(condition, ifTrue, ifFalse) + } + + final case class Fact[In, K <: Singleton with String, V](factDef: FactDefinition.KeyValue[K, V], value: Expr[In, V]) + extends Expr[In, Facts[(K, V)]] + final case class CombineFacts[In, V1, V2]( + left: Expr[In, Facts[V1]], + right: Expr[In, Facts[V2]] + ) extends Expr[In, Facts[V1 & V2]] + final case class Constant[Out](value: Out, tag: EngineType[Out]) extends Expr[Any, Out] final case class And[In](left: Expr[In, Boolean], right: Expr[In, Boolean]) extends Expr[In, Boolean] final case class Or[In](left: Expr[In, Boolean], right: Expr[In, Boolean]) extends Expr[In, Boolean] final case class Not[In](condition: Expr[In, Boolean]) extends Expr[In, Boolean] @@ -127,7 +246,8 @@ object graduation { op: NumericBinOpType, tag: Numeric[Out] ) extends Expr[In, Out] - + final case class IfThenElse[In, Out](condition: Expr[In, Boolean], ifTrue: Expr[In, Out], ifFalse: Expr[In, Out]) + extends Expr[In, Out] sealed trait NumericBinOpType object NumericBinOpType { case object Add extends NumericBinOpType @@ -137,21 +257,109 @@ object graduation { case object Modulo extends NumericBinOpType } - implicit def apply[Out](out: Out)(implicit tag: PrimitiveType[Out]): Expr[Any, Out] = Constant(out, tag) + implicit def apply[Out](out: Out)(implicit tag: PrimitiveType[Out]): Expr[Any, Out] = + Constant(out, EngineType.Primitive(tag)) + + implicit def apply[Out](out: Facts[Out]): Expr[Any, Facts[Out]] = + Constant(out, EngineType.fromFacts(out)) + + def evalWith[In, Out](in: Facts[In], expr: Expr[In, Out]): (EngineType[Out], Out) = ??? + + def eval[In, Out](in: Facts[In], expr: Expr[In, Out]): Out = + expr match { + case Fact(factDef, value) => + implicit val tag = factDef.tag + ??? + //Facts.empty.add[FactDefinition](factDef, value) + + case CombineFacts(lhs, rhs) => + val left = eval(in, lhs) + val right = eval(in, rhs) + left ++ right + + case Constant(value, tag) => value + + case And(lhs, rhs) => + val left = eval(in, lhs) + val right = eval(in, rhs) + left && right + + case Or(lhs, rhs) => ??? + val left = eval(in, lhs) + val right = eval(in, rhs) + left || right + + case Not(condition) => + !eval(in, condition) + + case EqualTo(lhs, rhs) => + val left = eval(in, lhs) + val right = eval(in, rhs) + + left == right + + case LessThan(lhs, rhs) => + val left = eval(in, lhs) + val right = eval(in, rhs) + ??? + + case Input(factDef) => + val fieldValue = Unsafe.unsafe { implicit u => + in.unsafe.get(factDef) + } + fieldValue // no need to cast it to Out ? + + case Pipe(left, right) => + ??? + + case BinaryNumericOp(lhs, rhs, op, tag0) => + val tag = tag0.asInstanceOf[Numeric[Out]] + val left: Out = eval(in, lhs) + val right: Out = eval(in, rhs) + tag(op)(left, right) + + case IfThenElse(condition, ifTrue, ifFalse) => + val bool = eval(in, condition) + if (bool) eval(in, ifTrue) + else eval(in, ifFalse) + + } + + def fact[In, K <: Singleton with String, V](factDef: FactDefinition.KeyValue[K, V], value: Expr[In, V]) = + Fact(factDef, value) + + def ifThenElse[In, Out]( + condition: Expr[In, Boolean] + )(ifTrue: Expr[In, Out], ifFalse: Expr[In, Out]): Expr[In, Out] = + Expr.IfThenElse(condition, ifTrue, ifFalse) def input[K <: Singleton with String, V](factDef: FactDefinition.KeyValue[K, V]): Expr[(K, V), V] = - Input(factDef) + Input(factDef) + + } - def fact[In, K <: Singleton with String, V](factDef: FactDefinition.KeyValue[K, V], value: Expr[In, V]) = Fact(factDef, value) + class FactsType[KeyValues] private (private val definitions: Chunk[FactDefinition[_]]) { self => + def ++ [KeyValues2](that: FactsType[KeyValues2]): FactsType[KeyValues & KeyValues2] = + new FactsType(self.definitions ++ that.definitions) + + def add[KeyValue](definition: FactDefinition[KeyValue]): FactsType[KeyValues & KeyValue] = + new FactsType(self.definitions :+ definition) } + object FactsType { + val empty: FactsType[Any] = new FactsType(Chunk.empty) + + def fromFacts[KeyValues](facts: Facts[KeyValues]): FactsType[KeyValues] = + new FactsType(facts.definitions) + } + sealed trait FactDefinition[KeyValue] { self => type Key <: Singleton with String type Value def name: Key - def paramType: PrimitiveType[Value] + def tag: EngineType[Value] // Added def get: Expr[(Key, Value), Value] = Expr.input(self.asInstanceOf[FactDefinition.KeyValue[Key, Value]]) @@ -159,55 +367,67 @@ object graduation { def set[In](value: Expr[In, Value]): Expr[In, Facts[(Key, Value)]] = Expr.fact(self.asInstanceOf[FactDefinition.KeyValue[Key, Value]], value) - def := [In](value: Expr[In, Value]) = set(value) - - override final def toString(): String = s"FactDefinition($name, $paramType)" + def :=[In](value: Expr[In, Value]) = set(value) + + override final def toString(): String = s"FactDefinition($name, $tag)" } object FactDefinition { type KeyValue[K <: Singleton with String, V] = FactDefinition[(K, V)] { type Key = K; type Value = V } - def apply[N <: Singleton with String, T](name0: N)(implicit paramType0: PrimitiveType[T]): KeyValue[N, T] = + def apply[N <: Singleton with String, T](name0: N, fact0: EngineType[T]): KeyValue[N, T] = + new FactDefinition[(N, T)] { + type Key = N + type Value = T + def name: N = name0 + def tag: EngineType[T] = fact0 + } + + def facts[N <: Singleton with String, Fields](name: N, factsType: FactsType[Fields]): KeyValue[N, Facts[Fields]] = + FactDefinition[N, Facts[Fields]](name, EngineType.Composite(factsType)) + + def prim[N <: Singleton with String, T](name0: N)(implicit tag0: PrimitiveType[T]): KeyValue[N, T] = new FactDefinition[(N, T)] { type Key = N type Value = T - def name: N = name0 - def paramType: PrimitiveType[T] = paramType0 + def name: N = name0 + def tag: EngineType[T] = EngineType.Primitive(tag0) } - def boolean[N <: Singleton with String](name0: N): KeyValue[N, Boolean] = FactDefinition[N, Boolean](name0) - def byte[N <: Singleton with String](name0: N): KeyValue[N, Byte] = FactDefinition[N, Byte](name0) + def boolean[N <: Singleton with String](name0: N): KeyValue[N, Boolean] = FactDefinition.prim[N, Boolean](name0) + + def byte[N <: Singleton with String](name0: N): KeyValue[N, Byte] = FactDefinition.prim[N, Byte](name0) - def char[N <: Singleton with String](name0: N): KeyValue[N, Char] = FactDefinition[N, Char](name0) + def char[N <: Singleton with String](name0: N): KeyValue[N, Char] = FactDefinition.prim[N, Char](name0) - def int[N <: Singleton with String](name0: N): KeyValue[N, Int] = FactDefinition[N, Int](name0) + def int[N <: Singleton with String](name0: N): KeyValue[N, Int] = FactDefinition.prim[N, Int](name0) - def long[N <: Singleton with String](name0: N): KeyValue[N, Long] = FactDefinition[N, Long](name0) + def long[N <: Singleton with String](name0: N): KeyValue[N, Long] = FactDefinition.prim[N, Long](name0) - def float[N <: Singleton with String](name0: N): KeyValue[N, Float] = FactDefinition[N, Float](name0) + def float[N <: Singleton with String](name0: N): KeyValue[N, Float] = FactDefinition.prim[N, Float](name0) - def double[N <: Singleton with String](name0: N): KeyValue[N, Double] = FactDefinition[N, Double](name0) + def double[N <: Singleton with String](name0: N): KeyValue[N, Double] = FactDefinition.prim[N, Double](name0) - def string[N <: Singleton with String](name0: N): KeyValue[N, String] = FactDefinition[N, String](name0) + def string[N <: Singleton with String](name0: N): KeyValue[N, String] = FactDefinition.prim[N, String](name0) def instant[N <: Singleton with String](name0: N): KeyValue[N, java.time.Instant] = - FactDefinition[N, java.time.Instant](name0) + FactDefinition.prim[N, java.time.Instant](name0) } final case class RuleEngine[-In, +Out](update: Facts[In] => Option[List[Out]]) { self => - def contramap[In2](f: Facts[In2] => Facts[In]): RuleEngine[In2, Out] = - RuleEngine(in => self.update(f(in))) - + def contramap[In2](f: Facts[In2] => Facts[In]): RuleEngine[In2, Out] = + RuleEngine(in => self.update(f(in))) + def orElse[In1 <: In, Out1 >: Out](that: RuleEngine[In1, Out1]): RuleEngine[In1, Out1] = - RuleEngine(in => self.update(in) orElse that.update(in)) + RuleEngine(in => self.update(in) orElse that.update(in)) def updateWith[Out1 >: Out](in: Facts[In])(defaultOut: Out1, combine: (Out1, Out1) => Out1): Out1 = self.update(in) match { case None => defaultOut - case Some(outs) => + case Some(outs) => outs.reduceOption(combine).getOrElse(defaultOut) } } @@ -219,26 +439,35 @@ object graduation { def fromFunction[In, Out](f: Facts[In] => Out): RuleEngine[In, Out] = RuleEngine(in => Some(List(f(in)))) - def fromRuleSet[In, Out](ruleSet: RuleSet[In, Out]): RuleEngine[In, Out] = - RuleEngine(???) + def fromRuleSet[In, Out](ruleSet: RuleSet[In, Out]): RuleEngine[In, Out] = { + val update: Facts[In] => Option[List[Out]] = execute(ruleSet, _) + + RuleEngine(update) + } + + private def execute[In, Out](ruleSet: RuleSet[In, Out], in: Facts[In]): Option[List[Out]] = + //ruleSet.rules.find(_.condition.eval(in)).map { rule => + // rule.action.update(in)} + + ??? } final case class Rule[-In, +Out](condition: Condition[In], action: Action[In, Out]) final case class RuleSet[-In, +Out](rules: Vector[Rule[In, Out]]) { self => - - def + [In1 <: In, Out1 >: Out](that: Rule[In1, Out1]): RuleSet[In1, Out1] = + + def +[In1 <: In, Out1 >: Out](that: Rule[In1, Out1]): RuleSet[In1, Out1] = RuleSet(self.rules :+ that) - + def ++[In1 <: In, Out1 >: Out](that: RuleSet[In1, Out1]): RuleSet[In1, Out1] = RuleSet(self.rules ++ that.rules) - - def addRule[In1 <: In, Out1 >: Out](that: Rule[In1, Out1]): RuleSet[In1, Out1] = + + def addRule[In1 <: In, Out1 >: Out](that: Rule[In1, Out1]): RuleSet[In1, Out1] = self + that } object RuleSet { - + def apply[In, Out](rule1: Rule[In, Out], rules: Rule[In, Out]*): RuleSet[In, Out] = RuleSet(rule1 +: rules.toVector) @@ -246,34 +475,38 @@ object graduation { } - /** * Contains a collection of facts, whose structure is described by a phantom * type parameter. */ sealed abstract case class Facts[+Types] private (private val data: Map[FactDefinition[_], Any]) { - def ++ [Types2](that: Facts[Types2]): Facts[Types & Types2] = + def ++[Types2](that: Facts[Types2]): Facts[Types & Types2] = new Facts[Types & Types2](data ++ that.data) {} + def definitions: Chunk[FactDefinition[_]] = Chunk.fromIterable(data.keys) + def get[Key <: Singleton with String, Value: PrimitiveType](pd: FactDefinition[(Key, Value)])(implicit subset: Types <:< (Key, Value) ): Value = data(pd).asInstanceOf[Value] /** - * Returns a new facts collection with the specified fact added. + * Returns a new facts collection with the specified primitive fact added. */ - def add[Key <: Singleton with String, Value: PrimitiveType]( - pd: FactDefinition[(Key, Value)], + def add[Key <: Singleton with String, Value]( + pd: FactDefinition.KeyValue[Key, Value], value: Value ): Facts[Types & (Key, Value)] = new Facts[Types & (Key, Value)](data + (pd -> value)) {} - private def add[Key <: Singleton with String, Value: PrimitiveType]( - name: Key, - value: Value - ): Facts[Types & (Key, Value)] = - new Facts[Types & (Key, Value)](data + (FactDefinition[Key, Value](name) -> value)) {} + /** + * Returns a new facts collection with the specified fact added. + */ + def add[Key <: Singleton with String, Types2]( + pd: FactDefinition.KeyValue[Key, Facts[Types2]], + value: Facts[Types2] + ): Facts[Types & (Key, Facts[Types2])] = + new Facts[Types & (Key, Facts[Types2])](data + (pd -> value)) {} object unsafe { def get(pd: FactDefinition[_])(implicit unsafe: Unsafe): Option[Any] = data.get(pd) @@ -286,145 +519,100 @@ object graduation { */ val empty: Facts[Any] = new Facts[Any](Map.empty) {} - def apply[Key <: Singleton with String, Value: PrimitiveType](key: Key, value: Value): Facts[(Key, Value)] = - empty.add(key, value) - - def apply[ - Key1 <: Singleton with String, - Value1: PrimitiveType, - Key2 <: Singleton with String, - Value2: PrimitiveType - ]( - tuple1: (Key1, Value1), - tuple2: (Key2, Value2) - ): Facts[(Key1, Value1) & (Key2, Value2)] = - empty.add[Key1, Value1](tuple1._1, tuple1._2).add[Key2, Value2](tuple2._1, tuple2._2) - - def apply[ - Key1 <: Singleton with String, - Value1: PrimitiveType, - Key2 <: Singleton with String, - Value2: PrimitiveType, - Key3 <: Singleton with String, - Value3: PrimitiveType - ]( - tuple1: (Key1, Value1), - tuple2: (Key2, Value2), - tuple3: (Key3, Value3) - ): Facts[(Key1, Value1) & (Key2, Value2) & (Key3, Value3)] = - empty - .add[Key1, Value1](tuple1._1, tuple1._2) - .add[Key2, Value2](tuple2._1, tuple2._2) - .add[Key3, Value3](tuple3._1, tuple3._2) - - def apply[ - Key1 <: Singleton with String, - Value1: PrimitiveType, - Key2 <: Singleton with String, - Value2: PrimitiveType, - Key3 <: Singleton with String, - Value3: PrimitiveType, - Key4 <: Singleton with String, - Value4: PrimitiveType - ]( - tuple1: (Key1, Value1), - tuple2: (Key2, Value2), - tuple3: (Key3, Value3), - tuple4: (Key4, Value4) - ): Facts[(Key1, Value1) & (Key2, Value2) & (Key3, Value3) & (Key4, Value4)] = - empty - .add[Key1, Value1](tuple1._1, tuple1._2) - .add[Key2, Value2](tuple2._1, tuple2._2) - .add[Key3, Value3](tuple3._1, tuple3._2) - .add[Key4, Value4](tuple4._1, tuple4._2) - - def apply[ - Key1 <: Singleton with String, - Value1: PrimitiveType, - Key2 <: Singleton with String, - Value2: PrimitiveType, - Key3 <: Singleton with String, - Value3: PrimitiveType, - Key4 <: Singleton with String, - Value4: PrimitiveType, - Key5 <: Singleton with String, - Value5: PrimitiveType - ]( - tuple1: (Key1, Value1), - tuple2: (Key2, Value2), - tuple3: (Key3, Value3), - tuple4: (Key4, Value4), - tuple5: (Key5, Value5) - ): Facts[(Key1, Value1) & (Key2, Value2) & (Key3, Value3) & (Key4, Value4) & (Key5, Value5)] = - empty - .add[Key1, Value1](tuple1._1, tuple1._2) - .add[Key2, Value2](tuple2._1, tuple2._2) - .add[Key3, Value3](tuple3._1, tuple3._2) - .add[Key4, Value4](tuple4._1, tuple4._2) - .add[Key5, Value5](tuple5._1, tuple5._2) + def engineTypeOf[Types](facts: Facts[Types]): EngineType[Facts[Types]] = + EngineType.Composite(FactsType.fromFacts(facts)) + } - final case class Condition[-In] (expr: Expr[In, Boolean]) { self => - def &&[In1 <: In](that: Condition[In1]): Condition[In1] = + final case class Condition[-In](expr: Expr[In, Boolean]) { self => + + def eval(facts: Facts[In]) = expr.eval(facts) + + def &&[In1 <: In](that: Condition[In1]): Condition[In1] = Condition(self.expr && that.expr) - def ||[In1 <: In](that: Condition[In1]): Condition[In1] = + def ||[In1 <: In](that: Condition[In1]): Condition[In1] = Condition(self.expr && that.expr) - + def unary_! : Condition[In] = Condition(!expr) } object Condition { val always: Condition[Any] = constant(true) - val never: Condition[Any] = constant(false) + val never: Condition[Any] = constant(false) def constant[In](value: Boolean): Condition[In] = Condition(Expr(value)) - - } sealed trait Action[-In, +Out] { self => - def ++[In1 <: In, Out1 >: Out](that: Action[In1, Out1]): Action[In1, Out1] = - Action.Concat(self, that) - - def >>>[Out2](that: Action[Out, Out2]): Action[In, Out2] = - Action.Pipe(self, that) + def ++[In1 <: In, Out1 >: Out](that: Action[In1, Out1]): Action[In1, Out1] = + Action.Concat(self, that) + + def >>>[Out2](that: Action[Out, Out2]): Action[In, Out2] = + Action.Pipe(self, that) } object Action { - final case class Concat[In, Out](left: Action[In, Out], right: Action[In, Out]) extends Action[In, Out] + final case class Concat[In, Out](left: Action[In, Out], right: Action[In, Out]) extends Action[In, Out] final case class Pipe[In, Out1, Out2](left: Action[In, Out1], right: Action[Out1, Out2]) extends Action[In, Out2] - final case class FromExpr[In, Out](expr: Expr[In, Out]) extends Action[In, Out] + final case class FromExpr[In, Out](expr: Expr[In, Out]) extends Action[In, Out] def fromExpr[In, Out](expr: Expr[In, Out]): Action[In, Out] = FromExpr(expr) - def constant[Out](out: Out)(implicit tag: PrimitiveType[Out]): Action[Any, Out] = fromExpr(Expr(out)) } object loyalty { import net.degoes.afd.examples.loyalty._ import net.degoes.afd.examples.loyalty.LoyaltyTier._ + object Flights { + val id = FactDefinition.string("id") + val number = FactDefinition.string("number") + + val factsType = + FactsType.empty.add(id).add(number) + } + + object Customer { + val id = FactDefinition.string("id") + val name = FactDefinition.string("name") + val email = FactDefinition.string("email") + val phone = FactDefinition.string("phone") + + val factsType = + FactsType.empty.add(id).add(name).add(email).add(phone) + } + object FlightBooking { - val id = FactDefinition.string("id") - val customer = FactDefinition.string("custoner") // FIXME: Support nested data - val flight = FactDefinition.string("flight") // FIXME: Suppor nested data - val price = FactDefinition.double("price") - val status = FactDefinition.string("status") + val id = FactDefinition.string("id") + val customer = FactDefinition.facts("customer", Customer.factsType) // FIXME: Support nested data + val flight = FactDefinition.facts("fligh", Flights.factsType) // FactDefinition.string("flight") // FIXME: Suppor nested data + val price = FactDefinition.double("price") + val status = FactDefinition.string("status") + + val factsType = + FactsType.empty.add(id).add(customer).add(flight).add(price).add(status) } object FlightBookingStatus { - val Confirmed = Expr("Confirmed") - val Cancelled = Expr("Cancelled") - val Pending = Expr("Pending") + val Confirmed = Expr("Confirmed") + val Cancelled = Expr("Cancelled") + val Pending = Expr("Pending") + } + + object LoyaltyProgram { + val tier = FactDefinition.string("tier") + val points = FactDefinition.int("points") } - (FlightBooking.price := FlightBooking.price.get + 1000.0) ++ (FlightBooking.status := FlightBookingStatus.Pending) + val isConfirmed = FlightBooking.status.get === FlightBookingStatus.Confirmed + val isExpensive = FlightBooking.price.get > 1000.0 - val statusCondition = - Condition(FlightBooking.status.get === FlightBookingStatus.Confirmed) - val priceCondition = Condition(FlightBooking.price.get > 1000.0) + //val setGold = LoyaltyProgram.tier := Action.fromExpr(Gold.toString) + val increasePoints = LoyaltyProgram.points := LoyaltyProgram.points.get + 100 - val exampleCondition = statusCondition && priceCondition + //isConfirmed.ifTrue(isConfirmed) + //(isConfirmed && isExpensive).ifTrue(Action.constant("true")).otherwise(Action.constant("false")) + // (isConfirmed && isExpensive).ifTrue(setGold ++ increasePoints) } From e72ee93b763101455bbb2c6fa4ff856184099dc7 Mon Sep 17 00:00:00 2001 From: Jean-Luc CANELA Date: Sun, 2 Oct 2022 12:26:37 +0200 Subject: [PATCH 5/8] add lessThan and equals to evalWithType --- .../degoes/afd/ruleengine/07-graduation.scala | 127 +++++++++++++----- 1 file changed, 91 insertions(+), 36 deletions(-) diff --git a/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala b/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala index e857bdc..6c573d8 100644 --- a/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala +++ b/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala @@ -21,10 +21,23 @@ import scala.language.implicitConversions object graduation { - sealed trait EngineType[A] + sealed trait EngineType[A] { + def equals(left: A, right: A): Boolean + def lessThan(left: A, right: A): Boolean + } object EngineType { - final case class Primitive[A](primitiveType: PrimitiveType[A]) extends EngineType[A] - final case class Composite[Fields](factsType: FactsType[Fields]) extends EngineType[Facts[Fields]] + + final case class Primitive[A](primitiveType: PrimitiveType[A]) extends EngineType[A] { + def equals(left: A, right: A): Boolean = primitiveType.ordering.equiv(left, right) + + def lessThan(left: A, right: A): Boolean = primitiveType.ordering.lt(left, right) + } + + final case class Composite[Fields](factsType: FactsType[Fields]) extends EngineType[Facts[Fields]] { + def equals(left: Facts[Fields], right: Facts[Fields]): Boolean = ??? + + def lessThan(left: Facts[Fields], right: Facts[Fields]): Boolean = ??? + } def fromPrimitive[A](implicit primitiveType: PrimitiveType[A]): EngineType[A] = Primitive(primitiveType) @@ -50,6 +63,17 @@ object graduation { def modulo(left: A, right: A): A + import PrimitiveType._ + def primitiveType: PrimitiveType[A] = + (this match { + case _: Numeric.ByteIsNumeric.type => PrimitiveType.ByteType + case _: Numeric.CharIsNumeric.type => PrimitiveType.CharType + case _: Numeric.IntIsNumeric.type => PrimitiveType.IntType + case _: Numeric.LongIsNumeric.type => PrimitiveType.LongType + case _: Numeric.FloatIsNumeric.type => PrimitiveType.FloatType + case _: Numeric.DoubleIsNumeric.type => PrimitiveType.DoubleType + }).asInstanceOf[PrimitiveType[A]] + def apply(binOp: NumericBinOpType)(left: A, right: A): A = binOp match { case Add => add(left, right) @@ -141,19 +165,40 @@ object graduation { * A type class that represents the supported types for fact values. */ @implicitNotFound("The type ${A} is not supported as a fact type and cannot be used for this method.") - sealed trait PrimitiveType[A] + sealed trait PrimitiveType[A] { + def ordering[T]: scala.math.Ordering[T] + } + object PrimitiveType { def apply[T](implicit factType: PrimitiveType[T]): PrimitiveType[T] = factType - implicit case object Int extends PrimitiveType[scala.Int] - implicit case object Long extends PrimitiveType[scala.Long] - implicit case object String extends PrimitiveType[java.lang.String] - implicit case object Double extends PrimitiveType[scala.Double] - implicit case object Byte extends PrimitiveType[scala.Byte] - implicit case object Char extends PrimitiveType[scala.Char] - implicit case object Float extends PrimitiveType[scala.Float] - implicit case object Boolean extends PrimitiveType[scala.Boolean] - implicit case object Instant extends PrimitiveType[java.time.Instant] + implicit case object IntType extends PrimitiveType[scala.Int] { + def ordering[T]: scala.math.Ordering[T] = Ordering[Int].asInstanceOf[Ordering[T]] + } + implicit case object LongType extends PrimitiveType[scala.Long]{ + def ordering[T]: scala.math.Ordering[T] = Ordering[Long].asInstanceOf[Ordering[T]] + } + implicit case object StringType extends PrimitiveType[java.lang.String]{ + def ordering[T]: scala.math.Ordering[T] = Ordering[String].asInstanceOf[Ordering[T]] + } + implicit case object DoubleType extends PrimitiveType[scala.Double]{ + def ordering[T]: scala.math.Ordering[T] = Ordering[Double].asInstanceOf[Ordering[T]] + } + implicit case object ByteType extends PrimitiveType[scala.Byte]{ + def ordering[T]: scala.math.Ordering[T] = Ordering[Byte].asInstanceOf[Ordering[T]] + } + implicit case object CharType extends PrimitiveType[scala.Char]{ + def ordering[T]: scala.math.Ordering[T] = Ordering[Char].asInstanceOf[Ordering[T]] + } + implicit case object FloatType extends PrimitiveType[scala.Float]{ + def ordering[T]: scala.math.Ordering[T] = Ordering[Float].asInstanceOf[Ordering[T]] + } + implicit case object BooleanType extends PrimitiveType[scala.Boolean]{ + def ordering[T]: scala.math.Ordering[T] = Ordering[Boolean].asInstanceOf[Ordering[T]] + } + implicit case object InstantType extends PrimitiveType[java.time.Instant]{ + def ordering[T]: scala.math.Ordering[T] = Ordering[java.time.Instant].asInstanceOf[Ordering[T]] + } } sealed trait Expr[-In, +Out] { self => @@ -263,65 +308,75 @@ object graduation { implicit def apply[Out](out: Facts[Out]): Expr[Any, Facts[Out]] = Constant(out, EngineType.fromFacts(out)) - def evalWith[In, Out](in: Facts[In], expr: Expr[In, Out]): (EngineType[Out], Out) = ??? + + def eval[In, Out](in: Facts[In], expr: Expr[In, Out]): Out = evalWithType(in, expr)._2 - def eval[In, Out](in: Facts[In], expr: Expr[In, Out]): Out = + def evalWithType[In, Out](in: Facts[In], expr: Expr[In, Out]): (EngineType[Out], Out) = expr match { case Fact(factDef, value) => implicit val tag = factDef.tag - ??? - //Facts.empty.add[FactDefinition](factDef, value) + + val result = Facts.empty.add(factDef, value) + (EngineType.fromFacts(result).asInstanceOf[EngineType[Out]], result) case CombineFacts(lhs, rhs) => val left = eval(in, lhs) val right = eval(in, rhs) - left ++ right - - case Constant(value, tag) => value + val results = left ++ right + (EngineType.fromFacts(results).asInstanceOf[EngineType[Out]], results) + + case Constant(value, tag) => + (tag.asInstanceOf[EngineType[Out]], value) case And(lhs, rhs) => val left = eval(in, lhs) val right = eval(in, rhs) - left && right + (EngineType.fromPrimitive[Boolean].asInstanceOf[EngineType[Out]], left && right) case Or(lhs, rhs) => ??? val left = eval(in, lhs) val right = eval(in, rhs) - left || right + (EngineType.fromPrimitive[Boolean].asInstanceOf[EngineType[Out]], left || right) case Not(condition) => - !eval(in, condition) + (EngineType.fromPrimitive[Boolean].asInstanceOf[EngineType[Out]], !eval(in, condition)) case EqualTo(lhs, rhs) => - val left = eval(in, lhs) - val right = eval(in, rhs) + val (leftType, left) = evalWithType(in, lhs) + val (rightType, right) = evalWithType(in, rhs) - left == right + import PrimitiveType._ + + (EngineType.fromPrimitive(PrimitiveType[Boolean]).asInstanceOf[EngineType[Out]], + leftType.equals(left, right)) case LessThan(lhs, rhs) => - val left = eval(in, lhs) - val right = eval(in, rhs) - ??? + val (leftType, left) = evalWithType(in, lhs) + val (rightType, right) = evalWithType(in, rhs) + + import PrimitiveType._ + + (EngineType.fromPrimitive(PrimitiveType[Boolean]).asInstanceOf[EngineType[Out]], + leftType.lessThan(left, right)) case Input(factDef) => val fieldValue = Unsafe.unsafe { implicit u => in.unsafe.get(factDef) } - fieldValue // no need to cast it to Out ? - - case Pipe(left, right) => - ??? + factDef.tag -> fieldValue.asInstanceOf[Out] // no need to cast it to Out ? + case Pipe(left, right) => ??? + case BinaryNumericOp(lhs, rhs, op, tag0) => val tag = tag0.asInstanceOf[Numeric[Out]] val left: Out = eval(in, lhs) val right: Out = eval(in, rhs) - tag(op)(left, right) + (EngineType.fromPrimitive(tag.primitiveType), tag(op)(left, right)) case IfThenElse(condition, ifTrue, ifFalse) => val bool = eval(in, condition) - if (bool) eval(in, ifTrue) - else eval(in, ifFalse) + if (bool) evalWithType(in, ifTrue) + else evalWithType(in, ifFalse) } From 63f2971062be673dff7f157e1c9c15e430b49a0c Mon Sep 17 00:00:00 2001 From: Jean-Luc CANELA Date: Sun, 2 Oct 2022 13:01:42 +0200 Subject: [PATCH 6/8] basic implementation of rule engine --- .../degoes/afd/ruleengine/07-graduation.scala | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala b/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala index 6c573d8..10edb00 100644 --- a/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala +++ b/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala @@ -34,9 +34,9 @@ object graduation { } final case class Composite[Fields](factsType: FactsType[Fields]) extends EngineType[Facts[Fields]] { - def equals(left: Facts[Fields], right: Facts[Fields]): Boolean = ??? + def equals(left: Facts[Fields], right: Facts[Fields]): Boolean = left == right - def lessThan(left: Facts[Fields], right: Facts[Fields]): Boolean = ??? + def lessThan(left: Facts[Fields], right: Facts[Fields]): Boolean = left.lessThan(right) } def fromPrimitive[A](implicit primitiveType: PrimitiveType[A]): EngineType[A] = @@ -501,10 +501,9 @@ object graduation { } private def execute[In, Out](ruleSet: RuleSet[In, Out], in: Facts[In]): Option[List[Out]] = - //ruleSet.rules.find(_.condition.eval(in)).map { rule => - // rule.action.update(in)} - - ??? + ruleSet.rules.find(_.condition.eval(in)).map { rule => + rule.action.eval(in) + } } final case class Rule[-In, +Out](condition: Condition[In], action: Action[In, Out]) @@ -534,22 +533,30 @@ object graduation { * Contains a collection of facts, whose structure is described by a phantom * type parameter. */ - sealed abstract case class Facts[+Types] private (private val data: Map[FactDefinition[_], Any]) { + sealed abstract case class Facts[+Types] private (private val data: Map[FactDefinition[_], Any]) { self => def ++[Types2](that: Facts[Types2]): Facts[Types & Types2] = new Facts[Types & Types2](data ++ that.data) {} def definitions: Chunk[FactDefinition[_]] = Chunk.fromIterable(data.keys) - def get[Key <: Singleton with String, Value: PrimitiveType](pd: FactDefinition[(Key, Value)])(implicit + override def equals(that: Any): Boolean = + that match { + case that: Facts[_] => self.data == that.data + case _ => false + } + + def lessThan(that: Facts[_]) = false + + def get[Key <: Singleton with String, Value: PrimitiveType](pd: FactDefinition[(Key, Value)])(implicit subset: Types <:< (Key, Value) ): Value = data(pd).asInstanceOf[Value] - /** - * Returns a new facts collection with the specified primitive fact added. - */ - def add[Key <: Singleton with String, Value]( - pd: FactDefinition.KeyValue[Key, Value], + /** + * Returns a new facts collection with the specified primitive fact added. + */ + def add[Key <: Singleton with String, Value]( + pd: FactDefinition.KeyValue[Key, Value], value: Value ): Facts[Types & (Key, Value)] = new Facts[Types & (Key, Value)](data + (pd -> value)) {} @@ -563,6 +570,7 @@ object graduation { ): Facts[Types & (Key, Facts[Types2])] = new Facts[Types & (Key, Facts[Types2])](data + (pd -> value)) {} + object unsafe { def get(pd: FactDefinition[_])(implicit unsafe: Unsafe): Option[Any] = data.get(pd) } @@ -605,6 +613,17 @@ object graduation { def >>>[Out2](that: Action[Out, Out2]): Action[In, Out2] = Action.Pipe(self, that) + + def eval(facts: Facts[In]): List[Out] = + self match { + case Action.Concat(left, right) => + left.eval(facts) ++ right.eval(facts) + + case Action.Pipe(left, right) => ??? + + case Action.FromExpr(expr) => + List(expr.eval(facts)) + } } object Action { final case class Concat[In, Out](left: Action[In, Out], right: Action[In, Out]) extends Action[In, Out] @@ -612,7 +631,7 @@ object graduation { final case class FromExpr[In, Out](expr: Expr[In, Out]) extends Action[In, Out] def fromExpr[In, Out](expr: Expr[In, Out]): Action[In, Out] = FromExpr(expr) - + } object loyalty { From 5caa543c737016fd4ed5ea36d8746d287c932e9b Mon Sep 17 00:00:00 2001 From: Jean-Luc CANELA Date: Sun, 2 Oct 2022 22:25:52 +0200 Subject: [PATCH 7/8] implement full Loyalty engine --- .../degoes/afd/ruleengine/07-graduation.scala | 124 ++++++++++++------ .../net/degoes/afd/model/ExampleSpec.scala | 60 ++++++++- 2 files changed, 140 insertions(+), 44 deletions(-) diff --git a/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala b/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala index 10edb00..de891ad 100644 --- a/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala +++ b/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala @@ -250,7 +250,7 @@ object graduation { final def >>>[Out2](that: Expr[Out, Out2]): Expr[In, Out2] = Expr.Pipe(self, that) - def eval(in: Facts[In]): Out = Expr.eval(in, self) + def eval(in: In): Out = Expr.eval(in, self) def ifTrue[In1 <: In, Out2](ifTrue: Expr[In1, Out2])(implicit ev: Out <:< Boolean): Expr.IfTrue[In1, Out2] = Expr.IfTrue(self.widen[Boolean], ifTrue) @@ -264,8 +264,6 @@ object graduation { object Expr { - Expr(true).ifTrue(42).otherwise(43) - final case class IfTrue[In, Out](condition: Expr[In, Boolean], ifTrue: Expr[In, Out]) { def otherwise(ifFalse: Expr[In, Out]) = IfThenElse(condition, ifTrue, ifFalse) } @@ -282,8 +280,8 @@ object graduation { final case class Not[In](condition: Expr[In, Boolean]) extends Expr[In, Boolean] final case class EqualTo[In, Out](lhs: Expr[In, Out], rhs: Expr[In, Out]) extends Expr[In, Boolean] final case class LessThan[In, Out](lhs: Expr[In, Out], rhs: Expr[In, Out]) extends Expr[In, Boolean] - final case class Input[K <: Singleton with String, V](factDef: FactDefinition.KeyValue[K, V]) - extends Expr[(K, V), V] // TODO : split into READ & GET operators + final case class Input[In](engineType: EngineType[In]) extends Expr[In, In] + final case class Get[In, K <: Singleton with String, V](expr: Expr[In, Facts[(K, V)]], factDef: FactDefinition.KeyValue[K, V]) extends Expr[In, V] final case class Pipe[In, Out1, Out2](left: Expr[In, Out1], right: Expr[Out1, Out2]) extends Expr[In, Out2] final case class BinaryNumericOp[In, Out]( lhs: Expr[In, Out], @@ -309,9 +307,9 @@ object graduation { Constant(out, EngineType.fromFacts(out)) - def eval[In, Out](in: Facts[In], expr: Expr[In, Out]): Out = evalWithType(in, expr)._2 + private def eval[In, Out](in: In, expr: Expr[In, Out]): Out = evalWithType(in, expr)._2 - def evalWithType[In, Out](in: Facts[In], expr: Expr[In, Out]): (EngineType[Out], Out) = + private def evalWithType[In, Out](in: In, expr: Expr[In, Out]): (EngineType[Out], Out) = expr match { case Fact(factDef, value) => implicit val tag = factDef.tag @@ -333,7 +331,7 @@ object graduation { val right = eval(in, rhs) (EngineType.fromPrimitive[Boolean].asInstanceOf[EngineType[Out]], left && right) - case Or(lhs, rhs) => ??? + case Or(lhs, rhs) => val left = eval(in, lhs) val right = eval(in, rhs) (EngineType.fromPrimitive[Boolean].asInstanceOf[EngineType[Out]], left || right) @@ -359,13 +357,18 @@ object graduation { (EngineType.fromPrimitive(PrimitiveType[Boolean]).asInstanceOf[EngineType[Out]], leftType.lessThan(left, right)) - case Input(factDef) => - val fieldValue = Unsafe.unsafe { implicit u => - in.unsafe.get(factDef) - } - factDef.tag -> fieldValue.asInstanceOf[Out] // no need to cast it to Out ? + case Input(engineType) => + engineType.asInstanceOf[EngineType[Out]] -> in.asInstanceOf[Out] + + case Get(expr, fd) => + val facts = eval(in, expr) - case Pipe(left, right) => ??? + val value = Unsafe.unsafe { implicit u => + facts.unsafe.get(fd).asInstanceOf[Out] + } + (fd.tag.asInstanceOf[EngineType[Out]],value) + + case Pipe(lhs, rhs) => evalWithType(eval(in, lhs), rhs) case BinaryNumericOp(lhs, rhs, op, tag0) => val tag = tag0.asInstanceOf[Numeric[Out]] @@ -388,8 +391,7 @@ object graduation { )(ifTrue: Expr[In, Out], ifFalse: Expr[In, Out]): Expr[In, Out] = Expr.IfThenElse(condition, ifTrue, ifFalse) - def input[K <: Singleton with String, V](factDef: FactDefinition.KeyValue[K, V]): Expr[(K, V), V] = - Input(factDef) + def input[A](engineType: EngineType[A]): Expr[A, A] = Input(engineType) } @@ -408,7 +410,7 @@ object graduation { new FactsType(facts.definitions) } - sealed trait FactDefinition[KeyValue] { self => + sealed trait FactDefinition[KeyValue] { self0 => type Key <: Singleton with String type Value @@ -416,11 +418,18 @@ object graduation { def tag: EngineType[Value] - // Added - def get: Expr[(Key, Value), Value] = Expr.input(self.asInstanceOf[FactDefinition.KeyValue[Key, Value]]) + def self: FactDefinition.KeyValue[Key, Value] = self0.asInstanceOf[FactDefinition.KeyValue[Key, Value]] + + val singletonType: EngineType[Facts[(Key, Value)]] = EngineType.Composite(FactsType.empty.add[(Key, Value)](self)) + def get: Expr[Facts[(Key, Value)], Value] = { + val factsExpr = Expr.input(singletonType) + + Expr.Get(factsExpr, self) + } + def set[In](value: Expr[In, Value]): Expr[In, Facts[(Key, Value)]] = - Expr.fact(self.asInstanceOf[FactDefinition.KeyValue[Key, Value]], value) + Expr.fact(self, value) def :=[In](value: Expr[In, Value]) = set(value) @@ -472,14 +481,14 @@ object graduation { } - final case class RuleEngine[-In, +Out](update: Facts[In] => Option[List[Out]]) { self => - def contramap[In2](f: Facts[In2] => Facts[In]): RuleEngine[In2, Out] = + final case class RuleEngine[-In, +Out](update: In => Option[List[Out]]) { self => + def contramap[In2](f: In2 => In): RuleEngine[In2, Out] = RuleEngine(in => self.update(f(in))) def orElse[In1 <: In, Out1 >: Out](that: RuleEngine[In1, Out1]): RuleEngine[In1, Out1] = RuleEngine(in => self.update(in) orElse that.update(in)) - def updateWith[Out1 >: Out](in: Facts[In])(defaultOut: Out1, combine: (Out1, Out1) => Out1): Out1 = + def updateWith[Out1 >: Out](in: In)(defaultOut: Out1, combine: (Out1, Out1) => Out1): Out1 = self.update(in) match { case None => defaultOut case Some(outs) => @@ -492,15 +501,15 @@ object graduation { def constant[Out](out: Out): RuleEngine[Any, Out] = fromFunction(_ => out) - def fromFunction[In, Out](f: Facts[In] => Out): RuleEngine[In, Out] = RuleEngine(in => Some(List(f(in)))) + def fromFunction[In, Out](f: In => Out): RuleEngine[In, Out] = RuleEngine(in => Some(List(f(in)))) def fromRuleSet[In, Out](ruleSet: RuleSet[In, Out]): RuleEngine[In, Out] = { - val update: Facts[In] => Option[List[Out]] = execute(ruleSet, _) + val update: In => Option[List[Out]] = execute(ruleSet, _) RuleEngine(update) } - private def execute[In, Out](ruleSet: RuleSet[In, Out], in: Facts[In]): Option[List[Out]] = + private def execute[In, Out](ruleSet: RuleSet[In, Out], in: In): Option[List[Out]] = ruleSet.rules.find(_.condition.eval(in)).map { rule => rule.action.eval(in) } @@ -572,7 +581,7 @@ object graduation { object unsafe { - def get(pd: FactDefinition[_])(implicit unsafe: Unsafe): Option[Any] = data.get(pd) + def get(pd: FactDefinition[_])(implicit unsafe: Unsafe): Any = data(pd) } } object Facts { @@ -589,7 +598,7 @@ object graduation { final case class Condition[-In](expr: Expr[In, Boolean]) { self => - def eval(facts: Facts[In]) = expr.eval(facts) + def eval(in: In) = expr.eval(in) def &&[In1 <: In](that: Condition[In1]): Condition[In1] = Condition(self.expr && that.expr) @@ -614,15 +623,15 @@ object graduation { def >>>[Out2](that: Action[Out, Out2]): Action[In, Out2] = Action.Pipe(self, that) - def eval(facts: Facts[In]): List[Out] = + def eval(in: In): List[Out] = self match { case Action.Concat(left, right) => - left.eval(facts) ++ right.eval(facts) + left.eval(in) ++ right.eval(in) - case Action.Pipe(left, right) => ??? + case Action.Pipe(lhs, rhs) => lhs.eval(in).flatMap(rhs.eval(_)) case Action.FromExpr(expr) => - List(expr.eval(facts)) + List(expr.eval(in)) } } object Action { @@ -678,16 +687,53 @@ object graduation { val points = FactDefinition.int("points") } - val isConfirmed = FlightBooking.status.get === FlightBookingStatus.Confirmed - val isExpensive = FlightBooking.price.get > 1000.0 + object LoyaltyAction { + val actionType = FactDefinition.string("action_type") + val points = FactDefinition.int("points") + val customer = FactDefinition.string("customer") + + def update(program: LoyaltyProgram, action: Facts[_]): LoyaltyProgram = + Unsafe.unsafe { implicit u => + action.unsafe.get(actionType) match { + case ActionType.DowngradeTier => program.copy(tier = LoyaltyTier.Bronze) + case ActionType.UpgradeTier => program.copy(tier = LoyaltyTier.Gold) + case ActionType.AddPoints => action.unsafe.get(points) match { + case Expr.Constant(value: Int, _) => program.copy(points = value) + case _ => program + } + case _ => program + } + } + + def update(program: LoyaltyProgram, actions: List[Facts[_]]) : LoyaltyProgram = + actions.foldLeft(program)( (program, action) => update(program, action)) + } + + object ActionType { + val AddPoints = Expr("add_points") + val UpgradeTier = Expr("upgrade_tier") + val DowngradeTier = Expr("downgrade_tier") + } - //val setGold = LoyaltyProgram.tier := Action.fromExpr(Gold.toString) - val increasePoints = LoyaltyProgram.points := LoyaltyProgram.points.get + 100 + val statusCondition = Condition(FlightBooking.status.get === FlightBookingStatus.Confirmed) + + val priceCondition = Condition(FlightBooking.price.get > 1000.0) + + val both = statusCondition && priceCondition - //isConfirmed.ifTrue(isConfirmed) - //(isConfirmed && isExpensive).ifTrue(Action.constant("true")).otherwise(Action.constant("false")) - // (isConfirmed && isExpensive).ifTrue(setGold ++ increasePoints) + val addPointsExpr = (LoyaltyAction.actionType := ActionType.AddPoints) ++ (LoyaltyAction.points := 100) + + val upgradeTierExpr = LoyaltyAction.actionType := ActionType.UpgradeTier + + val actions = Action.fromExpr(addPointsExpr) ++ Action.fromExpr(upgradeTierExpr) + + val rule = Rule(both, actions) + + val engine = RuleEngine.fromRuleSet(RuleSet(Vector(rule))) + + val facts = Facts.empty.add(FlightBooking.price, 100.0).add(FlightBooking.status, "Gold") } } + diff --git a/core/src/test/scala/net/degoes/afd/model/ExampleSpec.scala b/core/src/test/scala/net/degoes/afd/model/ExampleSpec.scala index 4e1d774..eca67a0 100644 --- a/core/src/test/scala/net/degoes/afd/model/ExampleSpec.scala +++ b/core/src/test/scala/net/degoes/afd/model/ExampleSpec.scala @@ -1,14 +1,64 @@ package net.degoes.afd +import zio._ import zio.test._ import zio.test.TestAspect._ object ExampleSpec extends ZIOSpecDefault { - def spec = - suite("ExampleSpec") { - test("example test") { - assertTrue(true) - } + import net.degoes.afd.ruleengine.graduation._ + import net.degoes.afd.ruleengine.graduation.loyalty._ + + val priceCondition = Condition(FlightBooking.price.get > 1000.0) + val upgradeTier = Action.fromExpr { + LoyaltyAction.actionType := ActionType.UpgradeTier } + val upgradeTierRule = Rule(priceCondition, upgradeTier) + + def emptyFacts[In, Out](action: Action[In, Out]): List[Out] = action.eval(Facts.empty.asInstanceOf[In]) + + def spec = + suite("EngineSpec") ( + test("empty ruleset produce no action"){ + val engine = RuleEngine.fromRuleSet(RuleSet(Vector())) + val facts = Facts.empty + assertTrue( engine.update(facts).isEmpty) + }, + test("insatisfied conditions produce no action"){ + val engine = RuleEngine.fromRuleSet(RuleSet(Vector(upgradeTierRule))) + val facts = Facts.empty.add(FlightBooking.price, 0.0) + val actions = engine.update(facts) + assertTrue(actions.isEmpty) + }, + test("satisfied conditions produce single action"){ + val engine = RuleEngine.fromRuleSet(RuleSet(Vector(upgradeTierRule))) + val facts = Facts.empty.add(FlightBooking.price, 2000.0) + val expectedAction = emptyFacts(upgradeTier) + val actions = engine.update(facts) + assertTrue(actions == Some(expectedAction)) + }, + test("satisfied composite conditions produce two actions"){ + val engine = RuleEngine.fromRuleSet(RuleSet(Vector(rule))) + val facts = Facts.empty.add(FlightBooking.price, 2000.0).add(FlightBooking.status, "Confirmed") + val actions = engine.update(facts) + assertTrue(actions.map(_.size).getOrElse(0) == 2) + }, + test("update loyalty program"){ + val engine = RuleEngine.fromRuleSet(RuleSet(Vector(rule))) + val facts = Facts.empty.add(FlightBooking.price, 2000.0).add(FlightBooking.status, "Confirmed") + val actions = engine.update(facts) + + import net.degoes.afd.examples.loyalty._ + import net.degoes.afd.examples.loyalty.LoyaltyTier._ + + val program = LoyaltyProgram("id", 0, LoyaltyTier.Bronze) + + val expectedProgram = LoyaltyProgram("id", 100, LoyaltyTier.Gold) + val updated = LoyaltyAction.update(program, actions.get) + + assertTrue(updated == expectedProgram) + }) + + + } \ No newline at end of file From 7ab508b888f8c497123f9b73d88befdc6c0bbd11 Mon Sep 17 00:00:00 2001 From: Jean-Luc CANELA Date: Sun, 2 Oct 2022 23:34:32 +0200 Subject: [PATCH 8/8] refactoring to graduation folder --- .../degoes/afd/ruleengine/07-graduation.scala | 740 ++---------------- .../ruleengine/graduation/EngineType.scala | 30 + .../afd/ruleengine/graduation/Expr.scala | 199 +++++ .../afd/ruleengine/graduation/Facts.scala | 62 ++ .../afd/ruleengine/graduation/FactsType.scala | 90 +++ .../afd/ruleengine/graduation/Numeric.scala | 116 +++ .../ruleengine/graduation/PrimitiveType.scala | 45 ++ .../ruleengine/graduation/RuleEngine.scala | 109 +++ .../net/degoes/afd/model/ExampleSpec.scala | 12 +- 9 files changed, 735 insertions(+), 668 deletions(-) create mode 100644 core/src/main/scala/net/degoes/afd/ruleengine/graduation/EngineType.scala create mode 100644 core/src/main/scala/net/degoes/afd/ruleengine/graduation/Expr.scala create mode 100644 core/src/main/scala/net/degoes/afd/ruleengine/graduation/Facts.scala create mode 100644 core/src/main/scala/net/degoes/afd/ruleengine/graduation/FactsType.scala create mode 100644 core/src/main/scala/net/degoes/afd/ruleengine/graduation/Numeric.scala create mode 100644 core/src/main/scala/net/degoes/afd/ruleengine/graduation/PrimitiveType.scala create mode 100644 core/src/main/scala/net/degoes/afd/ruleengine/graduation/RuleEngine.scala diff --git a/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala b/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala index de891ad..efae89c 100644 --- a/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala +++ b/core/src/main/scala/net/degoes/afd/ruleengine/07-graduation.scala @@ -18,637 +18,16 @@ import scala.language.implicitConversions * have a main function that executes a sample rule set on some sample data, and * prints out or executes the actions produced. */ -object graduation { +object graduation2 { + import graduation._ + import net.degoes.afd.examples.loyalty._ + import net.degoes.afd.examples.loyalty.LoyaltyTier._ - sealed trait EngineType[A] { - def equals(left: A, right: A): Boolean - def lessThan(left: A, right: A): Boolean - } - object EngineType { - - final case class Primitive[A](primitiveType: PrimitiveType[A]) extends EngineType[A] { - def equals(left: A, right: A): Boolean = primitiveType.ordering.equiv(left, right) - - def lessThan(left: A, right: A): Boolean = primitiveType.ordering.lt(left, right) - } - - final case class Composite[Fields](factsType: FactsType[Fields]) extends EngineType[Facts[Fields]] { - def equals(left: Facts[Fields], right: Facts[Fields]): Boolean = left == right - - def lessThan(left: Facts[Fields], right: Facts[Fields]): Boolean = left.lessThan(right) - } - - def fromPrimitive[A](implicit primitiveType: PrimitiveType[A]): EngineType[A] = - Primitive(primitiveType) - - def fromFacts[Types](facts: Facts[Types]): EngineType[Facts[Types]] = - EngineType.Composite(FactsType.fromFacts(facts)) - } - - sealed trait Numeric[A] { - - type NumericType = A - - import Expr.NumericBinOpType - import Expr.NumericBinOpType._ - - def add(left: A, right: A): A - - def subtract(left: A, right: A): A - - def multiply(left: A, right: A): A - - def divide(left: A, right: A): A - - def modulo(left: A, right: A): A - - import PrimitiveType._ - def primitiveType: PrimitiveType[A] = - (this match { - case _: Numeric.ByteIsNumeric.type => PrimitiveType.ByteType - case _: Numeric.CharIsNumeric.type => PrimitiveType.CharType - case _: Numeric.IntIsNumeric.type => PrimitiveType.IntType - case _: Numeric.LongIsNumeric.type => PrimitiveType.LongType - case _: Numeric.FloatIsNumeric.type => PrimitiveType.FloatType - case _: Numeric.DoubleIsNumeric.type => PrimitiveType.DoubleType - }).asInstanceOf[PrimitiveType[A]] - - def apply(binOp: NumericBinOpType)(left: A, right: A): A = - binOp match { - case Add => add(left, right) - case Subtract => subtract(left, right) - case Multiply => multiply(left, right) - case Divide => divide(left, right) - case Modulo => modulo(left, right) - } - - } - - object Numeric { - implicit case object ByteIsNumeric extends Numeric[Byte] { - def add(left: Byte, right: Byte): Byte = (left + right).toByte - - def subtract(left: Byte, right: Byte): Byte = (left - right).toByte - - def multiply(left: Byte, right: Byte): Byte = (left * right).toByte - - def divide(left: Byte, right: Byte): Byte = (left / right).toByte - - def modulo(left: Byte, right: Byte): Byte = (left % right).toByte - - } - implicit case object CharIsNumeric extends Numeric[Char] { - def add(left: Char, right: Char): Char = (left + right).toChar - - def subtract(left: Char, right: Char): Char = (left - right).toChar - - def multiply(left: Char, right: Char): Char = (left * right).toChar - - def divide(left: Char, right: Char): Char = (left / right).toChar - - def modulo(left: Char, right: Char): Char = (left % right).toChar - - } - implicit case object IntIsNumeric extends Numeric[Int] { - def add(left: Int, right: Int): Int = (left + right) - - def subtract(left: Int, right: Int): Int = (left - right) - - def multiply(left: Int, right: Int): Int = (left * right) - - def divide(left: Int, right: Int): Int = (left / right) - - def modulo(left: Int, right: Int): Int = (left % right) - - } - implicit case object LongIsNumeric extends Numeric[Long] { - def add(left: Long, right: Long): Long = (left + right) - - def subtract(left: Long, right: Long): Long = (left - right) - - def multiply(left: Long, right: Long): Long = (left * right) - - def divide(left: Long, right: Long): Long = (left / right) - - def modulo(left: Long, right: Long): Long = (left % right) - - } - implicit case object FloatIsNumeric extends Numeric[Float] { - def add(left: Float, right: Float): Float = (left + right) - - def subtract(left: Float, right: Float): Float = (left - right) - - def multiply(left: Float, right: Float): Float = (left * right) - - def divide(left: Float, right: Float): Float = (left / right) - - def modulo(left: Float, right: Float): Float = (left % right) - - } - implicit case object DoubleIsNumeric extends Numeric[Double] { - def add(left: Double, right: Double): Double = (left + right) - - def subtract(left: Double, right: Double): Double = (left - right) - - def multiply(left: Double, right: Double): Double = (left * right) - - def divide(left: Double, right: Double): Double = (left / right) - - def modulo(left: Double, right: Double): Double = (left % right) - - } - - } - - /** - * A type class that represents the supported types for fact values. - */ - @implicitNotFound("The type ${A} is not supported as a fact type and cannot be used for this method.") - sealed trait PrimitiveType[A] { - def ordering[T]: scala.math.Ordering[T] - } - - object PrimitiveType { - def apply[T](implicit factType: PrimitiveType[T]): PrimitiveType[T] = factType - - implicit case object IntType extends PrimitiveType[scala.Int] { - def ordering[T]: scala.math.Ordering[T] = Ordering[Int].asInstanceOf[Ordering[T]] - } - implicit case object LongType extends PrimitiveType[scala.Long]{ - def ordering[T]: scala.math.Ordering[T] = Ordering[Long].asInstanceOf[Ordering[T]] - } - implicit case object StringType extends PrimitiveType[java.lang.String]{ - def ordering[T]: scala.math.Ordering[T] = Ordering[String].asInstanceOf[Ordering[T]] - } - implicit case object DoubleType extends PrimitiveType[scala.Double]{ - def ordering[T]: scala.math.Ordering[T] = Ordering[Double].asInstanceOf[Ordering[T]] - } - implicit case object ByteType extends PrimitiveType[scala.Byte]{ - def ordering[T]: scala.math.Ordering[T] = Ordering[Byte].asInstanceOf[Ordering[T]] - } - implicit case object CharType extends PrimitiveType[scala.Char]{ - def ordering[T]: scala.math.Ordering[T] = Ordering[Char].asInstanceOf[Ordering[T]] - } - implicit case object FloatType extends PrimitiveType[scala.Float]{ - def ordering[T]: scala.math.Ordering[T] = Ordering[Float].asInstanceOf[Ordering[T]] - } - implicit case object BooleanType extends PrimitiveType[scala.Boolean]{ - def ordering[T]: scala.math.Ordering[T] = Ordering[Boolean].asInstanceOf[Ordering[T]] - } - implicit case object InstantType extends PrimitiveType[java.time.Instant]{ - def ordering[T]: scala.math.Ordering[T] = Ordering[java.time.Instant].asInstanceOf[Ordering[T]] - } - } - - sealed trait Expr[-In, +Out] { self => - - final def ++[In1 <: In, Fields1, Fields2](that: Expr[In1, Facts[Fields2]])(implicit - ev: Out <:< Facts[Fields1] - ): Expr[In1, Facts[Fields1 & Fields2]] = - Expr.CombineFacts(self.widen[Facts[Fields1]], that) - - final def +[In1 <: In, Out1 >: Out](that: Expr[In1, Out1])(implicit tag: Numeric[Out1]): Expr[In1, Out1] = - Expr.BinaryNumericOp(self.widen, that, Expr.NumericBinOpType.Add, tag) - - final def -[In1 <: In, Out1 >: Out](that: Expr[In1, Out1])(implicit tag: Numeric[Out1]): Expr[In1, Out1] = - Expr.BinaryNumericOp(self.widen, that, Expr.NumericBinOpType.Subtract, tag) - - final def *[In1 <: In, Out1 >: Out](that: Expr[In1, Out1])(implicit tag: Numeric[Out1]): Expr[In1, Out1] = - Expr.BinaryNumericOp(self.widen, that, Expr.NumericBinOpType.Multiply, tag) - - final def /[In1 <: In, Out1 >: Out](that: Expr[In1, Out1])(implicit tag: Numeric[Out1]): Expr[In1, Out1] = - Expr.BinaryNumericOp(self.widen, that, Expr.NumericBinOpType.Divide, tag) - - final def %[In1 <: In, Out1 >: Out](that: Expr[In1, Out1])(implicit tag: Numeric[Out1]): Expr[In1, Out1] = - Expr.BinaryNumericOp(self.widen, that, Expr.NumericBinOpType.Modulo, tag) - - final def &&[In1 <: In](that: Expr[In1, Boolean])(implicit ev: Out <:< Boolean): Expr[In1, Boolean] = - Expr.And(self.widen, that) - - final def ||[In1 <: In](that: Expr[In1, Boolean])(implicit ev: Out <:< Boolean): Expr[In1, Boolean] = - Expr.Or(self.widen, that) - - final def !=[In1 <: In, Out1 >: Out](that: Expr[In1, Out1]): Expr[In1, Boolean] = - !(self === that) - - final def <[In1 <: In, Out1 >: Out](that: Expr[In1, Out1]): Expr[In1, Boolean] = - Expr.LessThan(self, that) - - final def <=[In1 <: In, Out1 >: Out](that: Expr[In1, Out1]): Expr[In1, Boolean] = - Expr.LessThan(self, that) || (self === that) - - final def >[In1 <: In, Out1 >: Out](that: Expr[In1, Out1]): Expr[In1, Boolean] = - !(self <= that) - - final def >=[In1 <: In, Out1 >: Out](that: Expr[In1, Out1]): Expr[In1, Boolean] = - !(self < that) - - final def ===[In1 <: In, Out1 >: Out](that: Expr[In1, Out1]): Expr[In1, Boolean] = - Expr.EqualTo(self, that) - - final def >>>[Out2](that: Expr[Out, Out2]): Expr[In, Out2] = - Expr.Pipe(self, that) - - def eval(in: In): Out = Expr.eval(in, self) - - def ifTrue[In1 <: In, Out2](ifTrue: Expr[In1, Out2])(implicit ev: Out <:< Boolean): Expr.IfTrue[In1, Out2] = - Expr.IfTrue(self.widen[Boolean], ifTrue) - - final def unary_!(implicit ev: Out <:< Boolean): Expr[In, Boolean] = - Expr.Not(self.widen) - - final def widen[Out2](implicit ev: Out <:< Out2): Expr[In, Out2] = self.asInstanceOf[Expr[In, Out2]] - - } - - object Expr { - - final case class IfTrue[In, Out](condition: Expr[In, Boolean], ifTrue: Expr[In, Out]) { - def otherwise(ifFalse: Expr[In, Out]) = IfThenElse(condition, ifTrue, ifFalse) - } - - final case class Fact[In, K <: Singleton with String, V](factDef: FactDefinition.KeyValue[K, V], value: Expr[In, V]) - extends Expr[In, Facts[(K, V)]] - final case class CombineFacts[In, V1, V2]( - left: Expr[In, Facts[V1]], - right: Expr[In, Facts[V2]] - ) extends Expr[In, Facts[V1 & V2]] - final case class Constant[Out](value: Out, tag: EngineType[Out]) extends Expr[Any, Out] - final case class And[In](left: Expr[In, Boolean], right: Expr[In, Boolean]) extends Expr[In, Boolean] - final case class Or[In](left: Expr[In, Boolean], right: Expr[In, Boolean]) extends Expr[In, Boolean] - final case class Not[In](condition: Expr[In, Boolean]) extends Expr[In, Boolean] - final case class EqualTo[In, Out](lhs: Expr[In, Out], rhs: Expr[In, Out]) extends Expr[In, Boolean] - final case class LessThan[In, Out](lhs: Expr[In, Out], rhs: Expr[In, Out]) extends Expr[In, Boolean] - final case class Input[In](engineType: EngineType[In]) extends Expr[In, In] - final case class Get[In, K <: Singleton with String, V](expr: Expr[In, Facts[(K, V)]], factDef: FactDefinition.KeyValue[K, V]) extends Expr[In, V] - final case class Pipe[In, Out1, Out2](left: Expr[In, Out1], right: Expr[Out1, Out2]) extends Expr[In, Out2] - final case class BinaryNumericOp[In, Out]( - lhs: Expr[In, Out], - rhs: Expr[In, Out], - op: NumericBinOpType, - tag: Numeric[Out] - ) extends Expr[In, Out] - final case class IfThenElse[In, Out](condition: Expr[In, Boolean], ifTrue: Expr[In, Out], ifFalse: Expr[In, Out]) - extends Expr[In, Out] - sealed trait NumericBinOpType - object NumericBinOpType { - case object Add extends NumericBinOpType - case object Subtract extends NumericBinOpType - case object Multiply extends NumericBinOpType - case object Divide extends NumericBinOpType - case object Modulo extends NumericBinOpType - } - - implicit def apply[Out](out: Out)(implicit tag: PrimitiveType[Out]): Expr[Any, Out] = - Constant(out, EngineType.Primitive(tag)) - - implicit def apply[Out](out: Facts[Out]): Expr[Any, Facts[Out]] = - Constant(out, EngineType.fromFacts(out)) - - - private def eval[In, Out](in: In, expr: Expr[In, Out]): Out = evalWithType(in, expr)._2 - - private def evalWithType[In, Out](in: In, expr: Expr[In, Out]): (EngineType[Out], Out) = - expr match { - case Fact(factDef, value) => - implicit val tag = factDef.tag - - val result = Facts.empty.add(factDef, value) - (EngineType.fromFacts(result).asInstanceOf[EngineType[Out]], result) - - case CombineFacts(lhs, rhs) => - val left = eval(in, lhs) - val right = eval(in, rhs) - val results = left ++ right - (EngineType.fromFacts(results).asInstanceOf[EngineType[Out]], results) - - case Constant(value, tag) => - (tag.asInstanceOf[EngineType[Out]], value) - - case And(lhs, rhs) => - val left = eval(in, lhs) - val right = eval(in, rhs) - (EngineType.fromPrimitive[Boolean].asInstanceOf[EngineType[Out]], left && right) - - case Or(lhs, rhs) => - val left = eval(in, lhs) - val right = eval(in, rhs) - (EngineType.fromPrimitive[Boolean].asInstanceOf[EngineType[Out]], left || right) - - case Not(condition) => - (EngineType.fromPrimitive[Boolean].asInstanceOf[EngineType[Out]], !eval(in, condition)) - - case EqualTo(lhs, rhs) => - val (leftType, left) = evalWithType(in, lhs) - val (rightType, right) = evalWithType(in, rhs) - - import PrimitiveType._ - - (EngineType.fromPrimitive(PrimitiveType[Boolean]).asInstanceOf[EngineType[Out]], - leftType.equals(left, right)) - - case LessThan(lhs, rhs) => - val (leftType, left) = evalWithType(in, lhs) - val (rightType, right) = evalWithType(in, rhs) - - import PrimitiveType._ - - (EngineType.fromPrimitive(PrimitiveType[Boolean]).asInstanceOf[EngineType[Out]], - leftType.lessThan(left, right)) - - case Input(engineType) => - engineType.asInstanceOf[EngineType[Out]] -> in.asInstanceOf[Out] - - case Get(expr, fd) => - val facts = eval(in, expr) - - val value = Unsafe.unsafe { implicit u => - facts.unsafe.get(fd).asInstanceOf[Out] - } - (fd.tag.asInstanceOf[EngineType[Out]],value) - - case Pipe(lhs, rhs) => evalWithType(eval(in, lhs), rhs) - - case BinaryNumericOp(lhs, rhs, op, tag0) => - val tag = tag0.asInstanceOf[Numeric[Out]] - val left: Out = eval(in, lhs) - val right: Out = eval(in, rhs) - (EngineType.fromPrimitive(tag.primitiveType), tag(op)(left, right)) - - case IfThenElse(condition, ifTrue, ifFalse) => - val bool = eval(in, condition) - if (bool) evalWithType(in, ifTrue) - else evalWithType(in, ifFalse) - - } - - def fact[In, K <: Singleton with String, V](factDef: FactDefinition.KeyValue[K, V], value: Expr[In, V]) = - Fact(factDef, value) - - def ifThenElse[In, Out]( - condition: Expr[In, Boolean] - )(ifTrue: Expr[In, Out], ifFalse: Expr[In, Out]): Expr[In, Out] = - Expr.IfThenElse(condition, ifTrue, ifFalse) - - def input[A](engineType: EngineType[A]): Expr[A, A] = Input(engineType) - - } - - class FactsType[KeyValues] private (private val definitions: Chunk[FactDefinition[_]]) { self => - def ++ [KeyValues2](that: FactsType[KeyValues2]): FactsType[KeyValues & KeyValues2] = - new FactsType(self.definitions ++ that.definitions) - - def add[KeyValue](definition: FactDefinition[KeyValue]): FactsType[KeyValues & KeyValue] = - new FactsType(self.definitions :+ definition) - } - - object FactsType { - val empty: FactsType[Any] = new FactsType(Chunk.empty) - - def fromFacts[KeyValues](facts: Facts[KeyValues]): FactsType[KeyValues] = - new FactsType(facts.definitions) - } - - sealed trait FactDefinition[KeyValue] { self0 => - type Key <: Singleton with String - type Value - - def name: Key - - def tag: EngineType[Value] - - def self: FactDefinition.KeyValue[Key, Value] = self0.asInstanceOf[FactDefinition.KeyValue[Key, Value]] - - val singletonType: EngineType[Facts[(Key, Value)]] = EngineType.Composite(FactsType.empty.add[(Key, Value)](self)) - - def get: Expr[Facts[(Key, Value)], Value] = { - val factsExpr = Expr.input(singletonType) - - Expr.Get(factsExpr, self) - } - - def set[In](value: Expr[In, Value]): Expr[In, Facts[(Key, Value)]] = - Expr.fact(self, value) - - def :=[In](value: Expr[In, Value]) = set(value) - - override final def toString(): String = s"FactDefinition($name, $tag)" - } - - object FactDefinition { - - type KeyValue[K <: Singleton with String, V] = FactDefinition[(K, V)] { type Key = K; type Value = V } - - def apply[N <: Singleton with String, T](name0: N, fact0: EngineType[T]): KeyValue[N, T] = - new FactDefinition[(N, T)] { - type Key = N - type Value = T - def name: N = name0 - def tag: EngineType[T] = fact0 - } - - def facts[N <: Singleton with String, Fields](name: N, factsType: FactsType[Fields]): KeyValue[N, Facts[Fields]] = - FactDefinition[N, Facts[Fields]](name, EngineType.Composite(factsType)) - - def prim[N <: Singleton with String, T](name0: N)(implicit tag0: PrimitiveType[T]): KeyValue[N, T] = - new FactDefinition[(N, T)] { - type Key = N - type Value = T - def name: N = name0 - def tag: EngineType[T] = EngineType.Primitive(tag0) - } - - - def boolean[N <: Singleton with String](name0: N): KeyValue[N, Boolean] = FactDefinition.prim[N, Boolean](name0) - - def byte[N <: Singleton with String](name0: N): KeyValue[N, Byte] = FactDefinition.prim[N, Byte](name0) - - def char[N <: Singleton with String](name0: N): KeyValue[N, Char] = FactDefinition.prim[N, Char](name0) - - def int[N <: Singleton with String](name0: N): KeyValue[N, Int] = FactDefinition.prim[N, Int](name0) - - def long[N <: Singleton with String](name0: N): KeyValue[N, Long] = FactDefinition.prim[N, Long](name0) - - def float[N <: Singleton with String](name0: N): KeyValue[N, Float] = FactDefinition.prim[N, Float](name0) - - def double[N <: Singleton with String](name0: N): KeyValue[N, Double] = FactDefinition.prim[N, Double](name0) - - def string[N <: Singleton with String](name0: N): KeyValue[N, String] = FactDefinition.prim[N, String](name0) - - def instant[N <: Singleton with String](name0: N): KeyValue[N, java.time.Instant] = - FactDefinition.prim[N, java.time.Instant](name0) - - } - - final case class RuleEngine[-In, +Out](update: In => Option[List[Out]]) { self => - def contramap[In2](f: In2 => In): RuleEngine[In2, Out] = - RuleEngine(in => self.update(f(in))) - - def orElse[In1 <: In, Out1 >: Out](that: RuleEngine[In1, Out1]): RuleEngine[In1, Out1] = - RuleEngine(in => self.update(in) orElse that.update(in)) - - def updateWith[Out1 >: Out](in: In)(defaultOut: Out1, combine: (Out1, Out1) => Out1): Out1 = - self.update(in) match { - case None => defaultOut - case Some(outs) => - outs.reduceOption(combine).getOrElse(defaultOut) - } - } - - object RuleEngine { - val empty: RuleEngine[Any, Nothing] = RuleEngine(_ => None) - - def constant[Out](out: Out): RuleEngine[Any, Out] = fromFunction(_ => out) - - def fromFunction[In, Out](f: In => Out): RuleEngine[In, Out] = RuleEngine(in => Some(List(f(in)))) - - def fromRuleSet[In, Out](ruleSet: RuleSet[In, Out]): RuleEngine[In, Out] = { - val update: In => Option[List[Out]] = execute(ruleSet, _) - - RuleEngine(update) - } - - private def execute[In, Out](ruleSet: RuleSet[In, Out], in: In): Option[List[Out]] = - ruleSet.rules.find(_.condition.eval(in)).map { rule => - rule.action.eval(in) - } - } - - final case class Rule[-In, +Out](condition: Condition[In], action: Action[In, Out]) - - final case class RuleSet[-In, +Out](rules: Vector[Rule[In, Out]]) { self => - - def +[In1 <: In, Out1 >: Out](that: Rule[In1, Out1]): RuleSet[In1, Out1] = - RuleSet(self.rules :+ that) - - def ++[In1 <: In, Out1 >: Out](that: RuleSet[In1, Out1]): RuleSet[In1, Out1] = - RuleSet(self.rules ++ that.rules) - - def addRule[In1 <: In, Out1 >: Out](that: Rule[In1, Out1]): RuleSet[In1, Out1] = - self + that - } - - object RuleSet { - - def apply[In, Out](rule1: Rule[In, Out], rules: Rule[In, Out]*): RuleSet[In, Out] = - RuleSet(rule1 +: rules.toVector) - - val empty: RuleSet[Any, Nothing] = RuleSet(Vector.empty) - - } - - /** - * Contains a collection of facts, whose structure is described by a phantom - * type parameter. - */ - sealed abstract case class Facts[+Types] private (private val data: Map[FactDefinition[_], Any]) { self => - def ++[Types2](that: Facts[Types2]): Facts[Types & Types2] = - new Facts[Types & Types2](data ++ that.data) {} - - def definitions: Chunk[FactDefinition[_]] = Chunk.fromIterable(data.keys) - - override def equals(that: Any): Boolean = - that match { - case that: Facts[_] => self.data == that.data - case _ => false - } - - def lessThan(that: Facts[_]) = false - - def get[Key <: Singleton with String, Value: PrimitiveType](pd: FactDefinition[(Key, Value)])(implicit - subset: Types <:< (Key, Value) - ): Value = - data(pd).asInstanceOf[Value] - - /** - * Returns a new facts collection with the specified primitive fact added. - */ - def add[Key <: Singleton with String, Value]( - pd: FactDefinition.KeyValue[Key, Value], - value: Value - ): Facts[Types & (Key, Value)] = - new Facts[Types & (Key, Value)](data + (pd -> value)) {} - - /** - * Returns a new facts collection with the specified fact added. - */ - def add[Key <: Singleton with String, Types2]( - pd: FactDefinition.KeyValue[Key, Facts[Types2]], - value: Facts[Types2] - ): Facts[Types & (Key, Facts[Types2])] = - new Facts[Types & (Key, Facts[Types2])](data + (pd -> value)) {} - - - object unsafe { - def get(pd: FactDefinition[_])(implicit unsafe: Unsafe): Any = data(pd) - } - } - object Facts { - - /** - * An empty facts collection. - */ - val empty: Facts[Any] = new Facts[Any](Map.empty) {} - - def engineTypeOf[Types](facts: Facts[Types]): EngineType[Facts[Types]] = - EngineType.Composite(FactsType.fromFacts(facts)) - - } - - final case class Condition[-In](expr: Expr[In, Boolean]) { self => - - def eval(in: In) = expr.eval(in) - - def &&[In1 <: In](that: Condition[In1]): Condition[In1] = - Condition(self.expr && that.expr) - - def ||[In1 <: In](that: Condition[In1]): Condition[In1] = - Condition(self.expr && that.expr) - - def unary_! : Condition[In] = Condition(!expr) - } - - object Condition { - val always: Condition[Any] = constant(true) - val never: Condition[Any] = constant(false) - - def constant[In](value: Boolean): Condition[In] = Condition(Expr(value)) - } - - sealed trait Action[-In, +Out] { self => - def ++[In1 <: In, Out1 >: Out](that: Action[In1, Out1]): Action[In1, Out1] = - Action.Concat(self, that) - - def >>>[Out2](that: Action[Out, Out2]): Action[In, Out2] = - Action.Pipe(self, that) - - def eval(in: In): List[Out] = - self match { - case Action.Concat(left, right) => - left.eval(in) ++ right.eval(in) - - case Action.Pipe(lhs, rhs) => lhs.eval(in).flatMap(rhs.eval(_)) - - case Action.FromExpr(expr) => - List(expr.eval(in)) - } - } - object Action { - final case class Concat[In, Out](left: Action[In, Out], right: Action[In, Out]) extends Action[In, Out] - final case class Pipe[In, Out1, Out2](left: Action[In, Out1], right: Action[Out1, Out2]) extends Action[In, Out2] - final case class FromExpr[In, Out](expr: Expr[In, Out]) extends Action[In, Out] - - def fromExpr[In, Out](expr: Expr[In, Out]): Action[In, Out] = FromExpr(expr) - - } - - object loyalty { - import net.degoes.afd.examples.loyalty._ - import net.degoes.afd.examples.loyalty.LoyaltyTier._ + object loyalty_model { object Flights { - val id = FactDefinition.string("id") + val id = FactDefinition.string("id") val number = FactDefinition.string("number") val factsType = @@ -656,8 +35,8 @@ object graduation { } object Customer { - val id = FactDefinition.string("id") - val name = FactDefinition.string("name") + val id = FactDefinition.string("id") + val name = FactDefinition.string("name") val email = FactDefinition.string("email") val phone = FactDefinition.string("phone") @@ -668,11 +47,14 @@ object graduation { object FlightBooking { val id = FactDefinition.string("id") val customer = FactDefinition.facts("customer", Customer.factsType) // FIXME: Support nested data - val flight = FactDefinition.facts("fligh", Flights.factsType) // FactDefinition.string("flight") // FIXME: Suppor nested data - val price = FactDefinition.double("price") - val status = FactDefinition.string("status") + val flight = FactDefinition.facts( + "fligh", + Flights.factsType + ) // FactDefinition.string("flight") // FIXME: Suppor nested data + val price = FactDefinition.double("price") + val status = FactDefinition.string("status") - val factsType = + val factsType = FactsType.empty.add(id).add(customer).add(flight).add(price).add(status) } @@ -689,51 +71,85 @@ object graduation { object LoyaltyAction { val actionType = FactDefinition.string("action_type") - val points = FactDefinition.int("points") - val customer = FactDefinition.string("customer") + val points = FactDefinition.int("points") + val customer = FactDefinition.string("customer") - def update(program: LoyaltyProgram, action: Facts[_]): LoyaltyProgram = - Unsafe.unsafe { implicit u => - action.unsafe.get(actionType) match { - case ActionType.DowngradeTier => program.copy(tier = LoyaltyTier.Bronze) - case ActionType.UpgradeTier => program.copy(tier = LoyaltyTier.Gold) - case ActionType.AddPoints => action.unsafe.get(points) match { - case Expr.Constant(value: Int, _) => program.copy(points = value) - case _ => program - } - case _ => program - } - } - - def update(program: LoyaltyProgram, actions: List[Facts[_]]) : LoyaltyProgram = - actions.foldLeft(program)( (program, action) => update(program, action)) } - + object ActionType { - val AddPoints = Expr("add_points") - val UpgradeTier = Expr("upgrade_tier") + val AddPoints = Expr("add_points") + val UpgradeTier = Expr("upgrade_tier") val DowngradeTier = Expr("downgrade_tier") } + } + + object ActionExecutor { + + import loyalty_model._ + + def update(program: LoyaltyProgram, action: Facts[_]): LoyaltyProgram = + Unsafe.unsafe { implicit u => + action.unsafe.get(LoyaltyAction.actionType) match { + case ActionType.DowngradeTier => program.copy(tier = LoyaltyTier.Bronze) + case ActionType.UpgradeTier => program.copy(tier = LoyaltyTier.Gold) + case ActionType.AddPoints => + action.unsafe.get(LoyaltyAction.points) match { + case Expr.Constant(value: Int, _) => program.copy(points = value) + case _ => program + } + case _ => program + } + } + + def update(program: LoyaltyProgram, actions: List[Facts[_]]): LoyaltyProgram = + actions.foldLeft(program)((program, action) => update(program, action)) + } + + object fixture { + + import loyalty_model._ val statusCondition = Condition(FlightBooking.status.get === FlightBookingStatus.Confirmed) - + val priceCondition = Condition(FlightBooking.price.get > 1000.0) - + val both = statusCondition && priceCondition - + val addPointsExpr = (LoyaltyAction.actionType := ActionType.AddPoints) ++ (LoyaltyAction.points := 100) - + val upgradeTierExpr = LoyaltyAction.actionType := ActionType.UpgradeTier - + val actions = Action.fromExpr(addPointsExpr) ++ Action.fromExpr(upgradeTierExpr) + + val rule = Rule(both, actions) + + val facts = Facts.empty.add(FlightBooking.price, 2000.0).add(FlightBooking.status, "Confirmed") + + } - val rule = Rule(both, actions) +} - val engine = RuleEngine.fromRuleSet(RuleSet(Vector(rule))) +object LoyaltyExample { - val facts = Facts.empty.add(FlightBooking.price, 100.0).add(FlightBooking.status, "Gold") + def main(args: Array[String]) = { + import graduation._ + import graduation2._ + import fixture._ + import loyalty_model._ + + val engine = RuleEngine.fromRuleSet(RuleSet(Vector(rule))) + val actions = engine.update(facts) + + val loyaltyProgram = { + import net.degoes.afd.examples.loyalty._ + import net.degoes.afd.examples.loyalty.LoyaltyTier._ + + LoyaltyProgram("id", 0, LoyaltyTier.Bronze) + } + + val updated = actions.map(ActionExecutor.update(loyaltyProgram, _)).getOrElse(loyaltyProgram) + println(s"loyalty program:\n$loyaltyProgram\n\nupdated:\n$updated") } } - diff --git a/core/src/main/scala/net/degoes/afd/ruleengine/graduation/EngineType.scala b/core/src/main/scala/net/degoes/afd/ruleengine/graduation/EngineType.scala new file mode 100644 index 0000000..212ba96 --- /dev/null +++ b/core/src/main/scala/net/degoes/afd/ruleengine/graduation/EngineType.scala @@ -0,0 +1,30 @@ +package net.degoes.afd.ruleengine.graduation + +import zio._ +import scala.annotation._ +import scala.language.implicitConversions + +sealed trait EngineType[A] { + def equals(left: A, right: A): Boolean + def lessThan(left: A, right: A): Boolean +} +object EngineType { + + final case class Primitive[A](primitiveType: PrimitiveType[A]) extends EngineType[A] { + def equals(left: A, right: A): Boolean = primitiveType.ordering.equiv(left, right) + + def lessThan(left: A, right: A): Boolean = primitiveType.ordering.lt(left, right) + } + + final case class Composite[Fields](factsType: FactsType[Fields]) extends EngineType[Facts[Fields]] { + def equals(left: Facts[Fields], right: Facts[Fields]): Boolean = left == right + + def lessThan(left: Facts[Fields], right: Facts[Fields]): Boolean = left.lessThan(right) + } + + def fromPrimitive[A](implicit primitiveType: PrimitiveType[A]): EngineType[A] = + Primitive(primitiveType) + + def fromFacts[Types](facts: Facts[Types]): EngineType[Facts[Types]] = + EngineType.Composite(FactsType.fromFacts(facts)) +} diff --git a/core/src/main/scala/net/degoes/afd/ruleengine/graduation/Expr.scala b/core/src/main/scala/net/degoes/afd/ruleengine/graduation/Expr.scala new file mode 100644 index 0000000..a7786b1 --- /dev/null +++ b/core/src/main/scala/net/degoes/afd/ruleengine/graduation/Expr.scala @@ -0,0 +1,199 @@ +package net.degoes.afd.ruleengine.graduation + +import zio._ +import scala.annotation._ +import scala.language.implicitConversions + +sealed trait Expr[-In, +Out] { self => + + final def ++[In1 <: In, Fields1, Fields2](that: Expr[In1, Facts[Fields2]])(implicit + ev: Out <:< Facts[Fields1] + ): Expr[In1, Facts[Fields1 & Fields2]] = + Expr.CombineFacts(self.widen[Facts[Fields1]], that) + + final def +[In1 <: In, Out1 >: Out](that: Expr[In1, Out1])(implicit tag: Numeric[Out1]): Expr[In1, Out1] = + Expr.BinaryNumericOp(self.widen, that, Expr.NumericBinOpType.Add, tag) + + final def -[In1 <: In, Out1 >: Out](that: Expr[In1, Out1])(implicit tag: Numeric[Out1]): Expr[In1, Out1] = + Expr.BinaryNumericOp(self.widen, that, Expr.NumericBinOpType.Subtract, tag) + + final def *[In1 <: In, Out1 >: Out](that: Expr[In1, Out1])(implicit tag: Numeric[Out1]): Expr[In1, Out1] = + Expr.BinaryNumericOp(self.widen, that, Expr.NumericBinOpType.Multiply, tag) + + final def /[In1 <: In, Out1 >: Out](that: Expr[In1, Out1])(implicit tag: Numeric[Out1]): Expr[In1, Out1] = + Expr.BinaryNumericOp(self.widen, that, Expr.NumericBinOpType.Divide, tag) + + final def %[In1 <: In, Out1 >: Out](that: Expr[In1, Out1])(implicit tag: Numeric[Out1]): Expr[In1, Out1] = + Expr.BinaryNumericOp(self.widen, that, Expr.NumericBinOpType.Modulo, tag) + + final def &&[In1 <: In](that: Expr[In1, Boolean])(implicit ev: Out <:< Boolean): Expr[In1, Boolean] = + Expr.And(self.widen, that) + + final def ||[In1 <: In](that: Expr[In1, Boolean])(implicit ev: Out <:< Boolean): Expr[In1, Boolean] = + Expr.Or(self.widen, that) + + final def !=[In1 <: In, Out1 >: Out](that: Expr[In1, Out1]): Expr[In1, Boolean] = + !(self === that) + + final def <[In1 <: In, Out1 >: Out](that: Expr[In1, Out1]): Expr[In1, Boolean] = + Expr.LessThan(self, that) + + final def <=[In1 <: In, Out1 >: Out](that: Expr[In1, Out1]): Expr[In1, Boolean] = + Expr.LessThan(self, that) || (self === that) + + final def >[In1 <: In, Out1 >: Out](that: Expr[In1, Out1]): Expr[In1, Boolean] = + !(self <= that) + + final def >=[In1 <: In, Out1 >: Out](that: Expr[In1, Out1]): Expr[In1, Boolean] = + !(self < that) + + final def ===[In1 <: In, Out1 >: Out](that: Expr[In1, Out1]): Expr[In1, Boolean] = + Expr.EqualTo(self, that) + + final def >>>[Out2](that: Expr[Out, Out2]): Expr[In, Out2] = + Expr.Pipe(self, that) + + def eval(in: In): Out = Expr.eval(in, self) + + def ifTrue[In1 <: In, Out2](ifTrue: Expr[In1, Out2])(implicit ev: Out <:< Boolean): Expr.IfTrue[In1, Out2] = + Expr.IfTrue(self.widen[Boolean], ifTrue) + + final def unary_!(implicit ev: Out <:< Boolean): Expr[In, Boolean] = + Expr.Not(self.widen) + + final def widen[Out2](implicit ev: Out <:< Out2): Expr[In, Out2] = self.asInstanceOf[Expr[In, Out2]] + +} + +object Expr { + + final case class IfTrue[In, Out](condition: Expr[In, Boolean], ifTrue: Expr[In, Out]) { + def otherwise(ifFalse: Expr[In, Out]) = IfThenElse(condition, ifTrue, ifFalse) + } + + final case class Fact[In, K <: Singleton with String, V](factDef: FactDefinition.KeyValue[K, V], value: Expr[In, V]) + extends Expr[In, Facts[(K, V)]] + final case class CombineFacts[In, V1, V2]( + left: Expr[In, Facts[V1]], + right: Expr[In, Facts[V2]] + ) extends Expr[In, Facts[V1 & V2]] + final case class Constant[Out](value: Out, tag: EngineType[Out]) extends Expr[Any, Out] + final case class And[In](left: Expr[In, Boolean], right: Expr[In, Boolean]) extends Expr[In, Boolean] + final case class Or[In](left: Expr[In, Boolean], right: Expr[In, Boolean]) extends Expr[In, Boolean] + final case class Not[In](condition: Expr[In, Boolean]) extends Expr[In, Boolean] + final case class EqualTo[In, Out](lhs: Expr[In, Out], rhs: Expr[In, Out]) extends Expr[In, Boolean] + final case class LessThan[In, Out](lhs: Expr[In, Out], rhs: Expr[In, Out]) extends Expr[In, Boolean] + final case class Input[In](engineType: EngineType[In]) extends Expr[In, In] + final case class Get[In, K <: Singleton with String, V]( + expr: Expr[In, Facts[(K, V)]], + factDef: FactDefinition.KeyValue[K, V] + ) extends Expr[In, V] + final case class Pipe[In, Out1, Out2](left: Expr[In, Out1], right: Expr[Out1, Out2]) extends Expr[In, Out2] + final case class BinaryNumericOp[In, Out]( + lhs: Expr[In, Out], + rhs: Expr[In, Out], + op: NumericBinOpType, + tag: Numeric[Out] + ) extends Expr[In, Out] + final case class IfThenElse[In, Out](condition: Expr[In, Boolean], ifTrue: Expr[In, Out], ifFalse: Expr[In, Out]) + extends Expr[In, Out] + sealed trait NumericBinOpType + object NumericBinOpType { + case object Add extends NumericBinOpType + case object Subtract extends NumericBinOpType + case object Multiply extends NumericBinOpType + case object Divide extends NumericBinOpType + case object Modulo extends NumericBinOpType + } + + implicit def apply[Out](out: Out)(implicit tag: PrimitiveType[Out]): Expr[Any, Out] = + Constant(out, EngineType.Primitive(tag)) + + implicit def apply[Out](out: Facts[Out]): Expr[Any, Facts[Out]] = + Constant(out, EngineType.fromFacts(out)) + + private def eval[In, Out](in: In, expr: Expr[In, Out]): Out = evalWithType(in, expr)._2 + + private def evalWithType[In, Out](in: In, expr: Expr[In, Out]): (EngineType[Out], Out) = + expr match { + case Fact(factDef, value) => + implicit val tag = factDef.tag + + val result = Facts.empty.add(factDef, value) + (EngineType.fromFacts(result).asInstanceOf[EngineType[Out]], result) + + case CombineFacts(lhs, rhs) => + val left = eval(in, lhs) + val right = eval(in, rhs) + val results = left ++ right + (EngineType.fromFacts(results).asInstanceOf[EngineType[Out]], results) + + case Constant(value, tag) => + (tag.asInstanceOf[EngineType[Out]], value) + + case And(lhs, rhs) => + val left = eval(in, lhs) + val right = eval(in, rhs) + (EngineType.fromPrimitive[Boolean].asInstanceOf[EngineType[Out]], left && right) + + case Or(lhs, rhs) => + val left = eval(in, lhs) + val right = eval(in, rhs) + (EngineType.fromPrimitive[Boolean].asInstanceOf[EngineType[Out]], left || right) + + case Not(condition) => + (EngineType.fromPrimitive[Boolean].asInstanceOf[EngineType[Out]], !eval(in, condition)) + + case EqualTo(lhs, rhs) => + val (leftType, left) = evalWithType(in, lhs) + val (rightType, right) = evalWithType(in, rhs) + + import PrimitiveType._ + + (EngineType.fromPrimitive(PrimitiveType[Boolean]).asInstanceOf[EngineType[Out]], leftType.equals(left, right)) + + case LessThan(lhs, rhs) => + val (leftType, left) = evalWithType(in, lhs) + val (rightType, right) = evalWithType(in, rhs) + + import PrimitiveType._ + + (EngineType.fromPrimitive(PrimitiveType[Boolean]).asInstanceOf[EngineType[Out]], leftType.lessThan(left, right)) + + case Input(engineType) => + engineType.asInstanceOf[EngineType[Out]] -> in.asInstanceOf[Out] + + case Get(expr, fd) => + val facts = eval(in, expr) + + val value = Unsafe.unsafe { implicit u => + facts.unsafe.get(fd).asInstanceOf[Out] + } + (fd.tag.asInstanceOf[EngineType[Out]], value) + + case Pipe(lhs, rhs) => evalWithType(eval(in, lhs), rhs) + + case BinaryNumericOp(lhs, rhs, op, tag0) => + val tag = tag0.asInstanceOf[Numeric[Out]] + val left: Out = eval(in, lhs) + val right: Out = eval(in, rhs) + (EngineType.fromPrimitive(tag.primitiveType), tag(op)(left, right)) + + case IfThenElse(condition, ifTrue, ifFalse) => + val bool = eval(in, condition) + if (bool) evalWithType(in, ifTrue) + else evalWithType(in, ifFalse) + + } + + def fact[In, K <: Singleton with String, V](factDef: FactDefinition.KeyValue[K, V], value: Expr[In, V]) = + Fact(factDef, value) + + def ifThenElse[In, Out]( + condition: Expr[In, Boolean] + )(ifTrue: Expr[In, Out], ifFalse: Expr[In, Out]): Expr[In, Out] = + Expr.IfThenElse(condition, ifTrue, ifFalse) + + def input[A](engineType: EngineType[A]): Expr[A, A] = Input(engineType) + +} diff --git a/core/src/main/scala/net/degoes/afd/ruleengine/graduation/Facts.scala b/core/src/main/scala/net/degoes/afd/ruleengine/graduation/Facts.scala new file mode 100644 index 0000000..0ec1db5 --- /dev/null +++ b/core/src/main/scala/net/degoes/afd/ruleengine/graduation/Facts.scala @@ -0,0 +1,62 @@ +package net.degoes.afd.ruleengine.graduation + +import zio._ +import scala.annotation._ +import scala.language.implicitConversions + +/** + * Contains a collection of facts, whose structure is described by a phantom + * type parameter. + */ +sealed abstract case class Facts[+Types] private (private val data: Map[FactDefinition[_], Any]) { self => + def ++[Types2](that: Facts[Types2]): Facts[Types & Types2] = + new Facts[Types & Types2](data ++ that.data) {} + + def definitions: Chunk[FactDefinition[_]] = Chunk.fromIterable(data.keys) + + override def equals(that: Any): Boolean = + that match { + case that: Facts[_] => self.data == that.data + case _ => false + } + + def lessThan(that: Facts[_]) = false + + def get[Key <: Singleton with String, Value: PrimitiveType](pd: FactDefinition[(Key, Value)])(implicit + subset: Types <:< (Key, Value) + ): Value = + data(pd).asInstanceOf[Value] + + /** + * Returns a new facts collection with the specified primitive fact added. + */ + def add[Key <: Singleton with String, Value]( + pd: FactDefinition.KeyValue[Key, Value], + value: Value + ): Facts[Types & (Key, Value)] = + new Facts[Types & (Key, Value)](data + (pd -> value)) {} + + /** + * Returns a new facts collection with the specified fact added. + */ + def add[Key <: Singleton with String, Types2]( + pd: FactDefinition.KeyValue[Key, Facts[Types2]], + value: Facts[Types2] + ): Facts[Types & (Key, Facts[Types2])] = + new Facts[Types & (Key, Facts[Types2])](data + (pd -> value)) {} + + object unsafe { + def get(pd: FactDefinition[_])(implicit unsafe: Unsafe): Any = data(pd) + } +} +object Facts { + + /** + * An empty facts collection. + */ + val empty: Facts[Any] = new Facts[Any](Map.empty) {} + + def engineTypeOf[Types](facts: Facts[Types]): EngineType[Facts[Types]] = + EngineType.Composite(FactsType.fromFacts(facts)) + +} diff --git a/core/src/main/scala/net/degoes/afd/ruleengine/graduation/FactsType.scala b/core/src/main/scala/net/degoes/afd/ruleengine/graduation/FactsType.scala new file mode 100644 index 0000000..163d7de --- /dev/null +++ b/core/src/main/scala/net/degoes/afd/ruleengine/graduation/FactsType.scala @@ -0,0 +1,90 @@ +package net.degoes.afd.ruleengine.graduation + +import zio._ +import scala.annotation._ +import scala.language.implicitConversions + +class FactsType[KeyValues] private (private val definitions: Chunk[FactDefinition[_]]) { self => + def ++[KeyValues2](that: FactsType[KeyValues2]): FactsType[KeyValues & KeyValues2] = + new FactsType(self.definitions ++ that.definitions) + + def add[KeyValue](definition: FactDefinition[KeyValue]): FactsType[KeyValues & KeyValue] = + new FactsType(self.definitions :+ definition) +} + +object FactsType { + val empty: FactsType[Any] = new FactsType(Chunk.empty) + + def fromFacts[KeyValues](facts: Facts[KeyValues]): FactsType[KeyValues] = + new FactsType(facts.definitions) +} + +sealed trait FactDefinition[KeyValue] { self0 => + type Key <: Singleton with String + type Value + + def name: Key + + def tag: EngineType[Value] + + def self: FactDefinition.KeyValue[Key, Value] = self0.asInstanceOf[FactDefinition.KeyValue[Key, Value]] + + val singletonType: EngineType[Facts[(Key, Value)]] = EngineType.Composite(FactsType.empty.add[(Key, Value)](self)) + + def get: Expr[Facts[(Key, Value)], Value] = { + val factsExpr = Expr.input(singletonType) + + Expr.Get(factsExpr, self) + } + + def set[In](value: Expr[In, Value]): Expr[In, Facts[(Key, Value)]] = + Expr.fact(self, value) + + def :=[In](value: Expr[In, Value]) = set(value) + + override final def toString(): String = s"FactDefinition($name, $tag)" +} + +object FactDefinition { + + type KeyValue[K <: Singleton with String, V] = FactDefinition[(K, V)] { type Key = K; type Value = V } + + def apply[N <: Singleton with String, T](name0: N, fact0: EngineType[T]): KeyValue[N, T] = + new FactDefinition[(N, T)] { + type Key = N + type Value = T + def name: N = name0 + def tag: EngineType[T] = fact0 + } + + def facts[N <: Singleton with String, Fields](name: N, factsType: FactsType[Fields]): KeyValue[N, Facts[Fields]] = + FactDefinition[N, Facts[Fields]](name, EngineType.Composite(factsType)) + + def prim[N <: Singleton with String, T](name0: N)(implicit tag0: PrimitiveType[T]): KeyValue[N, T] = + new FactDefinition[(N, T)] { + type Key = N + type Value = T + def name: N = name0 + def tag: EngineType[T] = EngineType.Primitive(tag0) + } + + def boolean[N <: Singleton with String](name0: N): KeyValue[N, Boolean] = FactDefinition.prim[N, Boolean](name0) + + def byte[N <: Singleton with String](name0: N): KeyValue[N, Byte] = FactDefinition.prim[N, Byte](name0) + + def char[N <: Singleton with String](name0: N): KeyValue[N, Char] = FactDefinition.prim[N, Char](name0) + + def int[N <: Singleton with String](name0: N): KeyValue[N, Int] = FactDefinition.prim[N, Int](name0) + + def long[N <: Singleton with String](name0: N): KeyValue[N, Long] = FactDefinition.prim[N, Long](name0) + + def float[N <: Singleton with String](name0: N): KeyValue[N, Float] = FactDefinition.prim[N, Float](name0) + + def double[N <: Singleton with String](name0: N): KeyValue[N, Double] = FactDefinition.prim[N, Double](name0) + + def string[N <: Singleton with String](name0: N): KeyValue[N, String] = FactDefinition.prim[N, String](name0) + + def instant[N <: Singleton with String](name0: N): KeyValue[N, java.time.Instant] = + FactDefinition.prim[N, java.time.Instant](name0) + +} diff --git a/core/src/main/scala/net/degoes/afd/ruleengine/graduation/Numeric.scala b/core/src/main/scala/net/degoes/afd/ruleengine/graduation/Numeric.scala new file mode 100644 index 0000000..21fa74d --- /dev/null +++ b/core/src/main/scala/net/degoes/afd/ruleengine/graduation/Numeric.scala @@ -0,0 +1,116 @@ +package net.degoes.afd.ruleengine.graduation + +sealed trait Numeric[A] { + + type NumericType = A + + import Expr.NumericBinOpType + import Expr.NumericBinOpType._ + + def add(left: A, right: A): A + + def subtract(left: A, right: A): A + + def multiply(left: A, right: A): A + + def divide(left: A, right: A): A + + def modulo(left: A, right: A): A + + import PrimitiveType._ + def primitiveType: PrimitiveType[A] = + (this match { + case _: Numeric.ByteIsNumeric.type => PrimitiveType.ByteType + case _: Numeric.CharIsNumeric.type => PrimitiveType.CharType + case _: Numeric.IntIsNumeric.type => PrimitiveType.IntType + case _: Numeric.LongIsNumeric.type => PrimitiveType.LongType + case _: Numeric.FloatIsNumeric.type => PrimitiveType.FloatType + case _: Numeric.DoubleIsNumeric.type => PrimitiveType.DoubleType + }).asInstanceOf[PrimitiveType[A]] + + def apply(binOp: NumericBinOpType)(left: A, right: A): A = + binOp match { + case Add => add(left, right) + case Subtract => subtract(left, right) + case Multiply => multiply(left, right) + case Divide => divide(left, right) + case Modulo => modulo(left, right) + } + +} + +object Numeric { + implicit case object ByteIsNumeric extends Numeric[Byte] { + def add(left: Byte, right: Byte): Byte = (left + right).toByte + + def subtract(left: Byte, right: Byte): Byte = (left - right).toByte + + def multiply(left: Byte, right: Byte): Byte = (left * right).toByte + + def divide(left: Byte, right: Byte): Byte = (left / right).toByte + + def modulo(left: Byte, right: Byte): Byte = (left % right).toByte + + } + implicit case object CharIsNumeric extends Numeric[Char] { + def add(left: Char, right: Char): Char = (left + right).toChar + + def subtract(left: Char, right: Char): Char = (left - right).toChar + + def multiply(left: Char, right: Char): Char = (left * right).toChar + + def divide(left: Char, right: Char): Char = (left / right).toChar + + def modulo(left: Char, right: Char): Char = (left % right).toChar + + } + implicit case object IntIsNumeric extends Numeric[Int] { + def add(left: Int, right: Int): Int = (left + right) + + def subtract(left: Int, right: Int): Int = (left - right) + + def multiply(left: Int, right: Int): Int = (left * right) + + def divide(left: Int, right: Int): Int = (left / right) + + def modulo(left: Int, right: Int): Int = (left % right) + + } + implicit case object LongIsNumeric extends Numeric[Long] { + def add(left: Long, right: Long): Long = (left + right) + + def subtract(left: Long, right: Long): Long = (left - right) + + def multiply(left: Long, right: Long): Long = (left * right) + + def divide(left: Long, right: Long): Long = (left / right) + + def modulo(left: Long, right: Long): Long = (left % right) + + } + implicit case object FloatIsNumeric extends Numeric[Float] { + def add(left: Float, right: Float): Float = (left + right) + + def subtract(left: Float, right: Float): Float = (left - right) + + def multiply(left: Float, right: Float): Float = (left * right) + + def divide(left: Float, right: Float): Float = (left / right) + + def modulo(left: Float, right: Float): Float = (left % right) + + } + implicit case object DoubleIsNumeric extends Numeric[Double] { + def add(left: Double, right: Double): Double = (left + right) + + def subtract(left: Double, right: Double): Double = (left - right) + + def multiply(left: Double, right: Double): Double = (left * right) + + def divide(left: Double, right: Double): Double = (left / right) + + def modulo(left: Double, right: Double): Double = (left % right) + + } + +} diff --git a/core/src/main/scala/net/degoes/afd/ruleengine/graduation/PrimitiveType.scala b/core/src/main/scala/net/degoes/afd/ruleengine/graduation/PrimitiveType.scala new file mode 100644 index 0000000..4bba80b --- /dev/null +++ b/core/src/main/scala/net/degoes/afd/ruleengine/graduation/PrimitiveType.scala @@ -0,0 +1,45 @@ +package net.degoes.afd.ruleengine.graduation + +import zio._ +import scala.annotation._ +import scala.language.implicitConversions + + /** + * A type class that represents the supported types for fact values. + */ + @implicitNotFound("The type ${A} is not supported as a fact type and cannot be used for this method.") + sealed trait PrimitiveType[A] { + def ordering[T]: scala.math.Ordering[T] + } + + object PrimitiveType { + def apply[T](implicit factType: PrimitiveType[T]): PrimitiveType[T] = factType + + implicit case object IntType extends PrimitiveType[scala.Int] { + def ordering[T]: scala.math.Ordering[T] = Ordering[Int].asInstanceOf[Ordering[T]] + } + implicit case object LongType extends PrimitiveType[scala.Long]{ + def ordering[T]: scala.math.Ordering[T] = Ordering[Long].asInstanceOf[Ordering[T]] + } + implicit case object StringType extends PrimitiveType[java.lang.String]{ + def ordering[T]: scala.math.Ordering[T] = Ordering[String].asInstanceOf[Ordering[T]] + } + implicit case object DoubleType extends PrimitiveType[scala.Double]{ + def ordering[T]: scala.math.Ordering[T] = Ordering[Double].asInstanceOf[Ordering[T]] + } + implicit case object ByteType extends PrimitiveType[scala.Byte]{ + def ordering[T]: scala.math.Ordering[T] = Ordering[Byte].asInstanceOf[Ordering[T]] + } + implicit case object CharType extends PrimitiveType[scala.Char]{ + def ordering[T]: scala.math.Ordering[T] = Ordering[Char].asInstanceOf[Ordering[T]] + } + implicit case object FloatType extends PrimitiveType[scala.Float]{ + def ordering[T]: scala.math.Ordering[T] = Ordering[Float].asInstanceOf[Ordering[T]] + } + implicit case object BooleanType extends PrimitiveType[scala.Boolean]{ + def ordering[T]: scala.math.Ordering[T] = Ordering[Boolean].asInstanceOf[Ordering[T]] + } + implicit case object InstantType extends PrimitiveType[java.time.Instant]{ + def ordering[T]: scala.math.Ordering[T] = Ordering[java.time.Instant].asInstanceOf[Ordering[T]] + } + } diff --git a/core/src/main/scala/net/degoes/afd/ruleengine/graduation/RuleEngine.scala b/core/src/main/scala/net/degoes/afd/ruleengine/graduation/RuleEngine.scala new file mode 100644 index 0000000..28c6aac --- /dev/null +++ b/core/src/main/scala/net/degoes/afd/ruleengine/graduation/RuleEngine.scala @@ -0,0 +1,109 @@ +package net.degoes.afd.ruleengine.graduation + +import zio._ +import scala.annotation._ +import scala.language.implicitConversions + +final case class RuleEngine[-In, +Out](update: In => Option[List[Out]]) { self => + def contramap[In2](f: In2 => In): RuleEngine[In2, Out] = + RuleEngine(in => self.update(f(in))) + + def orElse[In1 <: In, Out1 >: Out](that: RuleEngine[In1, Out1]): RuleEngine[In1, Out1] = + RuleEngine(in => self.update(in) orElse that.update(in)) + + def updateWith[Out1 >: Out](in: In)(defaultOut: Out1, combine: (Out1, Out1) => Out1): Out1 = + self.update(in) match { + case None => defaultOut + case Some(outs) => + outs.reduceOption(combine).getOrElse(defaultOut) + } +} + +object RuleEngine { + val empty: RuleEngine[Any, Nothing] = RuleEngine(_ => None) + + def constant[Out](out: Out): RuleEngine[Any, Out] = fromFunction(_ => out) + + def fromFunction[In, Out](f: In => Out): RuleEngine[In, Out] = RuleEngine(in => Some(List(f(in)))) + + def fromRuleSet[In, Out](ruleSet: RuleSet[In, Out]): RuleEngine[In, Out] = { + val update: In => Option[List[Out]] = execute(ruleSet, _) + + RuleEngine(update) + } + + private def execute[In, Out](ruleSet: RuleSet[In, Out], in: In): Option[List[Out]] = + ruleSet.rules.find(_.condition.eval(in)).map { rule => + rule.action.eval(in) + } +} + +final case class Rule[-In, +Out](condition: Condition[In], action: Action[In, Out]) + +final case class RuleSet[-In, +Out](rules: Vector[Rule[In, Out]]) { self => + + def +[In1 <: In, Out1 >: Out](that: Rule[In1, Out1]): RuleSet[In1, Out1] = + RuleSet(self.rules :+ that) + + def ++[In1 <: In, Out1 >: Out](that: RuleSet[In1, Out1]): RuleSet[In1, Out1] = + RuleSet(self.rules ++ that.rules) + + def addRule[In1 <: In, Out1 >: Out](that: Rule[In1, Out1]): RuleSet[In1, Out1] = + self + that +} + +object RuleSet { + + def apply[In, Out](rule1: Rule[In, Out], rules: Rule[In, Out]*): RuleSet[In, Out] = + RuleSet(rule1 +: rules.toVector) + + val empty: RuleSet[Any, Nothing] = RuleSet(Vector.empty) + +} + +final case class Condition[-In](expr: Expr[In, Boolean]) { self => + + def eval(in: In) = expr.eval(in) + + def &&[In1 <: In](that: Condition[In1]): Condition[In1] = + Condition(self.expr && that.expr) + + def ||[In1 <: In](that: Condition[In1]): Condition[In1] = + Condition(self.expr && that.expr) + + def unary_! : Condition[In] = Condition(!expr) +} + +object Condition { + val always: Condition[Any] = constant(true) + val never: Condition[Any] = constant(false) + + def constant[In](value: Boolean): Condition[In] = Condition(Expr(value)) +} + +sealed trait Action[-In, +Out] { self => + def ++[In1 <: In, Out1 >: Out](that: Action[In1, Out1]): Action[In1, Out1] = + Action.Concat(self, that) + + def >>>[Out2](that: Action[Out, Out2]): Action[In, Out2] = + Action.Pipe(self, that) + + def eval(in: In): List[Out] = + self match { + case Action.Concat(left, right) => + left.eval(in) ++ right.eval(in) + + case Action.Pipe(lhs, rhs) => lhs.eval(in).flatMap(rhs.eval(_)) + + case Action.FromExpr(expr) => + List(expr.eval(in)) + } +} + +object Action { + final case class Concat[In, Out](left: Action[In, Out], right: Action[In, Out]) extends Action[In, Out] + final case class Pipe[In, Out1, Out2](left: Action[In, Out1], right: Action[Out1, Out2]) extends Action[In, Out2] + final case class FromExpr[In, Out](expr: Expr[In, Out]) extends Action[In, Out] + + def fromExpr[In, Out](expr: Expr[In, Out]): Action[In, Out] = FromExpr(expr) +} diff --git a/core/src/test/scala/net/degoes/afd/model/ExampleSpec.scala b/core/src/test/scala/net/degoes/afd/model/ExampleSpec.scala index eca67a0..b58749b 100644 --- a/core/src/test/scala/net/degoes/afd/model/ExampleSpec.scala +++ b/core/src/test/scala/net/degoes/afd/model/ExampleSpec.scala @@ -1,4 +1,4 @@ -package net.degoes.afd +package net.degoes.afd.ruleengine import zio._ import zio.test._ @@ -6,8 +6,10 @@ import zio.test.TestAspect._ object ExampleSpec extends ZIOSpecDefault { - import net.degoes.afd.ruleengine.graduation._ - import net.degoes.afd.ruleengine.graduation.loyalty._ + import graduation._ + import graduation2._ + import loyalty_model._ + import fixture._ val priceCondition = Condition(FlightBooking.price.get > 1000.0) val upgradeTier = Action.fromExpr { @@ -54,11 +56,9 @@ object ExampleSpec extends ZIOSpecDefault { val program = LoyaltyProgram("id", 0, LoyaltyTier.Bronze) val expectedProgram = LoyaltyProgram("id", 100, LoyaltyTier.Gold) - val updated = LoyaltyAction.update(program, actions.get) + val updated = ActionExecutor.update(program, actions.get) assertTrue(updated == expectedProgram) }) - - } \ No newline at end of file