On this article I’ll present you how one can create some helpful customized tags for the Leaf template engine, written in Swift.
Vapor
The way to prolong Leaf?
With the rebirth of Leaf we are able to really prolong the template engine and customized tags are only a factor of the previous. You realize in earlier variations of Leaf all the things was known as a tag and there was no differentiation between these little bastards beginning with the # image. Now issues have modified. There are various completely different entities in Leaf Tau.
- Blocks (e.g. #for, #whereas, #if, #elseif, #else)
- Capabilities (e.g. #Date, #Timestamp, and so forth.)
- Strategies (e.g. .rely(), .isEmpty, and so forth.)
This can be a great point and on prime of this you may create your very personal capabilities, strategies and even blocks. This brings us to a completely extensible template engine that may render all the things in a non-blocking asynchronous approach. How cool is that? 😎
Did I point out that Leaf you may prolong the context with customized LeafDataGenerators? Sure, that is a factor now, previously you can use the userInfo object to set a “world” accessible variable in Leaf, that was properly accessible in each single template file.
Now there are some particular variables accessible you can prolong:
The present context after all is what you cross to your template utilizing the render methodology written in Swift. It’s value to say that self is simply an alias to the present $context, so it would not issues which one you employ. The $app and $req scopes are empty by design, however you may prolong them. You possibly can even register your individual scope for instance $api
and set all the things you want globally below that variable. I will present you ways to do that afterward.
As you may see there are many choices accessible to increase Leaf. It’s important to suppose twice which path you’re taking, nevertheless it’s nice that we now have this many alternatives. Now we’ll stroll by of every of these items and I will present you how one can write customized extensions for Leaf Tau. 🥳
The way to prolong Leaf contexts?
Probably the most simple approach of extending Leaf is to supply customized context variables. We are able to simply write an extension for the Software
and the Request
object and return LeafDataGenerator
values with particular keys and afterward we are able to register these as extra context variables.
import Vapor
import Leaf
extension Software {
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 will be lazy or instant. Rapid values will probably be saved immediately, then again lazy values will produce generator blocks which can be going to be known as solely when the renderer wants them. Nothing particular, this works just like the lazy key phrase in Swift.
struct ScopeExtensionMiddleware: Middleware {
func reply(to req: Request, chainingTo subsequent: Responder) -> EventLoopFuture<Response> {
do {
strive req.leaf.context.register(turbines: req.software.customLeafVars, toScope: "app")
strive req.leaf.context.register(turbines: req.customLeafVars, toScope: "req")
}
catch {
return req.eventLoop.future(error: error)
}
return subsequent.reply(to: req)
}
}
We want an extension middleware that registers our generator variables to the given scope.
public func configure(_ app: Software) throws {
app.middleware.use(ScopeExtensionMiddleware())
}
Attempt to print these values in a template file, you may entry child-values utilizing 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.question)
Now we’re going to create a customized context to get some details about the host machine.
ultimate class ServerLeafContextGenerator: LeafContextPublisher {
var osName: String {
#if os(macOS)
return "macOS"
#elseif os(Linux)
return "Linux"
#elseif os(Home windows)
return "Home 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": .instant(ProcessInfo.processInfo.processorCount),
"reminiscence": .instant(ProcessInfo.processInfo.physicalMemory),
]
}
We are able to merely put this line subsequent to the opposite two within the scope extension middleware.
strive req.leaf.context.register(turbines: ServerLeafContextGenerator().leafVariables, toScope: "server")
This fashion we are able to get some additional information concerning the server in our Leaf templates by utilizing the $server
scope. One other approach is to increase a scope regionally with a generator.
app.get("server-info") { req -> EventLoopFuture<View> in
var context: LeafRenderer.Context = [
"title": "Server info",
]
strive context.register(object: ServerLeafContextGenerator(), toScope: "server")
return req.leaf.render(template: "server-info", context: context)
}
The distinction is that within the second case the server scope is barely accessible for a single endpoint, but when we register it by the middleware then it may be reached globally in each single Leaf file.
I believe scopes are very helpful, particularly Request associated ones. Previously we needed to create a customized Leaf tag to get the trail, however now we are able to use a scope extension and this information will probably be accessible in every single place. With the lazy load we additionally get some free efficiency enhancements.
Customized Leaf capabilities and strategies
You possibly can create customized capabilities and strategies for Leaf, I would say that this new API is the replacemenet of the outdated tag system. There are some variations and at first sight you would possibly suppose that it is more durable to create a perform with the brand new instruments, however in time you will get used to it.
public struct Hi there: LeafFunction, StringReturn, Invariant {
public static var callSignature: [LeafCallParameter] { [.string] }
public func consider(_ params: LeafCallValues) -> LeafData {
guard let title = params[0].string else {
return .error("`Hi there` have to be known as with a string parameter.")
}
return .string("Hi there (title)!")
}
}
This can be a very primary perform. Each single perform has a name signature, which is only a checklist of type-safe arguments. Capabilities can have return sorts, luckily there are pre-made protocols for these, so you do not have to implement the required stuff, however you may say that this capabilities is e.g. a StringReturn perform. Invariant signifies that the perform will all the time return the identical output for a similar enter. That is what you need more often than not, it additionally lets you keep away from side-effects.
Within the consider perform you may get entry to all of the enter parameters and it’s a must to return with a LeafData sort. If a parameter is lacking or it may well’t be casted to the correct sort you may all the time return with an error. Consider is prefer to the outdated render methodology, nevertheless it’s far more superior.
LeafConfiguration.entities.use(Hi there(), asFunction: "Hi there")
You additionally need to register this newly created perform below a give title.
#Hi there("Leaf Tau")
Oh by the best way strategies are simply particular capabilities so you may construct them the identical approach and register them by way of the asMethod:
property. If you wish to see extra examples, you need to check out my different put up about what’s new in Leaf Tau or scroll right down to the final part of this text.
The way to construct customized Leaf blocks?
This can be a very attention-grabbing and complicated subject. Blocks are particular sort of LeafFunctions, identical to strategies, however issues are just a bit bit extra sophisticated on this case. Instance 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) ? .as soon as : .discard
}
mutating func reEvaluateScope(_ variables: inout [String : LeafData]) -> EvalCount {
fatalError("Error: `Possibly` blocks cannot be re-evaluated.")
}
}
This block has a name signature with a labeled argument known as probability. It has an instantiate methodology which is utilized by the Leaf engine to create this block. It will not have any parseSignatures or scope variables, we’ll depart that for the for block (go and examine the supply in LeafKit in case you are curious & courageous sufficient). We set evaluable to false since we do not wish to make it callable by way of the #consider perform. Now let’s speak about scope analysis actual fast.
The evaluateScope methodology will probably be known as first when the block inside your template will get evaluated. It’s important to return an EvalCount on this methodology, which is able to determine what number of occasions ought to we print out the contents in between your block (#[name]:THIS PART#finish[name]).
Mainly when a LeafBlock is evaluated the primary time, it is by way of evaluateScope. If that returns a outcome somewhat than nil, any additional calls will use reEvaluateScope as a substitute. – tdotclare
If EvalCount is about to discard then the contents will probably be discarded, in any other case it will be evaluated as many occasions as you come. If the rely is .as soon as which means the tip of the story, but when it get’s evaluated a number of occasions and you do not want extra params for additional analysis, then the reEvaluateScope will probably be known as for all the opposite cycles.
LeafConfiguration.entities.use(MaybeBlock.self, asBlock: "perhaps")
Remember that we now have to register this block with a given title earlier than we might use it.
#perhaps(probability: 0.5):
<p>Is that this going to occur? 50-50.</p>
#endmaybe
That is it, we have simply prolonged Leaf with a primary block, you may attempt to construct your individual A/B testing Chained block if you wish to dig deeper, however that is fairly a complicated subject and there are not any docs accessible simply but so you have got to try the LeafKit supply recordsdata in many of the instances.
Helpful Leaf extensions.
I’ve made a bunch of helpful Leaf extensions accessible below the LeafFoundation repository. It is a work-in-progress undertaking, however hopefully it’s going to comprise lot extra attention-grabbing extensions by the point Leaf 4 will probably be formally launched. PR’s are welcomed. 😬