Instantly share code, notes, and snippets.

Embed
What would you like to do?
import cats.{ ApplicativeError, MonadError }
import cats.data.{ Kleisli, OptionT }
import cats.effect.Sync
import cats.effect.concurrent.Ref
import cats.syntax.all._
import io.circe.generic.auto._
import io.circe.syntax._
import org.http4s._
import org.http4s.circe.CirceEntityDecoder._
import org.http4s.circe._
import org.http4s.dsl.Http4sDsl
case class User(username: String, age: Int)
case class UserUpdateAge(age: Int)
sealed trait UserError extends Exception
case class UserAlreadyExists(username: String) extends UserError
case class UserNotFound(username: String) extends UserError
case class InvalidUserAge(age: Int) extends UserError
trait UserAlgebra[F[_]] {
def find(username: String): F[Option[User]]
def save(user: User): F[Unit]
def updateAge(username: String, age: Int): F[Unit]
}
object UserInterpreter {
def create[F[_]](implicit F: Sync[F]): F[UserAlgebra[F]] =
Ref.of[F, Map[String, User]](Map.empty).map { state =>
new UserAlgebra[F] {
private def validateAge(age: Int): F[Unit] =
if (age <= 0) F.raiseError(InvalidUserAge(age)) else F.unit
override def find(username: String): F[Option[User]] =
state.get.map(_.get(username))
override def save(user: User): F[Unit] =
validateAge(user.age) *>
find(user.username).flatMap {
case Some(_) =>
F.raiseError(UserAlreadyExists(user.username))
case None =>
state.update(_.updated(user.username, user))
}
override def updateAge(username: String, age: Int): F[Unit] =
validateAge(age) *>
find(username).flatMap {
case Some(user) =>
state.update(_.updated(username, user.copy(age = age)))
case None =>
F.raiseError(UserNotFound(username))
}
}
}
}
class UserRoutes[F[_]: Sync](userAlgebra: UserAlgebra[F]) extends Http4sDsl[F] {
val routes: HttpRoutes[F] = HttpRoutes.of[F] {
case GET -> Root / "users" / username =>
userAlgebra.find(username).flatMap {
case Some(user) => Ok(user.asJson)
case None => NotFound(username.asJson)
}
case req @ POST -> Root / "users" =>
req.as[User].flatMap { user =>
userAlgebra.save(user) *> Created(user.username.asJson)
}
case req @ PUT -> Root / "users" / username =>
req.as[UserUpdateAge].flatMap { userUpdate =>
userAlgebra.updateAge(username, userUpdate.age) *> Ok(username.asJson)
}
}
}
class UserRoutesAlt[F[_]: Sync](userAlgebra: UserAlgebra[F]) extends Http4sDsl[F] {
val routes: HttpRoutes[F] = HttpRoutes.of[F] {
case GET -> Root / "users" / username =>
userAlgebra.find(username).flatMap {
case Some(user) => Ok(user.asJson)
case None => NotFound(username.asJson)
}
case req @ POST -> Root / "users" =>
req.as[User].flatMap { user =>
userAlgebra.save(user) *> Created(user.username.asJson)
}.handleErrorWith { // compiles without giving you "match non-exhaustive" error
case UserAlreadyExists(username) => Conflict(username.asJson)
}
case req @ PUT -> Root / "users" / username =>
req.as[UserUpdateAge].flatMap { userUpdate =>
userAlgebra.updateAge(username, userUpdate.age) *> Ok(username.asJson)
}.handleErrorWith { // compiles without giving you "match non-exhaustive" error
case InvalidUserAge(age) => BadRequest(s"Invalid age $age".asJson)
}
}
}
trait HttpErrorHandler[F[_], E <: Throwable] {
def handle(routes: HttpRoutes[F]): HttpRoutes[F]
}
object RoutesHttpErrorHandler {
def apply[F[_]: ApplicativeError[?[_], E], E <: Throwable](routes: HttpRoutes[F])(handler: E => F[Response[F]]): HttpRoutes[F] =
Kleisli { req =>
OptionT {
routes.run(req).value.handleErrorWith(e => handler(e).map(Option(_)))
}
}
}
object HttpErrorHandler {
def apply[F[_], E <: Throwable](implicit ev: HttpErrorHandler[F, E]) = ev
}
class UserRoutesMTL[F[_]: Sync](userAlgebra: UserAlgebra[F])
(implicit H: HttpErrorHandler[F, UserError]) extends Http4sDsl[F] {
private val httpRoutes: HttpRoutes[F] = HttpRoutes.of[F] {
case GET -> Root / "users" / username =>
userAlgebra.find(username).flatMap {
case Some(user) => Ok(user.asJson)
case None => NotFound(username.asJson)
}
case req @ POST -> Root / "users" =>
req.as[User].flatMap { user =>
userAlgebra.save(user) *> Created(user.username.asJson)
}
case req @ PUT -> Root / "users" / username =>
req.as[UserUpdateAge].flatMap { userUpdate =>
userAlgebra.updateAge(username, userUpdate.age) *> Created(username.asJson)
}
}
val routes: HttpRoutes[F] = H.handle(httpRoutes)
}
class UserHttpErrorHandler[F[_]: MonadError[?[_], UserError]] extends HttpErrorHandler[F, UserError] with Http4sDsl[F] {
private val handler: UserError => F[Response[F]] = {
case InvalidUserAge(age) => BadRequest(s"Invalid age $age".asJson)
case UserAlreadyExists(username) => Conflict(username.asJson)
case UserNotFound(username) => NotFound(username.asJson)
}
override def handle(routes: HttpRoutes[F]): HttpRoutes[F] =
RoutesHttpErrorHandler(routes)(handler)
}
@PeterPerhac

This comment has been minimized.

Copy link

PeterPerhac commented Sep 4, 2018

Hello Gabriel, I found your article very good and thought provoking and I would like to fully digest (and play around with) the code, potentially then implement this idea of error handling in our project. While it's great all the relevant code is here in one place, I am somewhat disappointed that I now have to go hunt for libraries which I need to import into my sbt build in order to run it. Could you list your sbt library dependencies here so it could be easier to get up and running with your code sample? That would help others, I am sure. For me, I guess, I'll just hunt around and figure it out 😸

@PeterPerhac

This comment has been minimized.

Copy link

PeterPerhac commented Sep 4, 2018

Here's a git-clone-able, runnable, adjusted (cut down) version of the final (mtl-based) solution from your gist: https://github.com/PeterPerhac/errorhandling-with-optics-http4s

@gvolpe

This comment has been minimized.

Copy link
Owner

gvolpe commented Nov 16, 2018

Hey @PeterPerhac I'm just seeing this because I'm giving a talk on the topic today 😄 , unfortunately GitHub does not plan to add notifications to gist...

Sorry about that, you can always ping me on Gitter / Twitter if you have any other questions next time. I'll check out your code soon, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment