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.

Revisions

  1. gvolpe revised this gist Sep 2, 2018. 1 changed file with 0 additions and 2 deletions.
    2 changes: 0 additions & 2 deletions http4s-error-handling-mtl.scala
    Original file line number Diff line number Diff line change
    @@ -127,8 +127,6 @@ object HttpErrorHandler {
    class UserRoutesMTL[F[_]: Sync](userAlgebra: UserAlgebra[F])
    (implicit H: HttpErrorHandler[F, UserError]) extends Http4sDsl[F] {

    import syntax._

    private val httpRoutes: HttpRoutes[F] = HttpRoutes.of[F] {

    case GET -> Root / "users" / username =>
  2. gvolpe revised this gist Sep 2, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion http4s-error-handling-mtl.scala
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    import cats.{ ApplicativeErrpr, MonadError }
    import cats.{ ApplicativeError, MonadError }
    import cats.data.{ Kleisli, OptionT }
    import cats.effect.Sync
    import cats.effect.concurrent.Ref
  3. gvolpe revised this gist Sep 2, 2018. 1 changed file with 3 additions and 10 deletions.
    13 changes: 3 additions & 10 deletions http4s-error-handling-mtl.scala
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    import cats.MonadError
    import cats.{ ApplicativeErrpr, MonadError }
    import cats.data.{ Kleisli, OptionT }
    import cats.effect.Sync
    import cats.effect.concurrent.Ref
    @@ -112,7 +112,7 @@ trait HttpErrorHandler[F[_], E <: Throwable] {
    }

    object RoutesHttpErrorHandler {
    def apply[F[_]: MonadError[?[_], E], E <: Throwable](routes: HttpRoutes[F])(handler: E => F[Response[F]]): HttpRoutes[F] =
    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(_)))
    @@ -124,13 +124,6 @@ object HttpErrorHandler {
    def apply[F[_], E <: Throwable](implicit ev: HttpErrorHandler[F, E]) = ev
    }

    object syntax {
    implicit class HttpErrorHandlerOp[F[_], E <: Throwable](routes: HttpRoutes[F]) {
    def handleHttpErrorResponse(implicit H: HttpErrorHandler[F, E]): HttpRoutes[F] =
    H.handle(routes)
    }
    }

    class UserRoutesMTL[F[_]: Sync](userAlgebra: UserAlgebra[F])
    (implicit H: HttpErrorHandler[F, UserError]) extends Http4sDsl[F] {

    @@ -155,7 +148,7 @@ class UserRoutesMTL[F[_]: Sync](userAlgebra: UserAlgebra[F])
    }
    }

    val routes: HttpRoutes[F] = httpRoutes.handleHttpErrorResponse
    val routes: HttpRoutes[F] = H.handle(httpRoutes)

    }

  4. gvolpe revised this gist Aug 28, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion http4s-error-handling-mtl.scala
    Original file line number Diff line number Diff line change
    @@ -128,7 +128,7 @@ object syntax {
    implicit class HttpErrorHandlerOp[F[_], E <: Throwable](routes: HttpRoutes[F]) {
    def handleHttpErrorResponse(implicit H: HttpErrorHandler[F, E]): HttpRoutes[F] =
    H.handle(routes)
    }
    }
    }

    class UserRoutesMTL[F[_]: Sync](userAlgebra: UserAlgebra[F])
  5. gvolpe revised this gist Aug 27, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion http4s-error-handling-mtl.scala
    Original file line number Diff line number Diff line change
    @@ -13,7 +13,7 @@ import org.http4s.dsl.Http4sDsl
    case class User(username: String, age: Int)
    case class UserUpdateAge(age: Int)

    sealed trait UserError extends Throwable
    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
  6. gvolpe revised this gist Aug 27, 2018. 1 changed file with 47 additions and 40 deletions.
    87 changes: 47 additions & 40 deletions http4s-error-handling-mtl.scala
    Original file line number Diff line number Diff line change
    @@ -1,10 +1,12 @@
    import cats.MonadError
    import cats.data.{ Kleisli, OptionT }
    import cats.effect.Sync
    import cats.effect.concurrent.Ref
    import cats.syntax.all._
    import io.circe._
    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

    @@ -55,110 +57,115 @@ object UserInterpreter {

    }

    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 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)
    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)
    userAlgebra.updateAge(username, userUpdate.age) *> Ok(username.asJson)
    }
    }

    }

    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 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)
    userAlgebra.save(user) *> Created(user.username.asJson)
    }.handleErrorWith { // compiles without giving you "match non-exhaustive" error
    case UserAlreadyExists(username) => Conflict(username)
    case UserAlreadyExists(username) => Conflict(username.asJson)
    }

    case req @ PUT -> Root / "users" / username =>
    req.as[UserUpdateAge].flatMap { userUpdate =>
    userAlgebra.updateAge(username, userUpdate.age) *> Ok(username)
    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")
    case InvalidUserAge(age) => BadRequest(s"Invalid age $age".asJson)
    }
    }

    }

    trait HttpErrorHandler[F[_], E <: Throwable] {
    def handle(fa: F[Response[F]]): F[Response[F]]
    def handle(routes: HttpRoutes[F]): HttpRoutes[F]
    }

    object RoutesHttpErrorHandler {
    def apply[F[_]: MonadError[?[_], 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
    }

    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)
    }
    implicit class HttpErrorHandlerOp[F[_], E <: Throwable](routes: HttpRoutes[F]) {
    def handleHttpErrorResponse(implicit H: HttpErrorHandler[F, E]): HttpRoutes[F] =
    H.handle(routes)
    }
    }

    class UserRoutesMTL[F[_]: Sync: HttpErrorHandler[?[_], UserError]](userAlgebra: UserAlgebra[F]) extends Http4sDsl[F] {
    class UserRoutesMTL[F[_]: Sync](userAlgebra: UserAlgebra[F])
    (implicit H: HttpErrorHandler[F, UserError]) extends Http4sDsl[F] {

    import JsonCodecs._
    import syntax._

    val routes: HttpRoutes[F] = HttpRoutes.of[F] {
    private val httpRoutes: 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 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)
    }.handleHttpErrorResponse
    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)
    }.handleHttpErrorResponse
    userAlgebra.updateAge(username, userUpdate.age) *> Created(username.asJson)
    }
    }

    val routes: HttpRoutes[F] = httpRoutes.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")
    }
    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)
    }
  7. gvolpe revised this gist Aug 25, 2018. 1 changed file with 0 additions and 1 deletion.
    1 change: 0 additions & 1 deletion http4s-error-handling-mtl.scala
    Original file line number Diff line number Diff line change
    @@ -8,7 +8,6 @@ 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)

  8. gvolpe created this gist Aug 25, 2018.
    165 changes: 165 additions & 0 deletions http4s-error-handling-mtl.scala
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,165 @@
    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")
    }
    }