The words Under construction in black text on a yellow background with diagonal black stipes surrounding it
I'm in the process of moving my site. It's still a work in progress. Please excuse the mess and broken links.

Basic MiniJinja Template Structure For Working With Rust Objects

TODO: Pull subtitle into page object

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

}
Results
Data directly from object: ALFA

Data from object with argument: BRAVO


Data from included template: CHARLIE
    

Loop example:

~ DELTA

~ ECHO

~ FOXTROT

Details

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.

Debugging Stuff

I'm moving stuff around right now. All this below is helping me figure out where to put stuff

        -- title

Basic MiniJinja Template Structure For Working With Rust Objects

-- h2

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)

-- h2

The Code

-- code
-- rust
-- title: 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);

}

-- results/


Data directly from object: ALFA

Data from object with argument: BRAVO


Data from included template: CHARLIE
    

Loop example:

~ DELTA

~ ECHO

~ FOXTROT


-- /results


-- h2

Details

-- list 

- 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


-- h2

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. 


-- ref
-- title: MiniJinja Docs Homepage
-- url: https://docs.rs/minijinja/latest/minijinja/index.html

Really solid docs. There's a lot to it which is why
it took a while to figure out this approach


-- ref
-- url: https://docs.rs/minijinja/latest/minijinja/syntax/index.html
-- title: MiniJinja syntax

The main page for syntax to see what the templates
can do


-- ref
-- url: https://docs.rs/minijinja/latest/minijinja/fn.path_loader.html
-- title: MiniJinja path_loader()

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


-- ref
-- title: Load Multiple MiniJinja Templates From A Directory
-- url: https://www.alanwsmith.com/pages/2as2evj4/

This is the way I use `path_loader()`` to grab a directory
full of templates all in one go 


-- ref
-- url: https://docs.rs/minijinja/latest/minijinja/value/trait.Object.html#method.call_method
-- title: MiniJinja Object .call_method()

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




-- categories
-- Rust 
-- Minijinja

-- metadata
-- date: 2023-04-10 13:21:58
-- id: 2ofddkpp
-- site: aws
-- type: post
-- status: published