codingwell / scala-guice

Scala extensions for Google Guice
Apache License 2.0
341 stars 44 forks source link

TypeConversions support for complex types #88

Closed cacoco closed 4 years ago

cacoco commented 4 years ago

Problem

It is not currently possible to support building a TypeLiteral from a complex Scala type, e.g., Foo with Trait.

Solution

Add a case statement in the net.codingwell.scalaguice.ClassType#unapply to match on a scala.reflect.internal.RefinedType which is defined as a class representing the intersection of types with refinements of the form parents0 with parentsN. Then use this as signal to process the erasure form of the type passing it back through the unapply.

Add test cases. This addresses #87.

tsuckow commented 4 years ago

I suspect this replicates the behaviour of when we used manifests. As such does this mean in guice Foo with Bar looks the same as Foo alone? Meaning if you have a constructor looking for Foo with Bar and bind a plain old Foo, guice will try to inject it and you'll runtime error when you try to access something from Bar?

cacoco commented 4 years ago

I suspect this replicates the behaviour of when we used manifests. As such does this mean in guice Foo with Bar looks the same as Foo alone? Meaning if you have a constructor looking for Foo with Bar and bind a plain old Foo, guice will try to inject it and you'll runtime error when you try to access something from Bar?

That is what I noticed stepping through the debugger of binding this type with a standard Guice AbstractModule, the TypeLiteral built is just for type Foo. Let me test what happens when binding Foo with Bar and Foo to see what happens but I suspect this fails which makes a type of sense.

cacoco commented 4 years ago

As I expected this fails.

Welcome to Scala 2.12.10 (JDK 64-Bit Server VM, Java 1.8.0_222).
Type in expressions for evaluation. Or try :help.

scala> class SomeClient {
     |   private[this] val underlying: String = "Alice"
     |
     |   def doSomething: String = s"Hello, $underlying."
     | }
defined class SomeClient

scala>

scala> trait Augmentation { self: SomeClient =>
     |   override def doSomething: String = s"${self.doSomething} Welcome to Wonderland."
     | }
defined trait Augmentation

scala> import com.google.inject.{AbstractModule, Guice, Provides}
import com.google.inject.{AbstractModule, Guice, Provides}

scala> import net.codingwell.scalaguice.ScalaModule
import net.codingwell.scalaguice.ScalaModule

scala> import javax.inject.Singleton
import javax.inject.Singleton

scala> Guice.createInjector(
     |         new AbstractModule with ScalaModule {
     |           @Provides
     |           @Singleton
     |           def provideSomeClient: SomeClient with Augmentation = {
     |             new SomeClient with Augmentation
     |           }
     |         },
     |         new AbstractModule with ScalaModule {
     |           @Provides
     |           @Singleton
     |           def provideSomeClient: SomeClient = {
     |             new SomeClient
     |           }
     |         }
     |       )
com.google.inject.CreationException: Unable to create injector, see the following errors:

1) A binding to SomeClient was already configured at $anon$1.provideSomeClient().
  at $anon$3.provideSomeClient(<console>:29)

1 error
  at com.google.inject.internal.Errors.throwCreationExceptionIfErrorsExist(Errors.java:554)
  at com.google.inject.internal.InternalInjectorCreator.initializeStatically(InternalInjectorCreator.java:161)
  at com.google.inject.internal.InternalInjectorCreator.build(InternalInjectorCreator.java:108)
  at com.google.inject.Guice.createInjector(Guice.java:87)
  at com.google.inject.Guice.createInjector(Guice.java:69)
  at com.google.inject.Guice.createInjector(Guice.java:59)
  ... 49 elided
cacoco commented 4 years ago

And accessing the type Foo from the injector even though Foo with Bar was bound appears to work:

Welcome to Scala 2.12.10 (JDK 64-Bit Server VM, Java 1.8.0_222).
Type in expressions for evaluation. Or try :help.

scala> class SomeClient {
     |   protected val underlying: String = "Hello, Alice."
     |
     |   def doSomething: String = underlying
     | }
defined class SomeClient

scala>

scala> trait Augmentation { self: SomeClient =>
     |   override def doSomething: String = s"${self.underlying} Welcome to Wonderland."
     | }
defined trait Augmentation

scala> import com.google.inject.{AbstractModule, Guice, Provides}
import com.google.inject.{AbstractModule, Guice, Provides}

scala> import net.codingwell.scalaguice.ScalaModule
import net.codingwell.scalaguice.ScalaModule

scala> import javax.inject.Singleton
import javax.inject.Singleton

scala> import com.google.inject.Stage
import com.google.inject.Stage

scala> val i = Guice.createInjector(
     |       Stage.PRODUCTION,
     |       new AbstractModule with ScalaModule {
     |         @Provides
     |         @Singleton
     |         def provideSomeClient: SomeClient with Augmentation = {
     |           new SomeClient with Augmentation
     |         }
     |       })
i: com.google.inject.Injector = Injector{bindings=[InstanceBinding{key=Key[type=com.google.inject.Stage, annotation=[none]], source=[unknown source], instance=PRODUCTION}, ProviderInstanceBinding{key=Key[type=com.google.inject.Injector, annotation=[none]], source=[unknown source], scope=Scopes.NO_SCOPE, provider=Provider<Injector>}, ProviderInstanceBinding{key=Key[type=java.util.logging.Logger, annotation=[none]], source=[unknown source], scope=Scopes.NO_SCOPE, provider=Provider<Logger>}, ProviderInstanceBinding{key=Key[type=SomeClient, annotation=[none]], source=public SomeClient $anon$1.provideSomeClient(), scope=Scopes.SINGLETON, provider=@Provides $anon$1.provideSomeClient(<console>:23)}]}

scala> import net.codingwell.scalaguice.InjectorExtensions._
import net.codingwell.scalaguice.InjectorExtensions._

scala> val r = i.instance[SomeClient]
r: SomeClient = $anon$1$$anon$2@1a3611b1

scala> r.doSomething
res0: String = Hello, Alice. Welcome to Wonderland.

And appears to return the correct binding (that is Foo with Bar).

cacoco commented 4 years ago

And a ClassCastException when you try to use the Foo with Bar though only a Foo was bound:

scala>  import javax.inject.Inject
import javax.inject.Inject

scala> trait Augmentation { self: SomeClient =>
     |   override def doSomething: String = s"${self.underlying} Welcome to Wonderland."
     |
     |   def onlyHere: String = "Only on augmented classes."
     | }
defined trait Augmentation

scala> val i = Guice.createInjector(
     |       Stage.PRODUCTION,
     |       new AbstractModule with ScalaModule {
     |         @Provides
     |         @Singleton
     |         def provideSomeClient: SomeClient = {
     |           new SomeClient
     |         }
     |       })
i: com.google.inject.Injector = Injector{bindings=[InstanceBinding{key=Key[type=com.google.inject.Stage, annotation=[none]], source=[unknown source], instance=PRODUCTION}, ProviderInstanceBinding{key=Key[type=com.google.inject.Injector, annotation=[none]], source=[unknown source], scope=Scopes.NO_SCOPE, provider=Provider<Injector>}, ProviderInstanceBinding{key=Key[type=java.util.logging.Logger, annotation=[none]], source=[unknown source], scope=Scopes.NO_SCOPE, provider=Provider<Logger>}, ProviderInstanceBinding{key=Key[type=SomeClient, annotation=[none]], source=public SomeClient $anon$1.provideSomeClient(), scope=Scopes.SINGLETON, provider=@Provides $anon$1.provideSomeClient(<console>:26)}]}

scala> class Baz @Inject() (client: SomeClient with Augmentation) {
     | client.onlyHere
     | }
defined class Baz

scala> i.instance[Baz]
com.google.inject.ProvisionException: Unable to provision, see the following errors:

1) Error injecting constructor, java.lang.ClassCastException: SomeClient cannot be cast to Augmentation
  at Baz.<init>(<console>:25)
  while locating Baz

1 error
  at com.google.inject.internal.InternalProvisionException.toProvisionException(InternalProvisionException.java:226)
  at com.google.inject.internal.InjectorImpl$1.get(InjectorImpl.java:1097)
  at com.google.inject.internal.InjectorImpl.getInstance(InjectorImpl.java:1126)
  at net.codingwell.scalaguice.InjectorExtensions$ScalaInjector.instance(InjectorExtensions.scala:27)
  ... 34 elided
Caused by: java.lang.ClassCastException: SomeClient cannot be cast to Augmentation
  ... 36 more