use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::error::{JammiError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct TenantId(Uuid);
impl TenantId {
pub fn from_uuid(uuid: Uuid) -> Result<Self> {
if uuid.is_nil() {
return Err(JammiError::Tenant(
"tenant id must not be the nil UUID".into(),
));
}
Ok(Self(uuid))
}
pub const fn as_uuid(&self) -> Uuid {
self.0
}
}
impl fmt::Display for TenantId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl FromStr for TenantId {
type Err = JammiError;
fn from_str(s: &str) -> Result<Self> {
let uuid =
Uuid::from_str(s).map_err(|e| JammiError::Tenant(format!("tenant id '{s}': {e}")))?;
Self::from_uuid(uuid)
}
}
impl TryFrom<String> for TenantId {
type Error = JammiError;
fn try_from(s: String) -> Result<Self> {
Self::from_str(&s)
}
}
impl From<TenantId> for String {
fn from(t: TenantId) -> String {
t.to_string()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TenantContext {
Unscoped,
Scoped(TenantId),
}
impl TenantContext {
pub fn from_option(t: Option<TenantId>) -> Self {
match t {
Some(t) => TenantContext::Scoped(t),
None => TenantContext::Unscoped,
}
}
pub fn tenant(&self) -> Option<TenantId> {
match self {
TenantContext::Scoped(t) => Some(*t),
TenantContext::Unscoped => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_uuid_accepts_non_nil() {
let u = Uuid::parse_str("01906c83-d4c8-7e10-9c4f-3b6f7c5a8e9a").unwrap();
assert_eq!(TenantId::from_uuid(u).unwrap().as_uuid(), u);
}
#[test]
fn from_uuid_rejects_nil() {
let err = TenantId::from_uuid(Uuid::nil()).unwrap_err();
match err {
JammiError::Tenant(m) => assert!(m.contains("nil")),
other => panic!("expected Tenant error, got {other:?}"),
}
}
#[test]
fn from_str_rejects_nil_string() {
let err = TenantId::from_str("00000000-0000-0000-0000-000000000000").unwrap_err();
assert!(matches!(err, JammiError::Tenant(_)));
}
#[test]
fn from_str_rejects_malformed() {
assert!(TenantId::from_str("not-a-uuid").is_err());
}
#[test]
fn display_roundtrip_via_from_str() {
let original = TenantId::from_str("01906c83-d4c8-7e10-9c4f-3b6f7c5a8e9a").unwrap();
let s = original.to_string();
let parsed = TenantId::from_str(&s).unwrap();
assert_eq!(original, parsed);
}
#[test]
fn display_lowercase_hyphenated() {
let t = TenantId::from_str("01906C83-D4C8-7E10-9C4F-3B6F7C5A8E9A").unwrap();
assert_eq!(t.to_string(), "01906c83-d4c8-7e10-9c4f-3b6f7c5a8e9a");
}
#[test]
fn serde_roundtrip_through_string() {
let t = TenantId::from_str("01906c83-d4c8-7e10-9c4f-3b6f7c5a8e9a").unwrap();
let json = serde_json::to_string(&t).unwrap();
assert_eq!(json, "\"01906c83-d4c8-7e10-9c4f-3b6f7c5a8e9a\"");
let parsed: TenantId = serde_json::from_str(&json).unwrap();
assert_eq!(t, parsed);
}
#[test]
fn serde_deserialize_rejects_nil() {
let r: std::result::Result<TenantId, _> =
serde_json::from_str("\"00000000-0000-0000-0000-000000000000\"");
assert!(r.is_err());
}
#[test]
fn try_from_string_delegates_to_from_str() {
let s = String::from("01906c83-d4c8-7e10-9c4f-3b6f7c5a8e9a");
let t: TenantId = s.clone().try_into().unwrap();
assert_eq!(t.to_string(), s);
}
#[test]
fn into_string_via_display() {
let t = TenantId::from_str("01906c83-d4c8-7e10-9c4f-3b6f7c5a8e9a").unwrap();
let s: String = t.into();
assert_eq!(s, "01906c83-d4c8-7e10-9c4f-3b6f7c5a8e9a");
}
}