pub mod context;
pub mod middleware;
pub use context::{TenantContext, TenantInfo, TenantResolver, InMemoryTenantResolver};
pub use middleware::{tenant_middleware, TenantExtractor, TenantMiddlewareConfig};
#[cfg(feature = "database")]
pub use context::PostgresTenantResolver;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TenantId(pub String);
impl TenantId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<String> for TenantId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for TenantId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
impl From<Uuid> for TenantId {
fn from(uuid: Uuid) -> Self {
Self(uuid.to_string())
}
}
impl std::fmt::Display for TenantId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TenantConfig {
pub id: TenantId,
pub name: String,
pub subdomain: Option<String>,
pub database_url: Option<String>,
pub features: Vec<String>,
pub metadata: std::collections::HashMap<String, String>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub is_active: bool,
}
impl TenantConfig {
pub fn new(id: TenantId, name: String) -> Self {
Self {
id,
name,
subdomain: None,
database_url: None,
features: Vec::new(),
metadata: std::collections::HashMap::new(),
created_at: chrono::Utc::now(),
is_active: true,
}
}
pub fn with_subdomain(mut self, subdomain: String) -> Self {
self.subdomain = Some(subdomain);
self
}
pub fn with_database(mut self, url: String) -> Self {
self.database_url = Some(url);
self
}
pub fn with_features(mut self, features: Vec<String>) -> Self {
self.features = features;
self
}
pub fn with_metadata(mut self, key: String, value: String) -> Self {
self.metadata.insert(key, value);
self
}
pub fn has_feature(&self, feature: &str) -> bool {
self.features.iter().any(|f| f == feature)
}
pub fn set_active(mut self, active: bool) -> Self {
self.is_active = active;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IsolationStrategy {
Database,
Schema,
Hybrid,
}
impl Default for IsolationStrategy {
fn default() -> Self {
Self::Schema
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TenantPlan {
Free,
Basic,
Professional,
Enterprise,
Custom,
}
impl Default for TenantPlan {
fn default() -> Self {
Self::Free
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TenantLimits {
pub max_users: Option<u32>,
pub max_api_requests_per_hour: Option<u32>,
pub max_storage_bytes: Option<u64>,
pub max_projects: Option<u32>,
}
impl Default for TenantLimits {
fn default() -> Self {
Self {
max_users: Some(10),
max_api_requests_per_hour: Some(1000),
max_storage_bytes: Some(1_073_741_824), max_projects: Some(5),
}
}
}
impl TenantLimits {
pub fn unlimited() -> Self {
Self {
max_users: None,
max_api_requests_per_hour: None,
max_storage_bytes: None,
max_projects: None,
}
}
pub fn for_plan(plan: TenantPlan) -> Self {
match plan {
TenantPlan::Free => Self::default(),
TenantPlan::Basic => Self {
max_users: Some(25),
max_api_requests_per_hour: Some(5000),
max_storage_bytes: Some(5_368_709_120), max_projects: Some(10),
},
TenantPlan::Professional => Self {
max_users: Some(100),
max_api_requests_per_hour: Some(25000),
max_storage_bytes: Some(53_687_091_200), max_projects: Some(50),
},
TenantPlan::Enterprise | TenantPlan::Custom => Self::unlimited(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tenant_id_creation() {
let id = TenantId::new("tenant-123");
assert_eq!(id.as_str(), "tenant-123");
assert_eq!(id.to_string(), "tenant-123");
let id_from_string = TenantId::from("tenant-456".to_string());
assert_eq!(id_from_string.as_str(), "tenant-456");
let id_from_uuid = TenantId::from(Uuid::new_v4());
assert!(!id_from_uuid.as_str().is_empty());
}
#[test]
fn test_tenant_config() {
let config = TenantConfig::new(
TenantId::new("tenant-1"),
"Acme Corp".to_string(),
)
.with_subdomain("acme".to_string())
.with_features(vec!["premium".to_string(), "api".to_string()])
.with_metadata("industry".to_string(), "technology".to_string());
assert_eq!(config.name, "Acme Corp");
assert_eq!(config.subdomain, Some("acme".to_string()));
assert!(config.has_feature("premium"));
assert!(config.has_feature("api"));
assert!(!config.has_feature("enterprise"));
assert_eq!(config.metadata.get("industry"), Some(&"technology".to_string()));
assert!(config.is_active);
}
#[test]
fn test_isolation_strategy() {
let strategy = IsolationStrategy::default();
assert_eq!(strategy, IsolationStrategy::Schema);
let db_strategy = IsolationStrategy::Database;
assert_ne!(db_strategy, IsolationStrategy::Schema);
}
#[test]
fn test_tenant_limits() {
let limits = TenantLimits::default();
assert_eq!(limits.max_users, Some(10));
assert_eq!(limits.max_api_requests_per_hour, Some(1000));
let unlimited = TenantLimits::unlimited();
assert_eq!(unlimited.max_users, None);
assert_eq!(unlimited.max_api_requests_per_hour, None);
let professional = TenantLimits::for_plan(TenantPlan::Professional);
assert_eq!(professional.max_users, Some(100));
assert_eq!(professional.max_api_requests_per_hour, Some(25000));
}
#[test]
fn test_tenant_plan() {
let plan = TenantPlan::default();
assert_eq!(plan, TenantPlan::Free);
let limits = TenantLimits::for_plan(TenantPlan::Enterprise);
assert_eq!(limits.max_users, None); }
}