puerto 0.20.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.
use async_trait::async_trait;
use puerto_core::RepositoryError;

use super::{events::{{ pascal }}Event, model::{{ pascal }}};

/// One transaction over a {{ snake }} (ADR-0018 § 2): what it saves and the events it records are
/// written together on `commit`, or not at all. Dropping it without `commit` rolls it back, so an
/// early `?` is the rollback.
#[async_trait]
pub trait {{ pascal }}UnitOfWork: Send {
    async fn save(&mut self, entity: &{{ pascal }}) -> Result<(), RepositoryError>;
    async fn record(&mut self, event: {{ pascal }}Event) -> Result<(), RepositoryError>;
    async fn commit(self: Box<Self>) -> Result<(), RepositoryError>;
}

/// Opens a [`{{ pascal }}UnitOfWork`]: what every {{ snake }} write goes through. The repository only
/// reads, so a write that skips the outbox does not compile.
#[async_trait]
pub trait {{ pascal }}UnitOfWorkFactory: Send + Sync {
    async fn begin(&self) -> Result<Box<dyn {{ pascal }}UnitOfWork>, RepositoryError>;
}

#[cfg(any(test, feature = "test-utils"))]
pub mod mocks {
    use std::sync::{Arc, Mutex, MutexGuard};

    use super::*;

    /// Where a [`RecordingUnitOfWorkFactory`] fails, when scripted to.
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub enum FailAt {
        Begin,
        Save,
        Commit,
    }

    /// What the units of work of a [`RecordingUnitOfWorkFactory`] were asked to do.
    #[derive(Debug, Clone, Default)]
    pub struct Recorded {
        pub saved: Vec<{{ pascal }}>,
        pub events: Vec<{{ pascal }}Event>,
        pub committed: bool,
    }

    /// A fake factory for use case tests: its units of work record what they save and record and
    /// whether they committed, and fail where scripted.
    #[derive(Clone, Default)]
    pub struct RecordingUnitOfWorkFactory {
        recorded: Arc<Mutex<Recorded>>,
        failure: Option<(FailAt, RepositoryError)>,
    }

    impl RecordingUnitOfWorkFactory {
        pub fn new() -> Self {
            Self::default()
        }

        pub fn failing_at(at: FailAt, error: RepositoryError) -> Self {
            Self {
                failure: Some((at, error)),
                ..Self::default()
            }
        }

        pub fn recorded(&self) -> Recorded {
            lock(&self.recorded).clone()
        }

        fn fail_at(&self, at: FailAt) -> Result<(), RepositoryError> {
            match self.failure {
                Some((failing, error)) if failing == at => Err(error),
                _ => Ok(()),
            }
        }
    }

    fn lock(recorded: &Mutex<Recorded>) -> MutexGuard<'_, Recorded> {
        recorded.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
    }

    struct RecordingUnitOfWork {
        factory: RecordingUnitOfWorkFactory,
    }

    #[async_trait]
    impl {{ pascal }}UnitOfWork for RecordingUnitOfWork {
        async fn save(&mut self, entity: &{{ pascal }}) -> Result<(), RepositoryError> {
            self.factory.fail_at(FailAt::Save)?;
            lock(&self.factory.recorded).saved.push(entity.clone());
            Ok(())
        }

        async fn record(&mut self, event: {{ pascal }}Event) -> Result<(), RepositoryError> {
            lock(&self.factory.recorded).events.push(event);
            Ok(())
        }

        async fn commit(self: Box<Self>) -> Result<(), RepositoryError> {
            self.factory.fail_at(FailAt::Commit)?;
            lock(&self.factory.recorded).committed = true;
            Ok(())
        }
    }

    #[async_trait]
    impl {{ pascal }}UnitOfWorkFactory for RecordingUnitOfWorkFactory {
        async fn begin(&self) -> Result<Box<dyn {{ pascal }}UnitOfWork>, RepositoryError> {
            self.fail_at(FailAt::Begin)?;
            Ok(Box::new(RecordingUnitOfWork {
                factory: self.clone(),
            }))
        }
    }
}