puerto 0.15.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.
{% import "application/macros.tera" as macros %}use std::sync::Arc;

use async_trait::async_trait;
use puerto_core::RepositoryError;

use crate::domain::{{ snake }}::{
    errors::{{ pascal }}Error,
    events::{{ "{" }}{{ pascal }}Event, {{ pascal }}Snapshot},
    repository::{{ pascal }}RepositoryTrait,
    unit_of_work::{{ pascal }}UnitOfWorkFactory,
    use_cases::delete_{{ snake }}::{Delete{{ pascal }}Params, Delete{{ pascal }}UseCaseTrait},
};

pub struct Delete{{ pascal }}UseCaseImpl {
    pub repository: Arc<dyn {{ pascal }}RepositoryTrait>,
    pub uow_factory: Arc<dyn {{ pascal }}UnitOfWorkFactory>,
}

#[async_trait]
impl Delete{{ pascal }}UseCaseTrait for Delete{{ pascal }}UseCaseImpl {
    async fn execute(&self, params: Delete{{ pascal }}Params) -> Result<(), {{ pascal }}Error> {
        tracing::info!(id = %params.id, "delete_{{ snake }}: start");
        let failed = |e: &RepositoryError| tracing::error!(error = %e, "delete_{{ snake }}: repository failed");
        let mut entity = self
            .repository
            .find_by_id(params.id)
            .await
            .inspect_err(failed)?
            .ok_or_else(|| {
                let err = {{ pascal }}Error::NotFound;
                tracing::warn!(error = %err, "delete_{{ snake }}: rejected");
                err
            })?;
        let now = chrono::Utc::now();
        entity.deleted = true;
        entity.deleted_at = Some(now);
        entity.updated_at = now;
        let mut uow = self.uow_factory.begin().await.inspect_err(failed)?;
        uow.save(&entity).await.inspect_err(failed)?;
        uow.record({{ pascal }}Event::Deleted({{ pascal }}Snapshot::from_entity(&entity))).await.inspect_err(failed)?;
        uow.commit().await.inspect_err(failed)?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::domain::{{ snake }}::{
        model::{{ "{" }}{{ pascal }}, {{ pascal }}Id, {{ pascal }}Props},
        repository::mocks::Mock{{ pascal }}Repository,
        unit_of_work::mocks::RecordingUnitOfWorkFactory,
    };
    use puerto_core::DomainEvent;
{% for import in test_imports %}    use {{ import }};
{% endfor %}

    #[tokio::test]
    async fn should_record_{{ snake }}_deleted_when_id_exists() {
        // Arrange
        let entity = {{ pascal }}::new({{ pascal }}Props {
{{ macros::props(fields=fields, word="example") }}        }).unwrap();
        let entity_id = entity.id;
        let mut mock_repo = Mock{{ pascal }}Repository::new();
        mock_repo
            .expect_find_by_id()
            .returning(move |_| Ok(Some(entity.clone())));
        let uow_factory = RecordingUnitOfWorkFactory::new();
        let use_case = Delete{{ pascal }}UseCaseImpl {
            repository: Arc::new(mock_repo),
            uow_factory: Arc::new(uow_factory.clone()),
        };

        // Act
        let result = use_case
            .execute(Delete{{ pascal }}Params { id: entity_id })
            .await;

        // Assert
        assert!(result.is_ok());
        let recorded = uow_factory.recorded();
        assert!(recorded.committed);
        assert!(recorded.saved.iter().all(|saved| saved.deleted));
        assert_eq!(recorded.events.len(), 1);
        assert_eq!(recorded.events[0].event_type(), "{{ snake }}_deleted");
        assert_eq!(recorded.events[0].aggregate_id(), entity_id.value());
    }

    #[tokio::test]
    async fn should_return_not_found_and_record_nothing_when_the_{{ snake }}_does_not_exist() {
        // Arrange
        let mut mock_repo = Mock{{ pascal }}Repository::new();
        mock_repo.expect_find_by_id().returning(|_| Ok(None));
        let uow_factory = RecordingUnitOfWorkFactory::new();
        let use_case = Delete{{ pascal }}UseCaseImpl {
            repository: Arc::new(mock_repo),
            uow_factory: Arc::new(uow_factory.clone()),
        };

        // Act
        let result = use_case
            .execute(Delete{{ pascal }}Params { id: {{ pascal }}Id::generate() })
            .await;

        // Assert
        assert!(result.is_err());
        assert_eq!(result.unwrap_err().to_string(), "{{ snake }}.not_found");
        assert!(uow_factory.recorded().events.is_empty());
    }
}