What's new in Leaf 4 (Tau)?

Everything you should know about the upcoming Leaf template engine update and how to migrate your Vapor / Swift codebase.

Vapor

Using Leaf 4 Tau

Before we dive in, let's make a new Vapor project with the following package definition.

// swift-tools-version:5.3
import PackageDescription

let package = Package(
    name: "myProject",
    platforms: [
       .macOS(.v10_15)
    ],
    dependencies: [
        // πŸ’§ A server-side Swift web framework.
        .package(url: "https://github.com/vapor/vapor.git", from: "4.30.0"),
        .package(url: "https://github.com/vapor/leaf", .exact("4.0.0-tau.1")),
        .package(url: "https://github.com/vapor/leaf-kit", .exact("1.0.0-tau.1.1")),
    ],
    targets: [
        .target(name: "App", dependencies: [
            .product(name: "Vapor", package: "vapor"),
            .product(name: "Leaf", package: "leaf"),
        ]),
        .target(name: "Run", dependencies: ["App"]),
        .testTarget(name: "AppTests", dependencies: [
            .target(name: "App"),
            .product(name: "XCTVapor", package: "vapor"),
        ])
    ]
)

The very first thing I'd like to show you is that we have a new render method. In the past we were able to use the req.view.render function to render our template files. Consider the following really simple index.leaf file with two context variables that we'll give display real soon.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>#(title)</title>
    </head>
    <body>
        #(body)
    </body>
</html>

Now in our Vapor codebase we could use something like this to render the template.

import Vapor
import Leaf

public func configure(_ app: Application) throws {

    app.views.use(.leaf)

    app.get() { req -> EventLoopFuture<View> in
        struct Context: Encodable {
            let title: String
            let body: String
        }
        let context = Context(title: "Leaf 4", body:"Hello Leaf Tau!")
        return req.view.render("index", context)
    }
}

We can use an Encodable object and pass it around as a context variable. This is a convenient way of providing values for our Leaf variables. Before we continue I have to tell you that all of this will continue to work in Leaf Tau and you don't have to use the new methods. πŸ‘


New render methods

So let me show you the exact same thing using the new API.

import Vapor
import Leaf

public func configure(_ app: Application) throws {

    app.views.use(.leaf)

    app.get() { req -> EventLoopFuture<View> in
        let context: LeafRenderer.Context = [
            "title": "Leaf 4",
            "body": "Hello Leaf Tau!",
        ]
        return req.leaf.render(template: "index", context: context)
    }
}

That's not a big deal you could say at first sight. Well, the thing is that this new method provides type-safe values for our templates and this is just the tip of the iceberg. You should forget about the view property on the request object, since Leaf started to outgrow the view layer in Vapor.

import Vapor
import Leaf

public func configure(_ app: Application) throws {

    app.views.use(.leaf)

    app.get() { req -> EventLoopFuture<View> in
        let name = "Leaf Tau"
        let context: LeafRenderer.Context = [
            "title": "Leaf 4",
            "body": .string("Hello \(name)!"),
        ]
        return req.leaf.render(template: "index",
                               from: "default",
                               context: context,
                               options: [.caching(.bypass)])
    }
}

If you take a closer look at this similar example, you find out that the context object and the values are representable by various types, but if we try to use an interpolated string, we have to be a little bit more type specific. A LeafRenderer.Context object is somewhat a [String: LeafData] alias where LeafData has multiple static methods to initialize the built-in basic Swift types for Leaf. This is where the type-safety feature comes in Tau. You can use the static LeafData helper methods to send your values as given types. πŸ”¨

The from parameter can be a LeafSource key, if you are using multiple template locations or file sources then you can render a view using a specific one, ignoring the source loading order. There is another render method without the from parameter that'll use the default search order of sources.

There is a new argument that you can use to set predefined options. You can disable the cache mechanism with the .caching(.bypass) value or the built-in warning message through .missingVariableThrows(false) if a variable is not defined in your template, but you are trying to use it. You can update the timeout using .timeout(Double) or the encoding via .encoding(.utf8) and grant access to some nasty entities by including the .grantUnsafeEntityAccess(true) value plus there is a embeddedASTRawLimit option. More about this later on.

It is also possible to disable Leaf cache globally through the LeafRenderer.Context property:

if !app.environment.isRelease {
    LeafRenderer.Option.caching = .bypass
}

If the cache is disabled Leaf will re-parse template files every time you try to render something. Anything that can be configured globally for LeafKit is marked with the @LeafRuntimeGuard property wrapper, you can change any of the settings at application setup time, but they're locked as soon as a LeafRenderer is created. πŸ”’


Context and data representation

You can conform to the LeafDataRepresentable protocol to submit a custom type as a context value. You just have to implement one leafData property.

struct User {
    let id: UUID?
    let email: String
    let birthYear: Int?
    let isAdmin: Bool
}
extension User: LeafDataRepresentable {
    var leafData: LeafData {
        .dictionary([
            "id": .string(id?.uuidString),
            "email": .string(email),
            "birthYear": .int(birthYear),
            "isAdmin": .bool(isAdmin),
            "permissions": .array(["read", "write"]),
            "empty": .nil(.string),
        ])
    }
}

As you can see there are plenty of LeafData helper methods to represent Swift types. Every single type has built-in optional support, so you can send nil values without spending additional effort on value checks or nil coalescing.

app.get() { req -> EventLoopFuture<View> in
    let user = User(id: .init(),
                email: "root@localhost.com",
                birthYear: 1980,
                isAdmin: false)

    return req.leaf.render(template: "profile", context: [
        "user": user.leafData,
    ])
}

You can construct a LeafDataRepresentable object, but you still have to use the LeafRenderer.Context as a context value. Fortunately that type can be expressed using a dictionary where keys are strings and values are LeafData types, so this will reduce the amount of code that you have to type.


Constants, variables, nil coalescing

Now let's move away a little bit from Swift and talk about the new features in Leaf. In Leaf Tau you can define variables using template files with real dictionary and array support. πŸ₯³

#var(x = 2)
<p>2 + 2 = #(x + 2)</p>
<hr>
#let(user = ["name": "Guest"])
<p>Hello #(user.name)</p>
<hr>
#(optional ?? "fallback")

Just like in Swift, we can create variables and constants with any of the supported types. When you inline a template variables can be accessed in both templates, that's pretty handy because you don't have to repeat the same code again and again, but you can use variables and reuse chunks of Leaf code in a clean and efficient way. Let me show you how this works.

It is also possible to use the coalescing operator to provide fallback values for nil variables.


Define, Evaluate, Inline

One of the biggest debate in Leaf is the whole template hierarchy system. In Tau, the entire approach is rebuilt under the hood (the whole thing is more powerful now), but from the end-user perspective only a few keywords have changed.

Inline

Extend is now replaced with the new inline block. The inline method literally puts the content of a template into another. You can even use raw values if you don't want to perform other operations (such as evaluating Leaf variables and tags) on the inlined template.

<!-- index.leaf -->
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Leaf 4</title>
    </head>
    <body>
        #inline("home", as: raw)
    </body>
</html>

<!-- home.leaf -->
<h1>Hello Leaf Tau!</h1>

As you can see we're simply putting the content of the home template into the body section of the index template.

Now it's more interesting when we skip the raw part and we inline a regular template that contains other expressions. We are going to flip things just a little bit and render the home template instead of the index.

app.get() { req -> EventLoopFuture<View> in
    req.leaf.render(template: "home", context: [
        "title": "Leaf 4",
        "body": "Hello Leaf Tau!",
    ])
}

So how can I reuse my index template? Should I simply print the body variable and see what happens? Well, we can try that...

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>#(title)</title>
    </head>
    <body>
        #(body)
    </body>
</html>

<!-- home.leaf -->
<h1>Hello Leaf Tau!</h1>
#inline("index")

Wait a minute... this code is not going to work. In the home template first we print the body variable, then we inline the index template and print its contents. That's not what we want. I want to use the contents of the home template and place it in between the body tags. πŸ’ͺ

Evaluate

Meet evaluate, a function that can evaluate a Leaf definition. You can think of this as a block variable definition in Swift. You can create a variable with a given name and later on call that variable (evaluate) using parentheses after the name of the variable. Now you can do the same thin in Leaf by using the evaluate keyword or directly calling the block like a function.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>#(title)</title>
    </head>
    <body>
        #evaluate(bodyBlock) (# or you can use the `#bodyBlock()` syntax #)
    </body>
</html>

In this template we can evaluate the bodyBlock and later on we'll be able to define it somewhere else.

Define

Definitions. Finally arrived to the very last component that we'll need to compose templates. Now we can create our body block in the home template.

#define(bodyBlock):
<h1>#(body)</h1>
#enddefine

#inline("index")

Now if you reload the browser (Leaf cache must be disabled) everything should work as it is expected. Magic... or science, whatever, feel free to choose one. πŸ’«

Special thanks goes to tdotclare who worked day and night to make Leaf better. πŸ™

So what's going on here? The #define(bodyBlock) section is responsible for building a block variable called bodyBlock that is callable and we can evaluate it later on. We simply print out the body context variable inside this block, the body variable is a context variable coming from Swift, that's quite straightforward. Next we inline the index template (imagine copy-pasting entire content of the index template into the home template) which will print out the title context variable and evaluates the bodyBlock. The bodyBlock will be available since we've just defined it before our inline statement. Easy peasy. 😝

<!-- var, let -->
#var(x = 10)
#let(foo = "bar")

<!-- define -->
#define(resultBlock = x + 1)
#define(bodyBlock):
    <h2>Hello, world!</h2>
    <p>I'm a multi-line block definition</p>
#endblock

<!-- evaluate -->
#evaluate(resultBlock)
#bodyBlock()

I'm really happy about these changes, because Leaf is heading into the right direction, and those people who have not used the pre-released Leaf 4 versions yet these changes won't cause that much trouble. This new approach follows more like the original Leaf 3 behavior.


Goodbye tags. Hello entities!

Nothing is a tag anymore, but they are separated to the following things:

  • Blocks (e.g. #for, #while, #if, #elseif, #else)
  • Functions (e.g. #Date, #Timestamp, etc.)
  • Methods (e.g. .count(), .isEmpty, etc.)

You can now create your very own functions, methods and even blocks. πŸ”₯

public struct Hello: LeafFunction, StringReturn, Invariant {
    public static var callSignature: [LeafCallParameter] { [.string] }

    public func evaluate(_ params: LeafCallValues) -> LeafData {
        guard let name = params[0].string else {
            return .error("`Hello` must be called with a string parameter.")
        }
        return .string("Hello \(name)!")
    }
}

public func configure(_ app: Application) throws {

    LeafConfiguration.entities.use(Hello(), asFunction: "Hello")
    // or maybe...
    // LeafEngine.entities.use(Hello(), asFunction: "Hello")

    //...
}

Now you can use this function in your templates like this:

#Hello("Leaf Tau")

You can event overload the same function with different argument labels

public struct HelloPrefix: LeafFunction, StringReturn, Invariant {

    public static var callSignature: [LeafCallParameter] { [
        .string(labeled: "name"),
        .string(labeled: "prefix", optional: true, defaultValue: "Hello")]
    }

    public func evaluate(_ params: LeafCallValues) -> LeafData {
        guard let name = params[0].string else {
            return .error("`Hello` must be called with a string parameter.")
        }
        let prefix = params[1].string!
        return .string("\(prefix) \(name)!")
    }
}

public func configure(_ app: Application) throws {


    LeafConfiguration.entities.use(Hello(), asFunction: "Hello")
    LeafConfiguration.entities.use(HelloPrefix(), asFunction: "Hello")

    //...
}

This way you can use multiple versions of the same functionality.

#Hello("Leaf Tau")
#Hello(name: "Leaf Tau", prefix: "Hi")

Here's another example of a custom Leaf method:

public struct DropLast: LeafNonMutatingMethod, StringReturn, Invariant {
    public static var callSignature: [LeafCallParameter] { [.string] }

    public func evaluate(_ params: LeafCallValues) -> LeafData {
        .string(String(params[0].string!.dropLast()))
    }
}

public func configure(_ app: Application) throws {

    LeafConfiguration.entities.use(DropLast(), asMethod: "dropLast")
    //...
}

You can define your own Leaf entities (extensions) via protocols. You don't have to remember them all, because there is quite a lot of them, but this is the pattern that you should look for Leaf*[Method|Function|Block] for the return types: [type]Return. If you don't know invariant is a function that produces the same output for a given input and it has no side effects.

You can register these entities as[Function|Method|Block] through the entities property. It's going to take a while until you get familiar with them, but fortunately Leaf 4 comes with quite a good set of built-in entities, hopefully the official documentation will cover most of them. πŸ˜‰

public struct Path: LeafUnsafeEntity, LeafFunction, StringReturn {
    public var unsafeObjects: UnsafeObjects? = nil

    public static var callSignature: [LeafCallParameter] { [] }

    public func evaluate(_ params: LeafCallValues) -> LeafData {
        guard let req = req else { return .error("Needs unsafe access to Request") }
        return .string(req.url.path)
    }
}


public func configure(_ app: Application) throws {

    LeafConfiguration.entities.use(Path(), asFunction: "Path")

    // usage: #Path()
    // output e.g. /home
}

Oh, I almost forgot to mention that if you need special access to the app or req property you have to define an unsafe entity, which will be considered as a bad practice, but fortunately we have something else to replace the need for accessing these things...


Scopes

If you need to pass special things to your Leaf templates you will be able to define custom scopes.

extension Request {
    var customLeafVars: [String: LeafDataGenerator] {
        [
            "url": .lazy([
                        "isSecure": LeafData.bool(self.url.scheme?.contains("https")),
                        "host": LeafData.string(self.url.host),
                        "port": LeafData.int(self.url.port),
                        "path": LeafData.string(self.url.path),
                        "query": LeafData.string(self.url.query)
                    ]),
        ]
    }
}
extension Application {
    var customLeafVars: [String: LeafDataGenerator] {
        [
            "isDebug": .lazy(LeafData.bool(!self.environment.isRelease && self.environment != .production))
        ]
    }
}

struct ScopeExtensionMiddleware: Middleware {

    func respond(to req: Request, chainingTo next: Responder) -> EventLoopFuture<Response> {
        do {
            try req.leaf.context.register(generators: req.customLeafVars, toScope: "req")
            try req.leaf.context.register(generators: req.application.customLeafVars, toScope: "app")
        }
        catch {
            return req.eventLoop.makeFailedFuture(error)
        }
        return next.respond(to: req)
    }
}

public func configure(_ app: Application) throws {

    app.middleware.use(ScopeExtensionMiddleware())

    //...
}

Long story short, you can put LeafData values into a custom scope, the nice thing about this approach is that they can be lazy, so Leaf will only compute the corresponding values if when are being used. The question is, how do we access the scope? πŸ€”

<ul>
    <li><b>ctx:</b>: #($context)</li>
    <li><b>self:</b>: #(self)</li>
    <li><b>req:</b>: #($req)</li>
    <li><b>app:</b>: #($app)</li>
</ul>

You should know that self is an alias to $context, and you can access your own context variables using the $ sign. You can also build your own LeafContextPublisher object that can use to alter the scope.

final class VersionInfo: LeafContextPublisher {

    let major: Int
    let minor: Int
    let patch: Int
    let flags: String?

    init(major: Int, minor: Int, patch: Int, flags: String? = nil) {
        self.major = major
        self.minor = minor
        self.patch = patch
        self.flags = flags
    }

    var versionInfo: String {
        let version = "\(major).\(minor).\(patch)"
        if let flags = flags {
            return version + "-" + flags
        }
        return version
    }

    lazy var leafVariables: [String: LeafDataGenerator] = [
        "version": .lazy([
            "major": LeafData.int(self.major),
            "minor": LeafData.int(self.minor),
            "patch": LeafData.int(self.patch),
            "flags": LeafData.string(self.flags),
            "string": LeafData.string(self.versionInfo),
        ])
    ]
}

public func configure(_ app: Application) throws {

    app.views.use(.leaf)

    app.middleware.use(LeafCacheDropperMiddleware())

    app.get(.catchall) { req -> EventLoopFuture<View> in
        var context: LeafRenderer.Context = [
            "title": .string("Leaf 4"),
            "body": .string("Hello Leaf Tau!"),
        ]
        let versionInfo = VersionInfo(major: 1, minor: 0, patch: 0, flags: "rc.1")
        try context.register(object: versionInfo, toScope: "api")
        return req.leaf.render(template: "home", context: context)
    }

    // #($api)
    // #($api.version.major)
    // #($api.version.string)

}

What if you want to extend a scope? No problem, you can do that by registering a generator

extension VersionInfo {

    var extendedVariables: [String: LeafDataGenerator] {[
        "isRelease": .lazy(self.major > 0)
    ]}
}

//...

let versionInfo = VersionInfo(major: 1, minor: 0, patch: 0, flags: "rc.1")
try context.register(object: versionInfo, toScope: "api")
try context.register(generators: versionInfo.extendedVariables, toScope: "api")
return req.leaf.render(template: "home", context: context)

// #($api.isRelease)

There is an app and req scope available by default, so you can extend those through an extension that can return a [String: LeafDataGenerator] variable.


Summary

As you can see Leaf improved quite a lot compared to the previous versions. Even in the beta / rc period of the 4th major version of this async template engine brought us so many great stuff.

Hopefully this article will help you during the migration process, and I believe that you will be able to utilize most of these built-in functionalities. The brand new render and context mechanism gives us more flexibility without the need of declaring additional local structures, Leaf variables and the redesigned hierarchy system will support us to design even more powerful reusable templates. Through entity and the scope API we will be able to bring Leaf to a completely new level. πŸƒ

Share this article on Twitter.
Thank you. πŸ™

Picture of Tibor BΓΆdecs

Tibor BΓΆdecs

Creator of https://theswiftdev.com (weekly Swift articles), server side Swift enthusiast, full-time dad. -- Follow me & feel free to say hi. 🀘🏻 -- #iOSDev #SwiftLang

Twitter · GitHub


πŸ“¬

100% Swift news, delivered right into your mailbox

Subscribe to my monthly newsletter. On the first Monday of every month, you'll get an update about the most important Swift community news, including my articles.