Skip to content

Instantly share code, notes, and snippets.

@gvolpe
Last active December 17, 2022 14:02
Show Gist options
  • Save gvolpe/3fa32dd1b6abce2a5466efbf0eca9e94 to your computer and use it in GitHub Desktop.
Save gvolpe/3fa32dd1b6abce2a5466efbf0eca9e94 to your computer and use it in GitHub Desktop.
import cats.MonadError
import cats.effect.Sync
import cats.effect.concurrent.Ref
import cats.syntax.all._
import io.circe._
import io.circe.generic.auto._
import org.http4s._
import org.http4s.circe._
import org.http4s.dsl.Http4sDsl
// format: off
case class User(username: String, age: Int)
case class UserUpdateAge(age: Int)
sealed trait UserError extends Throwable
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))
}
}
}
}
object JsonCodecs {
implicit def jsonDecoder[A <: Product: Decoder, F[_]: Sync]: EntityDecoder[F, A] = jsonOf[F, A]
implicit def jsonEncoder[A <: Product: Encoder, F[_]: Sync]: EntityEncoder[F, A] = jsonEncoderOf[F, A]
}
class UserRoutes[F[_]: Sync](userAlgebra: UserAlgebra[F]) extends Http4sDsl[F] {
import JsonCodecs._
val routes: HttpRoutes[F] = HttpRoutes.of[F] {
case GET -> Root / "users" / username =>
userAlgebra.find(username).flatMap {
case Some(user) => Ok(user)
case None => NotFound(username)
}
case req @ POST -> Root / "users" =>
req.as[User].flatMap { user =>
userAlgebra.save(user) *> Created(user.username)
}
case req @ PUT -> Root / "users" / username =>
req.as[UserUpdateAge].flatMap { userUpdate =>
userAlgebra.updateAge(username, userUpdate.age) *> Ok(username)
}
}
}
class UserRoutesAlt[F[_]: Sync](userAlgebra: UserAlgebra[F]) extends Http4sDsl[F] {
import JsonCodecs._
val routes: HttpRoutes[F] = HttpRoutes.of[F] {
case GET -> Root / "users" / username =>
userAlgebra.find(username).flatMap {
case Some(user) => Ok(user)
case None => NotFound(username)
}
case req @ POST -> Root / "users" =>
req.as[User].flatMap { user =>
userAlgebra.save(user) *> Created(user.username)
}.handleErrorWith { // compiles without giving you "match non-exhaustive" error
case UserAlreadyExists(username) => Conflict(username)
}
case req @ PUT -> Root / "users" / username =>
req.as[UserUpdateAge].flatMap { userUpdate =>
userAlgebra.updateAge(username, userUpdate.age) *> Ok(username)
}.handleErrorWith { // compiles without giving you "match non-exhaustive" error
case InvalidUserAge(age) => BadRequest(s"Invalid age $age")
}
}
}
trait HttpErrorHandler[F[_], E <: Throwable] {
def handle(fa: F[Response[F]]): F[Response[F]]
}
object HttpErrorHandler {
def apply[F[_], E <: Throwable](implicit ev: HttpErrorHandler[F, E]) = ev
}
object syntax {
implicit class HttpErrorHandlerOps[F[_], E <: Throwable](fa: F[Response[F]]) {
def handleHttpErrorResponse(implicit H: HttpErrorHandler[F, E]): F[Response[F]] =
H.handle(fa)
}
}
class UserRoutesMTL[F[_]: Sync: HttpErrorHandler[?[_], UserError]](userAlgebra: UserAlgebra[F]) extends Http4sDsl[F] {
import JsonCodecs._
import syntax._
val routes: HttpRoutes[F] = HttpRoutes.of[F] {
case GET -> Root / "users" / username =>
userAlgebra.find(username).flatMap {
case Some(user) => Ok(user)
case None => NotFound(username)
}.handleHttpErrorResponse
case req @ POST -> Root / "users" =>
req.as[User].flatMap { user =>
userAlgebra.save(user) *> Created(user.username)
}.handleHttpErrorResponse
case req @ PUT -> Root / "users" / username =>
req.as[UserUpdateAge].flatMap { userUpdate =>
userAlgebra.updateAge(username, userUpdate.age) *> Created(username)
}.handleHttpErrorResponse
}
}
class UserHttpErrorHandler[F[_]: MonadError[?[_], UserError]] extends HttpErrorHandler[F, UserError] with Http4sDsl[F] {
// match may not be exhaustive
override def handle(fa: F[Response[F]]): F[Response[F]] =
fa.handleErrorWith { // It would fail on the following inputs: UserAlreadyExists(_), UserNotFound(_)
case InvalidUserAge(age) => BadRequest(s"Invalid age $age")
}
}
@PeterPerhac
Copy link

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
Copy link

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
Copy link
Author

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