puerto 0.11.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 %}{% set required = fields | filter(attribute="required_string", value=true) %}{% set string_vos = fields | filter(attribute="string_vo", value=true) %}use std::sync::Arc;

use async_trait::async_trait;
{% for import in vo_imports %}use {{ import }};
{% endfor %}
use crate::domain::{{ snake }}::{
    errors::{{ pascal }}Error,
    model::{{ pascal }},
    repository::{{ pascal }}RepositoryTrait,
    use_cases::update_{{ snake }}::{Update{{ pascal }}Params, Update{{ pascal }}UseCaseTrait},
};

pub struct Update{{ pascal }}UseCaseImpl {
    pub repository: Arc<dyn {{ pascal }}RepositoryTrait>,
}

#[async_trait]
impl Update{{ pascal }}UseCaseTrait for Update{{ pascal }}UseCaseImpl {
    async fn execute(&self, params: Update{{ pascal }}Params) -> Result<{{ pascal }}, {{ pascal }}Error> {
        tracing::info!(id = %params.id, "update_{{ snake }}: start");
        let mut entity = self
            .repository
            .find_by_id(params.id)
            .await
            .inspect_err(|e| tracing::error!(error = %e, "update_{{ snake }}: repository failed"))?
            .ok_or_else(|| {
                let err = {{ pascal }}Error::NotFound;
                tracing::warn!(error = %err, "update_{{ snake }}: rejected");
                err
            })?;
{% for f in required %}        if params.{{ f.name }}.trim().is_empty() {
            let err = {{ pascal }}Error::ValidationError("{{ f.name }}_empty".into());
            tracing::warn!(error = %err, "update_{{ snake }}: rejected");
            return Err(err);
        }
{% endfor %}{% for f in fields | filter(attribute="is_vo", value=true) %}let {{ f.name }}{% if f.is_vec_vo %}: Vec<{{ f.vo }}>{% endif %} = {{ f.from_params }};
{% endfor %}{% for f in fields %}        entity.{{ f.name }} = {% if not f.is_vo %}params.{% endif %}{{ f.name }};
{% endfor %}        entity.updated_at = chrono::Utc::now();
        self.repository.save(&entity).await.inspect_err(|e| tracing::error!(error = %e, "update_{{ snake }}: repository failed"))?;
        Ok(entity)
    }
}

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

    #[tokio::test]
    async fn should_update_{{ snake }}_when_params_are_valid() {
        // Arrange
        let entity = {{ pascal }}::new({{ pascal }}Props {
{{ macros::props(fields=fields, word="original") }}        }).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())));
        mock_repo.expect_save().returning(|_| Ok(()));
        let use_case = Update{{ pascal }}UseCaseImpl {
            repository: Arc::new(mock_repo),
        };

        // Act
        let result = use_case
            .execute(Update{{ pascal }}Params {
            id: entity_id,
{{ macros::params(fields=fields, word="updated") }}            })
            .await;

        // Assert
        {% if required %}assert_eq!(result.unwrap().{{ required.0.name }}, "updated");{% elif string_vos %}assert_eq!(result.unwrap().{{ string_vos.0.name }}.value(), "updated");{% else %}assert!(result.is_ok());{% endif %}
    }

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

        // Act
        let result = use_case
            .execute(Update{{ pascal }}Params {
            id: Uuid::new_v4(),
{{ macros::params(fields=fields, word="new") }}            })
            .await;

        // Assert
        assert!(result.is_err());
        assert_eq!(result.unwrap_err().to_string(), "{{ snake }}.not_found");
    }
{% for f in required | concat(with=string_vos) %}
    #[tokio::test]
    async fn should_return_error_when_{{ f.name }}_is_empty() {
        // Arrange
        let entity = {{ pascal }}::new({{ pascal }}Props {
{{ macros::props(fields=fields, word="original") }}        }).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 use_case = Update{{ pascal }}UseCaseImpl {
            repository: Arc::new(mock_repo),
        };

        // Act
        let result = use_case
            .execute(Update{{ pascal }}Params {
            id: entity_id,
{{ macros::params(fields=fields, word="updated", empty=f.name) }}            })
            .await;

        // Assert
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            "{% if f.is_vo %}{{ snake }}.invalid_{{ f.vo_snake }}{% else %}{{ snake }}.validation_error.{{ f.name }}_empty{% endif %}"
        );
    }
{% endfor %}}