Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save ahjohannessen/7a82c860440fa2f29f5929e3fb031508 to your computer and use it in GitHub Desktop.
Save ahjohannessen/7a82c860440fa2f29f5929e3fb031508 to your computer and use it in GitHub Desktop.
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)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment