1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4use uuid::Uuid;
5use chrono::{DateTime, Utc};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct TenantId(String);
11
12impl TenantId {
13 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 pub fn generate() -> Self {
32 TenantId(Uuid::new_v4().to_string())
33 }
34
35 pub fn as_str(&self) -> &str {
37 &self.0
38 }
39
40 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
84pub enum IsolationLevel {
85 Database,
87 Application,
89 Row,
91}
92
93#[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), max_concurrent_streams: Some(100),
109 max_projections: Some(50),
110 max_aggregates: Some(100_000),
111 }
112 }
113}
114
115#[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 pub fn is_active(&self) -> bool {
145 matches!(self.status, TenantStatus::Active)
146 }
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub enum TenantStatus {
152 Active,
153 Suspended,
154 Disabled,
155 PendingDeletion,
156}
157
158#[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#[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#[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 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 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}