use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct TenantId(String);
impl TenantId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
pub fn validate(&self) -> Result<(), String> {
if self.0.is_empty() {
return Err("Tenant ID cannot be empty".to_string());
}
if self.0.len() > 64 {
return Err("Tenant ID cannot exceed 64 characters".to_string());
}
if !self
.0
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return Err(
"Tenant ID can only contain alphanumeric characters, hyphens, and underscores"
.to_string(),
);
}
Ok(())
}
}
impl fmt::Display for TenantId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for TenantId {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TenantMetadata {
pub name: String,
pub description: Option<String>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub attributes: std::collections::HashMap<String, serde_json::Value>,
}
impl TenantMetadata {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
description: None,
created_at: chrono::Utc::now(),
attributes: std::collections::HashMap::new(),
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_attribute(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.attributes.insert(key.into(), value);
self
}
}
#[derive(Debug, Clone)]
pub struct TenantContext {
pub id: TenantId,
pub metadata: TenantMetadata,
pub active: bool,
}
impl TenantContext {
pub fn new(id: TenantId, metadata: TenantMetadata) -> Result<Self, String> {
id.validate()?;
Ok(Self {
id,
metadata,
active: true,
})
}
pub fn with_name(id: impl Into<String>, name: impl Into<String>) -> Result<Self, String> {
let id = TenantId::new(id);
id.validate()?;
Ok(Self {
id,
metadata: TenantMetadata::new(name),
active: true,
})
}
pub fn deactivate(&mut self) {
self.active = false;
}
pub fn activate(&mut self) {
self.active = true;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tenant_id_validation() {
assert!(TenantId::new("tenant-123").validate().is_ok());
assert!(TenantId::new("tenant_123").validate().is_ok());
assert!(TenantId::new("acme-corp").validate().is_ok());
assert!(TenantId::new("").validate().is_err());
assert!(TenantId::new("tenant@123").validate().is_err());
assert!(TenantId::new("a".repeat(65)).validate().is_err());
}
#[test]
fn test_tenant_context_creation() {
let context = TenantContext::with_name("acme", "ACME Corp").unwrap();
assert_eq!(context.id.as_str(), "acme");
assert_eq!(context.metadata.name, "ACME Corp");
assert!(context.active);
}
#[test]
fn test_tenant_activation() {
let mut context = TenantContext::with_name("test", "Test Tenant").unwrap();
assert!(context.active);
context.deactivate();
assert!(!context.active);
context.activate();
assert!(context.active);
}
}