uvb-storage-memory 0.2.1

In-memory storage backend for UVB testing and development
Documentation
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use uvb_core::TenantId;
use uvb_storage_api::{SecretError, SecretRecord, SecretStore};

/// Type alias for secret index: (user_id, tenant_id, factor_id) -> secret_id
type SecretIndex = HashMap<(String, TenantId, String), String>;

/// In-memory secret store (NOT for production - secrets should be encrypted at rest)
pub struct InMemorySecretStore {
    secrets: Arc<RwLock<HashMap<String, SecretRecord>>>,
    // Index: (user_id, tenant_id, factor_id) -> secret_id
    index: Arc<RwLock<SecretIndex>>,
}

impl InMemorySecretStore {
    pub fn new() -> Self {
        Self {
            secrets: Arc::new(RwLock::new(HashMap::new())),
            index: Arc::new(RwLock::new(HashMap::new())),
        }
    }
}

impl Default for InMemorySecretStore {
    fn default() -> Self {
        Self::new()
    }
}

#[async_trait]
impl SecretStore for InMemorySecretStore {
    async fn set(
        &self,
        user_id: &str,
        tenant_id: &TenantId,
        factor_id: &str,
        secret_data: &[u8],
        metadata: serde_json::Value,
    ) -> Result<String, SecretError> {
        let id = uuid::Uuid::new_v4().to_string();
        let now = std::time::SystemTime::now();

        let record = SecretRecord {
            id: id.clone(),
            user_id: user_id.to_string(),
            tenant_id: tenant_id.clone(),
            factor_id: factor_id.to_string(),
            secret_data: secret_data.to_vec(),
            metadata,
            created_at: now,
            updated_at: now,
        };

        let key = (
            user_id.to_string(),
            tenant_id.clone(),
            factor_id.to_string(),
        );

        self.secrets.write().await.insert(id.clone(), record);
        self.index.write().await.insert(key.clone(), id.clone());

        tracing::info!(
            secret_id = %id,
            user_id = %user_id,
            tenant_id = %tenant_id,
            factor_id = %factor_id,
            "Stored secret in memory"
        );

        Ok(id)
    }

    async fn get(
        &self,
        user_id: &str,
        tenant_id: &TenantId,
        factor_id: &str,
    ) -> Result<Option<SecretRecord>, SecretError> {
        let key = (
            user_id.to_string(),
            tenant_id.clone(),
            factor_id.to_string(),
        );

        let index = self.index.read().await;
        let total_secrets = self.secrets.read().await.len();
        let total_index_entries = index.len();

        if let Some(id) = index.get(&key) {
            let secrets = self.secrets.read().await;
            let found = secrets.get(id).is_some();
            tracing::info!(
                user_id = %user_id,
                tenant_id = %tenant_id,
                factor_id = %factor_id,
                secret_id = %id,
                found = found,
                total_secrets = total_secrets,
                total_index_entries = total_index_entries,
                "Retrieved secret from memory"
            );
            Ok(secrets.get(id).cloned())
        } else {
            tracing::warn!(
                user_id = %user_id,
                tenant_id = %tenant_id,
                factor_id = %factor_id,
                total_secrets = total_secrets,
                total_index_entries = total_index_entries,
                "Secret not found in memory index"
            );
            Ok(None)
        }
    }

    async fn get_by_id(&self, id: &str) -> Result<Option<SecretRecord>, SecretError> {
        let secrets = self.secrets.read().await;
        Ok(secrets.get(id).cloned())
    }

    async fn delete(&self, id: &str) -> Result<(), SecretError> {
        let mut secrets = self.secrets.write().await;
        if let Some(record) = secrets.remove(id) {
            let key = (record.user_id, record.tenant_id, record.factor_id);
            self.index.write().await.remove(&key);
            Ok(())
        } else {
            Err(SecretError::NotFound)
        }
    }

    async fn list(
        &self,
        user_id: &str,
        tenant_id: &TenantId,
        factor_id: Option<&str>,
    ) -> Result<Vec<SecretRecord>, SecretError> {
        let secrets = self.secrets.read().await;
        let results = secrets
            .values()
            .filter(|s| {
                s.user_id == user_id
                    && &s.tenant_id == tenant_id
                    && (factor_id.is_none() || factor_id == Some(s.factor_id.as_str()))
            })
            .cloned()
            .collect();

        Ok(results)
    }
}