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};
11pub use mockforge_foundation::multi_tenant_types::{MultiTenantConfig, RoutingStrategy};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::sync::{Arc, RwLock};
15
16#[derive(Debug, Clone)]
18pub struct TenantWorkspace {
19 pub workspace: Workspace,
21 pub route_registry: Arc<RwLock<RouteRegistry>>,
23 pub last_accessed: DateTime<Utc>,
25 pub enabled: bool,
27 pub stats: WorkspaceStats,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct WorkspaceStats {
34 pub total_requests: u64,
36 pub active_routes: usize,
38 pub last_request_at: Option<DateTime<Utc>>,
40 pub created_at: DateTime<Utc>,
42 pub avg_response_time_ms: f64,
44}
45
46impl Default for WorkspaceStats {
47 fn default() -> Self {
48 Self {
49 total_requests: 0,
50 active_routes: 0,
51 last_request_at: None,
52 created_at: Utc::now(),
53 avg_response_time_ms: 0.0,
54 }
55 }
56}
57
58#[derive(Debug, Clone)]
60pub struct MultiTenantWorkspaceRegistry {
61 workspaces: Arc<RwLock<HashMap<EntityId, TenantWorkspace>>>,
63 default_workspace_id: EntityId,
65 config: MultiTenantConfig,
67 global_logger: Arc<CentralizedRequestLogger>,
69}
70
71impl MultiTenantWorkspaceRegistry {
72 pub fn new(config: MultiTenantConfig) -> Self {
74 let default_workspace_id = config.default_workspace.clone();
75
76 Self {
77 workspaces: Arc::new(RwLock::new(HashMap::new())),
78 default_workspace_id,
79 config,
80 global_logger: Arc::new(CentralizedRequestLogger::new(10000)), }
82 }
83
84 pub fn with_default_workspace(workspace_name: String) -> Self {
86 let config = MultiTenantConfig {
87 default_workspace: "default".to_string(),
88 ..Default::default()
89 };
90
91 let mut registry = Self::new(config);
92
93 let default_workspace = Workspace::new(workspace_name);
95 let _ = registry.register_workspace("default".to_string(), default_workspace);
96
97 registry
98 }
99
100 pub fn register_workspace(&mut self, workspace_id: String, workspace: Workspace) -> Result<()> {
102 if let Some(max) = self.config.max_workspaces {
104 let current_count = self
105 .workspaces
106 .read()
107 .map_err(|e| Error::internal(format!("Failed to read workspaces: {}", e)))?
108 .len();
109
110 if current_count >= max {
111 return Err(Error::validation(format!(
112 "Maximum number of workspaces ({}) exceeded",
113 max
114 )));
115 }
116 }
117
118 let tenant_workspace = TenantWorkspace {
119 workspace,
120 route_registry: Arc::new(RwLock::new(RouteRegistry::new())),
121 last_accessed: Utc::now(),
122 enabled: true,
123 stats: WorkspaceStats::default(),
124 };
125
126 self.workspaces
127 .write()
128 .map_err(|e| Error::internal(format!("Failed to write workspaces: {}", e)))?
129 .insert(workspace_id, tenant_workspace);
130
131 Ok(())
132 }
133
134 pub fn get_workspace(&self, workspace_id: &str) -> Result<TenantWorkspace> {
136 let workspaces = self
137 .workspaces
138 .read()
139 .map_err(|e| Error::internal(format!("Failed to read workspaces: {}", e)))?;
140
141 workspaces
142 .get(workspace_id)
143 .cloned()
144 .ok_or_else(|| Error::not_found("Workspace", workspace_id))
145 }
146
147 pub fn get_default_workspace(&self) -> Result<TenantWorkspace> {
149 self.get_workspace(&self.default_workspace_id)
150 }
151
152 pub fn update_workspace(&mut self, workspace_id: &str, workspace: Workspace) -> Result<()> {
154 let mut workspaces = self
155 .workspaces
156 .write()
157 .map_err(|e| Error::internal(format!("Failed to write workspaces: {}", e)))?;
158
159 if let Some(tenant_workspace) = workspaces.get_mut(workspace_id) {
160 tenant_workspace.workspace = workspace;
161 Ok(())
162 } else {
163 Err(Error::not_found("Workspace", workspace_id))
164 }
165 }
166
167 pub fn remove_workspace(&mut self, workspace_id: &str) -> Result<()> {
169 if workspace_id == self.default_workspace_id {
171 return Err(Error::validation("Cannot remove default workspace"));
172 }
173
174 self.workspaces
175 .write()
176 .map_err(|e| Error::internal(format!("Failed to write workspaces: {}", e)))?
177 .remove(workspace_id)
178 .ok_or_else(|| Error::not_found("Workspace", workspace_id))?;
179
180 Ok(())
181 }
182
183 pub fn list_workspaces(&self) -> Result<Vec<(String, TenantWorkspace)>> {
185 let workspaces = self
186 .workspaces
187 .read()
188 .map_err(|e| Error::internal(format!("Failed to read workspaces: {}", e)))?;
189
190 Ok(workspaces.iter().map(|(id, ws)| (id.clone(), ws.clone())).collect())
191 }
192
193 pub fn resolve_workspace(&self, workspace_id: Option<&str>) -> Result<TenantWorkspace> {
195 if let Some(id) = workspace_id {
196 self.get_workspace(id)
197 } else {
198 self.get_default_workspace()
199 }
200 }
201
202 pub fn touch_workspace(&mut self, workspace_id: &str) -> Result<()> {
204 let mut workspaces = self
205 .workspaces
206 .write()
207 .map_err(|e| Error::internal(format!("Failed to write workspaces: {}", e)))?;
208
209 if let Some(tenant_workspace) = workspaces.get_mut(workspace_id) {
210 tenant_workspace.last_accessed = Utc::now();
211 Ok(())
212 } else {
213 Err(Error::not_found("Workspace", workspace_id))
214 }
215 }
216
217 pub fn update_workspace_stats(
219 &mut self,
220 workspace_id: &str,
221 response_time_ms: f64,
222 ) -> Result<()> {
223 let mut workspaces = self
224 .workspaces
225 .write()
226 .map_err(|e| Error::internal(format!("Failed to write workspaces: {}", e)))?;
227
228 if let Some(tenant_workspace) = workspaces.get_mut(workspace_id) {
229 tenant_workspace.stats.total_requests += 1;
230 tenant_workspace.stats.last_request_at = Some(Utc::now());
231
232 let n = tenant_workspace.stats.total_requests as f64;
234 tenant_workspace.stats.avg_response_time_ms =
235 ((tenant_workspace.stats.avg_response_time_ms * (n - 1.0)) + response_time_ms) / n;
236
237 Ok(())
238 } else {
239 Err(Error::not_found("Workspace", workspace_id))
240 }
241 }
242
243 pub fn workspace_count(&self) -> Result<usize> {
245 let workspaces = self
246 .workspaces
247 .read()
248 .map_err(|e| Error::internal(format!("Failed to read workspaces: {}", e)))?;
249
250 Ok(workspaces.len())
251 }
252
253 pub fn workspace_exists(&self, workspace_id: &str) -> bool {
255 self.workspaces.read().map(|ws| ws.contains_key(workspace_id)).unwrap_or(false)
256 }
257
258 pub fn set_workspace_enabled(&mut self, workspace_id: &str, enabled: bool) -> Result<()> {
260 let mut workspaces = self
261 .workspaces
262 .write()
263 .map_err(|e| Error::internal(format!("Failed to write workspaces: {}", e)))?;
264
265 if let Some(tenant_workspace) = workspaces.get_mut(workspace_id) {
266 tenant_workspace.enabled = enabled;
267 Ok(())
268 } else {
269 Err(Error::not_found("Workspace", workspace_id))
270 }
271 }
272
273 pub fn global_logger(&self) -> &Arc<CentralizedRequestLogger> {
275 &self.global_logger
276 }
277
278 pub fn config(&self) -> &MultiTenantConfig {
280 &self.config
281 }
282
283 pub fn extract_workspace_id_from_path(&self, path: &str) -> Option<String> {
285 if !self.config.enabled {
286 return None;
287 }
288
289 let prefix = &self.config.workspace_prefix;
290
291 if !path.starts_with(prefix) {
293 return None;
294 }
295
296 let remaining = &path[prefix.len()..];
298
299 let remaining = remaining.strip_prefix('/').unwrap_or(remaining);
301
302 remaining.split('/').next().filter(|id| !id.is_empty()).map(|id| id.to_string())
304 }
305
306 pub fn strip_workspace_prefix(&self, path: &str, workspace_id: &str) -> String {
308 if !self.config.enabled {
309 return path.to_string();
310 }
311
312 let prefix = format!("{}/{}", self.config.workspace_prefix, workspace_id);
313
314 if path.starts_with(&prefix) {
315 let remaining = &path[prefix.len()..];
316 if remaining.is_empty() {
317 "/".to_string()
318 } else {
319 remaining.to_string()
320 }
321 } else {
322 path.to_string()
323 }
324 }
325}
326
327impl TenantWorkspace {
328 pub fn new(workspace: Workspace) -> Self {
330 Self {
331 workspace,
332 route_registry: Arc::new(RwLock::new(RouteRegistry::new())),
333 last_accessed: Utc::now(),
334 enabled: true,
335 stats: WorkspaceStats::default(),
336 }
337 }
338
339 pub fn id(&self) -> &str {
341 &self.workspace.id
342 }
343
344 pub fn name(&self) -> &str {
346 &self.workspace.name
347 }
348
349 pub fn route_registry(&self) -> &Arc<RwLock<RouteRegistry>> {
351 &self.route_registry
352 }
353
354 pub fn stats(&self) -> &WorkspaceStats {
356 &self.stats
357 }
358
359 pub fn rebuild_routes(&mut self) -> Result<()> {
361 let routes = self.workspace.get_routes();
362
363 let mut registry = self
364 .route_registry
365 .write()
366 .map_err(|e| Error::internal(format!("Failed to write route registry: {}", e)))?;
367
368 *registry = RouteRegistry::new();
370
371 for route in routes {
373 registry.add_http_route(route)?;
374 }
375
376 self.stats.active_routes = self.workspace.requests.len()
378 + self.workspace.folders.iter().map(Self::count_folder_requests).sum::<usize>();
379
380 Ok(())
381 }
382
383 fn count_folder_requests(folder: &crate::workspace::Folder) -> usize {
385 folder.requests.len()
386 + folder.folders.iter().map(Self::count_folder_requests).sum::<usize>()
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn test_multi_tenant_config_default() {
396 let config = MultiTenantConfig::default();
397 assert!(!config.enabled);
398 assert_eq!(config.routing_strategy, RoutingStrategy::Path);
399 assert_eq!(config.workspace_prefix, "/workspace");
400 assert_eq!(config.default_workspace, "default");
401 }
402
403 #[test]
404 fn test_multi_tenant_registry_creation() {
405 let config = MultiTenantConfig::default();
406 let registry = MultiTenantWorkspaceRegistry::new(config);
407 assert_eq!(registry.workspace_count().unwrap(), 0);
408 }
409
410 #[test]
411 fn test_register_workspace() {
412 let config = MultiTenantConfig::default();
413 let mut registry = MultiTenantWorkspaceRegistry::new(config);
414
415 let workspace = Workspace::new("Test Workspace".to_string());
416 registry.register_workspace("test".to_string(), workspace).unwrap();
417
418 assert_eq!(registry.workspace_count().unwrap(), 1);
419 assert!(registry.workspace_exists("test"));
420 }
421
422 #[test]
423 fn test_max_workspaces_limit() {
424 let config = MultiTenantConfig {
425 max_workspaces: Some(2),
426 ..Default::default()
427 };
428
429 let mut registry = MultiTenantWorkspaceRegistry::new(config);
430
431 registry
433 .register_workspace("ws1".to_string(), Workspace::new("WS1".to_string()))
434 .unwrap();
435
436 registry
438 .register_workspace("ws2".to_string(), Workspace::new("WS2".to_string()))
439 .unwrap();
440
441 let result =
443 registry.register_workspace("ws3".to_string(), Workspace::new("WS3".to_string()));
444 assert!(result.is_err());
445 }
446
447 #[test]
448 fn test_extract_workspace_id_from_path() {
449 let config = MultiTenantConfig {
450 enabled: true,
451 ..Default::default()
452 };
453
454 let registry = MultiTenantWorkspaceRegistry::new(config);
455
456 let workspace_id =
458 registry.extract_workspace_id_from_path("/workspace/project-a/api/users");
459 assert_eq!(workspace_id, Some("project-a".to_string()));
460
461 let workspace_id = registry.extract_workspace_id_from_path("/api/users");
463 assert_eq!(workspace_id, None);
464
465 let workspace_id = registry.extract_workspace_id_from_path("/workspace/test");
467 assert_eq!(workspace_id, Some("test".to_string()));
468 }
469
470 #[test]
471 fn test_strip_workspace_prefix() {
472 let config = MultiTenantConfig {
473 enabled: true,
474 ..Default::default()
475 };
476
477 let registry = MultiTenantWorkspaceRegistry::new(config);
478
479 let stripped =
481 registry.strip_workspace_prefix("/workspace/project-a/api/users", "project-a");
482 assert_eq!(stripped, "/api/users");
483
484 let stripped = registry.strip_workspace_prefix("/api/users", "project-a");
486 assert_eq!(stripped, "/api/users");
487
488 let stripped = registry.strip_workspace_prefix("/workspace/project-a", "project-a");
490 assert_eq!(stripped, "/");
491 }
492
493 #[test]
494 fn test_workspace_stats_update() {
495 let config = MultiTenantConfig::default();
496 let mut registry = MultiTenantWorkspaceRegistry::new(config);
497
498 let workspace = Workspace::new("Test Workspace".to_string());
499 registry.register_workspace("test".to_string(), workspace).unwrap();
500
501 registry.update_workspace_stats("test", 100.0).unwrap();
503
504 let tenant_ws = registry.get_workspace("test").unwrap();
505 assert_eq!(tenant_ws.stats.total_requests, 1);
506 assert_eq!(tenant_ws.stats.avg_response_time_ms, 100.0);
507
508 registry.update_workspace_stats("test", 200.0).unwrap();
510
511 let tenant_ws = registry.get_workspace("test").unwrap();
512 assert_eq!(tenant_ws.stats.total_requests, 2);
513 assert_eq!(tenant_ws.stats.avg_response_time_ms, 150.0);
514 }
515
516 #[test]
517 fn test_cannot_remove_default_workspace() {
518 let config = MultiTenantConfig {
519 default_workspace: "default".to_string(),
520 ..Default::default()
521 };
522
523 let mut registry = MultiTenantWorkspaceRegistry::new(config);
524
525 registry
526 .register_workspace("default".to_string(), Workspace::new("Default".to_string()))
527 .unwrap();
528
529 let result = registry.remove_workspace("default");
531 assert!(result.is_err());
532 }
533}