Skip to content

Commit

Permalink
Add EitherT convenience overloads in Entity
Browse files Browse the repository at this point in the history
  • Loading branch information
Jonas Chapuis authored and jchapuis committed Nov 8, 2021
1 parent ff60ac9 commit d525e26
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 11 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ inThisBuild(
scalaVersion := "2.13.7",
Global / onChangedBuildSource := ReloadOnSourceChanges,
PB.protocVersion := "3.17.3", // works on Apple Silicon,
versionPolicyIntention := Compatibility.BinaryAndSourceCompatible,
versionPolicyIntention := Compatibility.None,
versionPolicyIgnoredInternalDependencyVersions := Some(
"^\\d+\\.\\d+\\.\\d+\\+\\d+".r
) // Support for versions generated by sbt-dynver
Expand Down
42 changes: 32 additions & 10 deletions core/src/main/scala/endless/core/typeclass/entity/Entity.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package endless.core.typeclass.entity

import cats.Monad
import cats.data.EitherT
import endless.\/
import endless.core.typeclass.event.EventWriter
import cats.syntax.either._
Expand Down Expand Up @@ -31,8 +32,8 @@ trait Entity[F[_], S, E] extends StateReader[F, S] with EventWriter[F, E] with M
* @param ifUnknown
* left value in case of missing entity
*/
def ifKnown[A, Error](fa: S => A)(ifUnknown: => Error): F[Error \/ A] =
ifKnownF[A, Error](s => pure(fa(s)))(ifUnknown)
def ifKnown[Error, A](fa: S => A)(ifUnknown: => Error): F[Error \/ A] =
ifKnownF[Error, A](s => pure(fa(s)))(ifUnknown)

/** Convenience function which applies `fa` on the state if entity exists and wraps this in a
* `Right`, otherwise returns a `Left` with the provided error value.
Expand All @@ -41,8 +42,8 @@ trait Entity[F[_], S, E] extends StateReader[F, S] with EventWriter[F, E] with M
* @param ifUnknown
* left value in case of missing entity
*/
def ifKnownF[A, Error](fa: S => F[A])(ifUnknown: => Error): F[Error \/ A] =
ifKnownFE[A, Error](s => map(fa(s))(_.asRight))(ifUnknown)
def ifKnownF[Error, A](fa: S => F[A])(ifUnknown: => Error): F[Error \/ A] =
ifKnownFE[Error, A](s => map(fa(s))(_.asRight))(ifUnknown)

/** Convenience function which applies `fa` on the state if entity exists, otherwise returns a
* `Left` with the provided error value.
Expand All @@ -51,21 +52,31 @@ trait Entity[F[_], S, E] extends StateReader[F, S] with EventWriter[F, E] with M
* @param ifUnknown
* left value in case of missing entity
*/
def ifKnownFE[A, Error](fa: S => F[Error \/ A])(ifUnknown: => Error): F[Error \/ A] =
def ifKnownFE[Error, A](fa: S => F[Error \/ A])(ifUnknown: => Error): F[Error \/ A] =
flatMap(read) {
case Some(state) => fa(state)
case None => pure(ifUnknown.asLeft)
}

/** Convenience function which applies `fa` on the state if entity exists and unwraps EitherT
* value, otherwise returns a `Left` with the provided error value.
* @param fa
* function to apply on state
* @param ifUnknown
* left value in case of missing entity
*/
def ifKnownT[Error, A](fa: S => EitherT[F, Error, A])(ifUnknown: => Error): F[Error \/ A] =
ifKnownFE(s => fa(s).value)(ifUnknown)

/** Convenience function which returns a in a `Right` if entity doesn't yet exist, otherwise calls
* `ifKnown` with the state and wraps this in a `Left`.
* @param fa
* success value when entity doesn't exist yet
* @param ifKnown
* function to compute left value in case of existing entity
*/
def ifUnknown[A, Error](a: => A)(ifKnown: S => Error): F[Error \/ A] =
ifUnknownF[A, Error](pure(a))(ifKnown)
def ifUnknown[Error, A](a: => A)(ifKnown: S => Error): F[Error \/ A] =
ifUnknownF[Error, A](pure(a))(ifKnown)

/** Convenience function which invokes `fa` if entity doesn't yet exist and wraps this in a
* `Right`, otherwise calls `ifKnown` with the state and wraps this in a `Left`.
Expand All @@ -74,8 +85,8 @@ trait Entity[F[_], S, E] extends StateReader[F, S] with EventWriter[F, E] with M
* @param ifKnown
* function to compute left value in case of existing entity
*/
def ifUnknownF[A, Error](fa: => F[A])(ifKnown: S => Error): F[Error \/ A] =
ifUnknownFE[A, Error](map(fa)(_.asRight))(ifKnown)
def ifUnknownF[Error, A](fa: => F[A])(ifKnown: S => Error): F[Error \/ A] =
ifUnknownFE[Error, A](map(fa)(_.asRight))(ifKnown)

/** Convenience function which invokes `fa` if entity doesn't yet exist and wraps this in a
* `Right`, otherwise calls `ifKnown` with the state and wraps this in a `Left`.
Expand All @@ -84,11 +95,22 @@ trait Entity[F[_], S, E] extends StateReader[F, S] with EventWriter[F, E] with M
* @param ifKnown
* function to compute left value in case of existing entity
*/
def ifUnknownFE[A, Error](fa: => F[Error \/ A])(ifKnown: S => Error): F[Error \/ A] =
def ifUnknownFE[Error, A](fa: => F[Error \/ A])(ifKnown: S => Error): F[Error \/ A] =
flatMap(read) {
case None =>
fa
case Some(state) =>
pure(ifKnown(state).asLeft)
}

/** Convenience function which applies `fa` on the state if entity exists and unwraps EitherT
* value, otherwise returns a `Left` with the provided error value.
* @param fa
* function to apply on state
* @param ifUnknown
* left value in case of missing entity
*/
def ifUnknownT[Error, A](fa: => EitherT[F, Error, A])(ifUnknown: S => Error): F[Error \/ A] =
ifUnknownFE(fa.value)(ifUnknown)

}
192 changes: 192 additions & 0 deletions core/src/test/scala/endless/core/typeclass/entity/EntitySuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package endless.core.typeclass.entity
import cats.data.EitherT
import cats.syntax.either._
import cats.{Id, Monad}
import org.scalacheck.Prop.forAll

class EntitySuite extends munit.ScalaCheckSuite {
property("ifKnown with state") {
forAll { (state: State) =>
val entity = new TestEntity(Some(state))
assertEquals(
entity.ifKnown(identity)(Error.EntityNotFound).getOrElse(fail("empty state")),
state
)
}
}

property("ifKnown with no state") {
val entity = new TestEntity(None)
assertEquals(
entity.ifKnown(identity)(Error.EntityNotFound).left.getOrElse(fail("non-empty state")),
Error.EntityNotFound
)
}

property("ifUnknown with state") {
forAll { (state: State) =>
val entity = new TestEntity(Some(state))
assertEquals(
entity.ifUnknown("foo")(_ => Error.EntityAlreadyExists).left.getOrElse(fail("empty state")),
Error.EntityAlreadyExists
)
}
}

property("ifUnknown with no state") {
val entity = new TestEntity(None)
assertEquals(
entity.ifUnknown("foo")(_ => Error.EntityAlreadyExists).getOrElse(fail("non-empty state")),
"foo"
)
}

property("ifKnownF with state") {
forAll { (state: State) =>
val entity = new TestEntity(Some(state))
assertEquals(
entity.ifKnownF(identity)(Error.EntityNotFound).getOrElse(fail("empty state")),
state
)
}
}

property("ifKnownF with no state") {
val entity = new TestEntity(None)
assertEquals(
entity.ifKnownF(identity)(Error.EntityNotFound).left.getOrElse(fail("non-empty state")),
Error.EntityNotFound
)
}

property("ifUnknownF with state") {
forAll { (state: State) =>
val entity = new TestEntity(Some(state))
assertEquals(
entity
.ifUnknownF("foo")(_ => Error.EntityAlreadyExists)
.left
.getOrElse(fail("empty state")),
Error.EntityAlreadyExists
)
}
}

property("ifUnknownF with no state") {
val entity = new TestEntity(None)
assertEquals(
entity.ifUnknownF("foo")(_ => Error.EntityAlreadyExists).getOrElse(fail("non-empty state")),
"foo"
)
}

property("ifKnownFE with state") {
forAll { (state: State) =>
val entity = new TestEntity(Some(state))
assertEquals(
entity
.ifKnownFE(_.asRight[Error])(Error.EntityNotFound)
.getOrElse(fail("empty state")),
state
)
}
}

property("ifKnownFE with no state") {
val entity = new TestEntity(None)
assertEquals(
entity
.ifKnownFE(_.asRight[Error])(Error.EntityNotFound)
.left
.getOrElse(fail("non-empty state")),
Error.EntityNotFound
)
}

property("ifUnknownFE with state") {
forAll { (state: State) =>
val entity = new TestEntity(Some(state))
assertEquals(
entity
.ifUnknownFE("foo".asRight[Error])(_ => Error.EntityAlreadyExists)
.left
.getOrElse(fail("empty state")),
Error.EntityAlreadyExists
)
}
}

property("ifUnknownFE with no state") {
val entity = new TestEntity(None)
assertEquals(
entity
.ifUnknownFE("foo".asRight[Error])(_ => Error.EntityAlreadyExists)
.getOrElse(fail("non-empty state")),
"foo"
)
}

property("ifKnownT with state") {
forAll { (state: State) =>
val entity = new TestEntity(Some(state))
assertEquals(
entity
.ifKnownT(EitherT.pure[Id, Error](_))(Error.EntityNotFound)
.getOrElse(fail("empty state")),
state
)
}
}

property("ifKnownT with no state") {
val entity = new TestEntity(None)
assertEquals(
entity
.ifKnownT(EitherT.pure[Id, Error](_))(Error.EntityNotFound)
.left
.getOrElse(fail("non-empty state")),
Error.EntityNotFound
)
}

property("ifUnknownT with state") {
forAll { (state: State) =>
val entity = new TestEntity(Some(state))
assertEquals(
entity
.ifUnknownT(EitherT.pure[Id, Error]("foo"))(_ => Error.EntityAlreadyExists)
.left
.getOrElse(fail("empty state")),
Error.EntityAlreadyExists
)
}
}

property("ifUnknownT with no state") {
val entity = new TestEntity(None)
assertEquals(
entity
.ifUnknownT(EitherT.pure[Id, Error]("foo"))(_ => Error.EntityAlreadyExists)
.getOrElse(fail("non-empty state")),
"foo"
)
}

type State = Int
type Event = String
sealed trait Error
object Error {
object EntityNotFound extends Error
object EntityAlreadyExists extends Error
case class Odd(state: State) extends Error
}

class TestEntity(state: Option[State]) extends Entity[Id, State, Event] {
private val queue = new scala.collection.mutable.Queue[Event]
def pure[A](x: A): Id[A] = x
def read: Id[Option[State]] = state
def write(event: Event, other: Event*): Id[Unit] = queue += event ++= other
def flatMap[A, B](fa: Id[A])(f: A => Id[B]): Id[B] = implicitly[Monad[Id]].flatMap(fa)(f)
def tailRecM[A, B](a: A)(f: A => Id[Either[A, B]]): Id[B] = implicitly[Monad[Id]].tailRecM(a)(f)
}
}

0 comments on commit d525e26

Please sign in to comment.