Search code examples
scalascala-catscats-effect

Akka Http and Circe, how to serialize entities wrapped in the tagless final approach (Cats Effect)


I'm building a toy project to learn Scala 3 and i'm stuck in one problem, first of all i'm following the tagless-final approach using cats-effect, the approach is working as expected except for the entity serialization, when i try to create a route using akka-http i have the following problem:

def routes: Route = pathPrefix("security") {
(path("auth") & post) {
  entity(as[LoginUserByCredentialsCommand]) {
    (command: LoginUserByCredentialsCommand) =>
      complete {
        login(command)
      }
  }
}}

F[
  Either[com.moralyzr.magickr.security.core.errors.AuthError, 
    com.moralyzr.magickr.security.core.types.TokenType.Token
  ]
]
Required: akka.http.scaladsl.marshalling.ToResponseMarshallable
where:    F is a type in class SecurityApi with bounds <: [_] =>> Any

For what i understood, akka-http does not know how to serialize the F highly-kinded type, by searching a little bit i found the following solution, it consists of creating an implicit called marshallable to show the akka-http how to serialize the type, however when i implement it i get a StackOverflow error :(

import akka.http.scaladsl.marshalling.ToResponseMarshaller
import cats.effect.IO

trait Marshallable[F[_]]:
  def marshaller[A: ToResponseMarshaller]: ToResponseMarshaller[F[A]]

object Marshallable:
  implicit def marshaller[F[_], A : ToResponseMarshaller](implicit M: Marshallable[F]): ToResponseMarshaller[F[A]] =
    M.marshaller

  given ioMarshaller: Marshallable[IO] with
    def marshaller[A: ToResponseMarshaller] = implicitly

I'm really stuck right now, does anyone have an idea on how can i fix this problem? The complete code can be found here

Edit: This is the login code

For clarity, here are the class that instantiate the security api and the security api itself

  object Magickr extends IOApp:

  override def run(args: List[String]): IO[ExitCode] =
    val server = for {
      // Actors
      actorsSystem <- ActorsSystemResource[IO]()
      streamMaterializer <- AkkaMaterializerResource[IO](actorsSystem)
      // Configs
      configs <- Resource.eval(MagickrConfigs.makeConfigs[IO]())
      httpConfigs = AkkaHttpConfig[IO](configs)
      databaseConfigs = DatabaseConfig[IO](configs)
      flywayConfigs = FlywayConfig[IO](configs)
      jwtConfig = JwtConfig[IO](configs)
      // Interpreters
      jwtManager = JwtBuilder[IO](jwtConfig)
      authentication = InternalAuthentication[IO](
        passwordValidationAlgebra = new SecurityValidationsInterpreter(),
        jwtManager = jwtManager
      )
      // Database
      _ <- Resource.eval(
        DbMigrations.migrate[IO](flywayConfigs, databaseConfigs)
      )
      transactor <- DatabaseConnection.makeTransactor[IO](databaseConfigs)
      userRepository = UserRepository[IO](transactor)
      // Services
      securityManagement = SecurityManagement[IO](
        findUser = userRepository,
        authentication = authentication
      )
      // Api
      secApi = new SecurityApi[IO](securityManagement)
      routes = pathPrefix("api") {
        secApi.routes()
      }
      akkaHttp <- AkkaHttpResource.makeHttpServer[IO](
        akkaHttpConfig = httpConfigs,
        routes = routes,
        actorSystem = actorsSystem,
        materializer = streamMaterializer
      )
    } yield (actorsSystem)
    return server.useForever

And

class SecurityApi[F[_]: Async](
    private val securityManagement: SecurityManagement[F]
) extends LoginUserByCredentials[F]
    with SecurityProtocols:

  def routes()(using marshaller: Marshallable[F]): Route = pathPrefix("security") {
    (path("auth") & post) {
      entity(as[LoginUserByCredentialsCommand]) {
        (command: LoginUserByCredentialsCommand) =>
          complete {
            login(command)
          }
      }
    }
  }

override def login(
  command: LoginUserByCredentialsCommand
): F[Either[AuthError, Token]] =
  securityManagement.loginWithCredentials(command = command).value

================= EDIT 2 ========================================= With the insight provided by Luis Miguel, it makes a clearer sense that i need to unwrap the IO into a Future at the Marshaller level, something like this:

  def ioToResponseMarshaller[A: ToResponseMarshaller](
      M: Marshallable[IO]
  ): ToResponseMarshaller[IO[A]] =
    Marshaller.futureMarshaller.compose(M.entity.unsafeToFuture())

However, i have this problem:

Found:    cats.effect.unsafe.IORuntime => scala.concurrent.Future[A]
Required: cats.effect.IO[A] => scala.concurrent.Future[A]

I think i'm close! Is there a way to unwrap the IO keeping the IO type?


Solution

  • I managed to make it work! Thanks to @luismiguel insight, the problem was that the Akka HTTP Marshaller was not able to deal with Cats-Effect IO monad, so the solution was an implementation who unwraps the IO monad using the unsafeToFuture inside the marshaller, that way i was able to keep the Tagless-Final style from point to point, here's the solution:

    This implicit fetches the internal marshaller for the type

    import akka.http.scaladsl.marshalling.ToResponseMarshaller
    import cats.effect.IO
    
    trait Marshallable[F[_]]:
      def marshaller[A: ToResponseMarshaller]: ToResponseMarshaller[F[A]]
    
    object Marshallable:
      implicit def marshaller[F[_], A: ToResponseMarshaller](implicit
          M: Marshallable[F]
      ): ToResponseMarshaller[F[A]] = M.marshaller
    
      given ioMarshallable: Marshallable[IO] with
        def marshaller[A: ToResponseMarshaller] = CatsEffectsMarshallers.ioMarshaller
    

    This one unwraps the IO monad and flatMaps the marshaller using a future, which akka-http knows how to deal with.

    import akka.http.scaladsl.marshalling.{
      LowPriorityToResponseMarshallerImplicits,
      Marshaller,
      ToResponseMarshaller
    }
    import cats.effect.IO
    import cats.effect.unsafe.implicits.global
    
    trait CatsEffectsMarshallers extends LowPriorityToResponseMarshallerImplicits:
      implicit def ioMarshaller[A](implicit
          m: ToResponseMarshaller[A]
      ): ToResponseMarshaller[IO[A]] =
        Marshaller(implicit ec => _.unsafeToFuture().flatMap(m(_)))
    
    object CatsEffectsMarshallers extends CatsEffectsMarshallers