auth_framework/tenant/
context.rs1use serde::{Deserialize, Serialize};
4use std::fmt;
5
6#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
8pub struct TenantId(String);
9
10impl TenantId {
11 pub fn new(id: impl Into<String>) -> Self {
13 Self(id.into())
14 }
15
16 pub fn as_str(&self) -> &str {
18 &self.0
19 }
20
21 pub fn into_inner(self) -> String {
23 self.0
24 }
25
26 pub fn validate(&self) -> Result<(), String> {
33 if self.0.is_empty() {
34 return Err("Tenant ID cannot be empty".to_string());
35 }
36
37 if self.0.len() > 64 {
38 return Err("Tenant ID cannot exceed 64 characters".to_string());
39 }
40
41 if !self
42 .0
43 .chars()
44 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
45 {
46 return Err(
47 "Tenant ID can only contain alphanumeric characters, hyphens, and underscores"
48 .to_string(),
49 );
50 }
51
52 Ok(())
53 }
54}
55
56impl fmt::Display for TenantId {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 write!(f, "{}", self.0)
59 }
60}
61
62impl AsRef<str> for TenantId {
63 fn as_ref(&self) -> &str {
64 &self.0
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct TenantMetadata {
71 pub name: String,
73
74 pub description: Option<String>,
76
77 pub created_at: chrono::DateTime<chrono::Utc>,
79
80 pub attributes: std::collections::HashMap<String, serde_json::Value>,
82}
83
84impl TenantMetadata {
85 pub fn new(name: impl Into<String>) -> Self {
87 Self {
88 name: name.into(),
89 description: None,
90 created_at: chrono::Utc::now(),
91 attributes: std::collections::HashMap::new(),
92 }
93 }
94
95 pub fn with_description(mut self, description: impl Into<String>) -> Self {
97 self.description = Some(description.into());
98 self
99 }
100
101 pub fn with_attribute(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
103 self.attributes.insert(key.into(), value);
104 self
105 }
106}
107
108#[derive(Debug, Clone)]
110pub struct TenantContext {
111 pub id: TenantId,
113
114 pub metadata: TenantMetadata,
116
117 pub active: bool,
119}
120
121impl TenantContext {
122 pub fn new(id: TenantId, metadata: TenantMetadata) -> Result<Self, String> {
124 id.validate()?;
125
126 Ok(Self {
127 id,
128 metadata,
129 active: true,
130 })
131 }
132
133 pub fn with_name(id: impl Into<String>, name: impl Into<String>) -> Result<Self, String> {
135 let id = TenantId::new(id);
136 id.validate()?;
137
138 Ok(Self {
139 id,
140 metadata: TenantMetadata::new(name),
141 active: true,
142 })
143 }
144
145 pub fn deactivate(&mut self) {
147 self.active = false;
148 }
149
150 pub fn activate(&mut self) {
152 self.active = true;
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn test_tenant_id_validation() {
162 assert!(TenantId::new("tenant-123").validate().is_ok());
164 assert!(TenantId::new("tenant_123").validate().is_ok());
165 assert!(TenantId::new("acme-corp").validate().is_ok());
166
167 assert!(TenantId::new("").validate().is_err());
169 assert!(TenantId::new("tenant@123").validate().is_err());
170 assert!(TenantId::new("a".repeat(65)).validate().is_err());
171 }
172
173 #[test]
174 fn test_tenant_context_creation() {
175 let context = TenantContext::with_name("acme", "ACME Corp").unwrap();
176 assert_eq!(context.id.as_str(), "acme");
177 assert_eq!(context.metadata.name, "ACME Corp");
178 assert!(context.active);
179 }
180
181 #[test]
182 fn test_tenant_activation() {
183 let mut context = TenantContext::with_name("test", "Test Tenant").unwrap();
184 assert!(context.active);
185
186 context.deactivate();
187 assert!(!context.active);
188
189 context.activate();
190 assert!(context.active);
191 }
192}