prax_query/tenant/
context.rs1use std::any::Any;
4use std::collections::HashMap;
5use std::fmt;
6use std::sync::Arc;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub struct TenantId(String);
11
12impl TenantId {
13 pub fn new(id: impl Into<String>) -> Self {
15 Self(id.into())
16 }
17
18 pub fn as_str(&self) -> &str {
20 &self.0
21 }
22
23 pub fn into_inner(self) -> String {
25 self.0
26 }
27}
28
29impl fmt::Display for TenantId {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 write!(f, "{}", self.0)
32 }
33}
34
35impl From<&str> for TenantId {
36 fn from(s: &str) -> Self {
37 Self::new(s)
38 }
39}
40
41impl From<String> for TenantId {
42 fn from(s: String) -> Self {
43 Self::new(s)
44 }
45}
46
47impl From<uuid::Uuid> for TenantId {
48 fn from(u: uuid::Uuid) -> Self {
49 Self::new(u.to_string())
50 }
51}
52
53impl From<i64> for TenantId {
54 fn from(i: i64) -> Self {
55 Self::new(i.to_string())
56 }
57}
58
59impl From<i32> for TenantId {
60 fn from(i: i32) -> Self {
61 Self::new(i.to_string())
62 }
63}
64
65#[derive(Debug, Clone, Default)]
67pub struct TenantInfo {
68 pub name: Option<String>,
70 pub schema: Option<String>,
72 pub database: Option<String>,
74 pub is_superuser: bool,
76 pub is_system: bool,
78 metadata: HashMap<String, Arc<dyn Any + Send + Sync>>,
80}
81
82impl TenantInfo {
83 pub fn new() -> Self {
85 Self::default()
86 }
87
88 pub fn with_name(mut self, name: impl Into<String>) -> Self {
90 self.name = Some(name.into());
91 self
92 }
93
94 pub fn with_schema(mut self, schema: impl Into<String>) -> Self {
96 self.schema = Some(schema.into());
97 self
98 }
99
100 pub fn with_database(mut self, database: impl Into<String>) -> Self {
102 self.database = Some(database.into());
103 self
104 }
105
106 pub fn as_superuser(mut self) -> Self {
108 self.is_superuser = true;
109 self
110 }
111
112 pub fn as_system(mut self) -> Self {
114 self.is_system = true;
115 self
116 }
117
118 pub fn with_metadata<T: Any + Send + Sync>(mut self, key: impl Into<String>, value: T) -> Self {
120 self.metadata.insert(key.into(), Arc::new(value));
121 self
122 }
123
124 pub fn get_metadata<T: Any + Send + Sync>(&self, key: &str) -> Option<&T> {
126 self.metadata.get(key).and_then(|v| v.downcast_ref())
127 }
128}
129
130#[derive(Debug, Clone)]
135pub struct TenantContext {
136 pub id: TenantId,
138 pub info: TenantInfo,
140}
141
142impl TenantContext {
143 pub fn new(id: impl Into<TenantId>) -> Self {
145 Self {
146 id: id.into(),
147 info: TenantInfo::default(),
148 }
149 }
150
151 pub fn with_info(id: impl Into<TenantId>, info: TenantInfo) -> Self {
153 Self {
154 id: id.into(),
155 info,
156 }
157 }
158
159 pub fn system() -> Self {
161 Self {
162 id: TenantId::new("__system__"),
163 info: TenantInfo::new().as_system().as_superuser(),
164 }
165 }
166
167 pub fn should_bypass(&self) -> bool {
169 self.info.is_superuser || self.info.is_system
170 }
171
172 pub fn schema(&self) -> Option<&str> {
174 self.info.schema.as_deref()
175 }
176
177 pub fn database(&self) -> Option<&str> {
179 self.info.database.as_deref()
180 }
181}
182
183#[cfg(feature = "thread-local-tenant")]
185mod thread_local {
186 use super::TenantContext;
187 use std::cell::RefCell;
188
189 thread_local! {
190 static CURRENT_TENANT: RefCell<Option<TenantContext>> = const { RefCell::new(None) };
191 }
192
193 pub fn set_current_tenant(ctx: TenantContext) {
195 CURRENT_TENANT.with(|t| {
196 *t.borrow_mut() = Some(ctx);
197 });
198 }
199
200 pub fn get_current_tenant() -> Option<TenantContext> {
202 CURRENT_TENANT.with(|t| t.borrow().clone())
203 }
204
205 pub fn clear_current_tenant() {
207 CURRENT_TENANT.with(|t| {
208 *t.borrow_mut() = None;
209 });
210 }
211
212 pub fn with_tenant<F, R>(ctx: TenantContext, f: F) -> R
214 where
215 F: FnOnce() -> R,
216 {
217 let previous = get_current_tenant();
218 set_current_tenant(ctx);
219 let result = f();
220 if let Some(prev) = previous {
221 set_current_tenant(prev);
222 } else {
223 clear_current_tenant();
224 }
225 result
226 }
227}
228
229#[cfg(feature = "thread-local-tenant")]
230#[allow(unused_imports)]
231pub use thread_local::{clear_current_tenant, get_current_tenant, set_current_tenant, with_tenant};
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn test_tenant_id_creation() {
239 let id1 = TenantId::new("tenant-123");
240 assert_eq!(id1.as_str(), "tenant-123");
241
242 let id2: TenantId = "tenant-456".into();
243 assert_eq!(id2.as_str(), "tenant-456");
244
245 let id3: TenantId = 123_i64.into();
246 assert_eq!(id3.as_str(), "123");
247 }
248
249 #[test]
250 fn test_tenant_context() {
251 let ctx = TenantContext::new("tenant-123");
252 assert_eq!(ctx.id.as_str(), "tenant-123");
253 assert!(!ctx.should_bypass());
254 }
255
256 #[test]
257 fn test_system_context() {
258 let ctx = TenantContext::system();
259 assert!(ctx.should_bypass());
260 assert!(ctx.info.is_system);
261 assert!(ctx.info.is_superuser);
262 }
263
264 #[test]
265 fn test_tenant_info() {
266 let info = TenantInfo::new()
267 .with_name("Acme Corp")
268 .with_schema("tenant_acme")
269 .with_metadata("plan", "enterprise".to_string());
270
271 assert_eq!(info.name, Some("Acme Corp".to_string()));
272 assert_eq!(info.schema, Some("tenant_acme".to_string()));
273 assert_eq!(
274 info.get_metadata::<String>("plan"),
275 Some(&"enterprise".to_string())
276 );
277 }
278}