use crate::request_logger::CentralizedRequestLogger;
use crate::routing::RouteRegistry;
use crate::workspace::{EntityId, Workspace};
use crate::{Error, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct MultiTenantConfig {
pub enabled: bool,
pub routing_strategy: RoutingStrategy,
pub workspace_prefix: String,
pub default_workspace: String,
pub max_workspaces: Option<usize>,
#[serde(default)]
pub workspace_ports: HashMap<String, u16>,
pub auto_discover: bool,
pub config_directory: Option<String>,
}
impl Default for MultiTenantConfig {
fn default() -> Self {
Self {
enabled: false,
routing_strategy: RoutingStrategy::Path,
workspace_prefix: "/workspace".to_string(),
default_workspace: "default".to_string(),
max_workspaces: None,
workspace_ports: HashMap::new(),
auto_discover: false,
config_directory: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "lowercase")]
pub enum RoutingStrategy {
Path,
Port,
Both,
}
#[derive(Debug, Clone)]
pub struct TenantWorkspace {
pub workspace: Workspace,
pub route_registry: Arc<RwLock<RouteRegistry>>,
pub last_accessed: DateTime<Utc>,
pub enabled: bool,
pub stats: WorkspaceStats,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceStats {
pub total_requests: u64,
pub active_routes: usize,
pub last_request_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub avg_response_time_ms: f64,
}
impl Default for WorkspaceStats {
fn default() -> Self {
Self {
total_requests: 0,
active_routes: 0,
last_request_at: None,
created_at: Utc::now(),
avg_response_time_ms: 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct MultiTenantWorkspaceRegistry {
workspaces: Arc<RwLock<HashMap<EntityId, TenantWorkspace>>>,
default_workspace_id: EntityId,
config: MultiTenantConfig,
global_logger: Arc<CentralizedRequestLogger>,
}
impl MultiTenantWorkspaceRegistry {
pub fn new(config: MultiTenantConfig) -> Self {
let default_workspace_id = config.default_workspace.clone();
Self {
workspaces: Arc::new(RwLock::new(HashMap::new())),
default_workspace_id,
config,
global_logger: Arc::new(CentralizedRequestLogger::new(10000)), }
}
pub fn with_default_workspace(workspace_name: String) -> Self {
let config = MultiTenantConfig {
default_workspace: "default".to_string(),
..Default::default()
};
let mut registry = Self::new(config);
let default_workspace = Workspace::new(workspace_name);
let _ = registry.register_workspace("default".to_string(), default_workspace);
registry
}
pub fn register_workspace(&mut self, workspace_id: String, workspace: Workspace) -> Result<()> {
if let Some(max) = self.config.max_workspaces {
let current_count = self
.workspaces
.read()
.map_err(|e| Error::internal(format!("Failed to read workspaces: {}", e)))?
.len();
if current_count >= max {
return Err(Error::validation(format!(
"Maximum number of workspaces ({}) exceeded",
max
)));
}
}
let tenant_workspace = TenantWorkspace {
workspace,
route_registry: Arc::new(RwLock::new(RouteRegistry::new())),
last_accessed: Utc::now(),
enabled: true,
stats: WorkspaceStats::default(),
};
self.workspaces
.write()
.map_err(|e| Error::internal(format!("Failed to write workspaces: {}", e)))?
.insert(workspace_id, tenant_workspace);
Ok(())
}
pub fn get_workspace(&self, workspace_id: &str) -> Result<TenantWorkspace> {
let workspaces = self
.workspaces
.read()
.map_err(|e| Error::internal(format!("Failed to read workspaces: {}", e)))?;
workspaces
.get(workspace_id)
.cloned()
.ok_or_else(|| Error::not_found("Workspace", workspace_id))
}
pub fn get_default_workspace(&self) -> Result<TenantWorkspace> {
self.get_workspace(&self.default_workspace_id)
}
pub fn update_workspace(&mut self, workspace_id: &str, workspace: Workspace) -> Result<()> {
let mut workspaces = self
.workspaces
.write()
.map_err(|e| Error::internal(format!("Failed to write workspaces: {}", e)))?;
if let Some(tenant_workspace) = workspaces.get_mut(workspace_id) {
tenant_workspace.workspace = workspace;
Ok(())
} else {
Err(Error::not_found("Workspace", workspace_id))
}
}
pub fn remove_workspace(&mut self, workspace_id: &str) -> Result<()> {
if workspace_id == self.default_workspace_id {
return Err(Error::validation("Cannot remove default workspace"));
}
self.workspaces
.write()
.map_err(|e| Error::internal(format!("Failed to write workspaces: {}", e)))?
.remove(workspace_id)
.ok_or_else(|| Error::not_found("Workspace", workspace_id))?;
Ok(())
}
pub fn list_workspaces(&self) -> Result<Vec<(String, TenantWorkspace)>> {
let workspaces = self
.workspaces
.read()
.map_err(|e| Error::internal(format!("Failed to read workspaces: {}", e)))?;
Ok(workspaces.iter().map(|(id, ws)| (id.clone(), ws.clone())).collect())
}
pub fn resolve_workspace(&self, workspace_id: Option<&str>) -> Result<TenantWorkspace> {
if let Some(id) = workspace_id {
self.get_workspace(id)
} else {
self.get_default_workspace()
}
}
pub fn touch_workspace(&mut self, workspace_id: &str) -> Result<()> {
let mut workspaces = self
.workspaces
.write()
.map_err(|e| Error::internal(format!("Failed to write workspaces: {}", e)))?;
if let Some(tenant_workspace) = workspaces.get_mut(workspace_id) {
tenant_workspace.last_accessed = Utc::now();
Ok(())
} else {
Err(Error::not_found("Workspace", workspace_id))
}
}
pub fn update_workspace_stats(
&mut self,
workspace_id: &str,
response_time_ms: f64,
) -> Result<()> {
let mut workspaces = self
.workspaces
.write()
.map_err(|e| Error::internal(format!("Failed to write workspaces: {}", e)))?;
if let Some(tenant_workspace) = workspaces.get_mut(workspace_id) {
tenant_workspace.stats.total_requests += 1;
tenant_workspace.stats.last_request_at = Some(Utc::now());
let n = tenant_workspace.stats.total_requests as f64;
tenant_workspace.stats.avg_response_time_ms =
((tenant_workspace.stats.avg_response_time_ms * (n - 1.0)) + response_time_ms) / n;
Ok(())
} else {
Err(Error::not_found("Workspace", workspace_id))
}
}
pub fn workspace_count(&self) -> Result<usize> {
let workspaces = self
.workspaces
.read()
.map_err(|e| Error::internal(format!("Failed to read workspaces: {}", e)))?;
Ok(workspaces.len())
}
pub fn workspace_exists(&self, workspace_id: &str) -> bool {
self.workspaces.read().map(|ws| ws.contains_key(workspace_id)).unwrap_or(false)
}
pub fn set_workspace_enabled(&mut self, workspace_id: &str, enabled: bool) -> Result<()> {
let mut workspaces = self
.workspaces
.write()
.map_err(|e| Error::internal(format!("Failed to write workspaces: {}", e)))?;
if let Some(tenant_workspace) = workspaces.get_mut(workspace_id) {
tenant_workspace.enabled = enabled;
Ok(())
} else {
Err(Error::not_found("Workspace", workspace_id))
}
}
pub fn global_logger(&self) -> &Arc<CentralizedRequestLogger> {
&self.global_logger
}
pub fn config(&self) -> &MultiTenantConfig {
&self.config
}
pub fn extract_workspace_id_from_path(&self, path: &str) -> Option<String> {
if !self.config.enabled {
return None;
}
let prefix = &self.config.workspace_prefix;
if !path.starts_with(prefix) {
return None;
}
let remaining = &path[prefix.len()..];
let remaining = remaining.strip_prefix('/').unwrap_or(remaining);
remaining.split('/').next().filter(|id| !id.is_empty()).map(|id| id.to_string())
}
pub fn strip_workspace_prefix(&self, path: &str, workspace_id: &str) -> String {
if !self.config.enabled {
return path.to_string();
}
let prefix = format!("{}/{}", self.config.workspace_prefix, workspace_id);
if path.starts_with(&prefix) {
let remaining = &path[prefix.len()..];
if remaining.is_empty() {
"/".to_string()
} else {
remaining.to_string()
}
} else {
path.to_string()
}
}
}
impl TenantWorkspace {
pub fn new(workspace: Workspace) -> Self {
Self {
workspace,
route_registry: Arc::new(RwLock::new(RouteRegistry::new())),
last_accessed: Utc::now(),
enabled: true,
stats: WorkspaceStats::default(),
}
}
pub fn id(&self) -> &str {
&self.workspace.id
}
pub fn name(&self) -> &str {
&self.workspace.name
}
pub fn route_registry(&self) -> &Arc<RwLock<RouteRegistry>> {
&self.route_registry
}
pub fn stats(&self) -> &WorkspaceStats {
&self.stats
}
pub fn rebuild_routes(&mut self) -> Result<()> {
let routes = self.workspace.get_routes();
let mut registry = self
.route_registry
.write()
.map_err(|e| Error::internal(format!("Failed to write route registry: {}", e)))?;
*registry = RouteRegistry::new();
for route in routes {
registry.add_http_route(route)?;
}
self.stats.active_routes = self.workspace.requests.len()
+ self.workspace.folders.iter().map(Self::count_folder_requests).sum::<usize>();
Ok(())
}
fn count_folder_requests(folder: &crate::workspace::Folder) -> usize {
folder.requests.len()
+ folder.folders.iter().map(Self::count_folder_requests).sum::<usize>()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multi_tenant_config_default() {
let config = MultiTenantConfig::default();
assert!(!config.enabled);
assert_eq!(config.routing_strategy, RoutingStrategy::Path);
assert_eq!(config.workspace_prefix, "/workspace");
assert_eq!(config.default_workspace, "default");
}
#[test]
fn test_multi_tenant_registry_creation() {
let config = MultiTenantConfig::default();
let registry = MultiTenantWorkspaceRegistry::new(config);
assert_eq!(registry.workspace_count().unwrap(), 0);
}
#[test]
fn test_register_workspace() {
let config = MultiTenantConfig::default();
let mut registry = MultiTenantWorkspaceRegistry::new(config);
let workspace = Workspace::new("Test Workspace".to_string());
registry.register_workspace("test".to_string(), workspace).unwrap();
assert_eq!(registry.workspace_count().unwrap(), 1);
assert!(registry.workspace_exists("test"));
}
#[test]
fn test_max_workspaces_limit() {
let config = MultiTenantConfig {
max_workspaces: Some(2),
..Default::default()
};
let mut registry = MultiTenantWorkspaceRegistry::new(config);
registry
.register_workspace("ws1".to_string(), Workspace::new("WS1".to_string()))
.unwrap();
registry
.register_workspace("ws2".to_string(), Workspace::new("WS2".to_string()))
.unwrap();
let result =
registry.register_workspace("ws3".to_string(), Workspace::new("WS3".to_string()));
assert!(result.is_err());
}
#[test]
fn test_extract_workspace_id_from_path() {
let config = MultiTenantConfig {
enabled: true,
..Default::default()
};
let registry = MultiTenantWorkspaceRegistry::new(config);
let workspace_id =
registry.extract_workspace_id_from_path("/workspace/project-a/api/users");
assert_eq!(workspace_id, Some("project-a".to_string()));
let workspace_id = registry.extract_workspace_id_from_path("/api/users");
assert_eq!(workspace_id, None);
let workspace_id = registry.extract_workspace_id_from_path("/workspace/test");
assert_eq!(workspace_id, Some("test".to_string()));
}
#[test]
fn test_strip_workspace_prefix() {
let config = MultiTenantConfig {
enabled: true,
..Default::default()
};
let registry = MultiTenantWorkspaceRegistry::new(config);
let stripped =
registry.strip_workspace_prefix("/workspace/project-a/api/users", "project-a");
assert_eq!(stripped, "/api/users");
let stripped = registry.strip_workspace_prefix("/api/users", "project-a");
assert_eq!(stripped, "/api/users");
let stripped = registry.strip_workspace_prefix("/workspace/project-a", "project-a");
assert_eq!(stripped, "/");
}
#[test]
fn test_workspace_stats_update() {
let config = MultiTenantConfig::default();
let mut registry = MultiTenantWorkspaceRegistry::new(config);
let workspace = Workspace::new("Test Workspace".to_string());
registry.register_workspace("test".to_string(), workspace).unwrap();
registry.update_workspace_stats("test", 100.0).unwrap();
let tenant_ws = registry.get_workspace("test").unwrap();
assert_eq!(tenant_ws.stats.total_requests, 1);
assert_eq!(tenant_ws.stats.avg_response_time_ms, 100.0);
registry.update_workspace_stats("test", 200.0).unwrap();
let tenant_ws = registry.get_workspace("test").unwrap();
assert_eq!(tenant_ws.stats.total_requests, 2);
assert_eq!(tenant_ws.stats.avg_response_time_ms, 150.0);
}
#[test]
fn test_cannot_remove_default_workspace() {
let config = MultiTenantConfig {
default_workspace: "default".to_string(),
..Default::default()
};
let mut registry = MultiTenantWorkspaceRegistry::new(config);
registry
.register_workspace("default".to_string(), Workspace::new("Default".to_string()))
.unwrap();
let result = registry.remove_workspace("default");
assert!(result.is_err());
}
}