Skip to main content

punch_types/
tenant.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5/// Unique identifier for a tenant/organization.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
7#[serde(transparent)]
8pub struct TenantId(pub Uuid);
9
10impl TenantId {
11    pub fn new() -> Self {
12        Self(Uuid::new_v4())
13    }
14}
15
16impl Default for TenantId {
17    fn default() -> Self {
18        Self::new()
19    }
20}
21
22impl std::fmt::Display for TenantId {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        write!(f, "{}", self.0)
25    }
26}
27
28/// A tenant represents an organization or user account.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct Tenant {
31    pub id: TenantId,
32    pub name: String,
33    pub api_key: String,
34    pub status: TenantStatus,
35    pub quota: TenantQuota,
36    pub created_at: DateTime<Utc>,
37}
38
39/// The operational status of a tenant.
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum TenantStatus {
43    Active,
44    Suspended,
45    Trial,
46}
47
48impl std::fmt::Display for TenantStatus {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        match self {
51            Self::Active => write!(f, "active"),
52            Self::Suspended => write!(f, "suspended"),
53            Self::Trial => write!(f, "trial"),
54        }
55    }
56}
57
58/// Resource quotas for a tenant.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct TenantQuota {
61    /// Maximum number of fighters the tenant can spawn.
62    pub max_fighters: usize,
63    /// Maximum number of gorillas the tenant can register.
64    pub max_gorillas: usize,
65    /// Maximum number of concurrent bouts.
66    pub max_bouts: usize,
67    /// Maximum tokens the tenant can consume per day.
68    pub max_tokens_per_day: u64,
69    /// Allowed tool names. Empty means all tools are allowed.
70    #[serde(default)]
71    pub max_tools: Vec<String>,
72}
73
74impl Default for TenantQuota {
75    fn default() -> Self {
76        Self {
77            max_fighters: 10,
78            max_gorillas: 5,
79            max_bouts: 50,
80            max_tokens_per_day: 1_000_000,
81            max_tools: Vec::new(),
82        }
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn test_tenant_id_display() {
92        let uuid = Uuid::nil();
93        let id = TenantId(uuid);
94        assert_eq!(id.to_string(), uuid.to_string());
95    }
96
97    #[test]
98    fn test_tenant_id_new_is_unique() {
99        let id1 = TenantId::new();
100        let id2 = TenantId::new();
101        assert_ne!(id1, id2);
102    }
103
104    #[test]
105    fn test_tenant_id_default() {
106        let id = TenantId::default();
107        assert_ne!(id.0, Uuid::nil());
108    }
109
110    #[test]
111    fn test_tenant_id_serde_transparent() {
112        let uuid = Uuid::new_v4();
113        let id = TenantId(uuid);
114        let json = serde_json::to_string(&id).expect("serialize");
115        assert_eq!(json, format!("\"{}\"", uuid));
116        let deser: TenantId = serde_json::from_str(&json).expect("deserialize");
117        assert_eq!(deser, id);
118    }
119
120    #[test]
121    fn test_tenant_id_copy_clone() {
122        let id = TenantId::new();
123        let copied = id;
124        let cloned = id.clone();
125        assert_eq!(id, copied);
126        assert_eq!(id, cloned);
127    }
128
129    #[test]
130    fn test_tenant_id_hash() {
131        let id = TenantId::new();
132        let mut set = std::collections::HashSet::new();
133        set.insert(id);
134        set.insert(id);
135        assert_eq!(set.len(), 1);
136    }
137
138    #[test]
139    fn test_tenant_status_display() {
140        assert_eq!(TenantStatus::Active.to_string(), "active");
141        assert_eq!(TenantStatus::Suspended.to_string(), "suspended");
142        assert_eq!(TenantStatus::Trial.to_string(), "trial");
143    }
144
145    #[test]
146    fn test_tenant_status_serde_roundtrip() {
147        let statuses = vec![
148            TenantStatus::Active,
149            TenantStatus::Suspended,
150            TenantStatus::Trial,
151        ];
152        for status in &statuses {
153            let json = serde_json::to_string(status).expect("serialize");
154            let deser: TenantStatus = serde_json::from_str(&json).expect("deserialize");
155            assert_eq!(&deser, status);
156        }
157    }
158
159    #[test]
160    fn test_tenant_quota_default() {
161        let quota = TenantQuota::default();
162        assert_eq!(quota.max_fighters, 10);
163        assert_eq!(quota.max_gorillas, 5);
164        assert_eq!(quota.max_bouts, 50);
165        assert_eq!(quota.max_tokens_per_day, 1_000_000);
166        assert!(quota.max_tools.is_empty());
167    }
168
169    #[test]
170    fn test_tenant_quota_serde_roundtrip() {
171        let quota = TenantQuota {
172            max_fighters: 20,
173            max_gorillas: 10,
174            max_bouts: 100,
175            max_tokens_per_day: 5_000_000,
176            max_tools: vec!["read_file".to_string(), "web_fetch".to_string()],
177        };
178        let json = serde_json::to_string(&quota).expect("serialize");
179        let deser: TenantQuota = serde_json::from_str(&json).expect("deserialize");
180        assert_eq!(deser.max_fighters, 20);
181        assert_eq!(deser.max_tools.len(), 2);
182    }
183
184    #[test]
185    fn test_tenant_serde_roundtrip() {
186        let tenant = Tenant {
187            id: TenantId::new(),
188            name: "Acme Corp".to_string(),
189            api_key: "pk_test_abc123".to_string(),
190            status: TenantStatus::Active,
191            quota: TenantQuota::default(),
192            created_at: chrono::Utc::now(),
193        };
194        let json = serde_json::to_string(&tenant).expect("serialize");
195        let deser: Tenant = serde_json::from_str(&json).expect("deserialize");
196        assert_eq!(deser.id, tenant.id);
197        assert_eq!(deser.name, "Acme Corp");
198        assert_eq!(deser.api_key, "pk_test_abc123");
199        assert_eq!(deser.status, TenantStatus::Active);
200    }
201}