use std::any::Any;
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TenantId(String);
impl TenantId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl fmt::Display for TenantId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<&str> for TenantId {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl From<String> for TenantId {
fn from(s: String) -> Self {
Self::new(s)
}
}
impl From<uuid::Uuid> for TenantId {
fn from(u: uuid::Uuid) -> Self {
Self::new(u.to_string())
}
}
impl From<i64> for TenantId {
fn from(i: i64) -> Self {
Self::new(i.to_string())
}
}
impl From<i32> for TenantId {
fn from(i: i32) -> Self {
Self::new(i.to_string())
}
}
#[derive(Debug, Clone, Default)]
pub struct TenantInfo {
pub name: Option<String>,
pub schema: Option<String>,
pub database: Option<String>,
pub is_superuser: bool,
pub is_system: bool,
metadata: HashMap<String, Arc<dyn Any + Send + Sync>>,
}
impl TenantInfo {
pub fn new() -> Self {
Self::default()
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn with_schema(mut self, schema: impl Into<String>) -> Self {
self.schema = Some(schema.into());
self
}
pub fn with_database(mut self, database: impl Into<String>) -> Self {
self.database = Some(database.into());
self
}
pub fn as_superuser(mut self) -> Self {
self.is_superuser = true;
self
}
pub fn as_system(mut self) -> Self {
self.is_system = true;
self
}
pub fn with_metadata<T: Any + Send + Sync>(mut self, key: impl Into<String>, value: T) -> Self {
self.metadata.insert(key.into(), Arc::new(value));
self
}
pub fn get_metadata<T: Any + Send + Sync>(&self, key: &str) -> Option<&T> {
self.metadata.get(key).and_then(|v| v.downcast_ref())
}
}
#[derive(Debug, Clone)]
pub struct TenantContext {
pub id: TenantId,
pub info: TenantInfo,
}
impl TenantContext {
pub fn new(id: impl Into<TenantId>) -> Self {
Self {
id: id.into(),
info: TenantInfo::default(),
}
}
pub fn with_info(id: impl Into<TenantId>, info: TenantInfo) -> Self {
Self {
id: id.into(),
info,
}
}
pub fn system() -> Self {
Self {
id: TenantId::new("__system__"),
info: TenantInfo::new().as_system().as_superuser(),
}
}
pub fn should_bypass(&self) -> bool {
self.info.is_superuser || self.info.is_system
}
pub fn schema(&self) -> Option<&str> {
self.info.schema.as_deref()
}
pub fn database(&self) -> Option<&str> {
self.info.database.as_deref()
}
}
#[cfg(feature = "thread-local-tenant")]
mod thread_local {
use super::TenantContext;
use std::cell::RefCell;
thread_local! {
static CURRENT_TENANT: RefCell<Option<TenantContext>> = const { RefCell::new(None) };
}
pub fn set_current_tenant(ctx: TenantContext) {
CURRENT_TENANT.with(|t| {
*t.borrow_mut() = Some(ctx);
});
}
pub fn get_current_tenant() -> Option<TenantContext> {
CURRENT_TENANT.with(|t| t.borrow().clone())
}
pub fn clear_current_tenant() {
CURRENT_TENANT.with(|t| {
*t.borrow_mut() = None;
});
}
pub fn with_tenant<F, R>(ctx: TenantContext, f: F) -> R
where
F: FnOnce() -> R,
{
let previous = get_current_tenant();
set_current_tenant(ctx);
let result = f();
if let Some(prev) = previous {
set_current_tenant(prev);
} else {
clear_current_tenant();
}
result
}
}
#[cfg(feature = "thread-local-tenant")]
#[allow(unused_imports)]
pub use thread_local::{clear_current_tenant, get_current_tenant, set_current_tenant, with_tenant};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tenant_id_creation() {
let id1 = TenantId::new("tenant-123");
assert_eq!(id1.as_str(), "tenant-123");
let id2: TenantId = "tenant-456".into();
assert_eq!(id2.as_str(), "tenant-456");
let id3: TenantId = 123_i64.into();
assert_eq!(id3.as_str(), "123");
}
#[test]
fn test_tenant_context() {
let ctx = TenantContext::new("tenant-123");
assert_eq!(ctx.id.as_str(), "tenant-123");
assert!(!ctx.should_bypass());
}
#[test]
fn test_system_context() {
let ctx = TenantContext::system();
assert!(ctx.should_bypass());
assert!(ctx.info.is_system);
assert!(ctx.info.is_superuser);
}
#[test]
fn test_tenant_info() {
let info = TenantInfo::new()
.with_name("Acme Corp")
.with_schema("tenant_acme")
.with_metadata("plan", "enterprise".to_string());
assert_eq!(info.name, Some("Acme Corp".to_string()));
assert_eq!(info.schema, Some("tenant_acme".to_string()));
assert_eq!(
info.get_metadata::<String>("plan"),
Some(&"enterprise".to_string())
);
}
}