use alloc::string::String;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ActionTopics {
pub goal: String,
pub cancel: String,
pub result: String,
pub feedback: String,
pub status: String,
}
impl ActionTopics {
#[must_use]
pub fn from_action(ros_action_name: &str) -> Self {
let base = ros_action_name.trim_start_matches('/');
Self {
goal: format!("rq/{base}/_action/send_goalRequest"),
cancel: format!("rq/{base}/_action/cancel_goalRequest"),
result: format!("rr/{base}/_action/get_resultReply"),
feedback: format!("rt/{base}/_action/feedback"),
status: format!("rt/{base}/_action/status"),
}
}
#[must_use]
pub const fn count(&self) -> usize {
5
}
#[must_use]
pub fn all_topics(&self) -> [&str; 5] {
[
&self.goal,
&self.cancel,
&self.result,
&self.feedback,
&self.status,
]
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn topic_set_has_exactly_5_topics() {
let a = ActionTopics::from_action("/turtle1/rotate_absolute");
assert_eq!(a.count(), 5);
assert_eq!(a.all_topics().len(), 5);
}
#[test]
fn goal_topic_uses_rq_prefix_and_send_goal_suffix() {
let a = ActionTopics::from_action("/turtle1/rotate_absolute");
assert_eq!(
a.goal,
"rq/turtle1/rotate_absolute/_action/send_goalRequest"
);
}
#[test]
fn cancel_topic_uses_rq_prefix_and_cancel_suffix() {
let a = ActionTopics::from_action("/turtle1/rotate_absolute");
assert_eq!(
a.cancel,
"rq/turtle1/rotate_absolute/_action/cancel_goalRequest"
);
}
#[test]
fn result_topic_uses_rr_prefix_and_reply_suffix() {
let a = ActionTopics::from_action("/turtle1/rotate_absolute");
assert_eq!(
a.result,
"rr/turtle1/rotate_absolute/_action/get_resultReply"
);
}
#[test]
fn feedback_and_status_use_rt_prefix() {
let a = ActionTopics::from_action("/x");
assert!(a.feedback.starts_with("rt/"));
assert!(a.status.starts_with("rt/"));
}
#[test]
fn handles_action_name_without_leading_slash() {
let a = ActionTopics::from_action("turtle1/rotate_absolute");
assert!(a.goal.starts_with("rq/turtle1/"));
}
}