Is it possible to call a struct method inside a Tera html template?

Hi,

Currently, I'm learning how to render HTML templates via Tera. Here is my test case if you wish to setup the very same project in your local environment by creating a new binary crate and verify it

Cargo.toml

[package]
name = "project_01"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
tera = "1"
serde = { version = "1.0.137", features = ["derive"] }

At the same directory level as Cargo.toml I have created a directory named templates with the following two files:

index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Test tera</title>
    <meta name="Tera_tutorial" content="Tera tutorial">
    <meta name="author" content="Practice">
    <link rel="stylesheet" type="text/css" href="styles.css">
    <script src=""></script>
  </head>
  <body>
    {% if users  %}

    <div class="div_table">
          <!-- Table header -->
          <div class="div_table_row">
            <div class="div_table_cell div_table_header">
              <p>First name</p>
            </div>
            <div class="div_table_cell div_table_header">
              <p>Last name</p>
            </div>
            <div class="div_table_cell div_table_header">
              <p>Email</p>
            </div>
          </div>

          <!-- Users table content -->
          {% for user in users %}
          <div class="div_table_row">
            <div class="div_table_cell">
              <p>{{ user.first_name }}</p>
            </div>
            <div class="div_table_cell">
              <p>{{ user.last_name }}</p>
            </div>
            <div class="div_table_cell">
              <p>{{ user.get_email() }}</p>
            </div>
          </div>
          {% endfor %}
    </div>
    {% endif %}
 
  </body>
</html>

And its CSS file in the same directory: styles.css

@import url('https://fonts.googleapis.com/css?family=Open+Sans');

.div_table {
    display: table;
    border-collapse: collapse;
}

.div_table_row {
    display: table-row;
}

.div_table_header {
    font-weight: bold;
    text-align: center;
}

.div_table_cell {
    display: table-cell;
    padding-left: 10px;
    padding-right: 10px;
    font-family: "Open Sans";
    font-size: 11px;
    border-top: 1px solid #000000;
}

And finally the main program : main.rs


use std::fs;
use tera::Tera;
use tera::Context;
use serde::Serialize;


#[derive(Debug, Serialize)]
struct User {
    first_name: String,
    last_name: String,
}

impl User {
    fn get_email(&self) -> String {
        format!(
            "{}{}{}{}",
            self.first_name,
            ".",
            self.last_name,
            "@mycorporate.com"
        )
    }
}

fn init() -> Result<String, tera::Error> {
    // Use globbing
    let tera = match Tera::new("templates/*.html") {
        Ok(t) => t,
        Err(e) => {
            println!("Parsing error(s): {}", e);
            ::std::process::exit(1);
        }
    };

    let mut users: Vec<User> = Vec::new();

    users.push(User {
        first_name: String::from("fname-01"),
        last_name: String::from("lname-01"),
    });

    users.push(User {
        first_name: String::from("fname-02"),
        last_name: String::from("lname-02"),
    });

    users.push(User {
        first_name: String::from("fname-03"),
        last_name: String::from("lname-03"),
    });

    let mut context = Context::new();
    context.insert("users", &users);

    let result = {
        tera.render("index.html", &context)?
    };

    Ok(result)
}

fn main() -> std::io::Result<()> {
    let result = init();
    match result {
        Ok(value) => {
            let output_file_name = "templates/result_template.html";
            fs::File::create(output_file_name).unwrap();
            fs::write(output_file_name, value).unwrap();
            ()
        },
        Err(e) => {
            panic!("{}", e);
        }
    }

    Ok(())
}

This fails with the following error message:

* Failed to parse "templates\\index.html"
  --> 39:35
   |
39 |               <p>{{ user.get_email() }}</p>␍␊
   |                                   ^---
   |
   = expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, a filter, or a variable end (`}}`)
error: process didn't exit successfully: `target\debug\project_01.exe` (exit code: 1)

cargo-run exited abnormally with code 1 at Thu May 26 16:55:06

And this is the part of my HTML template that causes the error:

          <div class="div_table_cell">
              <p>{{ user.get_email() }}</p>
          </div>

Therefore, I would like to know whether is possible to call a struct's method in Tera templates. I mean, in the above code, I can use {{ user.first_name }} and {{ user.last_name }} which are both defined in User struct. So I don't understand why the method syntax on the same struct which returns an expression (String) causes a problem. Is it that in Tera it is impossible to call methods or it's me who's not writing and calling the function correctly in the template?

Thanks in advance

When you insert into a Context, the data gets serialized into a Value. So the method doesn't exist from the template's point of view, as it's no longer operating on a User.

I would recommend either:

  • Inserting the email into the context seperately.
  • Converting the User into a form that includes the email (either by manually converting to Value, or by defining another struct and converting to that), and then put that into the context instead.
2 Likes

Thank you very much for your answer.

Indeed, now that I'm looking to the source code of the Context, this is what we can see:

pub struct Context {
    data: BTreeMap<String, Value>,
}

Indeed, it's all about static values and there is no way to call functions/methods at run time while rendering. This is very constraining as I'll have really loads of such calls in my project. But I think even in Java and other languages/environments similar limits should exist.

The example, that I provided was just a simple example to see whether it was possible to call a method/function "during" the rendering and in this particular example of course I could have provided the email field in the same User structure.

Thank you very much for your time and clarifying this.

[Edit] :

And here is finally how I changed the code by picking the first solution that you suggested, that is, inserting emails separately to the context:

main.rs


use std::fs;
use tera::Tera;
use tera::Context;
use serde::Serialize;


#[derive(Debug, Serialize)]
struct User {
    userid: String,
    first_name: String,
    last_name: String,
}

impl User {
    fn get_email(&self) -> String {
        format!(
            "{}{}{}{}",
            self.first_name,
            ".",
            self.last_name,
            "@mycorporate.com"
        )
    }
}

fn init() -> Result<String, tera::Error> {
    // Use globbing
    let tera = match Tera::new("templates/*.html") {
        Ok(t) => t,
        Err(e) => {
            println!("Parsing error(s): {}", e);
            ::std::process::exit(1);
        }
    };

    let mut users: Vec<User> = Vec::new();

    users.push(User {
        userid: String::from("id-01"),
        first_name: String::from("fname-01"),
        last_name: String::from("lname-01"),
    });

    users.push(User {
        userid: String::from("id-02"),
        first_name: String::from("fname-02"),
        last_name: String::from("lname-02"),
    });

    users.push(User {
        userid: String::from("id-03"),
        first_name: String::from("fname-03"),
        last_name: String::from("lname-03"),
    });

    let mut context = Context::new();
    context.insert("users", &users);

    let mut emails: Vec<(String, String)> = Vec::new();
    for user in users {
        emails.push((user.userid.clone(), user.get_email().clone()));
    }
    context.insert("emails", &emails);

    let result = {
        tera.render("index.html", &context)?
    };

    Ok(result)
}

fn main() -> std::io::Result<()> {
    let result = init();
    match result {
        Ok(value) => {
            let output_file_name = "templates/result_template.html";
            fs::File::create(output_file_name).unwrap();
            fs::write(output_file_name, value).unwrap();
            ()
        },
        Err(e) => {
            panic!("{}", e);
        }
    }

    Ok(())
}

And the new version of my template : index.html


<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Test tera</title>
    <meta name="Tera_tutorial" content="Tera tutorial">
    <meta name="author" content="Practice">
    <link rel="stylesheet" type="text/css" href="styles.css">
    <script src=""></script>
  </head>
  <body>
    {% if users  %}

    <div class="div_table">
          <!-- Table header -->
          <div class="div_table_row">
            <div class="div_table_cell div_table_header">
              <p>First name</p>
            </div>
            <div class="div_table_cell div_table_header">
              <p>Last name</p>
            </div>
            <div class="div_table_cell div_table_header">
              <p>Email</p>
            </div>
          </div>

          <!-- Users table content -->
          {% for user in users %}
          <div class="div_table_row">
            <div class="div_table_cell">
              <p>{{ user.first_name }}</p>
            </div>
            <div class="div_table_cell">
              <p>{{ user.last_name }}</p>
            </div>
            <div class="div_table_cell">
              {% for email in emails %}
                {% if email.0 == user.userid %}
                  <p>{{ email.1 }}</p>
                {% endif %}
              {% endfor %}
            </div>
          </div>
          {% endfor %}
    </div>
    {% endif %}
 
  </body>
</html>

Thanks again for your help.

1 Like