Monday, November 23, 2009

Extending Soy Templates

One of the more interesting features of Soy templates is that you can extend the templating system. While working with Soy from JavaScript, I found that I needed to do exactly that.

In order to avoid some confusion as to how you can extend the template system in Scala, even though you will be using the templates from JavaScript, I'll quickly explain how Soy templates work when you're using JavaScript. Essentially, you precompile your .soy file into a .js file using a template compiler (which is written in Java). It's the compiler that you're extending and it's output is what you use in your web app. So you don't directly use the template from JavaScript.

What I wanted to do was something like this:

{switch typeof($msgparm)}
{case 'number'}
<i4>{$msgparm}</i4>
{case 'string'}
<string>{$msgparm}</string>
{/switch}

Unfortunatly, there is no typeof function "out of the box" so I needed to write my own in a plugin. As I was only interested in using this function in templates that I'm going to use in JavaScript, I needed to write a class that implements a single interface - SoyJsSrcFunction. If I wanted to use this function from Java, I'd need to implement SoyToFuFunction instead. There are only 3 methods to implement, 2 of which are one-liners in Scala. In this instance, all three are one liners, but computeForJsSrc could be more complicated, depending on what you're doing.


@Singleton
class TypeOfFunction @Inject() extends SoyJsSrcFunction {
def getName = "typeof"

def getValidArgsSizes: java.util.Set[Integer] = ImmutableSet.of[Integer](1);

def computeForJsSrc(args: java.util.List[JsExpr]): JsExpr = new JsExpr("typeof(" + args.get(0).getText() + ")", Integer.MAX_VALUE)

}


Scala Source Code

As you can probably tell, all this does is use JavaScript's typeof operator to implement the functionality I was looking for.

You might notice the @Singleton and @Inject annotations - Soy uses Google Guice for dependency injection. This means that we're not done yet - we also need to write a subclass of Guice's AbstractModule so that our extension can be injected into Soy. Fortunately it's trivial and XML-free:

class ExtendedFunctionsModule extends AbstractModule {
def configure() = {
val soyFunctionsSetBinder = Multibinder.newSetBinder(binder(), classOf[SoyFunction])
soyFunctionsSetBinder.addBinding().to(classOf[TypeOfFunction])
}

}

Scala Source Code

To use the extension all I need to do is add my Module class to the template compiler's pluginModules option:


java -cp typeof.jar:scala-library.jar:SoyToJsSrcCompiler.jar com.google.template.soy.SoyToJsSrcCompiler --pluginModules com.maethorechannen.widgets.ExtendedFunctionsModule --outputPathFormat rpc.js rpc.soy

Consice Java Maps in Scala

This post is a follow up to my previous post on using Closure Templates in Scala.

One of the things that bugged me in the Scala example was how verbose it was compared to the Groovy version. So I had an idea - use YAML and the snakeyaml library to generate a Java Map with my data. While it's still a couple lines longer than the Groovy version, it's a lot simpler than the Scala original.

The biggest change is this:

val y = """widget: {
name: Widget Name,
display: {
width: 240,
height: 320
}
}"""

val yaml = new Yaml()
val map = yaml.load(y).asInstanceOf[java.util.Map[String, _]]


Scala Source Code

Using Google's Closure Templates in Scala and Groovy

Google recently released it's Closure Tools - which is made up of a JavaScript compiler, a JavaScript framework and a templating system which can be used from both Java and JavaScript. I'm guessing from the package names that Closure Templates are actually called Soy templates internally at Google, and that's what I'm going to call them for the rest of this post.

I'm not going to go into the syntax of Soy templates - if you've ever used a templating system like FreeMarker or Django Templates then you shouldn't have any trouble using Soy templates (though you should be aware that Soy eats white space, so your output might not be what you expect if you don't use special character commands to put whitespace into the output).

Here's an example template (that I saved as "widget.soy"):

{namespace com.maethorechannen.widgets}

/** Widget template
* @param widget Widget Data
*/
{template .widget}
<?xml version="1.0" encoding="utf-8"?>{\n}
<widget>{\n}
{\t}<widgetname>{$widget.name}</widgetname>{\n}
{\t}<width>{$widget.display.width}</width>{\n}
{\t}<height>{$widget.display.height}</height>{\n}
</widget>
{/template}


Using the templates from Scala:

At a minimum, you'll need the following imports:

import com.google.template.soy.SoyFileSet
import com.google.template.soy.data.SoyMapData
import com.google.template.soy.tofu.SoyTofu
import java.io.File


To pass your data to the ToFu rendering engine, you'll need to have your data in either a SoyMapData object or in a plain old Java Map object. As I wanted to use Scala's sugar for quickly building up a Map, I needed to write up an implicit def from Scala's Map to a SoyMapData (quick aside - the sort of thing that stops me from loving Scala is having to do something like this - the impedence mismatch between Java collections and Scala collections is still too big). This function isn't complete, but it will convert the values I was using:


implicit def mapToSoy(m: Map[String, _]): SoyMapData = {
val sm = new SoyMapData()
m.keys.foreach {k =>
val v: Any = m(k)
v match {
case s: String => sm.put(k,s)
case mm: Map[String, _] => sm.put(k, mapToSoy(mm))
case i: Int => sm.put(k, i)
}
}
return sm
}

Here's the Map I'm passing in:

val map = Map[String, Any](
"widget" -> Map[String, Any](
"name" -> "Scala Test",
"display" -> Map[String, Any](
"width"-> 240,
"height" -> 320)))

To actually use the template, I need to load the template into a SoyFileSet, and then compile the template to a rendering object


val sfs = (new SoyFileSet.Builder()).add(new File("widget.soy")).build();
val tofu = sfs.compileToJavaObj()

Finally, I render the template with my data

println(tofu.render("com.maethorechannen.widgets.widget", map, null));

Which renders the following:

<?xml version="1.0" encoding="utf-8"?>
<widget>
<widgetname>Scala Test</widgetname>
<width>240</width>
<height>320</height>
</widget>


Scala Source Code

Using the templates from Groovy:

Using Soy from Groovy is almost identical to using Soy from Scala, but without the need for the implicit def (as Groovy Map sugar returns a Java Map).

import com.google.template.soy.SoyFileSet
import com.google.template.soy.data.SoyMapData
import com.google.template.soy.tofu.SoyTofu

def map = [widget: [
name: "Widget Name",
display: [
width: 240,
height: 320
]
]]

def sfs = (new SoyFileSet.Builder()).add(new File("widget.soy")).build();
def tofu = sfs.compileToJavaObj()
println(tofu.render("com.maethorechannen.widgets.widget", map, null));


Groovy Source Code