use crate::error::Result;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TenantId(String);
impl TenantId {
pub fn new(value: String) -> Result<Self> {
Self::validate(&value)?;
Ok(Self(value))
}
pub(crate) fn new_unchecked(value: String) -> Self {
Self(value)
}
pub fn default_tenant() -> Self {
Self("default".to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
fn validate(value: &str) -> Result<()> {
if value.is_empty() {
return Err(crate::error::AllSourceError::InvalidInput(
"Tenant ID cannot be empty".to_string(),
));
}
if value.len() > 64 {
return Err(crate::error::AllSourceError::InvalidInput(format!(
"Tenant ID cannot exceed 64 characters, got {}",
value.len()
)));
}
if !value
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return Err(crate::error::AllSourceError::InvalidInput(format!(
"Tenant ID '{value}' contains invalid characters. Only alphanumeric, hyphens, and underscores allowed"
)));
}
Ok(())
}
}
impl fmt::Display for TenantId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl TryFrom<&str> for TenantId {
type Error = crate::error::AllSourceError;
fn try_from(value: &str) -> Result<Self> {
TenantId::new(value.to_string())
}
}
impl TryFrom<String> for TenantId {
type Error = crate::error::AllSourceError;
fn try_from(value: String) -> Result<Self> {
TenantId::new(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_valid_tenant_id() {
let tenant_id = TenantId::new("acme-corp".to_string());
assert!(tenant_id.is_ok());
assert_eq!(tenant_id.unwrap().as_str(), "acme-corp");
let tenant_id = TenantId::new("tenant_123".to_string());
assert!(tenant_id.is_ok());
let tenant_id = TenantId::new("TenantABC".to_string());
assert!(tenant_id.is_ok());
let tenant_id = TenantId::new("tenant123".to_string());
assert!(tenant_id.is_ok());
}
#[test]
fn test_reject_empty_tenant_id() {
let result = TenantId::new(String::new());
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("cannot be empty"));
}
}
#[test]
fn test_reject_too_long_tenant_id() {
let long_id = "a".repeat(65);
let result = TenantId::new(long_id);
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("cannot exceed 64 characters"));
}
}
#[test]
fn test_accept_max_length_tenant_id() {
let max_id = "a".repeat(64);
let result = TenantId::new(max_id);
assert!(result.is_ok());
}
#[test]
fn test_reject_invalid_characters() {
let result = TenantId::new("tenant 123".to_string());
assert!(result.is_err());
let result = TenantId::new("tenant@123".to_string());
assert!(result.is_err());
let result = TenantId::new("tenant.123".to_string());
assert!(result.is_err());
let result = TenantId::new("tenant/123".to_string());
assert!(result.is_err());
}
#[test]
fn test_default_tenant() {
let tenant_id = TenantId::default_tenant();
assert_eq!(tenant_id.as_str(), "default");
}
#[test]
fn test_display_trait() {
let tenant_id = TenantId::new("test-tenant".to_string()).unwrap();
assert_eq!(format!("{tenant_id}"), "test-tenant");
}
#[test]
fn test_try_from_str() {
let tenant_id: Result<TenantId> = "valid-tenant".try_into();
assert!(tenant_id.is_ok());
assert_eq!(tenant_id.unwrap().as_str(), "valid-tenant");
let invalid: Result<TenantId> = "".try_into();
assert!(invalid.is_err());
}
#[test]
fn test_try_from_string() {
let tenant_id: Result<TenantId> = "valid-tenant".to_string().try_into();
assert!(tenant_id.is_ok());
let invalid: Result<TenantId> = String::new().try_into();
assert!(invalid.is_err());
}
#[test]
fn test_into_inner() {
let tenant_id = TenantId::new("test".to_string()).unwrap();
let inner = tenant_id.into_inner();
assert_eq!(inner, "test");
}
#[test]
fn test_equality() {
let tenant1 = TenantId::new("tenant-a".to_string()).unwrap();
let tenant2 = TenantId::new("tenant-a".to_string()).unwrap();
let tenant3 = TenantId::new("tenant-b".to_string()).unwrap();
assert_eq!(tenant1, tenant2);
assert_ne!(tenant1, tenant3);
}
#[test]
fn test_cloning() {
let tenant1 = TenantId::new("tenant".to_string()).unwrap();
let tenant2 = tenant1.clone();
assert_eq!(tenant1, tenant2);
}
#[test]
fn test_hash_consistency() {
use std::collections::HashSet;
let tenant1 = TenantId::new("tenant".to_string()).unwrap();
let tenant2 = TenantId::new("tenant".to_string()).unwrap();
let mut set = HashSet::new();
set.insert(tenant1);
assert!(set.contains(&tenant2));
}
#[test]
fn test_serde_serialization() {
let tenant_id = TenantId::new("test-tenant".to_string()).unwrap();
let json = serde_json::to_string(&tenant_id).unwrap();
assert_eq!(json, "\"test-tenant\"");
let deserialized: TenantId = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, tenant_id);
}
#[test]
fn test_new_unchecked() {
let tenant_id = TenantId::new_unchecked("invalid chars!@#".to_string());
assert_eq!(tenant_id.as_str(), "invalid chars!@#");
}
}