use serde::{Deserialize, Serialize};
tokio::task_local! {
pub static CURRENT_TENANT_ID: String;
}
pub fn current_tenant_id() -> String {
CURRENT_TENANT_ID
.try_with(|t| t.clone())
.unwrap_or_else(|_| "default".to_string())
}
pub async fn scope_tenant<F>(tenant_id: String, fut: F) -> F::Output
where
F: std::future::Future,
{
CURRENT_TENANT_ID.scope(tenant_id, fut).await
}
pub fn scope_tenant_blocking<F, R>(tenant_id: String, f: F) -> R
where
F: FnOnce() -> R,
{
CURRENT_TENANT_ID.sync_scope(tenant_id, f)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TenantPlan {
Starter,
Pro,
Enterprise,
}
impl TenantPlan {
pub fn from_str(s: &str) -> Self {
match s {
"pro" => Self::Pro,
"enterprise" => Self::Enterprise,
_ => Self::Starter,
}
}
}
impl std::fmt::Display for TenantPlan {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Starter => write!(f, "starter"),
Self::Pro => write!(f, "pro"),
Self::Enterprise => write!(f, "enterprise"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TenantContext {
pub tenant_id: String,
pub plan: TenantPlan,
}
impl TenantContext {
pub fn new(tenant_id: impl Into<String>, plan: TenantPlan) -> Self {
Self { tenant_id: tenant_id.into(), plan }
}
pub fn default_tenant() -> Self {
Self { tenant_id: "default".to_string(), plan: TenantPlan::Enterprise }
}
pub fn is_default(&self) -> bool {
self.tenant_id == "default"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tenant_plan_from_str() {
assert_eq!(TenantPlan::from_str("starter"), TenantPlan::Starter);
assert_eq!(TenantPlan::from_str("pro"), TenantPlan::Pro);
assert_eq!(TenantPlan::from_str("enterprise"), TenantPlan::Enterprise);
assert_eq!(TenantPlan::from_str("unknown"), TenantPlan::Starter);
}
#[test]
fn test_tenant_plan_display() {
assert_eq!(TenantPlan::Starter.to_string(), "starter");
assert_eq!(TenantPlan::Pro.to_string(), "pro");
assert_eq!(TenantPlan::Enterprise.to_string(), "enterprise");
}
#[test]
fn test_default_tenant() {
let ctx = TenantContext::default_tenant();
assert_eq!(ctx.tenant_id, "default");
assert!(ctx.is_default());
assert_eq!(ctx.plan, TenantPlan::Enterprise);
}
#[test]
fn test_tenant_context_new() {
let ctx = TenantContext::new("acme", TenantPlan::Pro);
assert_eq!(ctx.tenant_id, "acme");
assert_eq!(ctx.plan, TenantPlan::Pro);
assert!(!ctx.is_default());
}
#[test]
fn test_current_tenant_id_default_outside_scope() {
assert_eq!(current_tenant_id(), "default");
}
#[tokio::test]
async fn test_current_tenant_id_inside_scope() {
let result = CURRENT_TENANT_ID
.scope("example-tenant".to_string(), async { current_tenant_id() })
.await;
assert_eq!(result, "example-tenant");
}
#[tokio::test]
async fn test_current_tenant_id_nested_scope() {
let outer = CURRENT_TENANT_ID
.scope("tenant-a".to_string(), async {
let inner = CURRENT_TENANT_ID
.scope("tenant-b".to_string(), async { current_tenant_id() })
.await;
(current_tenant_id(), inner)
})
.await;
assert_eq!(outer.0, "tenant-a");
assert_eq!(outer.1, "tenant-b");
}
#[tokio::test]
async fn test_scope_tenant_public_primitive_binds_and_nests() {
let got = scope_tenant("acme".to_string(), async { current_tenant_id() }).await;
assert_eq!(got, "acme");
let doubled = scope_tenant("x".to_string(), async { 21 * 2 }).await;
assert_eq!(doubled, 42);
let (inner, outer) = scope_tenant("outer".to_string(), async {
let inner = scope_tenant("inner".to_string(), async { current_tenant_id() }).await;
(inner, current_tenant_id())
})
.await;
assert_eq!((inner.as_str(), outer.as_str()), ("inner", "outer"));
assert_eq!(current_tenant_id(), "default");
}
}