ruststructrust-sqlxaskama

Using Option<String>, and other non-implementers of Display, with askama::Template


I'd like to use identical struct for receiving data from sqlx and for passing that data into an askama::Template. (I will have many such structs.)

sqlx makes me use Option<String> as the column is a nullable varchar. askama::Template forbids me using Option<String> as it requires the Display trait.

If I try to derive an askama::Template from a struct which contains an Option<String> I get the error:

error[E0277]: `std::option::Option<std::string::String>` doesn't implement `std::fmt::Display`
  --> src/root.rs:35:10
   |
35 | #[derive(Template)]
   |          ^^^^^^^^ `std::option::Option<std::string::String>` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `std::option::Option<std::string::String>`, which is required by `&std::option::Option<std::string::String>: std::fmt::Display`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
   = note: this error originates in the macro `$crate::format_args` which comes from the expansion of the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)

How can I use one struct for both purposes?

This is my current work-around, using two similar structs and an implementation of From:

use askama::Template;
use axum::{extract::Query, Extension};
use chrono::{DateTime, Utc};
use serde::Deserialize;
use sqlx::PgPool;
//use tracing::debug;

#[derive(sqlx::FromRow, Debug)]
pub struct LatestProvisioningRequest {
    uid: String,
    ts: DateTime<Utc>,
    ip: Option<String>,
}

pub struct LatestProvisioningRequestForTemplate {
    uid: String,
    ts: DateTime<Utc>,
    ip: String,
}

impl From<LatestProvisioningRequest> for LatestProvisioningRequestForTemplate {
    fn from(value: LatestProvisioningRequest) -> Self {
        let ip = match value.ip {
            Some(str) => str,
            None => "".to_owned(),
        };
        LatestProvisioningRequestForTemplate {
            uid: value.uid,
            ts: value.ts,
            ip
        }
    }
}

#[derive(Template)]
#[template(path = "root.html")]
pub struct RootTemplate {
    requests: Vec<LatestProvisioningRequestForTemplate>
}

// Used to extract the "next" field from the query string.
#[derive(Debug, Deserialize)]
pub struct RootQueryParameters {
    // TODO
}

pub async fn fetch_unenrolled(pool: &PgPool) -> Vec<LatestProvisioningRequestForTemplate> {
    let rows = sqlx::query_as::<_, LatestProvisioningRequest>(r#"SELECT * FROM latest_provisioning_requests"#)
    .fetch_all(pool)
    .await
    .unwrap();

    rows.into_iter().map(|x| x.into()).collect()
}

pub async fn root(
    Extension(pool): Extension<PgPool>,
    Query(RootQueryParameters {}): Query<RootQueryParameters>
) -> RootTemplate {

    RootTemplate {
        requests: fetch_unenrolled(&pool).await
    }
}

Solution

  • Just to keep everything simple, let's assume we have the following struct Page:

    use askama::Template;
    
    #[derive(Template, Clone, Debug)]
    #[template(path = "page.html.jinja")]
    pub struct Page {
        text: Option<String>,
    }
    

    Now we want to always render text if it's Some or render nothing if it's None.


    This can be done in a few ways, e.g. using if let:

    {% if let Some(text) = self.text %}
        {{ text }}
    {% endif %}
    

    You can also use a match:

    {% match self.text %}
        {% when Some with (text) %}
            {{ text }}
        {% else %}
    {% endmatch %}
    

    Alternatively, you can also go all in and use if, .is_some(), and .unwrap():

    {% if self.text.is_some() %}
        {{ self.text.as_ref().unwrap() }}
    {% endif %}
    

    However, the solution I personally prefer, is using a custom filter:

    mod filters {
        pub fn display_some<T>(value: &Option<T>) -> askama::Result<String>
        where
            T: std::fmt::Display,
        {
            Ok(match value {
                Some(value) => value.to_string(),
                None => String::new(),
            })
        }
    }
    

    Note, this intentionally uses &Option<T>, instead of Option<T> or Option<&T>, as Askama auto references arguments.

    The mod filters must be in the same module as your template(s).

    {{ self.text|display_some }}
    

    If you have Option<Thing> where Thing doesn't implement fmt::Display, then you can manually implement fn display_thing(thing: &Option<Thing>) that handles rendering it.


    Instead of a filter, you can also use a normal function instead:

    pub fn display_some<T>(value: &Option<T>) -> String
    where
        T: std::fmt::Display,
    {
        match value {
            Some(value) => value.to_string(),
            None => String::new(),
        }
    }
    
    {{ self::display_some(self.text) }}
    

    This assumes that display_some() is in the same module as your template(s).


    Lastly, you can also do the following. But it's a bit more ugly, and only works for any T that can deref into &str:

    {{ self.text.as_deref().unwrap_or("") }}