I Trait My Best, I Swear
For web servers, my go-to framework for Rust has been Rocket, and my favourite templating library has been the mustache crate.
This post covers some interesting things I discovered when trying to write a helper trait for my projects.
Rendering Mustache Templates
{{ Mustache }} is a
specification for
logic-less text templates, usually used for generating
HTML. An example template might look like this:
<div class="card">
<h2 class="card-title">{{title}}</h2>
<hr>
<p>{{content}}</p>
<ul class="Tags">
{{#tags}}
<li>{{name}}</li>
{{/tags}}
</ul>
</div>
and the data used to fill out the template might look something like this:
{
"title": "A Cool Article",
"content": "This is a cool article full of fun stuff...",
"tags": [ { "name": "cool"}, { "name": "fun" }]
}
The mustache crate for Rust represents compiled templates
with the Template
struct
which can then be used to render the resulting HTML when
given the required data for the template.
In our case, we'll use the render function to to render
our templates. Any type that implement's serde's Serialize
trait
can be used here to supply the data to render templates.
For example:
use serde::Serialize;
#[derive(Serialize)]
struct Data {
heading: String,
content: String,
}
let template = mustache::compile_str(
r#"
<h1>{{heading}}</h1>
<p>{{content}}</p>
"#
).unwrap();
let mut bytes = vec![];
template.render(&mut bytes, &Data {
heading: "Hello, Heading!".into(),
content: "Some content to render.".into(),
}).unwrap();
The Goal
The idiomatic way to define route handlers in the rocket
crate is to use macros like '#[get("/example")]' that decorate
functions. These functions can then be mounted to parent
routes. These functions can return any type that implements
the Responder
trait.
As we're interested in returning HTML, the RawHtml
struct
seems like what we should be returning. RawHtml wraps
another type that implements Responder and sets the
appropriate headers for returning raw HTML. We might
use RawHtml<u8> then to render a template via:
#[get("/")]
fn index() -> RawHtml<u8> {
#[derive(Serialize)]
struct Data {
title: String,
content: String,
}
let template = mustache::compile_str(
r#"
<html>
<body>
<h1>{{title}}</h1>
<p>{{content}}</p>
</body>
</html>
"#
).unwrap();
let mut bytes = vec![];
template.render(&mut bytes, &Data {
title: "Index".into(),
content: "I wonder when I'll run out of example ideas?",
}).unwrap();
RawHtml(bytes)
}
What I'd like to do here, is be able to define a type here
like Data, then implement a single trait for Data and
be able to return Data from the responder. Something
like the following.
trait Templated {
const HTML: &str;
}
#[get("/")]
fn index() -> RawHtml<u8> {
#[derive(Serialize)]
struct Data {
title: String,
content: String,
}
impl Templated for Data {
const HTML: &str = r#"
<html>
<body>
<h1>{{title}}</h1>
<p>{{content}}</p>
</body>
</html>
"#;
}
Data {
title: "Index".into(),
content: "I wonder when I'll run out of example ideas?",
}
}
This isn't possible, but I'd like to see just how close I can get.
The First Problem
The first 'problem' is Rust's orphan rule that requires for any implementation of a trait defined in a crate, either the trait or the type the trait is implemented for must be defined in that crate.
The orphan rule exists because otherwise you would need
to specify the exact impl block you're referring to every
time you use the functionality of a trait (which would be
very messy).
To get around this we can just define a wrapper struct. Something like the following would do the trick.
struct Rendered<T: Templated>(T);
This actually is enough to to implement the trait we described and an example implementation is provided below.
use serde::Serialize;
use rocket::response::{Responder, content::RawHtml};
trait Templated: Serialize {
const HTML: &str;
}
struct Rendered<T: Templated>(T);
impl<'r, T: Templated> Responder<'r, 'r> for Rendered<T> {
fn respond_to(
self,
request: &'r rocket::Request<'_>,
) -> rocket::response::Result<'r> {
let template =
mustache::compile_str(T::HTML).unwrap();
let mut bytes = vec![];
template.render(&mut bytes, &self.0).unwrap();
RawHtml(bytes).respond_to(request)
}
}
This implementation does have one problem however...
Cache Me If You Can
The issue is with the call to mustache::compile_str that
we make in our implementation. Here, we're recompiling the
template every single time we render the HTML. What we'd
like to do is to either precompile or lazily compile the
templates.
Now, since we need a single instance of the template for
each type we implement Templated for, the first thing
that comes to mind might be one of the following:
trait Templated: Serialize {
const HTML: &str;
static TEMPLATE: mustache::Template =
mustache::compile_str(Self::HTML).unwrap();
}
trait Templated: Serialize {
const HTML: &str;
const TEMPLATE: mustache::Template =
mustache::compile_str(Self::HTML).unwrap();
}
Neither of these work however as associated statics are
not allowed, and mustache::compile_str is not const.
We could try and get tricky with a const pointer to a
static LazyLock or OnceLock. First we try the
LazyLock:
trait Templated: Serialize {
const HTML: &str;
const TEMPLATE: &LazyLock<Template> = {
static TEMPLATE: LazyLock<Template> =
LazyLock::new(|| {
mustache::compile_str(Self::HTML).unwrap()
});
&TEMPLATE
};
This doesn't work however; we can't use the Self
parameter from the outer item as the static is an
entirely separate item than the item that contains it.
To get around this, we can switch to a OnceLock and put
the code that initialises the cell in a place where we can
get at Self.
use std::sync::OnceLock;
use mustache::Template;
use serde::Serialize;
use rocket::response::{Responder, content::RawHtml};
trait Templated: Serialize {
const HTML: &str;
const TEMPLATE: &OnceLock<Template> = {
static TEMPLATE: OnceLock<Template> =
OnceLock::new();
&TEMPLATE
};
}
struct Rendered<T: Templated>(T);
impl<'r, T: Templated> Responder<'r, 'r> for Rendered<T> {
fn respond_to(
self,
request: &'r rocket::Request<'_>,
) -> rocket::response::Result<'r> {
let template = T::TEMPLATE.get_or_init(|| {
mustache::compile_str(T::HTML).unwrap()
});
let mut bytes = vec![];
template.render(&mut bytes, &self.0).unwrap();
RawHtml(bytes).respond_to(request)
}
}
This seems to work initially but we hit a snag because as
it turns out, there's actually only a single instance of
the static template for all types. This is happening
for the same reason we can't use Self in a static's
definition; the static is a completely separate item from
the item that contains it.
We encounter the problem even if we move the static into
the respond_to function. So, what's the actual solution?
The Imperfect Solution
The best solution I've found to this problem is to define the trait like this:
trait Templated: Serialize {
const HTML: &str;
const TEMPLATE: &OnceLock<Template>;
///...
}
This looks similar, but notice we don't provide a default
value for the TEMPLATE const. While this solves our
problem, it sadly means that each implementation of the
trait needs to provide a static reference to a unique
OnceLock<Template>.
impl Templated for Page {
const HTML: &str = r#"
<h1>{{title}}</h1>
<p>{{content}}</p>
"#;
const TEMPLATE: &OnceLock<Template> = {
static TEMPLATE: OnceLock<Template> =
OnceLock::new();
&TEMPLATE
};
}
This isn't optimal of course, but it does provide a unique
Template for each type as we need. A full implementation
of a basic webserver that uses this trait is provided below
to show how it works.
#[macro_use]
extern crate rocket;
mod proto1;
use std::{io::Write, sync::OnceLock};
use mustache::Template;
use rocket::response::{Responder, content::RawHtml};
use serde::Serialize;
trait Templated: Serialize {
const HTML: &str;
const TEMPLATE: &OnceLock<Template>;
fn template() -> &'static Template {
Self::TEMPLATE.get_or_init(|| {
mustache::compile_str(Self::HTML).unwrap()
})
}
fn render<W: Write>(&self, w: &mut W) {
Self::template().render(w, &self).unwrap()
}
}
struct Rendered<T: Templated>(T);
impl<'r, T: Templated> Responder<'r, 'r> for Rendered<T> {
fn respond_to(
self,
request: &'r rocket::Request<'_>,
) -> rocket::response::Result<'r> {
let mut bytes = vec![];
self.0.render(&mut bytes);
RawHtml(bytes).respond_to(request)
}
}
#[derive(serde::Serialize)]
struct Page {
title: String,
content: String,
}
impl Templated for Page {
const HTML: &str = r#"
<h1>{{title}}</h1>
<p>{{content}}</p>
"#;
const TEMPLATE: &OnceLock<Template> = {
static TEMPLATE: OnceLock<Template> =
OnceLock::new();
&TEMPLATE
};
}
#[get("/")]
fn index() -> Rendered<Page> {
Rendered(Page {
title: "Hello, Title!".into(),
content: "Here is some content.".into(),
})
}
#[launch]
fn launch() -> _ {
rocket::build().mount("/", routes![index])
}
What Did We Learn?
I yearn for the days of bytes,
Time to go to bed.