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

use async_trait::async_trait;
use business::domain::user::{
    model::{User, UserId},
    repository::UserRepositoryTrait,
};
use puerto_core::RepositoryError;

/// Keeps the users in process memory: they are lost when the process stops, and provisioned again
/// on their next request. It stands in until there is a database (`puerto new --db`).
#[derive(Default)]
pub struct InMemoryUserRepository {
    rows: RwLock<HashMap<UserId, User>>,
}

impl InMemoryUserRepository {
    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<UserId, User>> {
        self.rows.read().unwrap_or_else(PoisonError::into_inner)
    }

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

#[async_trait]
impl UserRepositoryTrait for InMemoryUserRepository {
    async fn find_by_subject(
        &self,
        issuer: &str,
        subject: &str,
    ) -> Result<Option<User>, RepositoryError> {
        Ok(self
            .read()
            .values()
            .find(|user| user.issuer == issuer && user.subject == subject)
            .cloned())
    }

    async fn find_by_id(&self, id: UserId) -> Result<Option<User>, RepositoryError> {
        Ok(self.read().get(&id).cloned())
    }

    async fn save(&self, user: &User) -> Result<(), RepositoryError> {
        let mut rows = self.write();
        let taken = rows.values().any(|other| {
            other.id != user.id && other.issuer == user.issuer && other.subject == user.subject
        });
        if taken {
            return Err(RepositoryError::Duplicate);
        }
        rows.insert(user.id, user.clone());
        Ok(())
    }
}

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

    fn ada() -> User {
        User::provision("issuer", "ada", Some("ada@example.com".into()), None)
    }

    #[tokio::test]
    async fn should_find_a_saved_user_when_looked_up_by_subject_or_id() {
        // Arrange
        let repository = InMemoryUserRepository::new();
        let user = ada();
        repository.save(&user).await.expect("saved");

        // Act
        let by_subject = repository.find_by_subject("issuer", "ada").await;
        let by_id = repository.find_by_id(user.id).await;

        // Assert
        assert_eq!(by_subject, Ok(Some(user.clone())));
        assert_eq!(by_id, Ok(Some(user)));
    }

    #[tokio::test]
    async fn should_find_nobody_when_the_subject_is_another_issuers() {
        // Arrange
        let repository = InMemoryUserRepository::new();
        repository.save(&ada()).await.expect("saved");

        // Act
        let found = repository.find_by_subject("other-issuer", "ada").await;

        // Assert
        assert_eq!(found, Ok(None));
    }

    #[tokio::test]
    async fn should_reject_a_second_user_when_the_subject_is_taken() {
        // Arrange
        let repository = InMemoryUserRepository::new();
        repository.save(&ada()).await.expect("saved");

        // Act
        let result = repository.save(&ada()).await;

        // Assert
        assert_eq!(result, Err(RepositoryError::Duplicate));
    }
}