Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A simple example of cat-effect and error handling, with MonadTransformers
import MyErrsIO.Controller.{Request, postTransfer}
import MyErrsIO.Models.Account
import cats.data.Validated.{Invalid, Valid}
import cats.data.{EitherT, OptionT, Validated, ValidatedNec}
import cats.effect._
import cats.implicits._
import scala.util.control.NonFatal
// A simple example of cat-effect and error handling, with MonadTransformers
object MyErrsIO extends IOApp {
trait DomainError
case class InsufficientFunds(actual: Double, withdraw: Double) extends DomainError
case class MaxBal(num: String) extends DomainError
case class AccountNotFound(num: String) extends DomainError
object Models {
val maxbal = 500
case class Account(num: String, bal: Double) {
def deposit(amount: Double): Either[DomainError, Account] = {
if (bal + amount <= maxbal) Right(this.copy(bal = bal + amount)) else Left(MaxBal(num))
}
def withdraw(amount: Double): Either[DomainError, Account] = {
if (bal >= amount) Right(this.copy(bal = bal - amount)) else Left(InsufficientFunds(bal, amount))
}
}
}
object Repo {
var data = Map.empty[String, Account]
def findAccount(num: String): IO[Option[Account]] = {
if (num == "xdie") IO.raiseError(new Exception("die")) else data.get(num).pure[IO]
}
def saveAccount(account: Account): IO[Unit] = {
(data += account.num -> account).pure[IO]
}
}
// Takes and returns primitives, i.e. strings, int, as if from a Form
// Validates values, if Valid does transfer
object Controller {
case class Request(fromAccount: String, toAccount: String, amountString: String)
case class Response(status: Int, body: String)
import Validations._
import Service._
def postTransfer(request: Request): IO[Response] = {
val response = {
val from = validateAccountNumber(request.fromAccount)
val to = validateAccountNumber(request.toAccount)
val amount = validateDouble(request.amountString)
(from, to, amount).tupled match {
case Valid((from, to, amount)) => transfer(from, to, amount).map {
case Right(()) => Response(200, "Transfer Succeeded")
case Left(error) => Response(400, error.toString)
}
case Invalid(errors) => Response(400, errors.toString).pure[IO]
}
}
response.handleErrorWith {
case NonFatal(ex) => Response(500, s"Internal server error ${ex.getMessage}").pure[IO]
}
}
}
object Service {
def transfer(from: String, to: String, amount: Double): IO[Either[DomainError, Unit]] = {
val accounts: EitherT[IO, DomainError, Unit] = for {
fromAccount <- OptionT(Repo.findAccount(from)).toRight(AccountNotFound(from))
toAccount <- OptionT(Repo.findAccount(to)).toRight(AccountNotFound(to))
updatedFrom <- EitherT(IO(fromAccount.withdraw(amount)))
updatedTo <- EitherT(IO(toAccount.deposit(amount)))
unit <- EitherT.right(Repo.saveAccount(updatedFrom) *> Repo.saveAccount(updatedTo))
} yield unit
accounts.value
}
}
object Validations {
type Valid[A] = ValidatedNec[String, A]
def validateDouble(s: String): Valid[Double] = {
Validated.fromOption(s.toDoubleOption, s"not a double $s").toValidatedNec
}
def validateAccountNumber(s: String): Valid[String] = {
Validated.condNec(s.startsWith("x"), s, s"not an account $s")
}
}
override def run(args: List[String]): IO[ExitCode] = {
for {
_ <- Repo.saveAccount(Account("xa", 490))
_ <- Repo.saveAccount(Account("xb", 20))
response <- postTransfer(Request("xa", "xb", "470"))
// response <- postTransfer(Request("xa", "xb", "485"))
// response <- postTransfer(Request("xdie", "xb", "470"))
// response <- postTransfer(Request("xnotthere", "xb", "470"))
// response <- postTransfer(Request("notanaccount", "xb", "notanumber470"))
_ <- IO.println(response)
a <- Repo.findAccount("xa")
b <- Repo.findAccount("xb")
_ <- IO.println((s"$a $b"))
} yield ExitCode.Success
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment