Skip to main content

rskit_http/
tenant.rs

1//! Tenant ID helpers for multi-tenant HTTP applications.
2
3use http::{Extensions, HeaderMap};
4
5/// Newtype wrapping a tenant ID string.
6#[derive(Debug, Clone)]
7pub struct TenantId(pub String);
8
9/// Configuration for tenant extraction via Tower middleware.
10#[derive(Debug, Clone)]
11pub struct TenantConfig {
12    /// Header name to read tenant ID from (default: "x-tenant-id").
13    pub header_name: String,
14    /// If true, requests without tenant ID are rejected.
15    pub required: bool,
16    /// Optional default tenant ID when header is missing and not required.
17    pub fallback: Option<String>,
18}
19
20impl Default for TenantConfig {
21    fn default() -> Self {
22        Self {
23            header_name: "x-tenant-id".to_string(),
24            required: false,
25            fallback: None,
26        }
27    }
28}
29
30impl TenantConfig {
31    /// Read a tenant id from headers according to this config.
32    #[must_use]
33    pub fn tenant_from_headers(&self, headers: &HeaderMap) -> Option<TenantId> {
34        headers
35            .get(&self.header_name)
36            .and_then(|value| value.to_str().ok())
37            .map(|value| TenantId(value.to_owned()))
38            .or_else(|| self.fallback.clone().map(TenantId))
39    }
40
41    /// Returns whether a request without tenant information should be rejected.
42    #[must_use]
43    pub const fn is_required(&self) -> bool {
44        self.required
45    }
46}
47
48/// Store a [`TenantId`] in request extensions.
49///
50/// This is useful when composing custom middleware that needs to set the tenant value.
51pub fn set_tenant_in_extensions(extensions: &mut Extensions, tenant_id: impl Into<String>) {
52    extensions.insert(TenantId(tenant_id.into()));
53}
54
55/// Retrieve the [`TenantId`] from request extensions.
56///
57/// Returns `None` when no tenant has been stored.
58pub fn tenant_from_extensions(extensions: &Extensions) -> Option<&TenantId> {
59    extensions.get::<TenantId>()
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn test_tenant_config_default() {
68        let cfg = TenantConfig::default();
69        assert_eq!(cfg.header_name, "x-tenant-id");
70        assert!(!cfg.required);
71        assert!(cfg.fallback.is_none());
72    }
73
74    #[test]
75    fn test_tenant_config_with_fallback() {
76        let cfg = TenantConfig {
77            header_name: "x-tenant-id".to_string(),
78            required: false,
79            fallback: Some("default".to_string()),
80        };
81        assert_eq!(cfg.fallback, Some("default".to_string()));
82    }
83
84    #[test]
85    fn test_tenant_from_headers() {
86        let mut headers = HeaderMap::new();
87        headers.insert("x-tenant-id", "tenant-123".parse().unwrap());
88        let tenant = TenantConfig::default().tenant_from_headers(&headers);
89        assert_eq!(tenant.unwrap().0, "tenant-123");
90    }
91
92    #[test]
93    fn test_tenant_from_headers_uses_fallback() {
94        let cfg = TenantConfig {
95            fallback: Some("default".to_string()),
96            ..Default::default()
97        };
98        assert_eq!(
99            cfg.tenant_from_headers(&HeaderMap::new()).unwrap().0,
100            "default"
101        );
102    }
103
104    // ── Extension helper tests ────────────────────────────────────────────────
105
106    #[test]
107    fn test_set_and_get_tenant_in_extensions() {
108        let mut ext = Extensions::new();
109        set_tenant_in_extensions(&mut ext, "org-42");
110
111        let tenant = tenant_from_extensions(&ext);
112        assert!(tenant.is_some());
113        assert_eq!(tenant.unwrap().0, "org-42");
114    }
115
116    #[test]
117    fn test_tenant_from_extensions_empty() {
118        let ext = Extensions::new();
119        assert!(tenant_from_extensions(&ext).is_none());
120    }
121
122    #[test]
123    fn test_set_tenant_overwrites_previous() {
124        let mut ext = Extensions::new();
125        set_tenant_in_extensions(&mut ext, "first");
126        set_tenant_in_extensions(&mut ext, "second");
127        assert_eq!(tenant_from_extensions(&ext).unwrap().0, "second");
128    }
129}