use serde::{Deserialize, Serialize};
use std::fmt;
pub fn sanitize_tenant_id(tenant_id: &str) -> String {
tenant_id.replace(':', "::")
}
pub fn sanitize_environment(environment: &str) -> String {
environment.replace(':', "::")
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Namespace {
tenant_id: String,
environment: String,
}
impl Namespace {
pub fn new(tenant_id: impl Into<String>, environment: impl Into<String>) -> Self {
Self {
tenant_id: tenant_id.into(),
environment: environment.into(),
}
}
pub fn tenant_id(&self) -> &str {
&self.tenant_id
}
pub fn environment(&self) -> &str {
&self.environment
}
pub fn prefix(&self) -> String {
format!(
"tenant:{}:env:{}",
sanitize_tenant_id(&self.tenant_id),
sanitize_environment(&self.environment)
)
}
pub fn qualify_key(&self, key: &str) -> String {
format!("{}:{}", self.prefix(), key)
}
}
impl fmt::Display for Namespace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.prefix())
}
}
impl Default for Namespace {
fn default() -> Self {
Self {
tenant_id: "global".to_string(),
environment: "development".to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_namespace_creation() {
let ns = Namespace::new("tenant-123", "production");
assert_eq!(ns.tenant_id(), "tenant-123");
assert_eq!(ns.environment(), "production");
}
#[test]
fn test_namespace_prefix() {
let ns = Namespace::new("acme", "prod");
assert_eq!(ns.prefix(), "tenant:acme:env:prod");
}
#[test]
fn test_namespace_qualify_key() {
let ns = Namespace::new("acme", "prod");
let qualified = ns.qualify_key("rl:user:123:rule1");
assert_eq!(qualified, "tenant:acme:env:prod:rl:user:123:rule1");
}
#[test]
fn test_namespace_display() {
let ns = Namespace::new("tenant-1", "staging");
assert_eq!(format!("{}", ns), "tenant:tenant-1:env:staging");
}
#[test]
fn test_namespace_default() {
let ns = Namespace::default();
assert_eq!(ns.tenant_id(), "global");
assert_eq!(ns.environment(), "development");
}
#[test]
fn test_namespace_equality() {
let ns1 = Namespace::new("tenant-1", "prod");
let ns2 = Namespace::new("tenant-1", "prod");
let ns3 = Namespace::new("tenant-2", "prod");
assert_eq!(ns1, ns2);
assert_ne!(ns1, ns3);
}
#[test]
fn test_namespace_hash() {
use ahash::AHashSet;
let ns1 = Namespace::new("tenant-1", "prod");
let ns2 = Namespace::new("tenant-1", "prod");
let ns3 = Namespace::new("tenant-2", "prod");
let mut set = AHashSet::new();
set.insert(ns1.clone());
set.insert(ns2.clone());
set.insert(ns3.clone());
assert_eq!(set.len(), 2);
assert!(set.contains(&ns1));
assert!(set.contains(&ns3));
}
#[test]
fn test_namespace_serialization() {
let ns = Namespace::new("tenant-123", "production");
let json = serde_json::to_string(&ns).unwrap();
let deserialized: Namespace = serde_json::from_str(&json).unwrap();
assert_eq!(ns, deserialized);
}
#[test]
fn test_namespace_with_empty_strings() {
let ns = Namespace::new("", "");
assert_eq!(ns.tenant_id(), "");
assert_eq!(ns.environment(), "");
assert_eq!(ns.prefix(), "tenant::env:");
}
#[test]
fn test_namespace_with_special_characters() {
let ns = Namespace::new("tenant_a/b:c@d!", "prod-v1.2.3");
assert_eq!(ns.tenant_id(), "tenant_a/b:c@d!");
assert_eq!(ns.environment(), "prod-v1.2.3");
assert_eq!(ns.prefix(), "tenant:tenant_a/b::c@d!:env:prod-v1.2.3");
}
#[test]
fn test_sanitize_tenant_id_escapes_colon() {
assert_eq!(sanitize_tenant_id("a:b"), "a::b");
assert_eq!(sanitize_tenant_id("a:b:c"), "a::b::c");
assert_eq!(sanitize_tenant_id("::"), "::::");
}
#[test]
fn test_sanitize_tenant_id_no_colon_unchanged() {
assert_eq!(sanitize_tenant_id("acme"), "acme");
assert_eq!(sanitize_tenant_id(""), "");
assert_eq!(sanitize_tenant_id("tenant-123"), "tenant-123");
}
#[test]
fn test_sanitize_environment_escapes_colon() {
assert_eq!(sanitize_environment("prod:v2"), "prod::v2");
assert_eq!(sanitize_environment("a:b:c"), "a::b::c");
}
#[test]
fn test_sanitize_environment_no_colon_unchanged() {
assert_eq!(sanitize_environment("production"), "production");
assert_eq!(sanitize_environment(""), "");
assert_eq!(sanitize_environment("prod-v1.2.3"), "prod-v1.2.3");
}
#[test]
fn test_namespace_prefix_injection_environment_no_collision() {
let ns_normal = Namespace::new("acme", "prod");
let ns_injected = Namespace::new("acme", "prod:rl");
let key_normal = ns_normal.qualify_key("rl:user");
let key_injected = ns_injected.qualify_key("user");
assert_ne!(
key_normal, key_injected,
"environment 中的 ':' 必须被转义,防止 qualified key 冲突"
);
}
#[test]
fn test_namespace_prefix_injection_tenant_id_no_collision() {
let ns_injected_tenant = Namespace::new("a:env:b", "c");
let ns_injected_env = Namespace::new("a", "b:env:c");
assert_ne!(
ns_injected_tenant.prefix(),
ns_injected_env.prefix(),
"tenant_id 中的 ':' 必须被转义,防止 prefix 冲突"
);
}
#[test]
fn test_namespace_qualify_key_stable_for_safe_ids() {
let ns = Namespace::new("acme-corp", "production");
assert_eq!(
ns.qualify_key("rl:user:123"),
"tenant:acme-corp:env:production:rl:user:123"
);
}
#[test]
fn test_namespace_qualify_key_edge_cases() {
let ns = Namespace::new("acme", "prod");
assert_eq!(ns.qualify_key(""), "tenant:acme:env:prod:");
assert_eq!(ns.qualify_key("::"), "tenant:acme:env:prod:::");
}
#[test]
fn test_namespace_display_matches_prefix() {
let ns = Namespace::new("tenant-1", "staging");
assert_eq!(format!("{}", ns), ns.prefix());
}
#[test]
fn test_namespace_clone_independent() {
let ns1 = Namespace::new("original", "prod");
let ns2 = ns1.clone();
assert_eq!(ns1, ns2);
let json2 = serde_json::to_string(&ns2).unwrap();
let ns2_roundtrip: Namespace = serde_json::from_str(&json2).unwrap();
assert_eq!(ns1, ns2_roundtrip);
}
}