puerto 0.19.0

Puerto — Rust full-stack DDD framework CLI. Scaffolds Clean Architecture projects with auto-wired DI, SQLx support, and zero manual wiring. Formerly puerto-framework.
//! Renders the `{{ name }}` mail from its templates and hands it to the mailer. Written once by
//! `puerto generate mail`; it is yours now. The text is in `infrastructure/mail/{{ name }}/`:
//! change it there — the tests below render it again, so a typo fails `make test`.

use std::sync::Arc;

use async_trait::async_trait;
use business::domain::mail::{{ name }}::{ {{- pascal }}Mail, {{ pascal }}Mailer};
use puerto_mail::{MailError, Mailer, Templates};
use serde::Serialize;
{%- for import in imports %}
use {{ import }};
{%- endfor %}

const TEMPLATES: Templates = Templates::new(
    include_str!("../../mail/{{ name }}/subject.txt"),
    include_str!("../../mail/{{ name }}/body.html"),
)
.with_text(include_str!("../../mail/{{ name }}/body.txt"));

/// What the templates see: the mail's fields but the recipient, under their own names.
#[derive(Serialize)]
struct Context<'a> {
{%- for f in fields %}
    {{ f.name }}: &'a {{ f.rust_type }},
{%- endfor %}
}

impl<'a> Context<'a> {
    fn of(mail: &'a {{ pascal }}Mail) -> Self {
        Self {
{%- for f in fields %}
            {{ f.name }}: &mail.{{ f.name }},
{%- endfor %}
        }
    }
}

/// The `{{ name }}` mail, rendered from `infrastructure/mail/{{ name }}/` and sent through `mailer`
/// (`puerto_mail::SmtpMailer` where it runs).
pub struct Template{{ pascal }}Mailer {
    mailer: Arc<dyn Mailer>,
}

impl Template{{ pascal }}Mailer {
    pub fn new(mailer: Arc<dyn Mailer>) -> Self {
        Self { mailer }
    }
}

#[async_trait]
impl {{ pascal }}Mailer for Template{{ pascal }}Mailer {
    async fn send(&self, mail: &{{ pascal }}Mail) -> Result<(), MailError> {
        let email = TEMPLATES.render(mail.to.clone(), Context::of(mail))?;
        self.mailer.send(&email).await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use puerto_mail::testing::RecordingMailer;

    fn {{ name }}() -> {{ pascal }}Mail {
        {{ pascal }}Mail {
            to: "ada@example.com".to_string(),
{%- for f in fields %}
            {{ f.name }}: {{ f.sample }},
{%- endfor %}
        }
    }

    #[test]
    fn should_render_every_template_when_the_mail_has_its_fields() {
        // Act
        let email = TEMPLATES.render("ada@example.com", Context::of(&{{ name }}()));

        // Assert
        let email = match email {
            Ok(email) => email,
            Err(err) => panic!("infrastructure/mail/{{ name }}/ does not render: {err}"),
        };
        assert!(!email.subject.is_empty());
        assert!(!email.html.is_empty());
        assert!(email.text.is_some());
    }

    #[tokio::test]
    async fn should_send_the_rendered_mail_to_its_recipient() {
        // Arrange
        let recorder = Arc::new(RecordingMailer::new());
        let mailer = Template{{ pascal }}Mailer::new(recorder.clone());

        // Act
        let result = mailer.send(&{{ name }}()).await;

        // Assert
        assert_eq!(result, Ok(()));
        let sent = recorder.sent();
        assert_eq!(sent.len(), 1);
        assert_eq!(sent[0].to, "ada@example.com");
    }

    #[tokio::test]
    async fn should_return_the_mailer_error_when_sending_fails() {
        // Arrange
        let rejected = MailError::Permanent("550 no such user".to_string());
        let mailer =
            Template{{ pascal }}Mailer::new(Arc::new(RecordingMailer::failing_with(rejected.clone())));

        // Act
        let result = mailer.send(&{{ name }}()).await;

        // Assert
        assert_eq!(result, Err(rejected));
    }
}