scalaakkaguicetagless-final

Akka Play guice bindings tagless final (TF) support


I have this binding to configure the Logger[IO] in my app (module with this line is in guice.conf file):


class CatsEffectModule extends AbstractModule with ScalaModule {

  override def configure(): Unit = {
    bind[Logger[IO]].toInstance(Slf4jLogger.getLogger[IO])
  }

}

Then in the app i can do this:

@Singleton
class MyClass @Inject()(implicit logger: Logger[IO]) { ... }

And this works fine in the application.

But it won't work when used in GuiceInjectorBuilder (for tests):

import play.api.inject.guice.GuiceInjectorBuilder


private val application: Injector = new GuiceInjectorBuilder()
    .bindings(bind[ExecutionContext].to(ExecutionContext.global))
    .bindings(bind[ApplicationLifecycle].to[DefaultApplicationLifecycle])
    .bindings(new CatsEffectModule())
    .build()

application.instanceOf[MyClass]

It gives me an error:

No implementation for io.chrisdavenport.log4cats.Logger was bound.
[info]   Did you mean?
[info]     io.chrisdavenport.log4cats.Logger<cats.effect.IO> bound  at guice.CatsEffectModule.configure(CatsEffectModule.scala:21) (via modules: com.google.inject.util.Modules$OverrideModule -> guice.CatsEffectModule)

And every TF-entity injection in tests fails like this. Is there some difference between how Akka Play runs Guice and how GuiceInjectorBuilder works?

Code example: https://github.com/DenisNovac/play-tf-test


Solution

  • The Scala-guice by codingwell (https://github.com/codingwell/scala-guice) allows to bind TF-classes (it is made for vanilla Guice, not the Play Guice). They will be binded to dependent classes correctly, but the GuiceInjectorBuilder won't let you get it by instanceOf method.

    But it seems that it works in both directions if you use both binds:

    import cats.effect.IO
    import com.google.inject.AbstractModule
    import net.codingwell.scalaguice.ScalaModule
    import org.scalatest.flatspec.AnyFlatSpec
    import org.scalatest.matchers.should.Matchers
    import play.api.inject.guice.GuiceInjectorBuilder
    import play.api.inject.{ApplicationLifecycle, DefaultApplicationLifecycle, Injector, bind}
    
    import scala.concurrent.ExecutionContext
    
    class WorkingModule extends AbstractModule with ScalaModule {
    
      override def configure(): Unit =
        bind[CustomTFInterface[IO]].toInstance(new CustomTFInterfaceImpl)
    }
    
    class HomeControllerSpec extends AnyFlatSpec with Matchers {
    
      it should "test1" in {
        val application: Injector = new GuiceInjectorBuilder()
          .bindings(bind[ExecutionContext].to(ExecutionContext.global))
          .bindings(bind[ApplicationLifecycle].to[DefaultApplicationLifecycle])
          .bindings(bind[CustomTFInterface[IO]].toInstance(new CustomTFInterfaceImpl))
          .bindings(new CatsEffectModule())
          .injector()
    
        application.instanceOf[CustomTFInterface[IO]] // works
        //application.instanceOf[InjecableWithTfDependencies] // fails
    
      }
    
      it should "test2" in {
        val application: Injector = new GuiceInjectorBuilder()
          .bindings(bind[ExecutionContext].to(ExecutionContext.global))
          .bindings(bind[ApplicationLifecycle].to[DefaultApplicationLifecycle])
          .bindings(new WorkingModule())
          .bindings(new CatsEffectModule())
          .injector()
    
        //application.instanceOf[CustomTFInterface[IO]] // fails
        application.instanceOf[InjecableWithTfDependencies] // works
      }
    
      it should "test3" in {
        val application: Injector = new GuiceInjectorBuilder()
          .bindings(bind[ExecutionContext].to(ExecutionContext.global))
          .bindings(bind[ApplicationLifecycle].to[DefaultApplicationLifecycle])
          // both binds together also works
          .bindings(new WorkingModule())
          .bindings(bind[CustomTFInterface[IO]].toInstance(new CustomTFInterfaceImpl))
          .bindings(new CatsEffectModule())
          .injector()
    
        application.instanceOf[CustomTFInterface[IO]]       // works
        application.instanceOf[InjecableWithTfDependencies] // works
      }
    
    }
    

    Perhaps there is a way to make scala-guice and play work together but i got no luck with it.

    Full example: https://github.com/DenisNovac/play-tf-test