soaprs-memory 0.5.0

Reference in-memory repository, auth, event, and CQRS adapters for soaprs
Documentation
//! Reference in-memory authentication persistence.

use std::{collections::BTreeMap, sync::RwLock};

use soaprs_auth::{Session, SessionId, SessionStore};
use soaprs_core::{BoxFuture, SoapError, SoapResult};

/// Thread-safe reference session store with replacement and idempotent deletion.
#[derive(Debug)]
pub struct MemorySessionStore<P> {
    sessions: RwLock<BTreeMap<SessionId, Session<P>>>,
}

impl<P> MemorySessionStore<P> {
    /// Creates an empty session store.
    pub const fn new() -> Self {
        Self {
            sessions: RwLock::new(BTreeMap::new()),
        }
    }
}

impl<P> Default for MemorySessionStore<P> {
    fn default() -> Self {
        Self::new()
    }
}

impl<P> SessionStore<P> for MemorySessionStore<P>
where
    P: Clone + Send + Sync + 'static,
{
    fn load<'a>(&'a self, id: &'a SessionId) -> BoxFuture<'a, SoapResult<Option<Session<P>>>> {
        Box::pin(async move {
            let sessions = self
                .sessions
                .read()
                .map_err(|_| SoapError::infrastructure("in-memory session read lock poisoned"))?;
            Ok(sessions.get(id).cloned())
        })
    }

    fn save(&self, session: Session<P>) -> BoxFuture<'_, SoapResult<()>> {
        Box::pin(async move {
            let mut sessions = self
                .sessions
                .write()
                .map_err(|_| SoapError::infrastructure("in-memory session write lock poisoned"))?;
            sessions.insert(session.id().clone(), session);
            Ok(())
        })
    }

    fn delete<'a>(&'a self, id: &'a SessionId) -> BoxFuture<'a, SoapResult<()>> {
        Box::pin(async move {
            let mut sessions = self
                .sessions
                .write()
                .map_err(|_| SoapError::infrastructure("in-memory session write lock poisoned"))?;
            sessions.remove(id);
            Ok(())
        })
    }
}

#[cfg(test)]
mod tests {
    use std::time::{Duration, UNIX_EPOCH};

    use soaprs_auth::{Session, SessionId, StandardPrincipal};
    use soaprs_contract_tests::{block_on, verify_session_store_contract};

    use super::MemorySessionStore;

    #[test]
    fn session_store_passes_the_shared_contract() {
        let Some(id) = SessionId::new("session-contract").ok() else {
            panic!("valid session ID");
        };
        let first = StandardPrincipal::new("user-1").and_then(|principal| {
            Session::new(
                id.clone(),
                principal,
                UNIX_EPOCH,
                UNIX_EPOCH + Duration::from_secs(60),
            )
        });
        let replacement = StandardPrincipal::new("user-2").and_then(|principal| {
            Session::new(
                id,
                principal,
                UNIX_EPOCH,
                UNIX_EPOCH + Duration::from_secs(120),
            )
        });
        let (Some(first), Some(replacement)) = (first.ok(), replacement.ok()) else {
            panic!("valid session fixtures");
        };
        let store = MemorySessionStore::new();

        assert!(block_on(verify_session_store_contract(&store, first, replacement)).is_ok());
    }
}