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