home ~ projects ~ socials

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);

}
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() and Value::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(). The match method_name statement in the Page 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 in Value::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 the context!(page => Value::from_object(p)). The page 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. The page 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 the match 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.

-- end of line --

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