use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Namespace {
prefix: String,
}
impl Namespace {
pub fn new(prefix: impl Into<String>) -> Self {
Self {
prefix: prefix.into(),
}
}
#[inline]
pub fn prefix(&self) -> &str {
&self.prefix
}
#[inline]
#[must_use]
pub fn topic(&self, topic: &str) -> String {
format!("{}/{topic}", self.prefix)
}
#[inline]
#[must_use]
pub fn key(&self, key: &str) -> String {
format!("{}:{key}", self.prefix)
}
#[inline]
#[must_use]
pub fn node_id(&self, node_id: &str) -> String {
format!("{}:{node_id}", self.prefix)
}
#[inline]
#[must_use]
pub fn strip_topic<'a>(&self, topic: &'a str) -> Option<&'a str> {
topic
.strip_prefix(self.prefix.as_str())
.and_then(|rest| rest.strip_prefix('/'))
}
#[inline]
#[must_use]
pub fn strip_key<'a>(&self, key: &'a str) -> Option<&'a str> {
key.strip_prefix(self.prefix.as_str())
.and_then(|rest| rest.strip_prefix(':'))
}
#[inline]
#[must_use]
pub fn pattern(&self, pattern: &str) -> String {
format!("{}/{pattern}", self.prefix)
}
#[inline]
#[must_use]
pub fn wildcard(&self) -> String {
format!("{}/#", self.prefix)
}
}
impl fmt::Display for Namespace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.prefix)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn topic_prefixing() {
let ns = Namespace::new("tenant-42");
assert_eq!(ns.topic("events/created"), "tenant-42/events/created");
assert_eq!(ns.topic("a/b/c"), "tenant-42/a/b/c");
}
#[test]
fn key_prefixing() {
let ns = Namespace::new("tenant-42");
assert_eq!(ns.key("api_requests"), "tenant-42:api_requests");
assert_eq!(ns.key("user:123"), "tenant-42:user:123");
}
#[test]
fn node_id_prefixing() {
let ns = Namespace::new("tenant-42");
assert_eq!(ns.node_id("gpu-node-1"), "tenant-42:gpu-node-1");
}
#[test]
fn pattern_prefixing() {
let ns = Namespace::new("org-1");
assert_eq!(ns.pattern("events/*"), "org-1/events/*");
assert_eq!(ns.pattern("workflow/#"), "org-1/workflow/#");
}
#[test]
fn wildcard() {
let ns = Namespace::new("org-1");
assert_eq!(ns.wildcard(), "org-1/#");
}
#[test]
fn strip_topic() {
let ns = Namespace::new("tenant-42");
assert_eq!(
ns.strip_topic("tenant-42/events/created"),
Some("events/created")
);
assert_eq!(ns.strip_topic("other/events/created"), None);
assert_eq!(ns.strip_topic("tenant-42"), None);
}
#[test]
fn strip_key() {
let ns = Namespace::new("tenant-42");
assert_eq!(ns.strip_key("tenant-42:api_requests"), Some("api_requests"));
assert_eq!(ns.strip_key("other:api_requests"), None);
}
#[test]
fn display() {
let ns = Namespace::new("org-1");
assert_eq!(format!("{ns}"), "org-1");
}
#[cfg(feature = "pubsub")]
#[tokio::test]
async fn namespaced_pubsub_isolation() {
use crate::pubsub::PubSub;
let hub = PubSub::new();
let ns_a = Namespace::new("tenant-a");
let ns_b = Namespace::new("tenant-b");
let mut rx_a = hub.subscribe(&ns_a.pattern("events/#"));
let mut rx_b = hub.subscribe(&ns_b.pattern("events/#"));
hub.publish(&ns_a.topic("events/created"), serde_json::json!({"a": 1}));
let msg = rx_a.recv().await.unwrap();
assert_eq!(ns_a.strip_topic(&msg.topic), Some("events/created"));
assert!(rx_b.try_recv().is_err());
}
#[cfg(feature = "ratelimit")]
#[test]
fn namespaced_ratelimit_isolation() {
use crate::ratelimit::RateLimiter;
let limiter = RateLimiter::new(1.0, 1); let ns_a = Namespace::new("tenant-a");
let ns_b = Namespace::new("tenant-b");
assert!(limiter.check(&ns_a.key("api")));
assert!(!limiter.check(&ns_a.key("api")));
assert!(limiter.check(&ns_b.key("api")));
}
}