use std::any::type_name; pub fn topic_for_type(prefix: &str) -> String { let raw = type_name::(); format!("{prefix}.{}", sanitize_topic_component(raw)) } pub fn sanitize_topic_component(raw: &str) -> String { let mut out = String::with_capacity(raw.len()); let mut last_was_underscore = false; for ch in raw.chars() { let allowed = ch.is_ascii_alphanumeric() || ch == '.' || ch == '-'; if allowed { out.push(ch); last_was_underscore = false; } else if !last_was_underscore { out.push('_'); last_was_underscore = true; } } let trimmed = out.trim_matches('_').to_string(); if trimmed.is_empty() { "unknown".to_string() } else { trimmed } } #[cfg(test)] mod tests { use super::*; #[test] fn strips_module_path() { let t = topic_for_type::("svc"); assert!(t.starts_with("svc.")); assert!(!t.contains("::")); assert!(!t.contains('<')); } #[test] fn replaces_special_chars() { let s = sanitize_topic_component("a::bd"); assert_eq!(s, "a_b_c_d"); } #[test] fn collapses_replacements() { let s = sanitize_topic_component("a:::b"); assert_eq!(s, "a_b"); } #[test] fn empty_fallback() { let s = sanitize_topic_component("<<<>>>"); assert_eq!(s, "unknown"); } mod crate_test_marker { pub struct Inner; } }