eventuali_core/tenancy/
tenant.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4use uuid::Uuid;
5use chrono::{DateTime, Utc};
6use std::collections::HashMap;
7
8/// Unique identifier for a tenant with validation and formatting
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct TenantId(String);
11
12impl TenantId {
13    /// Create a new TenantId with validation
14    pub fn new(id: String) -> Result<Self, TenantError> {
15        if id.is_empty() {
16            return Err(TenantError::InvalidTenantId("Tenant ID cannot be empty".to_string()));
17        }
18        
19        if id.len() > 128 {
20            return Err(TenantError::InvalidTenantId("Tenant ID too long (max 128 chars)".to_string()));
21        }
22        
23        if !id.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
24            return Err(TenantError::InvalidTenantId("Tenant ID must contain only alphanumeric, dash, or underscore".to_string()));
25        }
26        
27        Ok(TenantId(id))
28    }
29    
30    /// Generate a new UUID-based TenantId
31    pub fn generate() -> Self {
32        TenantId(Uuid::new_v4().to_string())
33    }
34    
35    /// Get the raw tenant ID string
36    pub fn as_str(&self) -> &str {
37        &self.0
38    }
39    
40    /// Get the database prefix for this tenant (for table/schema isolation)
41    pub fn db_prefix(&self) -> String {
42        format!("tenant_{}", self.0.replace('-', "_"))
43    }
44}
45
46impl fmt::Display for TenantId {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        write!(f, "{}", self.0)
49    }
50}
51
52impl FromStr for TenantId {
53    type Err = TenantError;
54    
55    fn from_str(s: &str) -> Result<Self, Self::Err> {
56        TenantId::new(s.to_string())
57    }
58}
59
60/// Configuration for a tenant
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct TenantConfig {
63    pub isolation_level: IsolationLevel,
64    pub resource_limits: ResourceLimits,
65    pub encryption_enabled: bool,
66    pub audit_enabled: bool,
67    pub custom_settings: HashMap<String, String>,
68}
69
70impl Default for TenantConfig {
71    fn default() -> Self {
72        Self {
73            isolation_level: IsolationLevel::Database,
74            resource_limits: ResourceLimits::default(),
75            encryption_enabled: true,
76            audit_enabled: true,
77            custom_settings: HashMap::new(),
78        }
79    }
80}
81
82/// Tenant isolation levels
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub enum IsolationLevel {
85    /// Database-level isolation with separate schemas/tables
86    Database,
87    /// Application-level isolation with tenant filtering
88    Application,
89    /// Row-level isolation with tenant_id columns
90    Row,
91}
92
93/// Resource limits for a tenant
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct ResourceLimits {
96    pub max_events_per_day: Option<u64>,
97    pub max_storage_mb: Option<u64>,
98    pub max_concurrent_streams: Option<u32>,
99    pub max_projections: Option<u32>,
100    pub max_aggregates: Option<u64>,
101}
102
103impl Default for ResourceLimits {
104    fn default() -> Self {
105        Self {
106            max_events_per_day: Some(1_000_000),
107            max_storage_mb: Some(10_000), // 10GB
108            max_concurrent_streams: Some(100),
109            max_projections: Some(50),
110            max_aggregates: Some(100_000),
111        }
112    }
113}
114
115/// Complete tenant information
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct TenantInfo {
118    pub id: TenantId,
119    pub name: String,
120    pub description: Option<String>,
121    pub created_at: DateTime<Utc>,
122    pub updated_at: DateTime<Utc>,
123    pub status: TenantStatus,
124    pub config: TenantConfig,
125    pub metadata: TenantMetadata,
126}
127
128impl TenantInfo {
129    pub fn new(id: TenantId, name: String) -> Self {
130        let now = Utc::now();
131        Self {
132            id,
133            name,
134            description: None,
135            created_at: now,
136            updated_at: now,
137            status: TenantStatus::Active,
138            config: TenantConfig::default(),
139            metadata: TenantMetadata::default(),
140        }
141    }
142    
143    /// Check if tenant is active and can perform operations
144    pub fn is_active(&self) -> bool {
145        matches!(self.status, TenantStatus::Active)
146    }
147}
148
149/// Tenant operational status
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub enum TenantStatus {
152    Active,
153    Suspended,
154    Disabled,
155    PendingDeletion,
156}
157
158/// Tenant metadata for monitoring and analytics
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct TenantMetadata {
161    pub total_events: u64,
162    pub total_aggregates: u64,
163    pub storage_used_mb: f64,
164    pub last_activity: Option<DateTime<Utc>>,
165    pub performance_metrics: PerformanceMetrics,
166    pub custom_metadata: HashMap<String, String>,
167}
168
169impl Default for TenantMetadata {
170    fn default() -> Self {
171        Self {
172            total_events: 0,
173            total_aggregates: 0,
174            storage_used_mb: 0.0,
175            last_activity: None,
176            performance_metrics: PerformanceMetrics::default(),
177            custom_metadata: HashMap::new(),
178        }
179    }
180}
181
182/// Performance metrics for tenant monitoring
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct PerformanceMetrics {
185    pub average_response_time_ms: f64,
186    pub events_per_second: f64,
187    pub error_rate: f64,
188    pub uptime_percentage: f64,
189}
190
191impl Default for PerformanceMetrics {
192    fn default() -> Self {
193        Self {
194            average_response_time_ms: 0.0,
195            events_per_second: 0.0,
196            error_rate: 0.0,
197            uptime_percentage: 100.0,
198        }
199    }
200}
201
202/// Tenant-related errors
203#[derive(Debug, thiserror::Error)]
204pub enum TenantError {
205    #[error("Invalid tenant ID: {0}")]
206    InvalidTenantId(String),
207    
208    #[error("Tenant not found: {0}")]
209    TenantNotFound(TenantId),
210    
211    #[error("Tenant already exists: {0}")]
212    TenantAlreadyExists(TenantId),
213    
214    #[error("Tenant is not active: {0}")]
215    TenantNotActive(TenantId),
216    
217    #[error("Resource limit exceeded for tenant {tenant_id}: {limit_type}")]
218    ResourceLimitExceeded {
219        tenant_id: TenantId,
220        limit_type: String,
221    },
222    
223    #[error("Tenant isolation violation: {0}")]
224    IsolationViolation(String),
225    
226    #[error("Database error: {0}")]
227    DatabaseError(String),
228}
229
230impl From<TenantError> for crate::error::EventualiError {
231    fn from(err: TenantError) -> Self {
232        crate::error::EventualiError::Tenant(err.to_string())
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn test_tenant_id_validation() {
242        // Valid IDs
243        assert!(TenantId::new("tenant1".to_string()).is_ok());
244        assert!(TenantId::new("tenant-123".to_string()).is_ok());
245        assert!(TenantId::new("tenant_456".to_string()).is_ok());
246        
247        // Invalid IDs
248        assert!(TenantId::new("".to_string()).is_err());
249        assert!(TenantId::new("tenant@123".to_string()).is_err());
250        assert!(TenantId::new("a".repeat(129)).is_err());
251    }
252    
253    #[test]
254    fn test_tenant_info_creation() {
255        let tenant_id = TenantId::new("test-tenant".to_string()).unwrap();
256        let tenant_info = TenantInfo::new(tenant_id.clone(), "Test Tenant".to_string());
257        
258        assert_eq!(tenant_info.id, tenant_id);
259        assert_eq!(tenant_info.name, "Test Tenant");
260        assert!(tenant_info.is_active());
261    }
262}