pamoja_ros2/key.rs
1//! Assembling the Zenoh key expression a `rmw_zenoh` peer subscribes to.
2//!
3//! `rmw_zenoh` puts every ROS 2 topic and service on a Zenoh key expression of the form
4//! `<domain_id>/<fully_qualified_name>/<dds_type_name>/<type_hash>`, so a pamoja peer that builds
5//! the same key talks to ROS 2 nodes over Zenoh with no DDS in the path. This assembles that key
6//! from its parts and validates the result as a Zenoh key expression through [`pamoja_zenoh`], so a
7//! malformed key is caught here rather than silently failing to match on the wire.
8
9use crate::name::is_fully_qualified;
10use crate::typehash::{dds_type_name, TypeHash};
11use alloc::format;
12use alloc::string::String;
13
14/// Builds the `rmw_zenoh` key expression for a ROS 2 topic or service.
15///
16/// # Arguments
17///
18/// * `domain_id` - the ROS domain id (the `ROS_DOMAIN_ID`, default 0).
19/// * `fqn` - the fully qualified name (starting with `/`), for example `/chatter`.
20/// * `ros_type` - the interface type as `package/namespace/Type`, for example `std_msgs/msg/String`.
21/// * `hash` - the message [`TypeHash`].
22///
23/// # Returns
24///
25/// `Some(key)` such as `0/chatter/std_msgs::msg::dds_::String_/RIHS01_...`; `None` if `fqn` is not
26/// fully qualified, if `ros_type` is not a valid three-part interface type, or if the assembled key
27/// is somehow not a valid Zenoh key expression.
28///
29/// # Examples
30///
31/// ```
32/// use pamoja_ros2::key::entity_key;
33/// use pamoja_ros2::typehash::TypeHash;
34///
35/// let hash =
36/// TypeHash::parse("RIHS01_df668c740482bbd48fb39d76a70dfd4bd59db1288021743503259e948f6b1a18")
37/// .unwrap();
38/// let key = entity_key(0, "/chatter", "std_msgs/msg/String", &hash).unwrap();
39/// assert_eq!(
40/// key,
41/// "0/chatter/std_msgs::msg::dds_::String_/RIHS01_df668c740482bbd48fb39d76a70dfd4bd59db1288021743503259e948f6b1a18",
42/// );
43/// ```
44pub fn entity_key(domain_id: u32, fqn: &str, ros_type: &str, hash: &TypeHash) -> Option<String> {
45 if !is_fully_qualified(fqn) {
46 return None;
47 }
48 let type_name = dds_type_name(ros_type)?;
49 // `fqn` starts with `/`, so `{domain_id}{fqn}` joins as `0/chatter` with a single separator.
50 let key = format!("{domain_id}{fqn}/{type_name}/{hash}");
51 if pamoja_zenoh::keyexpr::is_valid(&key) {
52 Some(key)
53 } else {
54 None
55 }
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 fn hash(text: &str) -> TypeHash {
63 TypeHash::parse(text).unwrap()
64 }
65
66 #[test]
67 fn matches_the_published_topic_key() {
68 let h = hash("RIHS01_df668c740482bbd48fb39d76a70dfd4bd59db1288021743503259e948f6b1a18");
69 let key = entity_key(0, "/chatter", "std_msgs/msg/String", &h).unwrap();
70 assert_eq!(
71 key,
72 "0/chatter/std_msgs::msg::dds_::String_/RIHS01_df668c740482bbd48fb39d76a70dfd4bd59db1288021743503259e948f6b1a18",
73 );
74 }
75
76 #[test]
77 fn matches_the_published_namespaced_key() {
78 let h = hash("RIHS01_df668c740482bbd48fb39d76a70dfd4bd59db1288021743503259e948f6b1a18");
79 let key = entity_key(0, "/robot1/chatter", "std_msgs/msg/String", &h).unwrap();
80 assert_eq!(
81 key,
82 "0/robot1/chatter/std_msgs::msg::dds_::String_/RIHS01_df668c740482bbd48fb39d76a70dfd4bd59db1288021743503259e948f6b1a18",
83 );
84 }
85
86 #[test]
87 fn matches_the_published_service_key() {
88 let h = hash("RIHS01_e118de6bf5eeb66a2491b5bda11202e7b68f198d6f67922cf30364858239c81a");
89 let key = entity_key(2, "/add_two_ints", "example_interfaces/srv/AddTwoInts", &h).unwrap();
90 assert_eq!(
91 key,
92 "2/add_two_ints/example_interfaces::srv::dds_::AddTwoInts_/RIHS01_e118de6bf5eeb66a2491b5bda11202e7b68f198d6f67922cf30364858239c81a",
93 );
94 }
95
96 #[test]
97 fn rejects_bad_inputs() {
98 let h = hash("RIHS01_df668c740482bbd48fb39d76a70dfd4bd59db1288021743503259e948f6b1a18");
99 assert!(entity_key(0, "chatter", "std_msgs/msg/String", &h).is_none()); // not absolute
100 assert!(entity_key(0, "/chatter", "std_msgs/String", &h).is_none()); // bad type
101 }
102}