Skip to content

Instantly share code, notes, and snippets.

@Fristi
Created November 7, 2018 07:13
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Fristi/bf4c19f57334e53a5f7f1f6153ace795 to your computer and use it in GitHub Desktop.
Save Fristi/bf4c19f57334e53a5f7f1f6153ace795 to your computer and use it in GitHub Desktop.
Http4s refined endpoint with tests
import cats.Show
import cats.data.EitherT
import cats.effect.{IO, Sync}
import eu.timepit.refined.api.Refined
import eu.timepit.refined.collection.NonEmpty
import eu.timepit.refined.types.numeric.PosInt
import io.chrisdavenport.log4cats.Logger
import io.chrisdavenport.log4cats.slf4j.Slf4jLogger
import io.circe._
import io.circe.refined._
import org.http4s.circe._
import org.http4s.dsl.io._
import org.http4s.{EntityDecoder, HttpRoutes, InvalidMessageBodyFailure, Request, Response}
object CourierService {
implicit def unsafeLogger[F[_]: Sync]: Logger[F] = Slf4jLogger.unsafeCreate[F]
type HttpRes[A] = EitherT[IO, Response[IO], A]
object HttpRes {
def requestAs[A: Decoder](request: Request[IO]): HttpRes[A] =
request.attemptAs[A].leftFlatMap {
case InvalidMessageBodyFailure(_, Some(err: Error)) => EitherT.left(BadRequest(Show[Error].show(err)))
case _ => EitherT.left(BadRequest(s"Decode error"))
}
def liftF[A](io: IO[A]): HttpRes[A] = EitherT.liftF(io)
}
val routes: HttpRoutes[IO] = {
HttpRoutes.of[IO] {
case req @ POST -> Root / "users" / "register" =>
(for {
user <- HttpRes.requestAs[RegisterUser](req)
resp <- HttpRes.liftF(Ok(s"Registered ${user.firstName}"))
} yield resp).merge
}
}
implicit def decoder[A](implicit D: Decoder[A]): EntityDecoder[IO, A] = jsonOf[IO, A]
}
final case class RegisterUser(
firstName: String Refined NonEmpty,
middleName: String Refined NonEmpty,
lastName: String Refined NonEmpty,
age: PosInt
)
object RegisterUser {
implicit val decoder: Decoder[RegisterUser] =
Decoder.forProduct4("firstName", "middleName", "lastName", "age")(RegisterUser.apply)
implicit val encoder: Encoder[RegisterUser] =
Encoder.forProduct4("firstName", "middleName", "lastName", "age")(u => (u.firstName, u.middleName, u.lastName, u.age))
}
import cats.effect.IO
import io.circe.literal._
import org.http4s._
import org.http4s.circe._
import org.http4s.implicits._
import org.specs2.matcher.{IOMatchers, Matcher, Matchers}
class CourierServiceSpec extends org.specs2.mutable.Specification with IOMatchers with Matchers {
"CourierService" >> {
"register" >> {
"successfully" >> {
val resp = runRequest(post(Uri.uri("/users/register"), json"""{"firstName": "Klaas", "middleName": "de", "lastName": "Vaak", "age": 3}"""))
resp must returnStatus(Status.Ok)
resp.body.asString must beEqualTo("Registered Klaas")
}
"fail when age is negative" >> {
val resp = runRequest(post(Uri.uri("/users/register"), json"""{"firstName": "Klaas", "middleName": "de", "lastName": "Vaak", "age": -1}"""))
resp must returnStatus(Status.BadRequest)
resp.body.asString must beEqualTo("DecodingFailure at .age: Predicate failed: (-1 > 0).")
}
}
}
def returnStatus(status: Status): Matcher[Response[IO]] = { s: Response[IO] =>
s.status must beEqualTo(status)
}
private def runRequest(request: IO[Request[IO]]): Response[IO] =
request.flatMap(CourierService.routes.orNotFound.run).unsafeRunSync()
private def post[A](uri: Uri, body: A)(implicit E: EntityEncoder[IO, A]): IO[Request[IO]] =
IO.pure(Request(Method.POST, uri, body = E.toEntity(body).body))
implicit class ByteVector2String(body: EntityBody[IO]) {
def asString: String = {
val array = body.compile.toList.unsafeRunSync().toArray
new String(array.map(_.toChar))
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment