use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct TenantId(String);
impl TenantId {
pub fn new(s: impl Into<String>) -> Result<Self, InvalidTenantId> {
let s = s.into();
if s.is_empty() {
return Err(InvalidTenantId::Empty);
}
if s.len() > 64 {
return Err(InvalidTenantId::TooLong { len: s.len() });
}
if let Some(c) = s.chars().find(|c| !is_valid_tenant_char(*c)) {
return Err(InvalidTenantId::BadChar { ch: c });
}
Ok(TenantId(s))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
fn is_valid_tenant_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || c == '-'
}
impl fmt::Display for TenantId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<TenantId> for String {
fn from(t: TenantId) -> String {
t.0
}
}
impl TryFrom<String> for TenantId {
type Error = InvalidTenantId;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::new(s)
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum InvalidTenantId {
#[error("tenant id is empty")]
Empty,
#[error("tenant id too long ({len} > 64)")]
TooLong {
len: usize,
},
#[error("tenant id contains disallowed character {ch:?}")]
BadChar {
ch: char,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_empty() {
assert_eq!(TenantId::new("").unwrap_err(), InvalidTenantId::Empty);
}
#[test]
fn rejects_too_long() {
let s: String = "a".repeat(65);
assert!(matches!(
TenantId::new(s).unwrap_err(),
InvalidTenantId::TooLong { len: 65 }
));
}
#[test]
fn rejects_disallowed_chars() {
for c in ['/', ' ', '.', '\u{00e9}', '\n', '*'] {
let s = format!("a{c}b");
assert!(matches!(
TenantId::new(&s).unwrap_err(),
InvalidTenantId::BadChar { ch } if ch == c
));
}
}
#[test]
fn accepts_valid() {
for s in ["a", "ABC_123", "x-y_z", "Z9"] {
let t = TenantId::new(s).unwrap();
assert_eq!(t.as_str(), s);
}
}
#[test]
fn round_trips_through_serde() {
let t = TenantId::new("acme_42").unwrap();
let j = serde_json::to_string(&t).unwrap();
assert_eq!(j, "\"acme_42\"");
let back: TenantId = serde_json::from_str(&j).unwrap();
assert_eq!(back, t);
}
}