Thursday, September 29, 2022
HomeiOS DevelopmentCustom Leaf tags in Vapor 4

Custom Leaf tags in Vapor 4

[ad_1]

In this article I’m going to show you how to create some useful custom tags for the Leaf template engine, written in Swift.

Vapor



How to extend Leaf?

With the rebirth of Leaf we can truly extend the template engine and custom tags are just a thing of the past. You know in previous versions of Leaf everything was called a tag and there was no differentiation between those little bastards starting with the # symbol. Now things have changed. There are many different entities in Leaf Tau.

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

This is a great thing and on top of this you can create your very own functions, methods and even blocks. This brings us to a fully extensible template engine that can render everything in a non-blocking asynchronous way. How cool is that? 😎

Did I mention that Leaf you can extend the context with custom LeafDataGenerators? Yes, that’s a thing now, in the past you could use the userInfo object to set a “global” available variable in Leaf, that was well available in every single template file.

Now there are some special variables available that you can extend:


The current context of course is what you pass to your template using the render method written in Swift. It is worth to mention that self is just an alias to the current $context, so it doesn’t matters which one you use. The $app and $req scopes are empty by design, but you can extend them. You can even register your own scope for example $api and set everything you need globally under that variable. I’ll show you how to do this later on.

As you can see there are plenty of options available to extend Leaf. You have to think twice which path you take, but it’s great that we have this many opportunities. Now we’ll walk through of each of these things and I’ll show you how to write custom extensions for Leaf Tau. đŸ„ł




How to extend Leaf contexts?

One of the most easy way of extending Leaf is to provide custom context variables. We can easily write an extension for the Application and the Request object and return LeafDataGenerator values with specific keys and later on we can register these as additional context variables.


import Vapor
import Leaf

extension Application {
    var customLeafVars: [String: LeafDataGenerator] {
        [
            "isDebug": .lazy(LeafData.bool(!self.environment.isRelease && self.environment != .production))
        ]
    }
}

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)
                    ]),
        ]
    }
}


A LeafDataGenerator object can be lazy or immediate. Immediate values will be stored instantly, on the other hand lazy values will produce generator blocks that are going to be called only when the renderer needs them. Nothing special, this works like the lazy keyword in Swift.


struct ScopeExtensionMiddleware: Middleware {

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


We need an extension middleware that registers our generator variables to the given scope.


public func configure(_ app: Application) throws {

    app.middleware.use(ScopeExtensionMiddleware())

    
}

Try to print these values in a template file, you can access child-values using the dot notation.

#(self)
#($context)

#($app)
#($app.isDebug)

#($req)
#($req.url)
#($req.url.host)
#($req.url.isSecure)
#($req.url.path)
#($req.url.port)
#($req.url.query)

Now we are going to create a custom context to get some information about the host machine.

final class ServerLeafContextGenerator: LeafContextPublisher {

    var osName: String {
        #if os(macOS)
        return "macOS"
        #elseif os(Linux)
        return "Linux"
        #elseif os(Windows)
        return "Windows"
        #else
        return "Unknown"
        #endif
        
    }

    lazy var leafVariables: [String: LeafDataGenerator] = [
        "os": .lazy([
            "name": LeafData.string(self.osName),
            "version": LeafData.string(ProcessInfo.processInfo.operatingSystemVersionString),
        ]),
        "cpu-cores": .immediate(ProcessInfo.processInfo.processorCount),
        "memory": .immediate(ProcessInfo.processInfo.physicalMemory),
    ]
}

We can simply put this line next to the other two in the scope extension middleware.

try req.leaf.context.register(generators: ServerLeafContextGenerator().leafVariables, toScope: "server")

This way we can get some additional info about the server in our Leaf templates by using the $server scope. Another way is to extend a scope locally with a generator.

app.get("server-info") { req -> EventLoopFuture<View> in
    var context: LeafRenderer.Context = [
        "title": "Server info",
    ]
    try context.register(object: ServerLeafContextGenerator(), toScope: "server")
    return req.leaf.render(template: "server-info", context: context)
}

The difference is that in the second case the server scope is only available for a single endpoint, but if we register it through the middleware then it can be reached globally in every single Leaf file.

I think scopes are very useful, especially Request related ones. In the past we had to create a custom Leaf tag to get the path, but now we can use a scope extension and this info will be available everywhere. With the lazy load we also get some free performance improvements.



Custom Leaf functions and methods

You can create custom functions and methods for Leaf, I’d say that this new API is the replacemenet of the old tag system. There are some differences and at first sight you might think that it’s harder to create a function with the new tools, but in time you’ll get used to it.


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)!")
    }
}

This is a very basic function. Every single function has a call signature, which is just a list of type-safe arguments. Functions can have return types, fortunately there are pre-made protocols for those, so you don’t have to implement the required stuff, but you can say that this functions is e.g. a StringReturn function. Invariant means that the function will always return the same output for the same input. This is what you want most of the time, it also helps you to avoid side-effects.

In the evaluate function you can get access to all the input parameters and you have to return with a LeafData type. If a parameter is missing or it can’t be casted to the proper type you can always return with an error. Evaluate is like to the old render method, but it’s way more advanced.

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

You also have to register this newly created function under a give name.

#Hello("Leaf Tau")

Oh by the way methods are just special functions so you can build them the same way and register them via the asMethod: property. If you want to see more examples, you should take a look at my other post about what’s new in Leaf Tau or scroll down to the last section of this article.




How to build custom Leaf blocks?

This is a very interesting and complex topic. Blocks are special kind of LeafFunctions, just like methods, but things are just a little bit more complicated in this case. Example time:

import Vapor
import Leaf


struct MaybeBlock: LeafBlock, VoidReturn, Invariant {
    
    static var parseSignatures: ParseSignatures? = nil
    static var evaluable: Bool = false
    var scopeVariables: [String]? = nil

    static var callSignature: [LeafCallParameter] { [.double(labeled: "chance")] }
         
    static func instantiate(_ signature: String?, _ params: [String]) throws -> MaybeBlock { .init() }

    mutating func evaluateScope(_ params: LeafCallValues, _ variables: inout [String: LeafData]) -> EvalCount {
        params[0].double! > Double.random(in: 0..<1) ? .once : .discard
    }
    
    mutating func reEvaluateScope(_ variables: inout [String : LeafData]) -> EvalCount {
        fatalError("Error: `Maybe` blocks can't be re-evaluated.")
    }
}


This block has a call signature with a labeled argument called chance. It has an instantiate method which is used by the Leaf engine to create this block. It won’t have any parseSignatures or scope variables, we’ll leave that for the for block (go and check the source in LeafKit if you are curious & brave enough). We set evaluable to false since we don’t want to make it callable via the #evaluate function. Now let’s talk about scope evaluation real quick.

The evaluateScope method will be called first when the block inside your template gets evaluated. You have to return an EvalCount in this method, which will decide how many times should we print out the contents in between your block (#[name]:THIS PART#end[name]).

Basically when a LeafBlock is evaluated the first time, it’s via evaluateScope. If that returns a result rather than nil, any further calls will use reEvaluateScope instead. – tdotclare

If EvalCount is set to discard then the contents will be discarded, otherwise it’ll be evaluated as many times as you return. If the count is .once that means the end of the story, but if it get’s evaluated multiple times and you don’t need additional params for further evaluation, then the reEvaluateScope will be called for all the other cycles.


LeafConfiguration.entities.use(MaybeBlock.self, asBlock: "maybe")


Don’t forget that we have to register this block with a given name before we could use it.


#maybe(chance: 0.5):
    <p>Is this going to happen? 50-50.</p>
#endmaybe


That’s it, we’ve just extended Leaf with a basic block, you can try to build your own A/B testing Chained block if you want to dig deeper, but this is quite an advanced topic and there are no docs available just yet so you have to take a look at the LeafKit source files in most of the cases.





Useful Leaf extensions.

I’ve made a bunch of useful Leaf extensions available under the LeafFoundation repository. It’s a work-in-progress project, but hopefully it is going to contain lot more interesting extensions by the time Leaf 4 will be officially released. PR’s are welcomed. 😬









[ad_2]

Source link

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments