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
}
}
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("") }}