use crate::error::ScimError;
use crate::providers::ResourceProvider;
use crate::scim_server::ScimServer;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TenantStrategy {
SingleTenant,
Subdomain,
PathBased,
}
impl Default for TenantStrategy {
fn default() -> Self {
TenantStrategy::SingleTenant
}
}
#[derive(Debug, Clone)]
pub struct ScimServerConfig {
pub base_url: String,
pub tenant_strategy: TenantStrategy,
pub scim_version: String,
}
impl Default for ScimServerConfig {
fn default() -> Self {
Self {
base_url: "https://localhost".to_string(),
tenant_strategy: TenantStrategy::SingleTenant,
scim_version: "v2".to_string(),
}
}
}
impl ScimServerConfig {
pub fn generate_ref_url(
&self,
tenant_id: Option<&str>,
resource_type: &str,
resource_id: &str,
) -> Result<String, ScimError> {
match &self.tenant_strategy {
TenantStrategy::SingleTenant => Ok(format!(
"{}/{}/{}/{}",
self.base_url, self.scim_version, resource_type, resource_id
)),
TenantStrategy::Subdomain => {
let tenant = tenant_id.ok_or_else(|| {
ScimError::invalid_request(
"Tenant ID required for subdomain strategy but not provided",
)
})?;
let url_without_protocol = self
.base_url
.strip_prefix("https://")
.or_else(|| self.base_url.strip_prefix("http://"))
.or_else(|| self.base_url.strip_prefix("mcp://"))
.ok_or_else(|| ScimError::internal("Invalid base URL format"))?;
let protocol = if self.base_url.starts_with("https://") {
"https"
} else if self.base_url.starts_with("http://") {
"http"
} else {
"mcp"
};
Ok(format!(
"{}://{}.{}/{}/{}/{}",
protocol,
tenant,
url_without_protocol,
self.scim_version,
resource_type,
resource_id
))
}
TenantStrategy::PathBased => {
let tenant = tenant_id.ok_or_else(|| {
ScimError::invalid_request(
"Tenant ID required for path-based strategy but not provided",
)
})?;
Ok(format!(
"{}/{}/{}/{}/{}",
self.base_url, tenant, self.scim_version, resource_type, resource_id
))
}
}
}
pub fn validate(&self) -> Result<(), ScimError> {
if self.base_url.is_empty() {
return Err(ScimError::internal("Base URL cannot be empty"));
}
if !self.base_url.starts_with("http://")
&& !self.base_url.starts_with("https://")
&& !self.base_url.starts_with("mcp://")
{
return Err(ScimError::internal(
"Base URL must start with http://, https://, or mcp://",
));
}
if self.scim_version.is_empty() {
return Err(ScimError::internal("SCIM version cannot be empty"));
}
Ok(())
}
}
pub struct ScimServerBuilder<P> {
provider: P,
config: ScimServerConfig,
}
impl<P: ResourceProvider> ScimServerBuilder<P> {
pub fn new(provider: P) -> Self {
Self {
provider,
config: ScimServerConfig::default(),
}
}
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.config.base_url = base_url.into();
self
}
pub fn with_tenant_strategy(mut self, strategy: TenantStrategy) -> Self {
self.config.tenant_strategy = strategy;
self
}
pub fn with_scim_version(mut self, version: impl Into<String>) -> Self {
self.config.scim_version = version.into();
self
}
pub fn build(self) -> Result<ScimServer<P>, ScimError> {
self.config.validate()?;
ScimServer::with_config(self.provider, self.config)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_single_tenant_ref_url_generation() {
let config = ScimServerConfig {
base_url: "https://scim.example.com".to_string(),
tenant_strategy: TenantStrategy::SingleTenant,
scim_version: "v2".to_string(),
};
let url = config.generate_ref_url(None, "Users", "12345").unwrap();
assert_eq!(url, "https://scim.example.com/v2/Users/12345");
}
#[test]
fn test_subdomain_tenant_ref_url_generation() {
let config = ScimServerConfig {
base_url: "https://scim.example.com".to_string(),
tenant_strategy: TenantStrategy::Subdomain,
scim_version: "v2".to_string(),
};
let url = config
.generate_ref_url(Some("acme"), "Groups", "67890")
.unwrap();
assert_eq!(url, "https://acme.scim.example.com/v2/Groups/67890");
}
#[test]
fn test_path_based_tenant_ref_url_generation() {
let config = ScimServerConfig {
base_url: "https://api.company.com".to_string(),
tenant_strategy: TenantStrategy::PathBased,
scim_version: "v2".to_string(),
};
let url = config
.generate_ref_url(Some("tenant1"), "Users", "abc123")
.unwrap();
assert_eq!(url, "https://api.company.com/tenant1/v2/Users/abc123");
}
#[test]
fn test_missing_tenant_error() {
let config = ScimServerConfig {
base_url: "https://scim.example.com".to_string(),
tenant_strategy: TenantStrategy::Subdomain,
scim_version: "v2".to_string(),
};
let result = config.generate_ref_url(None, "Users", "12345");
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Tenant ID required")
);
}
#[test]
fn test_config_validation() {
let mut config = ScimServerConfig::default();
assert!(config.validate().is_ok());
config.base_url = "".to_string();
assert!(config.validate().is_err());
config.base_url = "invalid-url".to_string();
assert!(config.validate().is_err());
config.base_url = "https://valid.com".to_string();
config.scim_version = "".to_string();
assert!(config.validate().is_err());
}
#[test]
fn test_builder_pattern() {
fn _test_builder_compiles() {
use crate::providers::StandardResourceProvider;
use crate::storage::InMemoryStorage;
let storage = InMemoryStorage::new();
let provider = StandardResourceProvider::new(storage);
let _builder = ScimServerBuilder::new(provider)
.with_base_url("https://test.com")
.with_tenant_strategy(TenantStrategy::PathBased)
.with_scim_version("v2.1");
}
}
}