puerto 0.19.1

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 std::collections::HashMap;
use std::sync::{PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};

use async_trait::async_trait;
use business::domain::{{ snake }}::{
    model::{{ "{" }}{{ pascal }}, {{ pascal }}Id},
    repository::{{ pascal }}RepositoryTrait,
};
use puerto_core::{Page, PageRequest, RepositoryError};

/// Keeps the {{ plural }} it is given in process memory: they are lost when the process stops.
/// Like the Postgres adapter, it hides deleted rows and lists newest first{% set uniques = fields | filter(attribute="unique", value=true) %}{% if uniques %};
/// a unique value another live row holds is `Duplicate`{% endif %}.
/// It stands in until there is a database (`puerto new --db` generates Postgres repositories).
#[derive(Default)]
pub struct InMemory{{ pascal }}Repository {
    rows: RwLock<HashMap<{{ pascal }}Id, {{ pascal }}>>,
}

impl InMemory{{ pascal }}Repository {
    pub fn new() -> Self {
        Self::default()
    }

    // A poisoned lock means a writer panicked; every write is a single insert, so the map is
    // still whole and is used as it is.
    fn read(&self) -> RwLockReadGuard<'_, HashMap<{{ pascal }}Id, {{ pascal }}>> {
        self.rows.read().unwrap_or_else(PoisonError::into_inner)
    }

    fn write(&self) -> RwLockWriteGuard<'_, HashMap<{{ pascal }}Id, {{ pascal }}>> {
        self.rows.write().unwrap_or_else(PoisonError::into_inner)
    }
}

#[async_trait]
impl {{ pascal }}RepositoryTrait for InMemory{{ pascal }}Repository {
    async fn find_all(&self, page: PageRequest) -> Result<Page<{{ pascal }}>, RepositoryError> {
        tracing::debug!(limit = page.limit(), offset = page.offset(), "find_all");
        let rows = self.read();
        let mut live: Vec<&{{ pascal }}> = rows.values().filter(|row| !row.deleted).collect();
        live.sort_by(|a, b| {
            b.created_at
                .cmp(&a.created_at)
                .then_with(|| a.id.value().cmp(&b.id.value()))
        });
        let total = live.len() as u64;
        let items = live
            .into_iter()
            .skip(page.offset() as usize)
            .take(page.limit() as usize)
            .cloned()
            .collect();
        Ok(Page::new(items, total, page))
    }

    async fn find_by_id(&self, id: {{ pascal }}Id) -> Result<Option<{{ pascal }}>, RepositoryError> {
        tracing::debug!(%id, "find_by_id");
        Ok(self.read().get(&id).filter(|row| !row.deleted).cloned())
    }

    async fn save(&self, entity: &{{ pascal }}) -> Result<(), RepositoryError> {
        tracing::debug!(id = %entity.id, "save");
        let mut rows = self.write();
{% if uniques %}        // Like the partial unique indexes: only live rows hold their values.
        let taken = !entity.deleted
            && rows.values().any(|row| {
                row.id != entity.id
                    && !row.deleted
                    && {% if uniques | length > 1 %}({% endif %}{% for u in uniques %}{% if not loop.first %} || {% endif %}row.{{ u.name }} == entity.{{ u.name }}{% endfor %}{% if uniques | length > 1 %}){% endif %}
            });
        if taken {
            tracing::warn!(id = %entity.id, "save rejected: a unique value is taken");
            return Err(RepositoryError::Duplicate);
        }
{% endif %}        rows.insert(entity.id, entity.clone());
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use business::domain::{{ snake }}::model::{{ pascal }}Props;
{% for import in test_imports %}    use {{ import }};
{% endfor %}
    fn new_{{ snake }}() -> {{ pascal }} {
        {{ pascal }}::new({{ pascal }}Props {
{% for f in fields %}            {{ f.name }}: {{ f.seed_literal }},
{% endfor %}        })
        .unwrap()
    }

    #[tokio::test]
    async fn should_find_a_saved_{{ snake }}_by_id() {
        // Arrange
        let repo = InMemory{{ pascal }}Repository::new();
        let entity = new_{{ snake }}();
        repo.save(&entity).await.unwrap();

        // Act
        let found = repo.find_by_id(entity.id).await.unwrap();

        // Assert
        assert_eq!(found.map(|row| row.id), Some(entity.id));
    }

    #[tokio::test]
    async fn should_update_the_row_when_saved_again() {
        // Arrange
        let repo = InMemory{{ pascal }}Repository::new();
        let mut entity = new_{{ snake }}();
        repo.save(&entity).await.unwrap();
        entity.updated_at = chrono::Utc::now();

        // Act
        repo.save(&entity).await.unwrap();

        // Assert
        let page = repo.find_all(PageRequest::default()).await.unwrap();
        assert_eq!(page.total, 1);
        assert_eq!(page.items[0].updated_at, entity.updated_at);
    }

    #[tokio::test]
    async fn should_not_find_a_deleted_{{ snake }}() {
        // Arrange
        let repo = InMemory{{ pascal }}Repository::new();
        let mut entity = new_{{ snake }}();
        entity.deleted = true;
        entity.deleted_at = Some(entity.updated_at);
        repo.save(&entity).await.unwrap();

        // Act
        let found = repo.find_by_id(entity.id).await.unwrap();
        let page = repo.find_all(PageRequest::default()).await.unwrap();

        // Assert
        assert!(found.is_none());
        assert_eq!(page.total, 0);
    }

    #[tokio::test]
    async fn should_return_the_requested_page_of_a_larger_list() {
        // Arrange
        let repo = InMemory{{ pascal }}Repository::new();
        for _ in 0..3 {
            repo.save(&new_{{ snake }}()).await.unwrap();
        }

        // Act
        let page = repo.find_all(PageRequest::new(Some(2), Some(2))).await.unwrap();

        // Assert
        assert_eq!(page.items.len(), 1);
        assert_eq!(page.total, 3);
        assert_eq!((page.limit, page.offset), (2, 2));
    }
{% for u in uniques %}
    #[tokio::test]
    async fn should_reject_a_duplicate_{{ u.name }}() {
        // Arrange
        let repo = InMemory{{ pascal }}Repository::new();
        let first = new_{{ snake }}();
        repo.save(&first).await.unwrap();
        let mut second = new_{{ snake }}();
        second.{{ u.name }} = first.{{ u.name }}{% if u.needs_clone %}.clone(){% endif %};

        // Act
        let result = repo.save(&second).await;

        // Assert
        assert_eq!(result.unwrap_err(), RepositoryError::Duplicate);
    }
{% endfor %}}