Basic MiniJinja Template Structure For Working With Rust Objects
Introduction
This is a bare-bones example of how I set up MiniJinja for my static site generator. Most examples in the official docs show how to pass data directly into the templates. I'm using a different approach. I pass an object that I call methods on to get values. It's a lot more flexible.
(details below the code)
The Code
Source
//! ```cargo
//! [dependencies]
//! minijinja = "1.0.10"
//! ```
use minijinja::value::{Object, Value};
use minijinja::{context, Environment};
use minijinja::Error;
use std::fmt::Display;
#[derive(Debug)]
pub struct Page {}
impl Page {
pub fn new() -> Page {
Page {}
}
}
impl Object for Page {
fn call_method(
&self,
_state: &minijinja::State,
method_name: &str,
args: &[Value],
) -> Result<Value, Error> {
match method_name {
"argument_data" => Ok(
Value::from_serializable(&self.argument_data(&args[0]))
),
"direct_data" => Ok(
Value::from_serializable(&self.direct_data())
),
"included_data" => Ok(
Value::from_serializable(&self.included_data())
),
"items_data" => Ok(
Value::from_serializable(&self.items_data())
),
_ => Ok(Value::from("")),
}
}
}
impl Page {
pub fn argument_data(&self, input: &Value) -> String {
input.to_string()
}
}
impl Page {
pub fn direct_data(&self) -> String {
"ALFA".to_string()
}
}
impl Page {
pub fn included_data(&self) -> String {
"CHARLIE".to_string()
}
}
impl Page {
pub fn items_data(&self) -> Vec<String> {
vec![
"DELTA".to_string(),
"ECHO".to_string(),
"FOXTROT".to_string(),
]
}
}
impl Display for Page {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "DEV_NOTE: This ``write!()`` is required display output")
}
}
fn main() {
let page_default_template = r#"
Data directly from object: {{ page.direct_data() }}
Data from object with argument: {{ page.argument_data("BRAVO") }}
{% include "section.widget.jinja" %}
Loop example:
{% for item in page.items_data() %}
~ {{ item }}
{% endfor %}
"#;
let section_widget_template = r#"
Data from included template: {{ page.included_data() }}
"#;
let mut env = Environment::new();
env.add_template("page.default.jinja", page_default_template).unwrap();
env.add_template("section.widget.jinja", section_widget_template).unwrap();
let template = env.get_template("page.default.jinja").unwrap();
let p = Page::new();
let output = template
.render(context!(page => Value::from_object(p)))
.unwrap();
println!("{}", output);
}
Data directly from object: ALFA
Data from object with argument: BRAVO
Data from included template: CHARLIE
Loop example:
~ DELTA
~ ECHO
~ FOXTROT
Details
-
The key to this methodology is the
Value::from_object()
andValue::from_serializable()
calls. That's what provide the ability to pass in an object and call methods on it -
Template call
page.some_method_name()
. Thematch method_name
statement in thePage
object looks to see if it can find the name (e.g.some_method_name
). If it does, it calls the struct/object method defined inValue::from_serializable
. (e.g.&self.direct_data()
Value::from_serializable(&self.some_method_name())
- The method names used in the template and the method names in the struct/object don't have to be the same. I just keep them that way to make them easier to track.
-
The
impl Display for Page {}
section has to be included for MiniJinja to be able to output properly. It doesn't need to be messed with -
I'm loading the template as inline strings so you can see them directly in the example. In production I load them from files. I've kept the names of the files (e.g.
page.default.jinja
which is my naming convention) in place, but there's nothing special about them. They could be underscores instead of dots (e.g.page_deafult
) -
I use
.jinja
as the file extension in prod. MiniJinja doesn't care about the specific extension and will use whatever it gets fed -
MiniJinja uses
{{
and}}
as the default template tokens for loading data and{%
with%}
for functions (like the for loop) -
The
Page
object is passed into the template via thecontext!(page => Value::from_object(p))
. Thepage
string there is what's gets used inside the templates to call the methods. -
{{ page.direct_data() }}
calls directly into the object to get a value without sending any arguments -
The
{{ page.argument_data("BRAVO") }}
version passes an argument (BRAVO
in this case) to the object where it's available for use. In this case, it's simply returned - Multiple arguments can be passed in as well
-
The
{% include "section.widget.jinja" %}
call shows a second template being called from inside the first. Thepage
is available in it as well (e.g. for the{{ page.included_data() }}
use inside the template -
Method calls can return arrays and hashes/dictionaries as well. For example, the
page.items_data()
is used to return a vec/array that gets used in the for loop -
The last
_ => Ok(Value::from(""))
arm of thematch method_name
statement makes it so that method calls to non-existing methods from inside the templates return an empty string. It's possible to put a message in there (e.g. "Invalid Method Call") but for me the chances of accidentally shipping something to prod that made that happen are too high. I'd rather stuff just not show up
Closing
I've been working with MiniJinja for a year. It was rough going at the start because I didn't figure out how to call methods in this way. I was serializing everything. This is way better.
I'm really enjoying working with it now and I hope this will help get you started if you're looking to play with it. It's pretty great.
References
-
Really solid docs. There's a lot to it which is why it took a while to figure out this approach
-
The main page for syntax to see what the templates can do
-
This is what to use to load a directory of templates. It's what I use for everything except these examples where I want to show the templates directly in the file to make them easier to follow
-
This is the way I use
path_loader()
to grab a directory full of templates all in one go -
This is the key to making all this stuff work. It took me forever to find it and figure out how to use it. It was totally worth the struggle