use alloc::format;
use alloc::string::String;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EntityKind {
Topic,
ServiceRequest,
ServiceResponse,
}
impl EntityKind {
pub fn prefix(self) -> &'static str {
match self {
EntityKind::Topic => "rt",
EntityKind::ServiceRequest => "rq",
EntityKind::ServiceResponse => "rr",
}
}
}
pub fn is_valid_name(name: &str) -> bool {
if name.is_empty() || name.ends_with('/') {
return false;
}
if name.contains("//") || name.contains("__") {
return false;
}
let bytes = name.as_bytes();
if let Some(pos) = name.find('~') {
if pos != 0 || (bytes.len() > 1 && bytes[1] != b'/') {
return false;
}
}
let mut brace_depth: i32 = 0;
let mut at_token_start = true;
for &b in bytes {
match b {
b'/' => {
if brace_depth != 0 {
return false; }
at_token_start = true;
}
b'~' => at_token_start = false, b'{' => {
brace_depth += 1;
at_token_start = false;
}
b'}' => {
brace_depth -= 1;
if brace_depth < 0 {
return false;
}
at_token_start = false;
}
b'0'..=b'9' => {
if at_token_start {
return false; }
}
b'a'..=b'z' | b'A'..=b'Z' | b'_' => at_token_start = false,
_ => return false,
}
}
brace_depth == 0
}
pub fn is_fully_qualified(name: &str) -> bool {
name.starts_with('/') && !name.contains('~') && !name.contains('{') && is_valid_name(name)
}
pub fn dds_topic(fqn: &str, kind: EntityKind) -> Option<String> {
if !is_fully_qualified(fqn) {
return None;
}
Some(format!("{}{}", kind.prefix(), fqn))
}
pub fn percent_mangle(name: &str) -> String {
name.replace('/', "%")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_well_formed_names() {
assert!(is_valid_name("chatter"));
assert!(is_valid_name("/chatter"));
assert!(is_valid_name("/robot1/camera_left/image_raw"));
assert!(is_valid_name("~/setpoint"));
assert!(is_valid_name("_hidden")); assert!(is_valid_name("/ns/{node}/out")); }
#[test]
fn rejects_malformed_names() {
assert!(!is_valid_name(""));
assert!(!is_valid_name("/foo/"));
assert!(!is_valid_name("/foo//bar"));
assert!(!is_valid_name("/foo__bar"));
assert!(!is_valid_name("/2foo")); assert!(!is_valid_name("/foo bar")); assert!(!is_valid_name("foo~bar")); assert!(!is_valid_name("~foo")); assert!(!is_valid_name("/ns/{node/out")); }
#[test]
fn fully_qualified_requires_absolute_and_plain() {
assert!(is_fully_qualified("/foo/bar"));
assert!(!is_fully_qualified("foo")); assert!(!is_fully_qualified("~/foo")); assert!(!is_fully_qualified("/{ns}/foo")); }
#[test]
fn maps_to_dds_topic_names() {
assert_eq!(
dds_topic("/foo", EntityKind::Topic).as_deref(),
Some("rt/foo")
);
assert_eq!(
dds_topic("/robot1/camera_left/image_raw", EntityKind::Topic).as_deref(),
Some("rt/robot1/camera_left/image_raw"),
);
assert_eq!(
dds_topic("/add_two_ints", EntityKind::ServiceRequest).as_deref(),
Some("rq/add_two_ints"),
);
assert_eq!(
dds_topic("/add_two_ints", EntityKind::ServiceResponse).as_deref(),
Some("rr/add_two_ints"),
);
assert_eq!(dds_topic("relative", EntityKind::Topic), None);
}
#[test]
fn percent_mangles_slashes() {
assert_eq!(percent_mangle("/chatter"), "%chatter");
assert_eq!(percent_mangle("/robot1/chatter"), "%robot1%chatter");
}
}