Conditional injection with scala play and guice
It’s been a crazy year for me. For those who don’t know I moved from the east coast to the west coast to work for a rather large softare company in seattle (I’ll let you figure which one out) and after a few short weeks realized I made a horrible mistake and left the team. I then found a cool job at a smaller .net startup that was based in SF and met some awesome people and learned a lot. But, I’ve been poached by an old coworker and am now going to go work at a place that uses more open source things so I decided to kick into gear and investigate scala and play.
For the most part I’m doing a mental mapping of .NET’s web api framework to the scala play framework, but the more I play in play (pun intended) the more I like it.
On one of my past projects a coworker of mine set up a really interesting framework leveraging ninject and web api where you can conditionally inject a data source for test data by supplying a query parameter to a rest API of “test”. So the end result looks something like:
[GET("foo/{name}")]
public void GetExample(string name, [IDataSource] dataProvider){
// act on data provider
}
The way it chose the correct data provider is by leveraging a custom parameter binder that will resolve the source from the ninject kernel based on the query parameters. I’ve found that this worked out really well in practice. It lets the team set up some sample data while testers/qa/ui devs can start building out consuming code before the db layers are even complete.
I really liked working with this pattern so I wanted to see how we can map this to the scala play framework. Forgive me if what I post isn’t idiomatic scala, I’ve only been at it for a day :)
First I want to define some data sources
trait DataSource{
def get : String
}
class ProdSource extends DataSource{
override def get: String = "prod"
}
class TestSource extends DataSource {
override def get : String = "test"
}
It should be pretty clear whats going on here. I’ve defined two classes that implement the data source trait. Which one that gets injected should be defined by a query parameter.
Guice lets you define bindings for the same trait (interface) to a target class based on “keys”. What this means is you can say “give me class A, and use the default binding”, or you can say “give me class A, but the one that is tagged with interface Test”. When you register the classes you can provider this extra tagging mechanism. This is going to be useful because you can now request different versions of the interface from the binding kernel.
Lets just walk through the remaining example. First we need the interface, but Guice wants it to be an annotation. Since scala has weird support for annotations and the JVM has shitty type erasure, I had to write the annotation in java
import com.google.inject.BindingAnnotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@BindingAnnotation
public @interface TestAnnotation {}
I’m honestly not even sure I need the @Target, but whatever.
Next we’re gonna create some binding modules for Guice to use where we can specify the conditional binding:
package Modules
import annotations.TestAnnotation
import com.google.inject.AbstractModule
import controllers.{DataSource, ProdSource, TestSource}
class SourceModule extends AbstractModule {
override def configure(): Unit = {
testable(classOf[DataSource], classOf[ProdSource], classOf[TestSource])
}
def testable[TInterface, TMain \<: TInterface, TDev \<: TInterface](
interface: Class[TInterface],
main: Class[TMain],
test: Class[TDev]) = {
val markerClass = classOf[TestAnnotation]
bind(interface).to(main)
bind(interface) annotatedWith markerClass to test
}
}
What this is saying is that given the 3 types (the main interface, the implementation of the main item, and the implementation of the dev item) to conditionally bind the dev item to the marker class of “TestAnnotation”. This will make sense when you see how its used.
As normal, guice is used to set up the controller instantation with the source module registered.
import Modules.{DbModule, SourceModule}
import com.google.inject.Guice
import play.api.GlobalSettings
object Global extends GlobalSettings {
val kernel = Guice.createInjector(new SourceModule())
override def getControllerInstance[A](controllerClass: Class[A]): A = {
kernel.getInstance(controllerClass)
}
}
Now comes the fun part of actually resolving the query parameter. I’m going to wrap an action and create a new action so we can get a nodejs style (datasource, request) =>
lambda.
trait Sourceable{
val kernelSource : Injector
def WithSource[T] (clazz : Class[T]) (f: ((T, Request[AnyContent]) =\> Result)) : Action[AnyContent] = {
Action { request =\> {
val binder =
request.getQueryString(sourceableQueryParamToggle) match {
case Some(\_) =\> kernelSource.getInstance(Key.get(clazz, classOf[TestAnnotation]))
case None =\> kernelSource.getInstance(clazz)
}
f(binder, request)
}}
}
def sourceableQueryParamToggle = "test"
}
The kernel never has to be registered since Guice will auto inject it when its asked for (its implicity available). Whats happening here is that we set up the kernel and the target interface type we want to get (i.e. DataSource). If the query string matches the sourceable query param toggle (i.e. the word “test”) then it’ll pick up the registered data source using the “test annotation” marker. Otherwise it uses the default.
Finally the controller now looks like this:
@Singleton
class Application @Inject() (db : DbAccess, kernel : Injector) extends Controller with Sourceable {
override val kernelSource: Injector = kernel
def binding(name : String) = WithSource(classOf[DataSource]){ (provider, request) =\>
{
val result = name + ": " + provider.get
Ok(result)
}}
}
And the route
GET /foo/:name @controllers.Application.binding(name: String)
The kernel value is provided to the trait and any other methods can now ask for a data provider of a particular type and get it.
Full source available at my github.