Skip to main content

modo/tenant/
id.rs

1use std::fmt;
2
3/// Identifier extracted from an HTTP request by a [`TenantStrategy`](super::TenantStrategy).
4///
5/// Each variant corresponds to a particular extraction strategy. The
6/// [`ApiKey`](Self::ApiKey) variant is **redacted** in both [`Display`](std::fmt::Display)
7/// and [`Debug`](std::fmt::Debug) output to prevent accidental secret logging.
8#[derive(Clone, PartialEq, Eq)]
9pub enum TenantId {
10    /// Tenant slug from [`subdomain()`](super::subdomain),
11    /// [`path_prefix()`](super::path_prefix), or [`path_param()`](super::path_param).
12    Slug(String),
13    /// Full domain name from [`domain()`](super::domain()) or the custom-domain
14    /// branch of [`subdomain_or_domain()`](super::subdomain_or_domain).
15    Domain(String),
16    /// Opaque identifier from [`header()`](super::header).
17    Id(String),
18    /// Raw API key from [`api_key_header()`](super::api_key_header).
19    /// **Redacted** in `Display` and `Debug`.
20    ApiKey(String),
21}
22
23impl TenantId {
24    /// Returns the inner string regardless of variant.
25    ///
26    /// For [`Self::ApiKey`] this returns the **raw, unredacted** secret —
27    /// use it only for resolver lookups, never for logging or display.
28    pub fn as_str(&self) -> &str {
29        match self {
30            Self::Slug(s) | Self::Domain(s) | Self::Id(s) | Self::ApiKey(s) => s,
31        }
32    }
33}
34
35impl fmt::Display for TenantId {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        match self {
38            Self::Slug(s) => write!(f, "slug:{s}"),
39            Self::Domain(s) => write!(f, "domain:{s}"),
40            Self::Id(s) => write!(f, "id:{s}"),
41            Self::ApiKey(_) => write!(f, "apikey:[REDACTED]"),
42        }
43    }
44}
45
46impl fmt::Debug for TenantId {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        match self {
49            Self::Slug(s) => f.debug_tuple("Slug").field(s).finish(),
50            Self::Domain(s) => f.debug_tuple("Domain").field(s).finish(),
51            Self::Id(s) => f.debug_tuple("Id").field(s).finish(),
52            Self::ApiKey(_) => f.debug_tuple("ApiKey").field(&"[REDACTED]").finish(),
53        }
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn display_slug() {
63        let id = TenantId::Slug("acme".into());
64        assert_eq!(id.to_string(), "slug:acme");
65    }
66
67    #[test]
68    fn display_domain() {
69        let id = TenantId::Domain("acme.com".into());
70        assert_eq!(id.to_string(), "domain:acme.com");
71    }
72
73    #[test]
74    fn display_id() {
75        let id = TenantId::Id("abc123".into());
76        assert_eq!(id.to_string(), "id:abc123");
77    }
78
79    #[test]
80    fn display_api_key_redacted() {
81        let id = TenantId::ApiKey("sk_live_secret".into());
82        assert_eq!(id.to_string(), "apikey:[REDACTED]");
83    }
84
85    #[test]
86    fn debug_api_key_redacted() {
87        let id = TenantId::ApiKey("sk_live_secret".into());
88        let debug = format!("{:?}", id);
89        assert!(!debug.contains("sk_live_secret"));
90        assert!(debug.contains("REDACTED"));
91    }
92
93    #[test]
94    fn as_str_returns_inner_value() {
95        assert_eq!(TenantId::Slug("acme".into()).as_str(), "acme");
96        assert_eq!(TenantId::Domain("acme.com".into()).as_str(), "acme.com");
97        assert_eq!(TenantId::Id("abc123".into()).as_str(), "abc123");
98        assert_eq!(TenantId::ApiKey("sk_live".into()).as_str(), "sk_live");
99    }
100
101    #[test]
102    fn equality() {
103        let a = TenantId::Slug("acme".into());
104        let b = TenantId::Slug("acme".into());
105        assert_eq!(a, b);
106
107        let c = TenantId::Domain("acme".into());
108        assert_ne!(a, c);
109    }
110}