mockforge_core/multi_tenant/
registry.rs1use crate::request_logger::CentralizedRequestLogger;
7use crate::routing::RouteRegistry;
8use crate::workspace::{EntityId, Workspace};
9use crate::{Error, Result};
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::sync::{Arc, RwLock};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct MultiTenantConfig {
18 pub enabled: bool,
20 pub routing_strategy: RoutingStrategy,
22 pub workspace_prefix: String,
24 pub default_workspace: String,
26 pub max_workspaces: Option<usize>,
28 #[serde(default)]
30 pub workspace_ports: HashMap<String, u16>,
31 pub auto_discover: bool,
33 pub config_directory: Option<String>,
35}
36
37impl Default for MultiTenantConfig {
38 fn default() -> Self {
39 Self {
40 enabled: false,
41 routing_strategy: RoutingStrategy::Path,
42 workspace_prefix: "/workspace".to_string(),
43 default_workspace: "default".to_string(),
44 max_workspaces: None,
45 workspace_ports: HashMap::new(),
46 auto_discover: false,
47 config_directory: None,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
54#[serde(rename_all = "lowercase")]
55pub enum RoutingStrategy {
56 Path,
58 Port,
60 Both,
62}
63
64#[derive(Debug, Clone)]
66pub struct TenantWorkspace {
67 pub workspace: Workspace,
69 pub route_registry: Arc<RwLock<RouteRegistry>>,
71 pub last_accessed: DateTime<Utc>,
73 pub enabled: bool,
75 pub stats: WorkspaceStats,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct WorkspaceStats {
82 pub total_requests: u64,
84 pub active_routes: usize,
86 pub last_request_at: Option<DateTime<Utc>>,
88 pub created_at: DateTime<Utc>,
90 pub avg_response_time_ms: f64,
92}
93
94impl Default for WorkspaceStats {
95 fn default() -> Self {
96 Self {
97 total_requests: 0,
98 active_routes: 0,
99 last_request_at: None,
100 created_at: Utc::now(),
101 avg_response_time_ms: 0.0,
102 }
103 }
104}
105
106#[derive(Debug, Clone)]
108pub struct MultiTenantWorkspaceRegistry {
109 workspaces: Arc<RwLock<HashMap<EntityId, TenantWorkspace>>>,
111 default_workspace_id: EntityId,
113 config: MultiTenantConfig,
115 global_logger: Arc<CentralizedRequestLogger>,
117}
118
119impl MultiTenantWorkspaceRegistry {
120 pub fn new(config: MultiTenantConfig) -> Self {
122 let default_workspace_id = config.default_workspace.clone();
123
124 Self {
125 workspaces: Arc::new(RwLock::new(HashMap::new())),
126 default_workspace_id,
127 config,
128 global_logger: Arc::new(CentralizedRequestLogger::new(10000)), }
130 }
131
132 pub fn with_default_workspace(workspace_name: String) -> Self {
134 let config = MultiTenantConfig {
135 default_workspace: "default".to_string(),
136 ..Default::default()
137 };
138
139 let mut registry = Self::new(config);
140
141 let default_workspace = Workspace::new(workspace_name);
143 let _ = registry.register_workspace("default".to_string(), default_workspace);
144
145 registry
146 }
147
148 pub fn register_workspace(&mut self, workspace_id: String, workspace: Workspace) -> Result<()> {
150 if let Some(max) = self.config.max_workspaces {
152 let current_count = self
153 .workspaces
154 .read()
155 .map_err(|e| Error::generic(format!("Failed to read workspaces: {}", e)))?
156 .len();
157
158 if current_count >= max {
159 return Err(Error::generic(format!(
160 "Maximum number of workspaces ({}) exceeded",
161 max
162 )));
163 }
164 }
165
166 let tenant_workspace = TenantWorkspace {
167 workspace,
168 route_registry: Arc::new(RwLock::new(RouteRegistry::new())),
169 last_accessed: Utc::now(),
170 enabled: true,
171 stats: WorkspaceStats::default(),
172 };
173
174 self.workspaces
175 .write()
176 .map_err(|e| Error::generic(format!("Failed to write workspaces: {}", e)))?
177 .insert(workspace_id, tenant_workspace);
178
179 Ok(())
180 }
181
182 pub fn get_workspace(&self, workspace_id: &str) -> Result<TenantWorkspace> {
184 let workspaces = self
185 .workspaces
186 .read()
187 .map_err(|e| Error::generic(format!("Failed to read workspaces: {}", e)))?;
188
189 workspaces
190 .get(workspace_id)
191 .cloned()
192 .ok_or_else(|| Error::generic(format!("Workspace '{}' not found", workspace_id)))
193 }
194
195 pub fn get_default_workspace(&self) -> Result<TenantWorkspace> {
197 self.get_workspace(&self.default_workspace_id)
198 }
199
200 pub fn update_workspace(&mut self, workspace_id: &str, workspace: Workspace) -> Result<()> {
202 let mut workspaces = self
203 .workspaces
204 .write()
205 .map_err(|e| Error::generic(format!("Failed to write workspaces: {}", e)))?;
206
207 if let Some(tenant_workspace) = workspaces.get_mut(workspace_id) {
208 tenant_workspace.workspace = workspace;
209 Ok(())
210 } else {
211 Err(Error::generic(format!("Workspace '{}' not found", workspace_id)))
212 }
213 }
214
215 pub fn remove_workspace(&mut self, workspace_id: &str) -> Result<()> {
217 if workspace_id == self.default_workspace_id {
219 return Err(Error::generic("Cannot remove default workspace".to_string()));
220 }
221
222 self.workspaces
223 .write()
224 .map_err(|e| Error::generic(format!("Failed to write workspaces: {}", e)))?
225 .remove(workspace_id)
226 .ok_or_else(|| Error::generic(format!("Workspace '{}' not found", workspace_id)))?;
227
228 Ok(())
229 }
230
231 pub fn list_workspaces(&self) -> Result<Vec<(String, TenantWorkspace)>> {
233 let workspaces = self
234 .workspaces
235 .read()
236 .map_err(|e| Error::generic(format!("Failed to read workspaces: {}", e)))?;
237
238 Ok(workspaces.iter().map(|(id, ws)| (id.clone(), ws.clone())).collect())
239 }
240
241 pub fn resolve_workspace(&self, workspace_id: Option<&str>) -> Result<TenantWorkspace> {
243 if let Some(id) = workspace_id {
244 self.get_workspace(id)
245 } else {
246 self.get_default_workspace()
247 }
248 }
249
250 pub fn touch_workspace(&mut self, workspace_id: &str) -> Result<()> {
252 let mut workspaces = self
253 .workspaces
254 .write()
255 .map_err(|e| Error::generic(format!("Failed to write workspaces: {}", e)))?;
256
257 if let Some(tenant_workspace) = workspaces.get_mut(workspace_id) {
258 tenant_workspace.last_accessed = Utc::now();
259 Ok(())
260 } else {
261 Err(Error::generic(format!("Workspace '{}' not found", workspace_id)))
262 }
263 }
264
265 pub fn update_workspace_stats(
267 &mut self,
268 workspace_id: &str,
269 response_time_ms: f64,
270 ) -> Result<()> {
271 let mut workspaces = self
272 .workspaces
273 .write()
274 .map_err(|e| Error::generic(format!("Failed to write workspaces: {}", e)))?;
275
276 if let Some(tenant_workspace) = workspaces.get_mut(workspace_id) {
277 tenant_workspace.stats.total_requests += 1;
278 tenant_workspace.stats.last_request_at = Some(Utc::now());
279
280 let n = tenant_workspace.stats.total_requests as f64;
282 tenant_workspace.stats.avg_response_time_ms =
283 ((tenant_workspace.stats.avg_response_time_ms * (n - 1.0)) + response_time_ms) / n;
284
285 Ok(())
286 } else {
287 Err(Error::generic(format!("Workspace '{}' not found", workspace_id)))
288 }
289 }
290
291 pub fn workspace_count(&self) -> Result<usize> {
293 let workspaces = self
294 .workspaces
295 .read()
296 .map_err(|e| Error::generic(format!("Failed to read workspaces: {}", e)))?;
297
298 Ok(workspaces.len())
299 }
300
301 pub fn workspace_exists(&self, workspace_id: &str) -> bool {
303 self.workspaces.read().map(|ws| ws.contains_key(workspace_id)).unwrap_or(false)
304 }
305
306 pub fn set_workspace_enabled(&mut self, workspace_id: &str, enabled: bool) -> Result<()> {
308 let mut workspaces = self
309 .workspaces
310 .write()
311 .map_err(|e| Error::generic(format!("Failed to write workspaces: {}", e)))?;
312
313 if let Some(tenant_workspace) = workspaces.get_mut(workspace_id) {
314 tenant_workspace.enabled = enabled;
315 Ok(())
316 } else {
317 Err(Error::generic(format!("Workspace '{}' not found", workspace_id)))
318 }
319 }
320
321 pub fn global_logger(&self) -> &Arc<CentralizedRequestLogger> {
323 &self.global_logger
324 }
325
326 pub fn config(&self) -> &MultiTenantConfig {
328 &self.config
329 }
330
331 pub fn extract_workspace_id_from_path(&self, path: &str) -> Option<String> {
333 if !self.config.enabled {
334 return None;
335 }
336
337 let prefix = &self.config.workspace_prefix;
338
339 if !path.starts_with(prefix) {
341 return None;
342 }
343
344 let remaining = &path[prefix.len()..];
346
347 let remaining = remaining.strip_prefix('/').unwrap_or(remaining);
349
350 remaining.split('/').next().filter(|id| !id.is_empty()).map(|id| id.to_string())
352 }
353
354 pub fn strip_workspace_prefix(&self, path: &str, workspace_id: &str) -> String {
356 if !self.config.enabled {
357 return path.to_string();
358 }
359
360 let prefix = format!("{}/{}", self.config.workspace_prefix, workspace_id);
361
362 if path.starts_with(&prefix) {
363 let remaining = &path[prefix.len()..];
364 if remaining.is_empty() {
365 "/".to_string()
366 } else {
367 remaining.to_string()
368 }
369 } else {
370 path.to_string()
371 }
372 }
373}
374
375impl TenantWorkspace {
376 pub fn new(workspace: Workspace) -> Self {
378 Self {
379 workspace,
380 route_registry: Arc::new(RwLock::new(RouteRegistry::new())),
381 last_accessed: Utc::now(),
382 enabled: true,
383 stats: WorkspaceStats::default(),
384 }
385 }
386
387 pub fn id(&self) -> &str {
389 &self.workspace.id
390 }
391
392 pub fn name(&self) -> &str {
394 &self.workspace.name
395 }
396
397 pub fn route_registry(&self) -> &Arc<RwLock<RouteRegistry>> {
399 &self.route_registry
400 }
401
402 pub fn stats(&self) -> &WorkspaceStats {
404 &self.stats
405 }
406
407 pub fn rebuild_routes(&mut self) -> Result<()> {
409 let routes = self.workspace.get_routes();
410
411 let mut registry = self
412 .route_registry
413 .write()
414 .map_err(|e| Error::generic(format!("Failed to write route registry: {}", e)))?;
415
416 *registry = RouteRegistry::new();
418
419 for route in routes {
421 registry.add_http_route(route)?;
422 }
423
424 self.stats.active_routes = self.workspace.requests.len()
426 + self.workspace.folders.iter().map(Self::count_folder_requests).sum::<usize>();
427
428 Ok(())
429 }
430
431 fn count_folder_requests(folder: &crate::workspace::Folder) -> usize {
433 folder.requests.len()
434 + folder.folders.iter().map(Self::count_folder_requests).sum::<usize>()
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441
442 #[test]
443 fn test_multi_tenant_config_default() {
444 let config = MultiTenantConfig::default();
445 assert!(!config.enabled);
446 assert_eq!(config.routing_strategy, RoutingStrategy::Path);
447 assert_eq!(config.workspace_prefix, "/workspace");
448 assert_eq!(config.default_workspace, "default");
449 }
450
451 #[test]
452 fn test_multi_tenant_registry_creation() {
453 let config = MultiTenantConfig::default();
454 let registry = MultiTenantWorkspaceRegistry::new(config);
455 assert_eq!(registry.workspace_count().unwrap(), 0);
456 }
457
458 #[test]
459 fn test_register_workspace() {
460 let config = MultiTenantConfig::default();
461 let mut registry = MultiTenantWorkspaceRegistry::new(config);
462
463 let workspace = Workspace::new("Test Workspace".to_string());
464 registry.register_workspace("test".to_string(), workspace).unwrap();
465
466 assert_eq!(registry.workspace_count().unwrap(), 1);
467 assert!(registry.workspace_exists("test"));
468 }
469
470 #[test]
471 fn test_max_workspaces_limit() {
472 let config = MultiTenantConfig {
473 max_workspaces: Some(2),
474 ..Default::default()
475 };
476
477 let mut registry = MultiTenantWorkspaceRegistry::new(config);
478
479 registry
481 .register_workspace("ws1".to_string(), Workspace::new("WS1".to_string()))
482 .unwrap();
483
484 registry
486 .register_workspace("ws2".to_string(), Workspace::new("WS2".to_string()))
487 .unwrap();
488
489 let result =
491 registry.register_workspace("ws3".to_string(), Workspace::new("WS3".to_string()));
492 assert!(result.is_err());
493 }
494
495 #[test]
496 fn test_extract_workspace_id_from_path() {
497 let config = MultiTenantConfig {
498 enabled: true,
499 ..Default::default()
500 };
501
502 let registry = MultiTenantWorkspaceRegistry::new(config);
503
504 let workspace_id =
506 registry.extract_workspace_id_from_path("/workspace/project-a/api/users");
507 assert_eq!(workspace_id, Some("project-a".to_string()));
508
509 let workspace_id = registry.extract_workspace_id_from_path("/api/users");
511 assert_eq!(workspace_id, None);
512
513 let workspace_id = registry.extract_workspace_id_from_path("/workspace/test");
515 assert_eq!(workspace_id, Some("test".to_string()));
516 }
517
518 #[test]
519 fn test_strip_workspace_prefix() {
520 let config = MultiTenantConfig {
521 enabled: true,
522 ..Default::default()
523 };
524
525 let registry = MultiTenantWorkspaceRegistry::new(config);
526
527 let stripped =
529 registry.strip_workspace_prefix("/workspace/project-a/api/users", "project-a");
530 assert_eq!(stripped, "/api/users");
531
532 let stripped = registry.strip_workspace_prefix("/api/users", "project-a");
534 assert_eq!(stripped, "/api/users");
535
536 let stripped = registry.strip_workspace_prefix("/workspace/project-a", "project-a");
538 assert_eq!(stripped, "/");
539 }
540
541 #[test]
542 fn test_workspace_stats_update() {
543 let config = MultiTenantConfig::default();
544 let mut registry = MultiTenantWorkspaceRegistry::new(config);
545
546 let workspace = Workspace::new("Test Workspace".to_string());
547 registry.register_workspace("test".to_string(), workspace).unwrap();
548
549 registry.update_workspace_stats("test", 100.0).unwrap();
551
552 let tenant_ws = registry.get_workspace("test").unwrap();
553 assert_eq!(tenant_ws.stats.total_requests, 1);
554 assert_eq!(tenant_ws.stats.avg_response_time_ms, 100.0);
555
556 registry.update_workspace_stats("test", 200.0).unwrap();
558
559 let tenant_ws = registry.get_workspace("test").unwrap();
560 assert_eq!(tenant_ws.stats.total_requests, 2);
561 assert_eq!(tenant_ws.stats.avg_response_time_ms, 150.0);
562 }
563
564 #[test]
565 fn test_cannot_remove_default_workspace() {
566 let config = MultiTenantConfig {
567 default_workspace: "default".to_string(),
568 ..Default::default()
569 };
570
571 let mut registry = MultiTenantWorkspaceRegistry::new(config);
572
573 registry
574 .register_workspace("default".to_string(), Workspace::new("Default".to_string()))
575 .unwrap();
576
577 let result = registry.remove_workspace("default");
579 assert!(result.is_err());
580 }
581}