@dynamicCallable Part 3: Mustacheable

After Shell commands as Swift functions and the Swift/ObjC Bridge, Part 3 in our quest to find a useful application for the Swift 5 Dynamic Callable feature: Mustache templates as a function (short: MaaF). This one may actually make some sense.

This sample for Dynamic Callable is much smaller and easier to follow along than the previous ones. Remember that you need to have Swift 5 via Xcode 10.2.

So what is Mustache? It is a super simple templating language. The ARI is providing an implementation for Swift: mustache. A template looks like this:

Hello {{name}}
You have just won {{& value}} dollars!
{{#in_ca}}
  Well, {{{taxed_value}}} dollars, after taxes.
{{/in_ca}}
{{#addresses}}
  Has address in: {{city}}
{{/addresses}}
{{^addresses}}
  Move to Germany.
{{/addresses}}

The template features value access: {{name}}, conditionals: {{#in_ca}}, as well as repetitions: {{#addresses}}.

The ARI Swift version comes w/ a simple Mirror based KVC implementation and you can invoke it like that:

let sampleDict  : [ String : Any ] = [
  "name"        : "Chris",
  "value"       : 10000,
  "taxed_value" : Int(10000 - (10000 * 0.4)),
  "in_ca"       : true,
  "addresses"   : [
    [ "city"    : "Cupertino" ]
  ]
]

let parser = MustacheParser()
let tree   = parser.parse(string: template)
let result = tree.render(object: sampleDict)

You get the idea.

Dynamic Callable

But if you think about it, the template is really a function which takes a set of input parameters and returns a String with the rendered result:

func generateHTMLForWinner(arguments) -> String

let html = generateHTMLForWinner(
             name: "Chris", value: 10000,
             taxed_value: 6000, in_ca: true,
             addresses: [[ "city": "Cupertino" ]]
           )

How that function is implemented, doesn’t matter at all. And using Dynamic Callable you can implement Swift functions in arbitrary ways. In this case it is super simple:

@dynamicCallable   // <===
struct Mustache {
  
  let template : MustacheNode
  
  init(_ template: String) {
    let parser = MustacheParser()
    self.template = parser.parse(string: template)
  }
  
  func dynamicallyCall(withKeywordArguments 
         arguments: KeyValuePairs<String, Any>) 
       -> String
  {
    let dictArgs = Dictionary(uniqueKeysWithValues:
          arguments.map { ( $0.key, $0.value) })
    return template.render(object: dictArgs)
  }
}

This defines a struct Mustache which wraps a (parsed) Mustache template and can be invoked as many times as you wish. We turn the struct into callable “function” by implementing Dynamic Callable.

Declaring a function which is implemented in Mustache, instead of Swift:

let generateHTMLForWinner = Mustache(
      """
      Hello {{name}}
      You have just won {{& value}} dollars!
      {{#in_ca}}
        Well, {{{taxed_value}}} dollars, after taxes.
      {{/in_ca}}
      {{#addresses}}
        Has address in: {{city}}
      {{/addresses}}
      {{^addresses}}
        Move to Germany.
      {{/addresses}}
      """)

And call that function:

let winners = [
      generateHTMLForWinner(
        name: "Chris", value: 10000,
        taxed_value: 6000, in_ca: true,
        addresses: [[ "city": "Cupertino" ]]
      ),
      generateHTMLForWinner(
        name: "Michael", value: 6000,
        taxed_value: 6000, in_ca: false,
        addresses: [[ "city": "Austin" ]]
      )
    ]

Pretty slick, isn’t it? To the API consumer it looks like the plain function:

func generateHTMLForWinner(arguments) -> String

It doesn’t matter to the consumer whether that HTML generation is backed by Swift code, by a Mustache template, by a WOTemplate, or by Objective-C.

Of course the approach has some obvious deficiencies. For example it is unclear from the API what kind of arguments are supported. But hey, that’s why it’s called dynamic 😎

Summary

You can find the finished implementation on GitHub. It even comes with tests! ⛑

The ARI mustache module can be consumed as a regular SPM module, and it also comes with an Xcode project.
µExpress supports Mustache templates. Here is our tutorial on how to enhance µExpress/SwiftNIO w/ Mustache template support: µExpress/NIO - Adding Templates.

Written on January 31, 2019