Converting an `Option[A]` to an Ok() or NotFound() inside an Http4s API

303 views Asked by At

I've got an API that looks like this:

object Comics {
  ...

  def impl[F[_]: Applicative]: Comics[F] = new Comics[F] {
    def getAuthor(slug: Authors.Slug): F[Option[Authors.Author]] =
      ...

and a routing that looks like this:

object Routes {
  def comicsRoutes[F[_]: Sync](comics: Comics[F]): HttpRoutes[F] = {
    val dsl = new Http4sDsl[F] {}
    import dsl._
    HttpRoutes.of[F] {
      case GET -> Root / "comics" / authorSlug =>
        comics
          .getAuthor(Authors.Slug(authorSlug))
          .flatMap {
            case Some(author) => Ok(author)
            case None         => NotFound()
          }

So when there is a None, it gets converted to a 404. Since there are several routes, the .flatMap { ... } gets duplicated.

Question: How do I move this into a separate .orNotFound helper function specific to my project?


My attempt:

To make things simple for me (and avoid parameterisation over F initially), I've tried to define this inside comicsRoutes:

  def comicsRoutes[F[_]: Sync](comics: Comics[F]): HttpRoutes[F] = {
    val dsl = new Http4sDsl[F] {}
    import dsl._

    def orNotFound[A](opt: Option[A]): ???[A] =
      opt match {
        case Some(value) => Ok(value)
        case None        => NotFound()
      }

    HttpRoutes.of[F] {
      case GET -> Root / "comics" / authorSlug =>
        comics
          .getAuthor(Authors.Slug(authorSlug))
          .flatMap(orNotFound)

But what's ??? here? It doesn't seem to be Response or Status. Also, the .flatMap { ... } was made under import dsl._, but I'd like to move this further out. What would a good place be? Does it go into the routes file, or do I put it in a separate ExtendedSomething extension file? (I expect that ??? and Something might be related, but I'm a little confused as to what the missing types are.)

(Equally importantly, how do I find out what ??? is here? I was hoping ??? at the type level might give me a "typed hole", and VSCode's hover function provides very sporadic documentation value.)

2

There are 2 answers

0
Mateusz Kubuszok On BEST ANSWER

The type returned by Http4sDsl[F] for your actions is F[Response[F]].

It has to be wrapped with F because your are using .flatMap on F. Response is parametrized with F because it will produce the result returned to caller using F.

To find that out you can use IntelliJ and then generate the annotation by IDE (Alt+Enter and then "Add type annotation to value definition"). You can also:

  • preview implicits to check that Ok object imported from Statuses trait is provided extension methods with http4sOkSyntax implicit conversion (Ctrl+Alt+Shift+Plus sign, you can press it a few times to expand implicits more, and Ctrl+Alt+Shift+Minut to hide them again)
  • find http4sOkSyntax by pressing Shift twice to open find window, and then pressing it twice again to include non-project symbols,
  • from there navigate with Ctrl+B through OkOps to EntityResponseGenerator class which is providing you the functionality you used (in apply) returning F[Resposne[F]].

So if you want to move things around/extract them, pay attention to what implicits are required to instantiate the DSL and extension methods.

(Shortcuts differ between Mac OS - which sometime use Cmd instead of Ctrl - and non-Mac OS systems so just check them in documentation if you have an issue).

1
sshine On

Thanks to Mateusz, I learned that ??? should be F[Response[F]].

To make this helper function work fully, two more type-related problems occurred:

  1. Since value: A is polymorphic, Http4s expects an implicit EntityEncoder[F, A] in order to serialize an arbitrary value. (This was not a problem with the original { case ... } match, since the type was concrete and not polymorphic.

  2. Adding this implicit annotation was, for some reason, not enough. Doing .flatMap(orNotFound) fails type inference. Doing .flatMap(orNotFound[Authors.Slug]) fixes this.

(Thanks to keynmol for pointing out the other two.)

Having all three changes, this results in:

  def comicsRoutes[F[_]: Sync](comics: Comics[F]): HttpRoutes[F] = {
    val dsl = new Http4sDsl[F] {}
    import dsl._

    def orNotFound[A](opt: Option[A])(implicit ee: EntityEncoder[F, A]): F[Response[F]] =
      opt match {
        case Some(value) => Ok(value)
        case None        => NotFound()
      }

    HttpRoutes.of[F] {
      case GET -> Root / "comics" / authorSlug =>
        comics
          .getAuthor(Authors.Slug(authorSlug))
          .flatMap(orNotFound[Authors.Author])
      ...