How do I get Hummingbird to use Mustache templates instead?
This article is part of a series.
As mentioned last post, there are lots of ways to generate HTML with Swift, if that’s how working with HTML works for you.
I’m perhaps a little old school and am more used to working with templates and template engines for web stuff. (Although I suppose there’s no reason not to write a command plugin to generate those templates for you.)
So I was pretty happy that the Hummingbird project already had a solution for that, the swift-mustache library. While it’s part of the Hummingbird Project ecosystem, it doesn’t require Hummingbird to use it.
This example and the hand-written HTML one are available at
Resources
- https://mustache.github.io
- https://en.wikipedia.org/wiki/Mustache_(template_system)
- https://swiftpackageindex.com/hummingbird-project/swift-mustache
- https://swiftonserver.com/templating-with-swift-mustache/
- https://swifttoolkit.dev/posts/swift-mustache
There are also several examples in the official Hummingbird examples repo.
Package.swift & Directory Structure Changes
To work with swift-mustache, first add the dependency
## from dir with the Package.swift
## add the dependency
swift package add-dependency https://github.com/hummingbird-project/swift-mustache --from 2.0.0
## make the folder
mkdir -p Sources/Templates
The Package.swift
will have to be updated by hand to add the dependency to the executable, and to create a Resources
folder so the template files will be seen by the build process and be incorporated in the bundle.(Another option includes an absolute path when initializing the MustacheLibrary
instance)
// swift-tools-version:6.2
import PackageDescription
let package = Package(
name: "htmlResponses",
platforms : [.macOS(.v14)],
products: [.executable(name: "htmlResponses", targets: ["htmlResponses"])],
dependencies: [
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
.package(url: "https://github.com/hummingbird-project/swift-mustache", from: "2.0.0")
],
targets: [
.executableTarget(
name: "htmlResponses",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "Mustache", package: "swift-mustache"),
],
resources: [.process("Templates")]
),
.testTarget(name: "addTestingTests",
dependencies: [
.byName(name: "htmlResponses"),
.product(name: "HummingbirdTesting", package: "hummingbird")
],
path: "Tests/AppTests"
)
]
)
The Templates
Next make a couple of simple templates to chuck into that folder.
This example uses two super short template, mostly just to show how to combine two templates.
base.mustache
(first) calls body.mustache
with the line {{> body }}
body.mustache
is referred to as a Partial. A nice little demo is on this mustache playground.
base.mustache
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
<meta charset="UTF-8">
</head>
{{> body }}
</html>
body.mustache
<body>
<p>__________{{ message }}__________________________</p>
<p><a href="/">back to home</a>.</p>
</body>
That’s it! Notice the double curlies? {{...}}
? hugo uses those too! (see [meta]/tags/meta/), as does the compatible JS library, Handlebars
While what’s in them will be different partially because Mustache is logic-less, the familiarity is nice.
The Type
Once the templates exist, a type that will process them and turn them into a Response
comes next.
The main difference between AcrobatHTML
and OrganPlayerHTML
will be the need to have a reference to a Mustache library and call to the library.render
function that pulls all the templates together.
import Hummingbird
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
import Mustache
//struct with all the needed info for the template.
//struct parameters must match names in the templates.
struct BasicInfo {
let title: String
let message: String
}
struct AcrobatHTML:ResponseGenerator {
// load mustache template library
let message: String
let library: MustacheLibrary
init(_ message: String, library:MustacheLibrary) {
self.message = message
self.library = library
}
private var html:String {
if let result = try? render() {
return result
} else {
return "??? oops."
}
}
private func render() throws -> String? {
let messagePost = BasicInfo(title: "I'm a Message!", message: message)
//func render(_ object: Any, withTemplate name: String, reload: Bool)
let rendered = library.render(
messagePost,
withTemplate: "base"
)
return rendered
}
func response(from request: Request, context: some RequestContext) throws -> Response {
let buffer = ByteBuffer(string: self.html)
return .init(status: .ok, headers: [.contentType: "text/html"], body: .init(byteBuffer: buffer))
}
}
The object “BasicInfo” will then be passed to base.mustache’s render function, which will recurse through any nested templates, matching up the tokenized information.
It’s a nice implementation of a render that could be fun to go through more completely in a future post! (a bit more on Lexical tokens, which I talked about a little bit [here]({{ relref 20240327-string-scanning }}), but there is much to say.)
Creating the Library Instance & Adding the Route
In this example the Mustache Library will be created in Application+build.swift
in the buildRouter
function.
As mentioned above, the templates are being put in the bundle, rather than in some absolute path on the local machine.
The suggested docker files all have a line for moving things that are in the bundle’s resource folder
# Copy resources bundled by SPM to staging area
RUN find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \;
so the transition to a containerized run should go smoothly.
//ADD TO TOP WITH OTHER IMPORTS
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
import Mustache
func buildRouter() async throws -> Router<AppRequestContext> {
let router = Router(context: AppRequestContext.self)
//...
let mustacheLibrary = try await MustacheLibrary(directory: Bundle.module.resourcePath!)
//let mustacheLibrary = MustacheLibrary()
router.get("/acrobat") { _, _ in "not here"}
router.get("acrobat/:message") { _, context in
guard let message = context.parameters.get("message", as: String.self) else {
throw HTTPError(.badRequest, "could not read a message from the data provided")
}
let decoded = message.removingPercentEncoding ?? "can't-balance-here"
return AcrobatHTML(decoded, library: mustacheLibrary)
}
//...
}
The Call
Back in the public folder, we can add the link to the index.html file.
Don’t forget to percent encode the string!
<!DOCTYPE html>
<html>
<head>
<title>Test Webpage: Site Index Page</title>
<meta charset="UTF-8">
</head>
<body>
<li>These links return generated HTML pages as their response bodies</li>
<ul>
<li><a href=/organ/LaTaDadaDadaLaTaDaDah>organ music</a></li>
<li><a href=/acrobat/not%20gonna%20fall>acrobat</a></li>
</ul>
</li>
</body>
</html>
Add some testing
Different routes mean different tests.
These are pretty minimal tests. They just make sure the routes exist with a known happy path.
Unhappy paths, valid HTML headers, valid HTML, that the content is in there as expected are all things one might test in a more comprehensive suite.
struct AppTests {
struct TestArguments: AppArguments {
let nameTag: String = "nwtTestServer"
let hostname = "127.0.0.1"
let port = 0
let logLevel: Logger.Level? = .trace
}
@Test func testCreateApp() async throws {
let app = try await buildApplication(TestArguments())
try await app.test(.router) { client in
let _ = try await client.execute(uri: "/ping", method: .get) { response in
#expect(response.status == .ok)
}
}
}
@Test func testOrgan() async throws {
let app = try await buildApplication(TestArguments())
try await app.test(.router) { client in
let message = "my%20precent%20encoded%20Message"
let _ = try await client.execute(uri: "/organ/\(message)", method: .get) { response in
#expect(response.status == .ok)
}
}
}
@Test func testAcrobat() async throws {
let app = try await buildApplication(TestArguments())
try await app.test(.router) { client in
//http://localhost:8080/acrobat/not%20gonna%20fall
let message = "my%20precent%20encoded%20Message"
let _ = try await client.execute(uri: "/acrobat/\(message)", method: .get) { response in
#expect(response.status == .ok)
}
}
}
}
Summary
That’s quick overview of how to get the bare minimum working with Mustache templates. The next post will be about making a CRUD interface for a codable type by hand, and the one after that will compare it to using the OpenAPI plugin for Swift packages.