use std::collections::HashMap;
use super::{NodeTopology, TopicRef, TopicTopology, TopologyService};
impl TopologyService {
pub(super) fn build_topic_view(&self, nodes: &[NodeTopology]) -> Vec<TopicTopology> {
let mut topic_map: HashMap<String, TopicTopology> = HashMap::new();
for node in nodes {
for topic_ref in &node.publishes {
let topic = topic_map
.entry(topic_ref.path.clone())
.or_insert_with(|| TopicTopology {
path: topic_ref.path.clone(),
message_type: topic_ref.message_type.clone(),
publishers: Vec::new(),
subscribers: Vec::new(),
});
if !topic.publishers.contains(&node.name) {
topic.publishers.push(node.name.clone());
}
}
for topic_ref in &node.subscribes {
let topic = topic_map
.entry(topic_ref.path.clone())
.or_insert_with(|| TopicTopology {
path: topic_ref.path.clone(),
message_type: topic_ref.message_type.clone(),
publishers: Vec::new(),
subscribers: Vec::new(),
});
if !topic.subscribers.contains(&node.name) {
topic.subscribers.push(node.name.clone());
}
}
}
let mut topics: Vec<TopicTopology> = topic_map.into_values().collect();
topics.sort_by(|a, b| a.path.cmp(&b.path));
topics
}
pub(super) fn parse_topics_from_json(&self, config: &serde_json::Value) -> (Vec<TopicRef>, Vec<TopicRef>) {
let mut publishes = Vec::new();
let mut subscribes = Vec::new();
if let Some(topics) = config.get("topics").and_then(|v| v.as_object()) {
if topics.contains_key("publishes") || topics.contains_key("subscribes") {
if let Some(pubs) = topics.get("publishes").and_then(|v| v.as_array()) {
for topic in pubs {
if let Some(topic_obj) = topic.as_object() {
for (_semantic_name, topic_path) in topic_obj {
if let Some(path_str) = topic_path.as_str() {
publishes.push(TopicRef {
path: path_str.to_string(),
message_type: None,
});
}
}
}
}
}
if let Some(subs) = topics.get("subscribes").and_then(|v| v.as_array()) {
for topic in subs {
if let Some(topic_obj) = topic.as_object() {
for (_semantic_name, topic_path) in topic_obj {
if let Some(path_str) = topic_path.as_str() {
subscribes.push(TopicRef {
path: path_str.to_string(),
message_type: None,
});
}
}
}
}
}
} else {
for (field_name, topic_path) in topics {
if let Some(path_str) = topic_path.as_str() {
if field_name.ends_with("_in") || field_name.ends_with("_sub") || field_name.contains("input") {
subscribes.push(TopicRef {
path: path_str.to_string(),
message_type: None,
});
} else if field_name.ends_with("_out")
|| field_name.ends_with("_pub")
|| field_name.contains("output")
{
publishes.push(TopicRef {
path: path_str.to_string(),
message_type: None,
});
}
}
}
}
}
if publishes.is_empty() && subscribes.is_empty() {
if let Some(input) = config.get("input_topic").and_then(|v| v.as_str()) {
subscribes.push(TopicRef {
path: input.to_string(),
message_type: None,
});
}
if let Some(output) = config.get("output_topic").and_then(|v| v.as_str()) {
publishes.push(TopicRef {
path: output.to_string(),
message_type: None,
});
}
if let Some(control) = config.get("control_topic").and_then(|v| v.as_str()) {
subscribes.push(TopicRef {
path: control.to_string(),
message_type: None,
});
}
}
(publishes, subscribes)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn create_test_service() -> TopologyService {
TopologyService::new(PathBuf::from("/tmp/test"))
}
#[test]
fn test_parse_array_format() {
let service = create_test_service();
let config: serde_json::Value = serde_json::json!({
"topics": {
"publishes": [
{ "output": "/vision/classification" },
{ "status": "/motor/status" }
],
"subscribes": [
{ "input": "/camera/rgb" }
]
}
});
let (publishes, subscribes) = service.parse_topics_from_json(&config);
assert_eq!(publishes.len(), 2);
assert_eq!(subscribes.len(), 1);
assert_eq!(publishes[0].path, "/vision/classification");
assert_eq!(publishes[1].path, "/motor/status");
assert_eq!(subscribes[0].path, "/camera/rgb");
}
#[test]
fn test_parse_flat_format() {
let service = create_test_service();
let config: serde_json::Value = serde_json::json!({
"topics": {
"command_in": "/ai/command",
"response_out": "/ai/response",
"camera_in": "/camera/rgb",
"nav_goal_out": "/nav/goal",
"motor_cmd_out": "/motor/cmd_vel",
"behavior_out": "/behavior/execute"
}
});
let (publishes, subscribes) = service.parse_topics_from_json(&config);
assert_eq!(publishes.len(), 4);
assert!(publishes.iter().any(|t| t.path == "/ai/response"));
assert!(publishes.iter().any(|t| t.path == "/nav/goal"));
assert!(publishes.iter().any(|t| t.path == "/motor/cmd_vel"));
assert!(publishes.iter().any(|t| t.path == "/behavior/execute"));
assert_eq!(subscribes.len(), 2);
assert!(subscribes.iter().any(|t| t.path == "/ai/command"));
assert!(subscribes.iter().any(|t| t.path == "/camera/rgb"));
}
#[test]
fn test_parse_root_level_format() {
let service = create_test_service();
let config: serde_json::Value = serde_json::json!({
"input_topic": "/robot/sensors/camera/rgb",
"output_topic": "/inference/detections",
"control_topic": "/inference/cmd"
});
let (publishes, subscribes) = service.parse_topics_from_json(&config);
assert_eq!(publishes.len(), 1);
assert_eq!(subscribes.len(), 2);
assert_eq!(publishes[0].path, "/inference/detections");
assert!(subscribes.iter().any(|t| t.path == "/robot/sensors/camera/rgb"));
assert!(subscribes.iter().any(|t| t.path == "/inference/cmd"));
}
}