Simple Jinja-like templating in Scala

(, en)

There are impressive templating engines available for complex challenges. And Scala has the s"..." construct. If you need something in the middle, probably in an external file under e.g. src/main/resources, a simple templating function comes in handy. I cannot say that I’m a big fan of Python’s Jinja, but the {{...}} syntax is well known and straight forward. Without further ado, the templating function:

import scala.util.matching.Regex

/**
 * Python's Jinja for poor people.
 *
 * A very simple function to replace variables in text using `` syntax.
 *
 * @param tmpl
 *   the template string/text
 * @param vars
 *   a map of variable -> replacement mapping
 * @param relaxed
 *   Don't raise a NoSuchElementException if a variable is not in the var map.
 * @return
 *   the text with variables replaced.
 */
def renderTemplate(tmpl: String, vars: Map[String, String], relaxed: Boolean = false): String =
  val pattern: Regex = """\{\{\s*(\w+)\s*\}\}""".r

  def handleMatch(m: Regex.Match): String =
    val key = m.group(1)
    vars.get(key) match
      case Some(value) => Regex.quoteReplacement(value)
      case None =>
        if relaxed then m.group(0)
        else throw new NoSuchElementException(s"$key is not in the supplied replacement variables")

  pattern.replaceAllIn(tmpl, handleMatch)

Example usage looks like this:

it should "replaces entries" in {
  val tmpl = """create table {{ns}}.{{table}}.{{ns}}
      |{{ns}}.{{foo}}""".stripMargin
  val actual = renderTemplate(tmpl, Map("ns" -> "lhd", "table" -> "core", "foo" -> "bar"))
  val expected =
    """create table lhd.core.lhd
      |lhd.bar""".stripMargin

  assert(actual == expected)
}

That is much better than concatenation in Scala code. If you want to load the file from the resources folder src/main/resources/template.txt, you can do this:

import scala.io.Source
def fromResource(path: String): String = Source.fromInputStream(getClass.getResourceAsStream(path)).mkString
val data = fromResource("/template.txt")
renderTemplate(data, Map(???))

Have fun!