use async_trait::async_trait;
use std::path::Path;
use std::sync::Arc;
use crate::types::{MemoryResult, MemoryTenantScope};
#[path = "store_adapter.rs"]
mod adapter;
#[path = "store_contract.rs"]
mod contract;
pub use contract::*;
pub async fn open_sqlite_memory_store(db_path: &Path) -> MemoryResult<Arc<dyn MemoryStore>> {
let database = crate::db::MemoryDatabase::new(db_path).await?;
Ok(Arc::new(database))
}
pub async fn open_memory_store(db_path: &Path) -> MemoryStoreResult<Arc<dyn MemoryStore>> {
match std::env::var("TANDEM_MEMORY_BACKEND")
.unwrap_or_else(|_| "sqlite".to_string())
.trim()
.to_ascii_lowercase()
.as_str()
{
"" | "sqlite" => open_sqlite_memory_store(db_path)
.await
.map_err(MemoryStoreError::from),
"postgres" | "postgresql" => {
#[cfg(feature = "postgres")]
{
let config = crate::postgres_store::PostgresMemoryStoreConfig::from_env()?;
let store = crate::postgres_store::PostgresMemoryStore::connect(config).await?;
Ok(Arc::new(store))
}
#[cfg(not(feature = "postgres"))]
{
Err(MemoryStoreError::unsupported(
"TANDEM_MEMORY_BACKEND=postgres requires the tandem-memory/postgres feature",
))
}
}
backend => Err(MemoryStoreError::invalid(format!(
"unsupported TANDEM_MEMORY_BACKEND value: {backend}"
))),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemoryReadAccess {
Scoped,
TrustedUnrestricted,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MemoryReadScope {
pub tenant: MemoryTenantScope,
pub org_unit: Option<String>,
pub subject: Option<String>,
pub access: MemoryReadAccess,
}
impl MemoryReadScope {
pub fn tenant(tenant: MemoryTenantScope) -> Self {
Self {
tenant,
org_unit: None,
subject: None,
access: MemoryReadAccess::Scoped,
}
}
pub fn trusted_unrestricted(tenant: MemoryTenantScope) -> Self {
Self {
tenant,
org_unit: None,
subject: None,
access: MemoryReadAccess::TrustedUnrestricted,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MemoryWriteScope {
pub tenant: MemoryTenantScope,
pub org_unit: Option<String>,
pub subject: Option<String>,
}
impl MemoryWriteScope {
pub fn tenant(tenant: MemoryTenantScope) -> Self {
Self {
tenant,
org_unit: None,
subject: None,
}
}
}
#[async_trait]
pub trait MemoryStore: Send + Sync {
async fn read(
&self,
request: MemoryStoreReadRequest,
) -> MemoryStoreResult<MemoryStoreReadResult>;
async fn query(
&self,
request: MemoryStoreQueryRequest,
) -> MemoryStoreResult<MemoryStoreQueryResult>;
async fn write(
&self,
request: MemoryStoreWriteRequest,
) -> MemoryStoreResult<MemoryStoreWriteResult>;
async fn mutate(
&self,
request: MemoryStoreMutationRequest,
) -> MemoryStoreResult<MemoryStoreMutationResult>;
async fn batch(
&self,
request: MemoryStoreBatchRequest,
) -> MemoryStoreResult<MemoryStoreBatchResult>;
async fn backend_health(
&self,
request: MemoryBackendHealthRequest,
) -> MemoryStoreResult<MemoryBackendHealthResult>;
async fn recover_backend(
&self,
_request: MemoryBackendRecoveryRequest,
) -> MemoryStoreResult<MemoryBackendRecoveryResult> {
Err(MemoryStoreError::unsupported(
"this memory backend does not expose recovery operations",
))
}
async fn migration_capabilities(
&self,
request: MemoryMigrationCapabilityRequest,
) -> MemoryStoreResult<MemoryMigrationCapabilityResult>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::MemoryDatabase;
const _: fn() = || {
fn assert_impl<T: MemoryStore>() {}
assert_impl::<MemoryDatabase>();
};
#[test]
fn read_scope_tenant_only_has_no_narrowing() {
let scope = MemoryReadScope::tenant(MemoryTenantScope::local());
assert!(scope.org_unit.is_none());
assert!(scope.subject.is_none());
assert_eq!(scope.tenant, MemoryTenantScope::local());
}
#[test]
fn write_scope_tenant_only_has_no_stamping() {
let scope = MemoryWriteScope::tenant(MemoryTenantScope::local());
assert!(scope.org_unit.is_none());
assert!(scope.subject.is_none());
}
#[test]
fn read_scope_represents_shared_and_private_visibility() {
let shared = MemoryReadScope::tenant(MemoryTenantScope::local());
assert!(shared.subject.is_none());
let mut private = shared.clone();
private.subject = Some("user-a".to_string());
private.org_unit = Some("finance".to_string());
assert_eq!(private.subject.as_deref(), Some("user-a"));
assert_eq!(private.org_unit.as_deref(), Some("finance"));
}
}
#[cfg(test)]
#[path = "store_contract_tests.rs"]
mod contract_tests;