Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple outputs? #858

Open
axos88 opened this issue Aug 30, 2023 · 3 comments
Open

Multiple outputs? #858

axos88 opened this issue Aug 30, 2023 · 3 comments

Comments

@axos88
Copy link

axos88 commented Aug 30, 2023

I am trying to use askama for generating emails, where both the subject and the body depends on the input variables.

How hard would it be to implement the ability to do something like:

struct Subject;
struct Body;

#[derive(Template)]
#[template(path = "email_subject.askama", escape = "None", part = Subject)
#[template(path = "email_subject.askama", escape = "None", part = Body)
struct EmailData {... }


let data = EmailData:new(....)

data.render::<Subject>() // Returns the subject
data.render::<Body>() // Returns the body.

data.render_part(Subject) // Returns the subject
data.render_part(Body) // Returns the body.

// I guess this won't work
data.render_subject() // Returns the subject
data.render_body() // Returns the body.

If part is not given, then I guess a renderer could be used by default to remain backwards compatible, also in most cases only one renderer is attributed to a data template.

@djc
Copy link
Owner

djc commented Aug 31, 2023

In this case, are the values needed by the subject a subset of the values needed by the body? If so, I'd just use one type for the former and a second type for the latter, which includes the former in one of its fields.

At work, we use a procedural macro to duplicate a type for emails that we send with both plaintext and HTML bodies.

Usage:

#[email(template = "contact-email")]
pub struct ContactEmail<'a> {
    pub site_name: &'a str,
    pub sender: &'a str,
    pub message: &'a str,
    pub inbox_url: &'a str,
}

Macro:

#[proc_macro_attribute]
pub fn email(meta: TokenStream, item: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(item as syn::DeriveInput);
    let meta = parse_macro_input!(meta as EmailMeta);

    // Introduce an `'email` lifetime for the reference to outer type.
    // We set up the generics such that all lifetimes used on the item should outlive the
    // `'email` lifetime, which is necessary to make some of the impls below work.

    let mut email_generics = ast.generics.clone();
    let email_lifetime = syn::LifetimeParam::new(syn::Lifetime::new("'email", Span::call_site()));
    for lt in email_generics.lifetimes_mut() {
        lt.bounds.push(email_lifetime.lifetime.clone());
    }
    email_generics
        .params
        .push(syn::GenericParam::Lifetime(email_lifetime));

    // Split the generics for use in impls and type definitions, below.

    let (_, inner_ty_generics, _) = ast.generics.split_for_impl();
    let (impl_generics, ty_generics, where_clause) = email_generics.split_for_impl();

    // Set up some bindings for use in the quote!() call below.

    let visibility = &ast.vis;
    let name = &ast.ident;
    let text_type = Ident::new(&format!("{name}Text"), Span::call_site());
    let text_template = format!("{}.txt", &meta.template);
    let html_type = Ident::new(&format!("{name}Html"), Span::call_site());
    let html_template = format!("{}.html", &meta.template);

    quote!(
        #ast

        impl #impl_generics email::BodyTemplates<'email> for #name #inner_ty_generics #where_clause {
            type Text = #text_type #ty_generics;
            type Html = #html_type #ty_generics;
        }

        #[derive(askama::Template)]
        #[template(path = #text_template)]
        #visibility struct #text_type #ty_generics(&'email #name #inner_ty_generics) #where_clause;

        impl #impl_generics From<&'email #name #inner_ty_generics> for #text_type #ty_generics #where_clause {
            fn from(email: &'email #name #inner_ty_generics) -> Self {
                Self(email)
            }
        }

        impl #impl_generics std::ops::Deref for #text_type #ty_generics {
            type Target = &'email #name #inner_ty_generics;

            fn deref(&self) -> &Self::Target {
                &self.0
            }
        }

        #[derive(askama::Template)]
        #[template(path = #html_template)]
        #visibility struct #html_type #ty_generics(&'email #name #inner_ty_generics) #where_clause;

        impl #impl_generics std::ops::Deref for #html_type #ty_generics  {
            type Target = &'email #name #inner_ty_generics;

            fn deref(&self) -> &Self::Target {
                &self.0
            }
        }

        impl #impl_generics From<&'email #name #inner_ty_generics> for #html_type #ty_generics #where_clause {
            fn from(email: &'email #name #inner_ty_generics) -> Self {
                Self(email)
            }
        }
    ).into()
}

struct EmailMeta {
    template: String,
}

impl Parse for EmailMeta {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        for field in Punctuated::<syn::MetaNameValue, Comma>::parse_terminated(input)? {
            if field.path.is_ident("template") {
                if let syn::Expr::Lit(lit) = &field.value {
                    if let syn::Lit::Str(lit) = &lit.lit {
                        return Ok(Self {
                            template: lit.value(),
                        });
                    }
                }
            }
        }

        panic!("require template key for email macro");
    }
}

@dvtkrlbs
Copy link

dvtkrlbs commented Jul 20, 2024

hey is it possible you could post a gist with the whole macro and related traits extracted? I am really curious about the BodyTemplates trait.

@djc
Copy link
Owner

djc commented Jul 21, 2024

Pretty sure that is already the whole macro.

pub trait BodyTemplates<'email>: 'email {
    type Text: From<&'email Self> + askama::Template;
    type Html: From<&'email Self> + askama::Template;

    fn text(&'email self) -> Result<String, askama::Error> {
        Template::render(&Self::Text::from(self))
    }

    fn html(&'email self) -> Result<String, askama::Error> {
        Template::render(&Self::Html::from(self))
    }
}

It should be super straightforward to do your own from here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants