Scala Templates

I've recently been messing around with Scala and figured I'd tackle a problem I've already solved in other languages. Here is the template kata implemented using a regex to match the placeholders:

class Template(val tmpl: String) {
    def render(context: Map[String, Object]): String = {
        return """\$(\w+)""".r.replaceAllIn(tmpl, m => {
            context.getOrElse(m.group(1), "").toString()
        })
    }
}

Here's a couple things I like:

  • Scala's Maps have a getOrElse method, yay! It's a little more verbose than Python's get method, but at least I don't have to write it my self.
  • It's actually shorter than my first implementation in JavaScript, including the type annotations.
  • I actually really like the class parameter, implicit field, thing (val tmpl: String). It handles the "The constructor takes and argument and binds it to a field." use-case very nicely.
  • Scala's lambda expressions are very concise, and it's hard not to like that.

Here are the tests written in a BDD style using ScalaTest:

import org.scalatest.FlatSpec
import org.scalatest.matchers.ShouldMatchers

class TemplateSpec extends FlatSpec with ShouldMatchers {
    val EMPTY = Map[String, Object]()

    private def test(tmpl: String, expected: String, ctx: Map[String, Object] = EMPTY) {
        new Template(tmpl).render(ctx) should equal (expected)
    }

    "A template with no tokens" should "return a blank string if the given string is blank" in {
        test("", "")
    }

    it should "return a non-blank string unchanged" in {
        val identity = "hello, world"
        test(identity, identity)
    }

    "A template with one token" should "replace missing values with the empty string" in {
        test("$name", "")
    }

    it should "replace the token with value from the context" in {
        test("$name", "Aaron", Map("name" -> "Aaron"))
    }

    it should "leave the rest of the string unchanged" in {
        test("Hello $name!", "Hello Aaron!", Map("name" -> "Aaron"))
    }

    "A template with two tokens" should "replace each token its value from the context" in {
        test("Hello $name! It is a $attitude day.", "Hello Aaron! It is a wonderful day.", Map(
            "name" -> "Aaron", "attitude" -> "wonderful"
        ))
    }

    it should "replace each missing value with the empty string" in {
        test("Hello $name! It is a $attitude day.", "Hello Aaron! It is a  day.", Map(
            "name" -> "Aaron"
        ))
    }

    it should "replace adjacent tokens with their respective values" in {
        test("$me$you", "AaronJohn", Map("me" ->"Aaron", "you" -> "John"))
    }

    it should "replace every occurrance of each placeholder" in {
        test("$a-$b-$a-$b", "AAA--AAA-", Map("a" -> "AAA"))
    }
}

They're a little verbose, but as I mentioned in a previous post, you can extract documentation from tests like these:

TemplateSpec:
A template with no tokens
- should return a blank string if the given string is blank
- should return a non-blank string unchanged

A template with one token
- should replace missing values with the empty string
- should replace the token with value from the context
- should leave the rest of the string unchanged

A template with two tokens
- should replace each token its value from the context
- should replace each missing value with the empty string
- should replace adjacent tokens with their respective values
- should replace every occurrance of each placeholder